Files
livewire-material/tests/Browser/CascadeTest.php
T
surtic86andClaude Opus 5 ebdc2ef2e1
tests / feature (8.4) (push) Successful in 1m50s
tests / feature (8.5) (push) Successful in 1m54s
tests / browser (chrome, chromium) (push) Successful in 7m52s
tests / browser (firefox, firefox) (push) Successful in 11m56s
tests / browser (safari, webkit) (push) Successful in 12m35s
Press once whatever a second press would open over or undo
The browser plugin runs every call on a page again when its first
attempt takes over a second, and on the runner a press can. For most
presses that costs nothing. For two kinds it breaks the test: a press
that opens a dialog, a sheet, a full-screen view or a modal rail over
its own trigger, whose second press can never land, and a press that
changes state a second one would change again — a menu trigger, a
toggle, a chip, a range picker's day, a paging key, a Save. The bottom
sheet with preset heights timed out on WebKit that way after the date
picker had on Firefox.

Every such press in the browser suite now goes through pressOnce(), 253
of them across sixteen files, not only the ones the runner happened to
catch; focus moves inside an open view, Escape, links and plain "set"
actions keep the retry, which is harmless for them.

Two samples that were still racing the machine:

- The standard side sheet's exit is caught half-way with its motion
  stretched, as every other mid-exit sample is, and fullSpeed() takes
  the stretch off again before the test times a reopen against the real
  exit. Under load on Linux WebKit it had failed two runs in five; it
  passes ten in ten.
- The switch-and-checkbox row measures its widths once, so it now waits
  for the resize to land and the brand face to load before it does.

Browser 300 passed on Firefox and WebKitGTK in a Linux container held to
two busy cores, and on Chrome, Firefox and WebKit on macOS. Feature 1159
passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 16:15:25 +02:00

187 lines
8.0 KiB
PHP

