Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Api/Concerns/MakesConsoleAssertions.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ public function assertNoAccessibilityIssues(int $level = 1): Webpage
);
}

$this->page->injectAxe();

/** @var Violations|null $violations */
$violations = Playwright::usingTimeout(max(5_000, Playwright::timeout()), fn () => $this->page->evaluate('async () => ((await window.axe.run()).violations)'));

Expand Down
8 changes: 1 addition & 7 deletions src/Playwright/InitScript.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,7 @@ final class InitScript
*/
public static function get(): string
{
$axe = (string) file_get_contents(
dirname(__DIR__, 2).'/resources/js/axe.min.js'
);

return <<<JS
$axe

return <<<'JS'
window.__pestBrowser = {
jsErrors: [],
consoleLogs: []
Expand Down
25 changes: 25 additions & 0 deletions src/Playwright/Page.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ final class Page
{
use Concerns\InteractsWithPlaywright;

/**
* The axe-core source, read on first use.
*/
private static ?string $axe = null;

/**
* Whether the page has been closed.
*/
Expand Down Expand Up @@ -483,6 +488,26 @@ public function brokenImages(): array

}

/**
* Loads axe-core into the page and its iframes, unless it is already there.
*/
public function injectAxe(): void
{
if ($this->evaluate('typeof window.axe === "undefined"') === true) {
$this->evaluate(self::$axe ??= (string) file_get_contents(
dirname(__DIR__, 2).'/resources/js/axe.min.js'
));
}

foreach ($this->locator('iframe')->all() as $iframe) {
$frame = $iframe->contentFrame();

if ($frame !== null && property_exists($frame, 'guid') && is_string($frame->guid)) {
new self($this->context, $frame->guid, $frame->guid)->injectAxe();
}
}
}

/**
* Get the JavaScript errors from the page, if any.
*
Expand Down
133 changes: 123 additions & 10 deletions tests/Browser/Webpage/AssertNoAccessibilityIssuesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,121 @@

declare(strict_types=1);

use Illuminate\Support\Facades\Route;
use Pest\Browser\Playwright\Playwright;
use PHPUnit\Framework\ExpectationFailedException;

it('asserts that there are no accessibility issues', function (): void {
Route::get('/', fn (): string => '<html lang="en"><head><title>Ok</title></head><body><main><h1>Ok</h1></main></body></html>');
Route::get('/', fn (): string => '
<!DOCTYPE html>
<html lang="en">
<head><title>Accessible</title></head>
<body>
<main>
<h1>Accessible page</h1>
<img src="data:image/gif;base64,R0lGODlhAQABAAAAACw=" alt="Logo">
</main>
</body>
</html>
');

visit('/')->assertNoAccessibilityIssues();
$page = visit('/');

$page->assertNoAccessibilityIssues();
});

it('asserts that there are accessibility issues', function (): void {
Route::get('/', fn (): string => '
<!DOCTYPE html>
<html lang="en">
<head><title>Inaccessible</title></head>
<body>
<main>
<img src="data:image/gif;base64,R0lGODlhAQABAAAAACw=">
</main>
</body>
</html>
');

$page = visit('/');

$page->assertNoAccessibilityIssues();
})->throws(ExpectationFailedException::class, 'image-alt');

it('asserts that there are accessibility issues inside an iframe', function (): void {
Route::get('/', fn (): string => '
<!DOCTYPE html>
<html lang="en">
<head><title>Main</title></head>
<body>
<main>
<h1>Main page</h1>
<iframe src="/frame" title="Embedded content"></iframe>
</main>
</body>
</html>
');

Route::get('/frame', fn (): string => '
<!DOCTYPE html>
<html lang="en">
<head><title>Frame</title></head>
<body>
<img src="data:image/gif;base64,R0lGODlhAQABAAAAACw=">
</body>
</html>
');

$page = visit('/');

$page->assertNoAccessibilityIssues();
})->throws(ExpectationFailedException::class, 'image-alt');

it('asserts that there are accessibility issues after navigating to another page', function (): void {
Route::get('/', fn (): string => '
<!DOCTYPE html>
<html lang="en">
<head><title>First</title></head>
<body>
<main>
<h1>First page</h1>
<a href="/second">Second page</a>
</main>
</body>
</html>
');

Route::get('/second', fn (): string => '
<!DOCTYPE html>
<html lang="en">
<head><title>Second</title></head>
<body>
<main>
<img src="data:image/gif;base64,R0lGODlhAQABAAAAACw=">
</main>
</body>
</html>
');

$page = visit('/');

$page->assertNoAccessibilityIssues()
->click('Second page')
->assertPathIs('/second')
->assertNoAccessibilityIssues();
})->throws(ExpectationFailedException::class, 'image-alt');

it('waits for axe as long as the configured timeout, not a fixed 5 seconds', function (): void {
/*
* axe on a large page can take longer than 5 seconds on a slow CI runner. The run used to be
* capped at a hard-coded 5_000 ms, ignoring `pest()->browser()->timeout(...)`.
* capped at a hard-coded 5_000 ms, ignoring `pest()->browser()->timeout(...)`. The page brings
* its own slow axe, which the assertion uses instead of loading axe-core.
*/
Route::get('/', fn (): string => '<html lang="en"><head><title>Slow</title></head><body><main><h1>Slow</h1></main>
<script>
window.addEventListener("load", () => {
const run = window.axe.run.bind(window.axe);
window.axe.run = async (...args) => {
await new Promise((resolve) => setTimeout(resolve, 6000));
return run(...args);
};
});
window.axe = {
run: () => new Promise((resolve) => setTimeout(() => resolve({ violations: [] }), 6000)),
};
</script></body></html>');

$previous = Playwright::timeout();
Expand All @@ -35,3 +128,23 @@
Playwright::setTimeout($previous);
}
});

it('does not load axe into the page until accessibility is asserted', function (): void {
Route::get('/', fn (): string => '
<!DOCTYPE html>
<html lang="en">
<head><title>Lazy</title></head>
<body>
<main>
<h1>Lazy page</h1>
</main>
</body>
</html>
');

$page = visit('/');

$page->assertScript('typeof window.axe', 'undefined')
->assertNoAccessibilityIssues()
->assertScript('typeof window.axe', 'object');
});
Loading