Files
livewire-material/tests/Pest.php
T
Andreas Reinhold / reiniandClaude Opus 5 94d6e31432
tests / feature (8.4) (push) Successful in 1m45s
tests / feature (8.5) (push) Successful in 2m0s
tests / browser (chrome, chromium) (push) Successful in 8m6s
tests / browser (firefox, firefox) (push) Failing after 12m52s
tests / browser (safari, webkit) (push) Failing after 13m1s
Catch an exit half-way on a slow machine, not only on a fast one
The browser suite passes on every engine here and fails on the runner,
which takes three times as long: each failing test triggers a close and
then samples for an in-between state — a rail part-way out, a scrim
part-way faded, a menu's exit copy part-way sunk — and a starved runner
takes its one sample after the 150-650ms exit has already finished.

So the tests that assert *that* something animates now stretch every
motion duration token to three seconds first (`slowMotion()`, beside
`ready()` in tests/Pest.php; ActionsTest's own copy of it goes). The two
polling helpers grew their budget to match: the rail panel and the sheet
slide on emphasized-accelerate, which is under 1% of its travel at a
quarter of the way through, so a 300ms window no longer reached the
threshold once the exit itself was three seconds long.

The two full-screen date picker tests waited for a resize through
click()'s own retry, which ate the whole 15s budget on Firefox; they now
wait for the new width and a settled document first. The bottom sheet's
preset test waits for its entry to finish before pressing the grip.

`hold()` in search.js reads the view's animations a frame after the
closed state, as the rail's settle() does, but one frame is not always
enough: an engine that starts them on its next tick shows none, and the
full-screen layout would end at once, mid-exit. An empty list is now
asked again on the following frame.

Feature 1159 passed. Browser 299 passed on Chrome, Firefox and WebKit,
and again on WebKit under ten spinning cores.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 23:57:43 +02:00

119 lines
4.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
use Illuminate\Support\Facades\File;
use NoNameWeb\LivewireMaterial\Tests\TestCase;
pest()->extend(TestCase::class)->in('Feature', 'Browser');
/**
* The attributes of a rendered component's tag, by name; a boolean attribute Blade renders as
* `name="name"`. Without `$tag`, it is the first tag anywhere in the rendered Blade (anchored to
* the start), and the element name comes back too, under `<` — the layout components' render
* tests, which assert what their root carries, read it this way. With `$tag` (`|`-separated for
* more than one, e.g. `'button|a'`), it is the first `<$tag>` anywhere in the rendered Blade
* instead, with no `<` key — for a component whose root is not the tag Blade renders first (a
* button or link inside a wrapper, a span), where the anchored match would read the wrong element.
*
* @return array<string, string>
*/
function layoutRoot(string $html, ?string $tag = null): array
{
if ($tag === null) {
preg_match('/^\s*<([a-z]+)\b([^>]*)>/s', $html, $match);
$root = ['<' => $match[1] ?? ''];
$attributes = $match[2] ?? '';
} else {
preg_match('/<(?:'.$tag.')\b([^>]*)>/', $html, $match);
$root = [];
$attributes = $match[1] ?? '';
}
preg_match_all('/([\w:.@-]+)(?:="([^"]*)")?/', $attributes, $pairs, PREG_SET_ORDER);
return $root + collect($pairs)->mapWithKeys(fn (array $pair): array => [$pair[1] => $pair[2] ?? ''])->all();
}
/**
* Stretches every motion duration token (resources/css/tokens/motion.css) on :root to $duration,
* so a test that has to catch an exit half-way is not racing the machine: a round trip for a
* separate script() and assertScript() already costs real time, easily as much as a real exit
* (150650ms), so a single sample taken after triggering one can just as easily land before it
* starts or after it has already finished on a loaded runner. Chain it, after the open has
* settled, immediately before the action that triggers the exit being sampled.
*
* Use this only in a test asserting *that* something animates — a part-way opacity, offset or
* transform. Never in a test asserting a duration value, which this would make wrong.
*/
function slowMotion(mixed $page, string $duration = '3s'): mixed
{
$tokens = [
'--md-sys-motion-spatial-fast-duration',
'--md-sys-motion-spatial-default-duration',
'--md-sys-motion-spatial-slow-duration',
'--md-sys-motion-effects-fast-duration',
'--md-sys-motion-effects-default-duration',
'--md-sys-motion-effects-slow-duration',
'--md-sys-motion-duration-short',
'--md-sys-motion-duration-medium',
'--md-sys-motion-duration-long',
];
$css = collect($tokens)->map(fn (string $token): string => "{$token}: {$duration} !important;")->implode(' ');
$page->script(<<<JS
document.head.insertAdjacentHTML('beforeend', '<style>:root { {$css} }</style>')
JS);
return $page;
}
/**
* The page is done loading, and Alpine and/or Livewire (whichever the page renders) have booted —
* chained after visit(), before a Browser test probe reads anything either one wires up. Most
* pages need both; one that renders no Livewire component needs Alpine alone, and vice versa.
*/
function ready(mixed $page, bool $alpine = true, bool $livewire = true): mixed
{
$checks = ["document.readyState === 'complete'"];
if ($alpine) {
$checks[] = "typeof window.Alpine !== 'undefined'";
}
if ($livewire) {
$checks[] = "typeof window.Livewire !== 'undefined'";
}
return $page->waitForEvent('networkidle')->assertScript(implode(' && ', $checks));
}
// A shared CI runner is slower than a workstation: an animation or a smooth scroll can take longer
// than the default five seconds to settle there. BROWSER_TIMEOUT (milliseconds) raises the limit.
if (($timeout = (int) getenv('BROWSER_TIMEOUT')) > 0) {
pest()->browser()->timeout($timeout);
}
/**
* The named block of resources/css/all.css — the content between its `/* Heading *\/` comment and
* the next one, or the end of the file for the last block ("Navigation"). components.css and
* layout.css grouped their imports the same way before all.css folded both together; this
* is the one place that block lookup lives now, so a stylesheet test asks "is this imported from
* the Containment block" without repeating the search in every file that needs it. It is here,
* rather than in tests/Feature/StylesheetsTest.php, because Pest.php is always loaded, whichever
* test file or path is run.
*/
function allCssBlock(string $heading): string
{
$all = File::get(__DIR__.'/../resources/css/all.css');
$marker = "/* {$heading} */";
$start = strpos($all, $marker);
if ($start === false) {
throw new RuntimeException("all.css has no `{$marker}` block.");
}
$next = strpos($all, '/*', $start + strlen($marker));
return $next === false ? substr($all, $start) : substr($all, $start, $next - $start);
}