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
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>
119 lines
4.9 KiB
PHP
119 lines
4.9 KiB
PHP
<?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
|
||
* (150–650ms), 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);
|
||
}
|