sinnbeck/laravel-dom-assertions

Maintainers

Package info

github.com/sinnbeck/laravel-dom-assertions

pkg:composer/sinnbeck/laravel-dom-assertions

Statistics

Installs: 297 722

Dependents: 8

Suggesters: 0

Stars: 106

Open Issues: 0

v3.0.10 2026-06-14 21:25 UTC

README

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads Laravel Compatibility

This package provides some extra assertion helpers to use in HTTP Tests. If you have ever needed more control over your view assertions than assertSee, assertSeeInOrder, assertSeeText, assertSeeTextInOrder, assertDontSee, and assertDontSeeText then this is the package for you.

Installation

You can install the package via composer:

Version 3.x and above requires PHP 8.1+ and Laravel 10+.

composer require sinnbeck/laravel-dom-assertions --dev

Note: If you're using PHP 8.0 or Laravel 9, please use version 2.x:

 composer require sinnbeck/laravel-dom-assertions:^2.0 --dev

Table of contents

Asserting on elements

Use assertElementExists() (or its alias assertElement()) on a test response to assert against the DOM. This package assumes a valid HTML document and will wrap your markup in <html>, <head>, and <body> tags if they are missing.

Called with no arguments, it simply asserts that a <body> element was parsed:

$this->get('/some-route')
    ->assertElementExists();

Pass a CSS selector as the first argument to target a specific element:

$this->get('/some-route')
    ->assertElementExists('#nav');

The second argument is a closure that receives an AssertElement instance, which is where all of the fluent assertions below live.

Asserting the element type

$this->get('/some-route')
    ->assertElementExists('#overview', function (AssertElement $assert) {
        $assert->is('div');
    });

Asserting attributes

Assert that an attribute is present, optionally with a specific value:

$this->get('/some-route')
    ->assertElementExists('#overview', function (AssertElement $assert) {
        $assert->has('x-data', '{foo: 1}');
    });

Or assert that it is absent:

$this->get('/some-route')
    ->assertElementExists('#overview', function (AssertElement $assert) {
        $assert->doesntHave('x-data', '{foo: 2}');
    });

Asserting on children

Confirm a child element exists:

$this->get('/some-route')
    ->assertElementExists('#overview', function (AssertElement $assert) {
        $assert->contains('div');
    });

Narrow it down with a CSS selector:

$this->get('/some-route')
    ->assertElementExists('#overview', function (AssertElement $assert) {
        $assert->contains('div:nth-of-type(3)');
    });

Assert that the child carries certain attributes:

$this->get('/some-route')
    ->assertElementExists('#overview', function (AssertElement $assert) {
        $assert->contains('li.list-item', ['x-data' => 'foobar']);
    });

Assert it appears an exact number of times by passing a count as the final argument:

$this->get('/some-route')
    ->assertElementExists('#overview', function (AssertElement $assert) {
        $assert->contains('li.list-item', ['x-data' => 'foobar'], 3);
    });

When you only care about the count, drop the attributes argument:

$this->get('/some-route')
    ->assertElementExists('#overview', function (AssertElement $assert) {
        $assert->contains('li.list-item', 3);
    });

Or assert that no matching child exists:

$this->get('/some-route')
    ->assertElementExists('#overview', function (AssertElement $assert) {
        $assert->doesntContain('li.list-item', ['x-data' => 'foobar']);
    });

Drilling into a child

find() selects the first matching child and lets you assert against it. Pass a closure to receive a fresh AssertElement for that child:

$this->get('/some-route')
    ->assertElementExists('#overview', function (AssertElement $assert) {
        $assert->find('li:nth-of-type(3)', function (AssertElement $element) {
            $element->is('li');
        });
    });

To assert against every matching element rather than just the first, use each():

$this->get('/some-route')
    ->assertElementExists('#overview', function (AssertElement $assert) {
        $assert->each('li', function (AssertElement $element) {
            $element->has('class', 'list-item');
        });
    });