<?php
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Route;
use Livewire\Component;
use Livewire\Livewire;
/**
* The central claims of the `@layer material` design — components carry their styles as
* `data-md-*` attributes inside `@layer material` — each on its own small probe page: an
* application's unlayered rule beats a component default; `[hidden]` beats a component's
* `display`; the layer order holds even when a component file is imported before the foundation;
* a `data-md-*` attribute survives a Livewire `$refresh`; an icon mirrors in a right-to-left
* document. Every other stylesheet claim (layers, imports, breakpoints) is StylesheetsTest.php's,
* asserted on the source rather than a rendered page.
*/
function cascadeReady(mixed $page): mixed
{
return ready($page);
}
it('lets an application\'s unlayered rule beat a component default', function () {
// The brief's own example: a caller's plain, unlayered `.app-wide` rule against button.css's
// layered `padding-inline: var(--md-button-padding)` and `inline-size: var(--md-button-width)`
// — unlayered CSS outranks every `@layer`, however specific the layered rule is.
Route::middleware('web')->get('/cascade-unlayered-probe', fn () => Blade::render(<<<'BLADE'
<!DOCTYPE html>
<html>
<head>
<link rel="icon" href="data:,">
<x-theme-script />
@vite(config('livewire-material.showcase.vite'))
@livewireStyles
<style>.app-wide { inline-size: 320px; }</style>
</head>
<body style="background-color: var(--md-sys-color-surface);">
<x-button label="Save" class="app-wide" data-test="button" />
@livewireScripts
</body>
</html>
BLADE));
cascadeReady(visit('/cascade-unlayered-probe'))
->assertScript("Math.round(document.querySelector('[data-test=\"button\"]').getBoundingClientRect().width) === 320");
});
it('lets [hidden] beat a component\'s display', function () {
// The reset's `[hidden]:where(:not([hidden='until-found'])) { display: none !important }`
// sits outside every layer, so it beats a component's own `display` (button.css's `inline-flex`,
// row.css's `flex`) whatever `@layer` that rule sits in.
Route::middleware('web')->get('/cascade-hidden-probe', fn () => Blade::render(<<<'BLADE'
<!DOCTYPE html>
<html>
<head>
<link rel="icon" href="data:,">
<x-theme-script />
@vite(config('livewire-material.showcase.vite'))
@livewireStyles
</head>
<body style="background-color: var(--md-sys-color-surface);">
<x-row hidden data-test="row"><p>One</p><p>Two</p></x-row>
<x-button hidden label="Ghost" data-test="button" />
@livewireScripts
</body>
</html>
BLADE));
$page = cascadeReady(visit('/cascade-hidden-probe'));
$page->assertScript("getComputedStyle(document.querySelector('[data-test=\"row\"]')).display === 'none'")
->assertScript("getComputedStyle(document.querySelector('[data-test=\"button\"]')).display === 'none'");
});
it('keeps the layer order even when a component file is imported before the foundation', function () {
// Every package stylesheet opens with the same full `@layer` statement, so the order it
// establishes holds whichever file a page reaches first — the error layout's copy of
// it, reached here before the foundation's. The two prebuilt error-page stylesheets, in that
// deliberate order: the layout's bundle, whose button.css rules sit in `material.components`,
// physically precedes the fallback's foundation reset, in `material.reset` — and still loses
// the layer priority fight, so the button's own padding survives the reset's `padding: 0`.
$css = File::get(__DIR__.'/../../resources/dist/error-page.css')
.File::get(__DIR__.'/../../resources/dist/error-page-fallback.css');
// Served as its own response, not inlined through Blade::render(): CSS's own `@media`,
// `@layer` and `@import` would otherwise reach Blade's directive compiler along with the page.
Route::middleware('web')->get('/cascade-layer-order-probe.css', fn () => response($css, 200, ['Content-Type' => 'text/css']));
Route::middleware('web')->get('/cascade-layer-order-probe', fn () => Blade::render(<<<'BLADE'
<!DOCTYPE html>
<html>
<head>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="/cascade-layer-order-probe.css">
</head>
<body>
<x-button label="Save" data-test="button" />
</body>
</html>
BLADE));
visit('/cascade-layer-order-probe')
->assertScript("getComputedStyle(document.querySelector('[data-test=\"button\"]')).paddingInlineStart !== '0px'");
});
/**
* A Livewire component whose `data-md-variant` attribute changes on every re-render — the same
* morph a plain `wire:click="$refresh"` drives, since `$refresh` is Livewire's own name for calling
* a method that changes nothing before it re-renders.
*/
class CascadeVariantProbe extends Component
{
public string $variant = 'filled';
public function toggle(): void
{
$this->variant = $this->variant === 'filled' ? 'outlined' : 'filled';
}
public function render(): string
{
return <<<'BLADE'
<div>
<x-button :variant="$variant" label="Toggle" wire:click="toggle" data-test="button" />
</div>
BLADE;
}
}
it('keeps a data-md-* attribute through a Livewire $refresh', function () {
Livewire::component('cascade-variant-probe', CascadeVariantProbe::class);
Route::middleware('web')->get('/cascade-variant-probe', fn () => Blade::render(<<<'BLADE'
<!DOCTYPE html>
<html>
<head>
<link rel="icon" href="data:,">
<x-theme-script />
@vite(config('livewire-material.showcase.vite'))
@livewireStyles
</head>
<body style="background-color: var(--md-sys-color-surface);">
<livewire:cascade-variant-probe />
@livewireScripts
</body>
</html>
BLADE));
$button = "document.querySelector('[data-test=\"button\"]')";
$page = cascadeReady(visit('/cascade-variant-probe'))
->assertScript("{$button}.getAttribute('data-md-variant') === 'filled'");
// A second press flips the variant back: pressOnce(), tests/Pest.php.
pressOnce($page)->click('[data-test="button"]');
$page->assertScript("{$button}.getAttribute('data-md-variant') === 'outlined'");
// Twice, so the morph is proven both ways round, not just once off the initial paint.
pressOnce($page)->click('[data-test="button"]');
$page->assertScript("{$button}.getAttribute('data-md-variant') === 'filled'");
});
it('mirrors an icon in a right-to-left document, not in left-to-right', function (string $dir, string $transform) {
Route::middleware('web')->get("/cascade-icon-mirror-probe-{$dir}", fn () => Blade::render(<<<BLADE
<!DOCTYPE html>
<html dir="{$dir}">
<head>
<link rel="icon" href="data:,">
<x-theme-script />
@vite(config('livewire-material.showcase.vite'))
@livewireStyles
</head>
<body style="background-color: var(--md-sys-color-surface);">
<x-icon name="arrow_back" mirror-rtl data-test="icon" />
@livewireScripts
</body>
</html>
BLADE));
cascadeReady(visit("/cascade-icon-mirror-probe-{$dir}"))
->assertScript("getComputedStyle(document.querySelector('[data-test=\"icon\"]')).transform === '{$transform}'");
})->with([
'right-to-left, mirrored' => ['rtl', 'matrix(-1, 0, 0, 1, 0, 0)'],
'left-to-right, unmirrored' => ['ltr', 'none'],
]);