ziadoz / assertable-html
Assertable HTML is an elegantly designed PHPUnit library that makes performing assertions on HTML responses from PHP and Laravel applications quick and enjoyable.
Requires
- php: ^8.4
- ext-dom: *
- ext-libxml: *
- laravel/framework: ^11.41.0
- symfony/var-dumper: ^7.1
Requires (Dev)
- laravel/pint: ^1.18
- orchestra/testbench: ^9.5
- phpunit/phpunit: ^11.3
This package is auto-updated.
Last update: 2025-05-06 19:29:48 UTC
README
Assertable HTML takes a fresh approach to testing HTML responses generated by templating engines such as Blade and Twig in PHP and Laravel web applications. It provides an elegant interface that allows developers to fluently navigate and target their HTML using modern CSS selectors, and then write effective test assertions against the results.
Key Features:
- Fluent Interface: Navigate and target HTML elements using an API similar to native JavaScript.
- Minimal Interface: Work with a stripped back API that’s focused on testing.
- Chainable Assertions: Quickly chain together multiple assertions on elements.
- Flexible Assertions: Supply callbacks for complex element assertions if the built-in ones aren’t sufficient.
- Element-Specific Assertions: Use element-specific assertions to quickly test forms and other elements (Coming Soon).
Important
There may be some breaking changes with this package until it hits v1.0.0.
Table Of Contents
🚀 Get Started
Requirements
- PHP 8.4+
- Composer
- Laravel >=11.41.0 (if applicable)
Installation
You can install the package using Composer:
composer install ziadoz/assertable-html
PHPUnit Installation
If you're using PHPUnit, simply include the trait in your test class:
<?php use PHPUnit\Framework\TestCase; use Ziadoz\AssertableHtml\Traits\AssertsHtml; use Ziadoz\AssertableHtml\Dom\AssertableDocument; class MyTest extends TestCase { use AssertsHtml; // Available methods: assertableHtml(), assertHtml(), assertHead(), assertBody(), assertElement() public function testHtml(): void { $html = <<<'HTML' <html> <body> <h1>Welcome, Archie!</h1> </body> </html> HTML; $this->assertHtml($html, function (AssertableDocument $html) { $html->querySelector('h1') ->assertTextEquals('Welcome, Archie!'); }); } }
Alternatively you can use the Ziadoz\AssertableHtml\Dom\AssertableDocument::createFromString()
and Ziadoz\AssertableHtml\Dom\AssertableDocument::createFromFile()
methods directly as needed:
public function testHtml(): void { AssertableDocument::createFromString('<p>Foo</p>', LIBXML_HTML_NOIMPLIED) ->querySelector('p') ->assertTextEquals('Foo'); }
Laravel Installation
If you're using Laravel, Assertable HTML will be automatically discovered, however you can register it manually if needed:
<?php namespace App\Providers; use Illuminate\Support\ServiceProvider; use Ziadoz\AssertableHtml\AssertableHtmlServiceProvider; class AppServiceProvider extends ServiceProvider { public function register(): void { $this->app->register(AssertableHtmlServiceProvider::class); } }
Assertable HTML adds several new methods to the TestResponse
, TestView
and TestComponent
classes in Laravel:
// Responses... // Available methods: assertableHtml(), assertHtml(), assertHead(), assertBody(), assertElement() public function testResponse(): void { /* <html> <body> <h1>Welcome, Archie!</h1> </body> </html> */ $this->get('/')->assertBody(function (AssertableElement $body) { $body->querySelector('h1') ->assertTextEquals('Welcome, Archie!'); }); }; // Views... // Available methods: assertableHtml(), assertView(), assertElement() public function testView(): void { /* <nav> <ul> <li class="nav-link">Foo<li> <li class="nav-link active-link">Bar<li> <li class="nav-link">Baz<li> <li class="nav-link">Qux<li> </ul> </nav> */ $this->view('nav')->assertView(function (AssertableDocument $div) { $div->assertTag('div'); $lis = $div->querySelectorAll('ul li') ->assertCount(4) ->assertAll(function (AssertableElement $li) { return $li->classes->contains('nav-link'); }); $lis[1]->assertClassContains('active-link'); }); } // Components... // Available methods: assertableHtml(), assertComponent(), assertElement() // Note: Only available in Laravel >= 11.41.0 public function testComponent: void { /* <form method="post" action="/foo/bar"> <input name="action" value="My New Action" class="form-input" required> <!-- ... --> </form> */ $this->component('action')->assertComponent(function (AssertableDocument $form) { $form->assertTag('form') ->assertAttributeEquals('method', 'post') ->assertAttributeEquals('action', '/foo/bar'); $form->with('input[name="name"]', function (AssertableElement $input) { $input->assertAttributeEquals('value', 'My New Action'); $input->assertAttributePresent('required'); $input->assertClassContains('form-input'); }); }); }
🔨 Usage
Basics
To start performing assertions on HTML, create an assertable document:
$document = AssertableDocument::createFromString(<<<'HTML' <ul> <li id="foo" class="foo" data-foo="foo"><strong>Foo</strong></li> <li id="bar" class="bar" data-bar="bar"><strong>Bar</strong></li> <li id="baz" class="baz" data-baz="baz"><strong>Baz</strong></li> <li id="qux" class="qux" data-qux="foo"><strong>Qux</strong></li> </ul> HTML, LIBXML_HTML_NOIMPLIED);
Now you can begin performing queries using querySelector()
and querySelectorAll()
, exactly like working with the DOM in JavaScript:
The querySelector()
method will return a Ziadoz\AssertableHtml\Dom\AssertableElement
instance containing the first matching element, which you can use to perform assertions on the element:
$element = $document->querySelector('li:first-of-type'); $element->assertIdEquals('foo'); $element->assertTextEquals('Foo');
If there are no elements matching the selector, your test will fail:
$element = $document->querySelector('foobar'); // The document doesn't contain an element matching the given selector [foobar].
Assertions are fluent, so if you prefer, you can chain then together:
$element = $document->querySelector('li:first-of-type') ->assertIdEquals('foo') ->assertTextEquals('Foo');
An assertable element has a handful of properties you can access:
echo $element->tag; // 'Foo' echo $element->html; // '<strong>Foo</strong>'' echo $element->id; // 'foo' echo $element->text; // 'Foo' echo $element->classes->toArray(); // ['foo'] echo $elements->attributes->toArray(); // ['id' => 'foo', 'class' => 'foo']
The text
, classes
and attributes
properties refer to further assertable classes:
text
:Ziadoz\AssertableHtml\Dom\AssertableText
.classes
:Ziadoz\AssertableHtml\Dom\AssertableClassesList
.attributes
:Ziadoz\AssertableHtml\Dom\AssertableAttributesList
.
// Text echo $element->text->value(); // ' Foo Bar ' echo $element->text->value(normaliseWhitespace: true) // 'Foo Bar' // Classes echo $element->classes->value(); // ' foo bar ' echo $element->classes->value(normaliseWhitespace: true); // 'foo bar' $element->classes->toArray(); // ['foo', 'bar'] $element->classes->empty(); // false $element->classes->contains('foo'); // true $element->classes->any(['foo', 'qux']); // true $element->classes->all(['foo', 'qux']); // false $element->classes->each(function (string $class, int $index) { echo $class; // 'foo' echo $index; // 0 }); $element->classes->sequence( fn (string $class, int $sequence): => $this->assertSame('foo', $class), fn (string $class, int $sequence): => $this->assertSame('bar', $class), ); // Attributes echo $element->attributes->value('data-foo'); // ' bar ' echo $element->attributes->value('data-foo', normaliseWhitespace: true); // 'bar' $element->attributes->toArray(); // ['class' => 'foo bar', 'data-foo' => 'bar'] $element->attributes->empty(); // false $element->attributes->names(); // ['class', 'data-foo'] $element->attributes->has('data-foo'); // true $element->attributes->each(function (string $attribute, ?string $value, int $index) { echo $attribute; // 'class' echo $value; // 'foo-bar' echo $index; // 0 }); $element->attributes->sequence( fn (string $attribute, ?string $value, int $sequence): => $this->assertSame('class', $attribute), fn (string $attribute, ?string $value, int $sequence): => $this->assertSame('data-foo', $attribute), );
You can perform assertions using these classes, however, in most cases the element has a proxy method that makes it more convenient to do from the element:
$element->assertClassContains('foo'); $element->assertIdEquals('foo'); $element->assertAttributEquals('foo', 'foo');
These classes can be useful when you want to perform more advanced custom assertions.
The querySelectorAll()
method returns a Ziadoz\AssertableHtml\Dom\AssertableElementsList
instance containing every matching element, which allows you to work with the matching elements as an array:
$elements = $document->querySelectorAll('ul > li'); // Access using methods... echo $elements->first()->id; // foo echo $elements->nth(1)->id // bar echo $elements->nth(2)->id // baz echo $elements->last()->id; // qux // Or regular array syntax... echo $elements[0]->id; // foo echo $elements[1]->id // bar echo $elements[2]->id // baz echo $elements[3]->id; // qux
If there are no elements matching the selector, you'll still get back an AssertableElementsList
, just in case you want to check there are no elements:
$elements->assertEmpty(); $elements->assertNotEmpty();
You can perform assertions and chain assertions on the matching elements:
$elements->assertCount(4); $elements->assertAll(function (AssertableElmeent $element) { return $element->attributes->has('class'); })->assertAny(function (AssertableElmeent $element) { return $element->classes->contains('foo'); }) $element->each(function (AssertableElement $element) { $element->assertClassContains('foo'); }); $element->sequence( fn (AssertableElement $el, int $sequence) => $el->assertTextEquals('Foo'), fn (AssertableElement $el, int $sequence) => $el->assertTextEquals('Bar'), fn (AssertableElement $el, int $sequence) => $el->assertTextEquals('Baz'), fn (AssertableElement $el, int $sequence) => $el->assertTextEquals('Qux'), ); $elements[0]->assertIdEquals('foo')->assertTextEquals('Foo'); $elements[1]->assertIdEquals('bar')->assertTextEquals('Bar'); $elements[2]->assertIdEquals('baz')->assertTextEquals('Baz'); $elements[3]->assertIdEquals('qux')->assertTextEquals('Qux');
You can also use getElementsByTagName()
or getElementById()
to query for elements if needed:
$document->getElementsByTagName('li'); $document->getElementById('bar');
Scopes
Sometimes your assertions need room to breathe. For this you can use with()
, many()
, elsewhere()
and scope()
to filter elements into a callback for better readability.
with()
: The first matching element in the current scope usingquerySelector()
.many()
: Every matching element in the current scope usingquerySelectorAll()
,elsewhere()
: The first matching element in the document scope usingquerySelector()
.scope()
: The current element.
Let's give them a try:
$document = AssertableDocument::createFromString(<<<'HTML' <div id="outer"> <div id="inner"> <div class="innermost"></div> <div class="innermost"></div> <div class="innermost"></div> </div> <div id="another-inner"> <div class="another-innermost"></div> <div class="another-innermost"></div> <div class="another-innermost"></div> </div> </div> HTML, LIBXML_HTML_NOIMPLIED); $document->with('div#inner', function (AssertableElement $inner) { $inner->assertIdEquals('inner'); $inner->many('div.innermost', function (AssertableElementsList $innerMosts) { $innerMosts->assertCount(3); }); $inner->elsewhere('div#another-inner', function (AssertableElement $anotherInner) { $anotherInner->assertIdEquals('another-inner'); }); $inner->scope(function (AssertableElement $inner) { $inner->assertIdEquals('inner'); }); });
The when()
method makes it possible to perform assertions conditionally, which can be useful when working with data providers or more complex tests:
$element->when( // Condition can be a boolean, or a callable that evaluates to a boolean... $condition, // Called when condition is true... fn (AssertableElement $element) => $element->assertTextEquals('Foo'), // Called when condition is false... fn (AssertableElement $element) => $element->assertTextEquals('Bar'), );
Assertions
Assertable HTML provides loads of assertions to help you test your HTML is exactly as expected. The majority of these assertions live on the AssertableElement
instance, and can be categorised as follows:
- Tag: `Assert the element's tag.
- Matches: Assert the element does or doesn't match a selector.
- Count: Assert the number of child elements matching a selector.
- Text: Assert the element's text.
- IDs: Assert the element's ID attribute.
- Classes: Assert the element's classes.
- Attributes: Assert the element's attributes.
Here are some examples:
$element->assertTagEquals('div'); $element->assertIdEquals('foo'); $element->assertMatchesSelector('span.foo'); $element->assertDoesntMatchSelector(':has(img)'); $element->assertElementsNotCount('ul', 0); $element->assertElementsCount('li.bullet', 3); $element->assertNumberOfElements('li.odd', '>', 42); // Supports =, !=, >, >=, < and <= comparisons. $element->assertTextContains('Welcome'); $element->assertTextDoesntContain('Foo!'); $element->assertClassContains('heading'); $element->assertClassDoesntContain('subheading'); $element->assertAttributeEquals('data-foo', 'bar'); $element->assertAttributeMissing('data-bar');
If you're using an IDE such as PhpStorm or VSCode, it should auto-complete the dozens of assertions available for you, along with their parameters.
Assertion Messages
All assertions include a final $message
parameter, which allows you to customise the failure message in your tests to your application:
$document->assertElementsCount('img.avatar', 0, 'The profile page is missing an avatar image.');
This can be useful when you need to identify test failures that are specific to your web application.
Flexible Assertions
Sometimes you have a scenario that just isn't possible to test with a built-in assertion. For those scenarios Assertable HTML provides various assertions that accept a callback. If the callback returns true
, the test will pass, otherwise it will fail:
// This is a good place to use the $element->classes, $element->attributes and $element->text properties... $element->assertElement(function (AssertableElement $element) { return ( $element->classes->contains('foo') && str_contains('-Bar-', $element->text->value()) && $element->attributes->has('data-foo'); ); });
Every assertable class has flexible assertions available, just in case:
- AssertableElement:
assertElement()
,assertText()
,assertClass()
,assertAttributes()
andassertAttribute()
. - AssertableElementsList:
assertElements()
. - AssertableClassesList:
assertClasses()
. - AssertableAttributesList:
assertAttributes()
andassertAttribute()
. - AssertableText:
assertText()
.
Element-Specific Assertions
Coming Soon
HTML Output
If you ever need to see the HTML of the element(s) you're working with, you can call dump()
and dd()
on the assertable instance:
$element->querySelector('p')->dump(); // <p>Foo</p> $element->querySelectorAll('p, span')->dump(); // <p>Foo</p> // <span>Bar</span>
You can also call getHtml()
to retrieve the HTML as a string:
echo $element->querySelector('p')->getHtml(); // <p>Foo</p>
👏 Thanks
This package wouldn't be possible without the following people and projects:
- Rachel ❤️, Archie 🐶 and Rigby 🐶
- Niels Dossche (PHP 8.4 HTML parsing API author)
- Laravel DOM Assertions for showing me the possibilities of HTML assertions
- Laravel Dusk for showing me the
with()
andelsewhere()
scoping syntax - Lexbor (the library that powers PHP 8.4's HTML parsing API)