Because each find() hands you another AssertElement, you can drill arbitrarily deep into the DOM:

$this->get('/some-route')
    ->assertElementExists(function (AssertElement $element) {
        $element->find('div', function (AssertElement $element) {
            $element->is('div');

            $element->find('p', function (AssertElement $element) {
                $element->is('p');
                $element->find('#label', fn (AssertElement $element) => $element->is('span'));
            });

            $element->find('p:nth-of-type(2)', function (AssertElement $element) {
                $element->is('p');
                $element->find('.sub-header', fn (AssertElement $element) => $element->is('h4'));
            });
        });
    });

Magic methods

Element type and attribute assertions have convenient magic-method shortcuts:

$assert->isDiv();                              // is('div')
$assert->hasXData('{foo: 1}');                 // has('x-data', '{foo: 1}')
$assert->containsDiv(['class' => 'foo'], 3);   // contains('div', ['class' => 'foo'], 3)
$assert->doesntContainSpan(['class' => 'foo']); // doesntContain('span', ['class' => 'foo'])
$assert->findDiv(fn (AssertElement $el) => $el->isDiv()); // find('div', ...)

Asserting on forms

Forms support every element assertion above, plus a handful of form-specific helpers. Use assertFormExists() (alias assertForm()), which targets the first <form> on the page by default:

$this->get('/some-route')
    ->assertFormExists();

Pass a selector to target a specific form:

$this->get('/some-route')
    ->assertFormExists('#users-form');

The closure receives an AssertForm instance. Assert on the action and method:

$this->get('/some-route')
    ->assertFormExists('#form1', function (AssertForm $form) {
        $form->hasAction('/logout')
            ->hasMethod('post');
    });

Omit the selector entirely and pass the closure directly to target the first form:

$this->get('/some-route')
    ->assertFormExists(function (AssertForm $form) {
        $form->hasAction('/logout')->hasMethod('post');
    });

CSRF tokens and method spoofing

$this->get('/some-route')
    ->assertFormExists(function (AssertForm $form) {
        $form->hasAction('/update-user')
            ->hasMethod('post')
            ->hasCSRF()
            ->hasSpoofMethod('PUT');
    });

Any method other than GET or POST is automatically treated as a spoofed method, so this is equivalent to calling hasSpoofMethod('PUT'):

$this->get('/some-route')
    ->assertFormExists(function (AssertForm $form) {
        $form->hasMethod('PUT');
    });

Arbitrary attributes are supported too, including via magic methods:

$this->get('/some-route')
    ->assertFormExists(function (AssertForm $form) {
        $form->has('x-data', 'foo')
            ->hasEnctype('multipart/form-data'); // magic method
    });

Inputs, textareas, and buttons

$this->get('/some-route')
    ->assertFormExists(function (AssertForm $form) {
        $form->containsInput(['name' => 'first_name', 'value' => 'Gunnar'])
            ->containsTextarea(['name' => 'comment', 'value' => '...']);
    });

You can also assert on arbitrary children, or their absence:

$this->get('/some-route')
    ->assertFormExists(function (AssertForm $form) {
        $form->contains('label', ['for' => 'username'])
            ->containsButton(['type' => 'submit']) // magic method
            ->doesntContain('label', ['for' => 'password']);
    });

Asserting on selects

findSelect() takes a selector and a closure that receives an AssertSelect instance. Assert on the select's own attributes:

$this->get('/some-route')
    ->assertFormExists(function (AssertForm $form) {
        $form->findSelect('select:nth-of-type(2)', function (AssertSelect $select) {
            $select->has('name', 'country');
        });
    });

Assert on its options. Check one at a time with containsOption(), or several at once with containsOptions():

