diff --git a/src/Assertions/Internal/AriaSnapshot.php b/src/Assertions/Internal/AriaSnapshot.php
new file mode 100644
index 0000000..3996c08
--- /dev/null
+++ b/src/Assertions/Internal/AriaSnapshot.php
@@ -0,0 +1,43 @@
+ substr($line, $shared), $lines));
+ }
+}
diff --git a/src/Assertions/LocatorAssertions.php b/src/Assertions/LocatorAssertions.php
index 28569a6..9b7bd6c 100644
--- a/src/Assertions/LocatorAssertions.php
+++ b/src/Assertions/LocatorAssertions.php
@@ -15,11 +15,74 @@
namespace Playwright\Assertions;
use Playwright\Assertions\Failure\AssertionException;
+use Playwright\Assertions\Internal\AriaSnapshot;
use Playwright\Assertions\Internal\Waiter;
use Playwright\Locator\LocatorInterface;
final class LocatorAssertions implements LocatorAssertionsInterface
{
+ /**
+ * Resolves one accessible text of the element and compares it with the
+ * expectation. `payload.kind` picks which text is resolved.
+ */
+ private const ACCESSIBLE_TEXT_JS = <<<'JS'
+ (element, payload) => {
+ const normalize = (value) => String(value ?? '').replace(/\s+/g, ' ').trim();
+ const root = element.getRootNode();
+ const scope = typeof root.getElementById === 'function' ? root : document;
+ const attribute = (name) => normalize(element.getAttribute(name));
+ const fromIdList = (list) => normalize(
+ list
+ .split(' ')
+ .map(id => scope.getElementById(id))
+ .filter(target => target !== null)
+ .map(target => target.textContent)
+ .join(' ')
+ );
+
+ const name = () => {
+ const label = attribute('aria-label');
+ if (label !== '') return label;
+
+ const labelledBy = fromIdList(attribute('aria-labelledby'));
+ if (labelledBy !== '') return labelledBy;
+
+ const labels = element.labels;
+ if (labels && labels.length > 0) {
+ return normalize(Array.from(labels, target => target.textContent).join(' '));
+ }
+
+ return normalize(element.textContent);
+ };
+
+ const description = () => {
+ const described = attribute('aria-description');
+ if (described !== '') return described;
+
+ const describedBy = fromIdList(attribute('aria-describedby'));
+ if (describedBy !== '') return describedBy;
+
+ return attribute('title');
+ };
+
+ const errorMessage = () => {
+ const flag = attribute('aria-invalid').toLowerCase();
+ const flagged = flag !== '' && flag !== 'false';
+ const failsConstraints = !!element.validity && !element.validity.valid;
+ if (!flagged && !failsConstraints) return '';
+
+ return fromIdList(attribute('aria-errormessage'));
+ };
+
+ const resolved = {name, description, errorMessage}[payload.kind]();
+ const expected = normalize(payload.expected);
+
+ return payload.ignoreCase
+ ? resolved.toLowerCase() === expected.toLowerCase()
+ : resolved === expected;
+ }
+ JS;
+
private bool $negated = false;
public function __construct(private LocatorInterface $locator)
@@ -340,6 +403,67 @@ public function toContainClass(string $expected, ?AssertionOptions $options = nu
);
}
+ public function toHaveAccessibleName(string $expected, ?AssertionOptions $options = null): self
+ {
+ return $this->assertAccessibleText(
+ 'name',
+ $expected,
+ $options,
+ sprintf('Expected locator to have accessible name "%s".', $expected),
+ sprintf('Expected locator not to have accessible name "%s".', $expected),
+ );
+ }
+
+ public function toHaveAccessibleDescription(string $expected, ?AssertionOptions $options = null): self
+ {
+ return $this->assertAccessibleText(
+ 'description',
+ $expected,
+ $options,
+ sprintf('Expected locator to have accessible description "%s".', $expected),
+ sprintf('Expected locator not to have accessible description "%s".', $expected),
+ );
+ }
+
+ public function toHaveAccessibleErrorMessage(string $expected, ?AssertionOptions $options = null): self
+ {
+ return $this->assertAccessibleText(
+ 'errorMessage',
+ $expected,
+ $options,
+ sprintf('Expected locator to have accessible error message "%s".', $expected),
+ sprintf('Expected locator not to have accessible error message "%s".', $expected),
+ );
+ }
+
+ public function toMatchAriaSnapshot(string $expected, ?AssertionOptions $options = null): self
+ {
+ $normalized = AriaSnapshot::normalize($expected);
+
+ return $this->assertState(
+ fn (): bool => AriaSnapshot::normalize($this->locator->ariaSnapshot()) === $normalized,
+ $options,
+ 'Expected locator to match the ARIA snapshot.',
+ 'Expected locator not to match the ARIA snapshot.',
+ );
+ }
+
+ private function assertAccessibleText(string $kind, string $expected, ?AssertionOptions $options, string $expectedMessage, string $negatedMessage): self
+ {
+ $payload = [
+ 'kind' => $kind,
+ 'expected' => $expected,
+ 'ignoreCase' => true === $options?->ignoreCase,
+ ];
+
+ return $this->assertState(
+ fn (): bool => true === $this->locator->evaluate(self::ACCESSIBLE_TEXT_JS, $payload),
+ $options,
+ $expectedMessage,
+ $negatedMessage,
+ );
+ }
+
/**
* @param callable(): bool $predicate
*/
diff --git a/src/Assertions/LocatorAssertionsInterface.php b/src/Assertions/LocatorAssertionsInterface.php
index 0bb1c50..8137e29 100644
--- a/src/Assertions/LocatorAssertionsInterface.php
+++ b/src/Assertions/LocatorAssertionsInterface.php
@@ -80,5 +80,43 @@ public function toHaveRole(string $role, ?AssertionOptions $options = null): sel
*/
public function toContainClass(string $expected, ?AssertionOptions $options = null): self;
+ /**
+ * Asserts the element's accessible name.
+ *
+ * Resolves the first non-empty of aria-label, aria-labelledby, the
+ * associated label element and the element's own text, which is short of
+ * the full accessible name computation. Whitespace is collapsed on both
+ * sides and AssertionOptions::$ignoreCase relaxes the comparison.
+ */
+ public function toHaveAccessibleName(string $expected, ?AssertionOptions $options = null): self;
+
+ /**
+ * Asserts the element's accessible description.
+ *
+ * Resolves the first non-empty of aria-description, aria-describedby and
+ * the title attribute, which is short of the full accessible description
+ * computation. Whitespace is collapsed on both sides and
+ * AssertionOptions::$ignoreCase relaxes the comparison.
+ */
+ public function toHaveAccessibleDescription(string $expected, ?AssertionOptions $options = null): self;
+
+ /**
+ * Asserts the error message the element exposes through aria-errormessage.
+ *
+ * Resolves to an empty string unless the element is flagged invalid by
+ * aria-invalid or fails constraint validation. Whitespace is collapsed on
+ * both sides and AssertionOptions::$ignoreCase relaxes the comparison.
+ */
+ public function toHaveAccessibleErrorMessage(string $expected, ?AssertionOptions $options = null): self;
+
+ /**
+ * Asserts the element's ARIA snapshot equals the expectation.
+ *
+ * Compares the YAML of LocatorInterface::ariaSnapshot() as text, ignoring
+ * blank lines, trailing whitespace and the indentation shared by every
+ * line. The expectation describes the whole subtree, not a subset of it.
+ */
+ public function toMatchAriaSnapshot(string $expected, ?AssertionOptions $options = null): self;
+
public function not(): self;
}
diff --git a/src/Assertions/PageAssertions.php b/src/Assertions/PageAssertions.php
index 18d52ad..c4252fc 100644
--- a/src/Assertions/PageAssertions.php
+++ b/src/Assertions/PageAssertions.php
@@ -15,6 +15,7 @@
namespace Playwright\Assertions;
use Playwright\Assertions\Failure\AssertionException;
+use Playwright\Assertions\Internal\AriaSnapshot;
use Playwright\Assertions\Internal\Waiter;
use Playwright\Page\PageInterface;
@@ -92,4 +93,50 @@ public function toHaveURL(string|\Stringable $expected, ?AssertionOptions $optio
return $this;
}
+
+ public function toMatchAriaSnapshot(string $expected, ?AssertionOptions $options = null): self
+ {
+ $normalized = AriaSnapshot::normalize($expected);
+
+ $this->assertState(
+ fn (): bool => AriaSnapshot::normalize($this->page->locator('body')->ariaSnapshot()) === $normalized,
+ $options,
+ 'Expected page to match the ARIA snapshot.',
+ 'Expected page not to match the ARIA snapshot.',
+ );
+
+ return $this;
+ }
+
+ /**
+ * @param callable(): bool $predicate
+ */
+ private function assertState(callable $predicate, ?AssertionOptions $options, string $expectedMessage, string $negatedMessage): void
+ {
+ $timeout = $options?->timeoutMs;
+ if (!is_int($timeout)) {
+ $timeout = Waiter::DEFAULT_TIMEOUT_MS;
+ }
+ $interval = $options?->intervalMs;
+ if (!is_int($interval)) {
+ $interval = 50;
+ }
+
+ $ok = true;
+ try {
+ Waiter::eventually($predicate, $timeout, $interval);
+ } catch (\Throwable) {
+ $ok = false;
+ }
+
+ $wasNegated = $this->negated;
+ if ($wasNegated) {
+ $ok = !$ok;
+ $this->negated = false;
+ }
+ if (!$ok) {
+ $message = $options instanceof AssertionOptions ? $options->message : null;
+ throw new AssertionException($message ?? ($wasNegated ? $negatedMessage : $expectedMessage));
+ }
+ }
}
diff --git a/src/Assertions/PageAssertionsInterface.php b/src/Assertions/PageAssertionsInterface.php
index 2ec39e9..48a753c 100644
--- a/src/Assertions/PageAssertionsInterface.php
+++ b/src/Assertions/PageAssertionsInterface.php
@@ -22,6 +22,17 @@ public function toHaveTitle(string|\Stringable $expected, ?AssertionOptions $opt
/** @return $this */
public function toHaveURL(string|\Stringable $expected, ?AssertionOptions $options = null): self;
+ /**
+ * Asserts the ARIA snapshot of the page body equals the expectation.
+ *
+ * Compares the YAML as text, ignoring blank lines, trailing whitespace and
+ * the indentation shared by every line. The expectation describes the whole
+ * body subtree, not a subset of it.
+ *
+ * @return $this
+ */
+ public function toMatchAriaSnapshot(string $expected, ?AssertionOptions $options = null): self;
+
/** @return $this */
public function not(): self;
}
diff --git a/tests/Integration/Assertions/LocatorAssertionsTest.php b/tests/Integration/Assertions/LocatorAssertionsTest.php
index f8633a3..73bf124 100644
--- a/tests/Integration/Assertions/LocatorAssertionsTest.php
+++ b/tests/Integration/Assertions/LocatorAssertionsTest.php
@@ -45,6 +45,21 @@ protected function setUp(): void
Explicit role
Off screen
+
+ Given name
+
+
+
+
+ Use your work address
+
+
+
+ Value is required
+
+
+
+
HTML,
]);
@@ -92,4 +107,66 @@ public function itRejectsAnElementOutsideTheViewportWithTheDefaultRatio(): void
$this->assertTrue(true);
}
+
+ #[Test]
+ public function itResolvesTheAccessibleNameFromEachSourceInOrder(): void
+ {
+ Expect::locator($this->page->locator('#aria-label-name'))->toHaveAccessibleName('Close dialog');
+ Expect::locator($this->page->locator('#labelled-by-name'))->toHaveAccessibleName('Given name');
+ Expect::locator($this->page->locator('#labelled-name'))->toHaveAccessibleName('Family name');
+ Expect::locator($this->page->locator('#text-name'))->toHaveAccessibleName('Save changes');
+
+ $this->assertTrue(true);
+ }
+
+ #[Test]
+ public function itComparesTheAccessibleNameCaseInsensitivelyOnDemand(): void
+ {
+ Expect::locator($this->page->locator('#text-name'))
+ ->toHaveAccessibleName('SAVE CHANGES', new AssertionOptions(ignoreCase: true));
+ Expect::locator($this->page->locator('#text-name'))
+ ->not()->toHaveAccessibleName('SAVE CHANGES', new AssertionOptions(timeoutMs: 0));
+
+ $this->assertTrue(true);
+ }
+
+ #[Test]
+ public function itResolvesTheAccessibleDescriptionFromEachSourceInOrder(): void
+ {
+ Expect::locator($this->page->locator('#described-attribute'))->toHaveAccessibleDescription('Inline description');
+ Expect::locator($this->page->locator('#described-by'))->toHaveAccessibleDescription('Use your work address');
+ Expect::locator($this->page->locator('#described-title'))->toHaveAccessibleDescription('Tooltip description');
+ Expect::locator($this->page->locator('#aria-label-name'))->toHaveAccessibleDescription('');
+
+ $this->assertTrue(true);
+ }
+
+ #[Test]
+ public function itReadsTheAccessibleErrorMessageOnlyFromAnInvalidElement(): void
+ {
+ Expect::locator($this->page->locator('#flagged-invalid'))->toHaveAccessibleErrorMessage('Value is required');
+ Expect::locator($this->page->locator('#constraint-invalid'))->toHaveAccessibleErrorMessage('Value is required');
+ Expect::locator($this->page->locator('#not-invalid'))->toHaveAccessibleErrorMessage('');
+ Expect::locator($this->page->locator('#not-invalid'))
+ ->not()->toHaveAccessibleErrorMessage('Value is required', new AssertionOptions(timeoutMs: 0));
+
+ $this->assertTrue(true);
+ }
+
+ #[Test]
+ public function itMatchesTheAriaSnapshotOfASubtree(): void
+ {
+ Expect::locator($this->page->locator('#snapshot'))->toMatchAriaSnapshot(<<<'YAML'
+ - list:
+ - listitem: One
+ - listitem: Two
+ YAML);
+
+ Expect::locator($this->page->locator('#snapshot'))->not()->toMatchAriaSnapshot(<<<'YAML'
+ - list:
+ - listitem: One
+ YAML, new AssertionOptions(timeoutMs: 0));
+
+ $this->assertTrue(true);
+ }
}
diff --git a/tests/Integration/Assertions/PageAssertionsTest.php b/tests/Integration/Assertions/PageAssertionsTest.php
new file mode 100644
index 0000000..00bb4be
--- /dev/null
+++ b/tests/Integration/Assertions/PageAssertionsTest.php
@@ -0,0 +1,71 @@
+setUpPlaywright();
+ $this->installRouteServer($this->page, [
+ '/index.html' => <<<'HTML'
+ Report
+
+ HTML,
+ ]);
+ $this->page->goto($this->routeUrl('/index.html'));
+ }
+
+ protected function tearDown(): void
+ {
+ $this->tearDownPlaywright();
+ }
+
+ #[Test]
+ public function itMatchesTheAriaSnapshotOfTheBody(): void
+ {
+ Expect::page($this->page)->toMatchAriaSnapshot(<<<'YAML'
+ - heading "Report" [level=1]
+ - list:
+ - listitem: One
+ - listitem: Two
+ YAML);
+
+ $this->assertTrue(true);
+ }
+
+ #[Test]
+ public function itRejectsAnAriaSnapshotThatDescribesADifferentBody(): void
+ {
+ Expect::page($this->page)->not()->toMatchAriaSnapshot(<<<'YAML'
+ - heading "Report" [level=1]
+ YAML, new AssertionOptions(timeoutMs: 0));
+
+ $this->assertTrue(true);
+ }
+}
diff --git a/tests/Unit/Assertions/Internal/AriaSnapshotTest.php b/tests/Unit/Assertions/Internal/AriaSnapshotTest.php
new file mode 100644
index 0000000..1234104
--- /dev/null
+++ b/tests/Unit/Assertions/Internal/AriaSnapshotTest.php
@@ -0,0 +1,58 @@
+assertSame(
+ "- list:\n - listitem: One",
+ AriaSnapshot::normalize("\n- list: \n\n - listitem: One\n\n")
+ );
+ }
+
+ public function testNormalizeStripsTheIndentationSharedByEveryLine(): void
+ {
+ $this->assertSame(
+ "- list:\n - listitem: One",
+ AriaSnapshot::normalize(" - list:\n - listitem: One")
+ );
+ }
+
+ public function testNormalizeKeepsIndentationThatIsNotShared(): void
+ {
+ $this->assertSame(
+ "- list:\n - listitem: One\n- heading \"Two\"",
+ AriaSnapshot::normalize("- list:\n - listitem: One\n- heading \"Two\"")
+ );
+ }
+
+ public function testNormalizeAcceptsWindowsAndClassicMacLineEndings(): void
+ {
+ $this->assertSame("- list:\n - listitem: One", AriaSnapshot::normalize("- list:\r\n - listitem: One"));
+ $this->assertSame("- list:\n - listitem: One", AriaSnapshot::normalize("- list:\r - listitem: One"));
+ }
+
+ public function testNormalizeReturnsAnEmptyStringForABlankSnapshot(): void
+ {
+ $this->assertSame('', AriaSnapshot::normalize(" \n\n"));
+ }
+}
diff --git a/tests/Unit/Assertions/LocatorAssertionsTest.php b/tests/Unit/Assertions/LocatorAssertionsTest.php
index 22c692e..ef540fd 100644
--- a/tests/Unit/Assertions/LocatorAssertionsTest.php
+++ b/tests/Unit/Assertions/LocatorAssertionsTest.php
@@ -141,6 +141,123 @@ public function testToContainClassRejectsAnEmptyClassList(): void
(new LocatorAssertions($this->createMock(LocatorInterface::class)))->toContainClass(' ');
}
+ public function testToHaveAccessibleNameSendsTheExpectationToTheBrowser(): void
+ {
+ $locator = $this->createMock(LocatorInterface::class);
+ $locator->expects($this->once())
+ ->method('evaluate')
+ ->with($this->isType('string'), ['kind' => 'name', 'expected' => 'Full name', 'ignoreCase' => false])
+ ->willReturn(true);
+
+ $this->assertInstanceOf(LocatorAssertions::class, (new LocatorAssertions($locator))->toHaveAccessibleName('Full name'));
+ }
+
+ public function testToHaveAccessibleNameForwardsTheIgnoreCaseOption(): void
+ {
+ $locator = $this->createMock(LocatorInterface::class);
+ $locator->expects($this->once())
+ ->method('evaluate')
+ ->with($this->isType('string'), ['kind' => 'name', 'expected' => 'FULL NAME', 'ignoreCase' => true])
+ ->willReturn(true);
+
+ $this->assertInstanceOf(
+ LocatorAssertions::class,
+ (new LocatorAssertions($locator))->toHaveAccessibleName('FULL NAME', new AssertionOptions(ignoreCase: true))
+ );
+ }
+
+ public function testToHaveAccessibleNameFailsWithADescriptiveMessage(): void
+ {
+ $locator = $this->createMock(LocatorInterface::class);
+ $locator->method('evaluate')->willReturn(false);
+
+ $this->expectException(AssertionException::class);
+ $this->expectExceptionMessage('Expected locator to have accessible name "Full name".');
+
+ (new LocatorAssertions($locator))->toHaveAccessibleName('Full name', new AssertionOptions(timeoutMs: 0));
+ }
+
+ public function testToHaveAccessibleDescriptionSelectsTheDescriptionResolver(): void
+ {
+ $locator = $this->createMock(LocatorInterface::class);
+ $locator->expects($this->once())
+ ->method('evaluate')
+ ->with($this->isType('string'), ['kind' => 'description', 'expected' => 'Hint', 'ignoreCase' => false])
+ ->willReturn(true);
+
+ $this->assertInstanceOf(LocatorAssertions::class, (new LocatorAssertions($locator))->toHaveAccessibleDescription('Hint'));
+ }
+
+ public function testToHaveAccessibleDescriptionFailsWithADescriptiveMessage(): void
+ {
+ $locator = $this->createMock(LocatorInterface::class);
+ $locator->method('evaluate')->willReturn(false);
+
+ $this->expectException(AssertionException::class);
+ $this->expectExceptionMessage('Expected locator to have accessible description "Hint".');
+
+ (new LocatorAssertions($locator))->toHaveAccessibleDescription('Hint', new AssertionOptions(timeoutMs: 0));
+ }
+
+ public function testToHaveAccessibleErrorMessageSelectsTheErrorMessageResolver(): void
+ {
+ $locator = $this->createMock(LocatorInterface::class);
+ $locator->expects($this->once())
+ ->method('evaluate')
+ ->with($this->isType('string'), ['kind' => 'errorMessage', 'expected' => 'Required', 'ignoreCase' => false])
+ ->willReturn(true);
+
+ $this->assertInstanceOf(LocatorAssertions::class, (new LocatorAssertions($locator))->toHaveAccessibleErrorMessage('Required'));
+ }
+
+ public function testToHaveAccessibleErrorMessageIsNegatable(): void
+ {
+ $locator = $this->createMock(LocatorInterface::class);
+ $locator->expects($this->once())->method('evaluate')->willReturn(false);
+
+ $this->assertInstanceOf(
+ LocatorAssertions::class,
+ (new LocatorAssertions($locator))->not()->toHaveAccessibleErrorMessage('Required', new AssertionOptions(timeoutMs: 0))
+ );
+ }
+
+ public function testToMatchAriaSnapshotIgnoresIndentationAndBlankLines(): void
+ {
+ $locator = $this->createMock(LocatorInterface::class);
+ $locator->expects($this->once())->method('ariaSnapshot')->willReturn("- list:\n - listitem: One\n");
+
+ $expected = <<<'YAML'
+
+ - list:
+ - listitem: One
+
+ YAML;
+
+ $this->assertInstanceOf(LocatorAssertions::class, (new LocatorAssertions($locator))->toMatchAriaSnapshot($expected));
+ }
+
+ public function testToMatchAriaSnapshotFailsOnADifferentTree(): void
+ {
+ $locator = $this->createMock(LocatorInterface::class);
+ $locator->method('ariaSnapshot')->willReturn('- list:');
+
+ $this->expectException(AssertionException::class);
+ $this->expectExceptionMessage('Expected locator to match the ARIA snapshot.');
+
+ (new LocatorAssertions($locator))->toMatchAriaSnapshot('- heading "One"', new AssertionOptions(timeoutMs: 0));
+ }
+
+ public function testToMatchAriaSnapshotIsNegatable(): void
+ {
+ $locator = $this->createMock(LocatorInterface::class);
+ $locator->expects($this->once())->method('ariaSnapshot')->willReturn('- list:');
+
+ $this->assertInstanceOf(
+ LocatorAssertions::class,
+ (new LocatorAssertions($locator))->not()->toMatchAriaSnapshot('- heading "One"', new AssertionOptions(timeoutMs: 0))
+ );
+ }
+
public function testToBeInViewportDefaultsToAnyVisiblePart(): void
{
$locator = $this->createMock(LocatorInterface::class);
diff --git a/tests/Unit/Assertions/PageAssertionsTest.php b/tests/Unit/Assertions/PageAssertionsTest.php
new file mode 100644
index 0000000..93572ad
--- /dev/null
+++ b/tests/Unit/Assertions/PageAssertionsTest.php
@@ -0,0 +1,88 @@
+createMock(LocatorInterface::class);
+ $locator->expects($this->once())->method('ariaSnapshot')->willReturn("- heading \"One\" [level=1]\n");
+
+ $page = $this->createMock(PageInterface::class);
+ $page->expects($this->once())->method('locator')->with('body')->willReturn($locator);
+
+ $assertions = new PageAssertions($page);
+
+ $this->assertSame($assertions, $assertions->toMatchAriaSnapshot('- heading "One" [level=1]'));
+ }
+
+ public function testToMatchAriaSnapshotFailsOnADifferentTree(): void
+ {
+ $assertions = new PageAssertions($this->pageSnapshotting('- list:'));
+
+ $this->expectException(AssertionException::class);
+ $this->expectExceptionMessage('Expected page to match the ARIA snapshot.');
+
+ $assertions->toMatchAriaSnapshot('- heading "One"', new AssertionOptions(timeoutMs: 0));
+ }
+
+ public function testToMatchAriaSnapshotUsesTheConfiguredFailureMessage(): void
+ {
+ $assertions = new PageAssertions($this->pageSnapshotting('- list:'));
+
+ $this->expectException(AssertionException::class);
+ $this->expectExceptionMessage('No heading on the page.');
+
+ $assertions->toMatchAriaSnapshot('- heading "One"', new AssertionOptions(timeoutMs: 0, message: 'No heading on the page.'));
+ }
+
+ public function testNegatedToMatchAriaSnapshotFailsOnAMatchingTree(): void
+ {
+ $assertions = new PageAssertions($this->pageSnapshotting('- list:'));
+
+ $this->expectException(AssertionException::class);
+ $this->expectExceptionMessage('Expected page not to match the ARIA snapshot.');
+
+ $assertions->not()->toMatchAriaSnapshot('- list:', new AssertionOptions(timeoutMs: 0));
+ }
+
+ public function testNegatedToMatchAriaSnapshotPassesOnADifferentTree(): void
+ {
+ $assertions = new PageAssertions($this->pageSnapshotting('- list:'));
+
+ $this->assertSame($assertions, $assertions->not()->toMatchAriaSnapshot('- heading "One"', new AssertionOptions(timeoutMs: 0)));
+ }
+
+ private function pageSnapshotting(string $snapshot): PageInterface
+ {
+ $locator = $this->createMock(LocatorInterface::class);
+ $locator->method('ariaSnapshot')->willReturn($snapshot);
+
+ $page = $this->createMock(PageInterface::class);
+ $page->method('locator')->willReturn($locator);
+
+ return $page;
+ }
+}