Inspired by nunomaduro/laravel-mojito
Test your Laravel views in isolation, interacting directly with the HTML.
$this->get('/')->expectView()->toContain('Laravel');
$this->view('menu', ['links' => $links])
->expectView()
->in('.links')
->first('a')
->toHave('href', 'https://laravel.com/docs')
->toHaveClass('btn')
->toHaveText('Documentation');
You can install the package via composer:
composer require --dev soyhuce/laravel-embuscade
The most basic way to access the ViewExpect is to create it with an HTML string :
use Soyhuce\LaravelEmbuscade\ViewExpect;
new ViewExpect($html);
As this is not the most convenient way, you can create the ViewExpect from various objects:
// From a TestResponse
$expect = $this->get('/')->expectView();
// From a TestView
$expect = $this->view('home', ['links' => $links])->expectView();
// From a TestComponent
$expect = $this->component(Home::class, ['links' => $links])->expectView();
If you use Livewire, you can also create a ViewExpect from a Livewire test component:
$expect = Livewire::test(HomePage::class, ['links' => $links])->expectView();
Once the ViewExpect is created, you can navigate the view using the following methods:
// Selects all elements matching the CSS selector
$expect->in($cssSeclector);
// Selects the nth element matching the CSS selector, index starts at 0 !
$expect->at($cssSelector, $index);
// Selects the first element matching the CSS selector
$expect->first($cssSelector);
// Selects the last element matching the CSS selector
$expect->last($cssSeclector);
// Selects the only element matching the CSS selector
$expect->sole($cssSelector);
$cssSelector
must be any valid CSS selector, like .class
, #id
, tag
, tag.class
, tag#id
, tag[attr=value]
, etc.
Note : Some pseudo-classes and pseudo-elements are not supported, like
:hover
,:before
,:after
,:has
, etc.
You can also use Embuscade selectors to navigate the view as navigating CSS selectors can be cumbersome:
<button data-embuscade="login-button">Login</button>
$expect->sole('@login-button')
->...
You can also use the @embuscade
directive to generate Embuscade selectors in your blade views:
<button @embuscade('login-button')>Login</button>
The data-embuscade
attribute will be added to the element, only on testing environment or is debug mode is enabled.
Note : Because @embuscade is not really a blade directive, it requires use of single quotes
'
to work and won't have access to execution context.@embuscade("login-button")
will not work.@foreach ($array as $key => $value) <div @embuscade('login-button-{{ $key }}')>{{ $value}}</div> @endforeachwill not work either.
You can customize the HTML attribute Embuscade will use for the selectors using selectorHtmlAttribute
method:
use Soyhuce\LaravelEmbuscade\Embuscade;
Embuscade::selectorHtmlAttribute('data-test'); // or 'dusk' if you use Dusk and want to leverage existing Dusk selectors.
If you run in production without dev-dependencies installed, you will need an extra setup in order to remove @embuscade
directives from your views.
In your AppServiceProvider::boot
method, you can add the following code:
if (!class_exists(EmbuscadeServiceProvider::class)) {
$this->app->get('blade.compiler')->prepareStringsForCompilationUsing(
fn (string $input) => preg_replace( '/@embuscade\\(\'([^)]+)\'\\)/x', '', $input)
);
}
There won't be any overhead here as cached views won't contain any @embuscade
directive.
Note : if you run in production with your dev-dependencies installed, you should definitively consider removing them.
Some expectations will be applied to the entire view:
// Expects the view to contain at least an element matching the CSS selector
$expect->toHave('.links a');
// Expects the view to contain exactly n elements matching the CSS selector
$expect->toHave('.links a', 2);
// Expects the view to contain at least one element pointing to $link
$expect->toHaveLink('https://laravel.com/docs');
// Expect the view contains a meta tag with the given attributes in head section
$expect->toHaveMeta(['property' => 'og:title', 'content' => 'Laravel']);
// Expect the view text equals given text
$expect->toHaveText('Laravel');
// Expect the view text contains given text
$expect->toContainText('Documentation');
// Expect the view text is empty
$expect->toBeEmpty();
// Expect the view html contains given content
$expect->toContain('<a href="https://laravel.com/docs">Documentation</a>');
Other expectation will only look at current root:
// Expect the element to have the given attribute
$expect->toHaveAttribute('disabled');
// Expect the element to have the given attribute with the given value
$expect->toHaveAttribute('href', 'https://laravel.com/docs');
// Expect the element to have the given attribute containing the given value
$expect->toHaveAttributeContaining('class', 'btn');
Some helpers are also available for you:
$expect->toAccept($value); // same as toHaveAttribute('accept', $value)
$expect->toBeDisabled(); // same as toHaveAttribute('disabled')
$expect->toHaveAlt($value); // same as toHaveAttribute('alt', $value)
$expect->toHaveClass($value); // same as toHaveAttributeContaining('class', $value)
$expect->toHaveHref($value); // same as toHaveAttribute('href', $value)
$expect->toHaveSrc($value); // same as toHaveAttribute('src', $value)
$expect->toHaveValue($value); // same as toHaveAttribute('value', $value)
You can negate any expectation by calling not
before the expectation:
$expect->not->toHave('.links a');
$expect->not->toBeDisabled();
The negation will only apply to the next expectation.
$this->view('menu', ['links' => $links])
->expectView()
->in('.links')
->first('a')
->toHave('href', 'https://laravel.com/docs')
->not->toHaveAttribute('target')
->toHaveClass('btn')
->toHaveText('Documentation');
You can navigate and apply expectations on elements in a single chain, in order to not loose focus on the current element: Given the following HTML:
<fieldset disabled>
<legend>Disabled fieldset</legend>
<p>
<label>
Name: <input type="radio" name="radio" value="regular" /> Regular
</label>
</p>
<p>
<label>Number: <input type="number" /></label>
</p>
</fieldset>
you can test it with the following code:
use Soyhuce\LaravelEmbuscade\ViewExpect;
$this->view('test')
->expectView()
->toBeDisabled()
->sole('legend', fn(ViewExpect $expect) => $expect->toHaveText('Disabled fieldset'))
->at('p input', 0, fn(ViewExpect $expect) => $expect->toHaveAttribute('type', 'radio'))
->at('p input', 1, fn(ViewExpect $expect) => $expect->toHaveAttribute('type', 'number'));
Every selection method will allow you to pass a closure that will receive a new ViewExpect, focused on the selected element.
The ViewExpect
class is macroable, so you can add your own expectations:
ViewExpect::macro('toHaveCharset', function (string $charset) {
return $this->in('head')->first('meta')->toHaveAttribute('charset', $charset);
});
});
$this->view('home')->expectView()->toHaveCharset('utf-8');
You can dump the current state of the ViewExpect using the dump
or dd
methods:
$this->view('home')->expectView()->in('a')->dump();
It will dump the current HTML node.
Embuscade is a French word meaning ambush. It makes reference to the original package name, Laravel Mojito, as "une embuscade" is also a famous local cocktail from Caen.
Each bar has its own recipe, but it could be something like:
- 20 cl of blond beer
- 12 cl of white wine
- 8 cl of calvados (cider brandy, 40% alcohol)
- 4 cl of blackcurrant syrup
- 4 cl of lemon syrup
Easy to drink but quite strong, be careful not to fall into the ambush!
composer test
Please see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
Please review our security policy on how to report security vulnerabilities.
- Bastien Philippe
- Nuno Maduro for the inspiration with laravel-mojito
- All Contributors
The MIT License (MIT). Please see License File for more information.