$this->get('/some-route')
    ->assertFormExists(function (AssertForm $form) {
        $form->findSelect('select:nth-of-type(2)', function (AssertSelect $select) {
            $select->containsOption([
                'x-data' => 'none',
                'value'  => 'none',
                'text'   => 'None',
            ])->containsOptions(
                ['value' => 'dk', 'text' => 'Denmark'],
                ['value' => 'us', 'text' => 'USA'],
            );
        });
    });

Assert on the selected value, or on multiple selected values for a multi-select:

$this->get('/some-route')
    ->assertFormExists('#form1', function (AssertForm $form) {
        $form->findSelect('select', function (AssertSelect $select) {
            $select->hasValue('da');
            $select->hasValues(['da', 'en']);
        });
    });

Asserting on datalists

Datalists work like selects via findDatalist(), which provides an AssertDatalist instance. The selector must be either datalist or an id such as #skills:

$this->get('/some-route')
    ->assertFormExists('#form1', function (AssertForm $form) {
        $form->findDatalist('#skills', function (AssertDatalist $list) {
            $list->containsOptions(
                ['value' => 'PHP'],
                ['value' => 'Javascript'],
            );
        });
    });

Asserting on text

containsText() and doesntContainText() assert against an element's text content:

$this->get('/some-route')
    ->assertElementExists('#overview', function (AssertElement $assert) {
        $assert->containsText('Hello World');
    });

For the common case of asserting a single element contains some text, assertElementContainsText() is a shorthand that skips the closure:

$this->get('/some-route')
    ->assertElementContainsText('#overview', 'Hello World')
    ->assertElementContainsText('#overview', 'hello world', ignoreCase: true);

It selects the element (failing if the selector matches nothing), asserts its text contains the needle, and returns the response so calls can be chained. It takes the same $ignoreCase and $normalizeWhitespace arguments as containsText():

$this->get('/some-route')
    ->assertElementContainsText('#overview', 'Hello World', normalizeWhitespace: true);

Whitespace normalisation

By default these comparisons match text exactly as it appears in the DOM. Templates often introduce a lot of incidental whitespace, such as indented Blade, multi-line content, or \r\n line endings, so you can collapse and trim it instead.

Enable it for a single call:

$assert->containsText('Hello World', ignoreCase: false, normalizeWhitespace: true);

Enable it globally from TestCase::setUp() or AppServiceProvider::boot():

config()->set('dom-assertions.normalize_whitespace', true);

Or publish the config file if you prefer:

php artisan vendor:publish --tag=dom-assertions-config

This creates config/dom-assertions.php:

return [
    /*
    | When enabled, text comparisons performed by `containsText` and
    | `doesntContainText` will collapse consecutive whitespace and trim
    | vertical whitespace from both the needle and haystack by default.
    |
    | This can still be overridden per-call by passing an explicit boolean
    | as the third argument to those assertions.
    */
    'normalize_whitespace' => true,
];

When normalizeWhitespace is left as null, it falls back to this config value. With the global default on, pass normalizeWhitespace: false to force strict matching for a single assertion.

Quick existence checks

For simple checks where a full closure is overkill, use assertContainsElement() and assertDoesntExist() directly on the response. assertContainsElement() optionally accepts an array of expected attributes:

$this->get('/some-route')
    ->assertContainsElement('#content')
    ->assertContainsElement('div.banner', ['text' => 'Successfully deleted', 'data-status' => 'success'])
    ->assertDoesntExist('div.not-here');

When a check fails, chain ddContent() to dump the parsed page and see what was actually rendered:

$this->blade('<x-some-blade>')
    ->assertContainsElement('#content')
    ->ddContent();

Tip

These methods are shared across the response, view, and component macros, so they are available anywhere this package can be used.

Usage with Livewire

Livewire's testing helpers return Laravel's TestResponse, so everything works without any changes:

Livewire::test(UserForm::class)
    ->assertElementExists('form', function (AssertElement $form) {
        $form->find('#submit', function (AssertElement $assert) {
            $assert->is('button');
            $assert->has('text', 'Submit');
        })->contains('[wire\:model="name"]', 1);
    });

