Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Phpunit Dom Assertions Laravel Package

phpunit/phpunit-dom-assertions

Adds expressive DOM assertions to PHPUnit for testing HTML and XML output, letting you assert elements, attributes, text content, and structure using a fluent, readable API built on Symfony's DomCrawler and CssSelector components.

Deep Wiki
Context7

Getting Started

composer require --dev phpunit/phpunit-dom-assertions

Before writing a single assertion, do three things:

  1. Read composer.json in the package. Check the phpunit/phpunit constraint and the PHP version constraint. Because this is a "drop-in replacement for deprecated DOM assertions", the PHPUnit major you are on (10.x, 11.x, 12.x) determines whether the package even installs. If your app is still on PHPUnit 9, the built-in assertSelectCount / assertSelectEquals / assertSelectRegExp may still exist and you do not need this package.
  2. Open src/ and identify the integration surface. It will be one of: a Trait you use inside your TestCase, a base TestCase you extend, or static methods added to an Assert-like helper. Everything else in this assessment depends on which of those it is — confirm the exact names before copying snippets below.
  3. Find the deprecation mapping in the README. The package exists to preserve the old argument order and semantics, so the signatures should mirror legacy PHPUnit:
$this->assertSelectCount('table tbody tr', 3, $html);
$this->assertSelectEquals('h1', 'Welcome', true, $html);
$this->assertSelectRegExp('li.item', '/^Item \d+$/', 3, $html);

First use case: a feature test that hits a Blade-rendered route and asserts the shape of the resulting table.

$response = $this->get('/orders');
$response->assertOk();

$this->assertSelectCount('table#orders tbody tr', 5, $response->getContent());
$this->assertSelectEquals('table#orders caption', 'Recent orders', true, $response->getContent());

That is the 80% case. If that works, the package is doing its job.

Implementation Patterns

Wire it once, in the base TestCase. If it ships a trait, put it in tests/TestCase.php (or a shared tests/Concerns/InteractsWithDom.php) so every test can use it without per-file boilerplate. Keep the use statement adjacent to RefreshDatabase / WithoutMiddleware so it's obvious where DOM assertions come from.

Assert against rendered content, not the response object. The assertions take a string. Always pass $response->getContent() (or $response->getContent() after assertSee sanity checks) rather than relying on any implicit global. This keeps failure output predictable.

Use fixture HTML for pure-parsing tests. For complex selectors, put an .html fixture under tests/Fixtures/ and load it with file_get_contents(). This decouples DOM-assertion logic from routing/auth/middleware noise and makes debugging selector problems trivial.

Drive selectors with data providers. The legacy API takes the selector as the first argument, which makes it a natural fit for a dataset:

#[DataProvider('navSelectors')]
public function testNavigation(string $selector, int $count): void
{
    $html = $this->get('/')->getContent();
    $this->assertSelectCount($selector, $count, $html);
}

public static function navSelectors(): array
{
    return [
        'top level links' => ['nav > ul > li', 4],
        'active item'     => ['nav li.active', 1],
    ];
}

Wrap repeated page structures in helpers. Because selectors are strings, they rot silently. Centralize them:

private function ordersTable(): string
{
    return 'table#orders tbody tr';
}

Fall back to XPath for anything CSS cannot express. CSS selector engines (usually Symfony's CssSelector) do not support :has(), sibling combinators across text nodes, or contains(text(), ...). If the package supports a raw XPath mode, use it for "element whose text contains X" cases; otherwise assert on a narrower selector plus assertSelectRegExp.

Pair with PHPUnit's own assertions, don't replace them. Use assertSee / assertSeeText for "the page mentions this string" and DOM selectors for "this string appears in this structural position". Mixing both gives better failure messages than DOM assertions alone.

Gotchas and Tips

It is explicitly work-in-progress. Treat the API as unstable: pin an exact version in composer.json ("phpunit/phpunit-dom-assertions": "x.y.z", no caret), and re-read the changelog before upgrading. A patch release here can plausibly change method signatures.

NOASSERTION license is a real blocker for many teams. The repository does not declare a recognized SPDX license. Run this past legal before it lands in a codebase you ship; even dev-only dependencies often get flagged by license scanners.

The legacy argument order is the biggest trap. The deprecated PHPUnit methods were inconsistent:

  • assertSelectCount($selector, $count, $actual, $message = '') — count second.
  • assertSelectEquals($selector, $content, $count, $actual, $message = '') — content second, count third.
  • assertSelectRegExp($selector, $pattern, $count, $actual, $message = '').

It is very easy to swap true and 1 or to pass the HTML as the count. Confirm each signature against src/ before trusting it, and never "fix" a failing assertion by shuffling arguments until it passes.

$count = true means "at least one", $count = false means "none", an integer means exactly N. This is legacy behaviour and surprises people. Use true/false deliberately, not as a sloppy 1/0.

Whitespace and entities will bite you. assertSelectEquals compares text content as DOMDocument sees it, so &nbsp;, &amp;, and indentation inside <td> can all produce mismatches. Prefer regex assertions with \s+ where the template output is formatted, or assert on a data-* attribute instead of text.

HTML5 parsing is lossy. DOMDocument is HTML4-era. <main>, <section>, <template>, and unclosed void elements get mangled or emitted with warnings. If the package lets you inject a DOMDocument, call libxml_use_internal_errors(true) first and prepend <meta charset="utf-8"> to your HTML string so UTF-8 text doesn't corrupt.

Watch for PHPUnit namespace collisions. If you are on a PHPUnit version where some assertSelect* methods still exist, the trait/base class may conflict with, or silently shadow, the built-in ones. Check whether the package's methods are guarded by method-existence checks; if not, upgrade PHPUnit past the removal point rather than mixing both.

Static analysis won't see trait methods by default. PHPStan and Psalm will report assertSelectCount as undefined if it arrives via a trait in your TestCase. Either add a stub, or (better) declare the methods in a tests/Concerns interface and @method annotate the base class.

Debugging recipe: when a selector fails, isolate it — dump $html to a temp file, open it in a browser, and run document.querySelectorAll(selector) in the console. If the browser finds elements and the assertion doesn't, the problem is the selector engine's CSS subset (likely missing :has, :nth-child edge cases, or attribute case-sensitivity), not your markup.

Extension point to know about: if the package exposes the underlying selector-to-XPath converter, use it in a single debug helper so you can print the generated XPath in failure output. That one helper will save more time than any other piece of glue code you add around this package.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package