Browser-test the five central cascade claims
One probe page per claim: an application's unlayered .app-wide rule beats button.css's layered inline-size and padding; [hidden] beats a component's display on both <x-row> and <x-button>; the layer order holds when Stylesheets::bundle() serves button.css before foundation.css, so the reset's padding: 0 still loses to the button's own; a data-md-variant attribute updates correctly through the same re-render/morph a plain wire:click="$refresh" drives, toggled both ways; <x-icon mirror-rtl> mirrors under dir="rtl" and does not under dir="ltr". Chromium: 6 assertions, all green, alongside ShowcaseTest.php (5 tests, 18 assertions). Plan step 42 (Phase F), Part A. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
34b77cfd31
commit
1d23f24500
@@ -0,0 +1,180 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Blade;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Livewire\Component;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
use NoNameWeb\LivewireMaterial\Support\Stylesheets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The central claims of the `@layer material` design (plan step 42, docs/plans/material-3-alignment.md
|
||||||
|
* Decisions § "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,
|
||||||
|
* no Tailwind) is StylesheetsTest.php's, asserted on the source rather than a rendered page.
|
||||||
|
*/
|
||||||
|
function cascadeReady(mixed $page): mixed
|
||||||
|
{
|
||||||
|
return $page->waitForEvent('networkidle')
|
||||||
|
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
<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>
|
||||||
|
<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 (plan step 33), so the
|
||||||
|
// order it establishes holds whichever file a page reaches first — button.css's own copy of it,
|
||||||
|
// reached here before foundation.css's. Inlined through Stylesheets::bundle(), the same PHP
|
||||||
|
// bundler the showcase and the error page's fallback use, in that deliberate order: button.css's
|
||||||
|
// rules, in `material.components`, physically precede foundation.css's reset, in
|
||||||
|
// `material.reset` — and still lose the layer priority fight, so the button's own padding
|
||||||
|
// survives the reset's `padding: 0`.
|
||||||
|
$css = Stylesheets::bundle([__DIR__.'/../../resources/css/components/button.css'])
|
||||||
|
.Stylesheets::bundle([__DIR__.'/../../resources/css/foundation.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="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>
|
||||||
|
<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'");
|
||||||
|
|
||||||
|
$page->click('[data-test="button"]')
|
||||||
|
->assertScript("{$button}.getAttribute('data-md-variant') === 'outlined'");
|
||||||
|
|
||||||
|
// Twice, so the morph is proven both ways round, not just once off the initial paint.
|
||||||
|
$page->click('[data-test="button"]')
|
||||||
|
->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>
|
||||||
|
<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'],
|
||||||
|
]);
|
||||||
Reference in New Issue
Block a user