Usage with Blade views and components

Test a Blade view directly:

$this->view('navigation')
    ->assertElementExists('nav > ul', function (AssertElement $ul) {
        $ul->contains('li', ['class' => 'active']);
    });

Or a Blade component:

$this->component(Navigation::class)
    ->assertElementExists('nav > ul', function (AssertElement $ul) {
        $ul->contains('li', ['class' => 'active']);
    });

Method reference

Element methods (AssertElement)

Method Description
is($type) Assert the element is of a given type (div, span, etc).
isDiv() Magic method. Same as is('div').
has($attribute, $value = null) Assert the element has an attribute, optionally with a given value.
hasXData('foo') Magic method. Same as has('x-data', 'foo').
doesntHave($attribute, $value = null) Assert the element does not have the attribute/value.
contains($selector, $attributes = [], $count = null) Assert a child element exists, optionally with attributes and/or an exact count.
containsDiv(['class' => 'foo'], 3) Magic method. Same as contains('div', ['class' => 'foo'], 3).
doesntContain($selector, $attributes = []) Assert no matching child exists.
doesntContainDiv(['class' => 'foo']) Magic method. Same as doesntContain('div', ['class' => 'foo']).
containsText($needle, $ignoreCase = false, $normalizeWhitespace = null) Assert the element's text contains a string. $normalizeWhitespace defaults to the dom-assertions.normalize_whitespace config value when null.
doesntContainText($needle, $ignoreCase = false, $normalizeWhitespace = null) Assert the element's text does not contain a string. Same $normalizeWhitespace behaviour.
find($selector, $callback) Drill into the first matching child and receive a new AssertElement.
findDiv(fn (AssertElement $el) => ...) Magic method. Same as find('div', ...).
each($selector, $callback) Run the callback against every matching child.

Form methods (AssertForm)

Method Description
hasAction($url) Assert the form posts to a given action.
hasMethod($method) Assert the form uses a given method (non-GET/POST forwards to hasSpoofMethod).
hasSpoofMethod($method) Assert the form contains a spoofed _method field.
hasCSRF() Assert the form contains a CSRF token.
containsInput($attributes) Assert a matching <input> exists.
containsTextarea($attributes) Assert a matching <textarea> exists.
findSelect($selector, $callback) Drill into a <select> and receive an AssertSelect.
findDatalist($selector, $callback) Drill into a <datalist> and receive an AssertDatalist.

All AssertElement methods are also available on forms.

Select methods (AssertSelect)

Method Description
hasValue($value) Assert the select's selected value.
hasValues($values) Assert the selected values of a multiple select.
containsOption($attributes) Assert a single option with the given attributes exists.
containsOptions(...$attributes) Assert several options exist (one array per option).

Rector rules

This package ships Rector rules to keep your assertions consistent as the package evolves.

Rule Description
AssertElementToAssertContainsElementRule Converts verbose assertElement() closures into flat assertContainsElement() chains.

Register a rule in your rector.php:

use Rector\Config\RectorConfig;
use Sinnbeck\DomAssertions\Rector\Rules\AssertElementToAssertContainsElementRule;

return RectorConfig::configure()
    ->withRules([
        AssertElementToAssertContainsElementRule::class,
    ]);

AssertElementToAssertContainsElementRule

Converts assertElement() calls whose closures only use find, contains, containsText, or has into flat assertContainsElement() chains:

// Before
$response->assertElement('#content', function (AssertElement $element) {
    $element->find('h1', function (AssertElement $element) {
        $element->containsText('Hello World');
    });
    $element->contains('p', ['class' => 'foo']);
});

// After
$response->assertContainsElement('#content h1', ['text' => 'Hello World'])
         ->assertContainsElement('#content p', ['class' => 'foo']);

Testing this package

vendor/bin/pest

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Credits

License

The MIT License (MIT). Please see License File for more information.