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 */ 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(); } /** * 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 plan step 37 folded both into all.css; 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); }