tests / feature (8.4) (push) Successful in 2m0s
tests / feature (8.5) (push) Successful in 2m0s
tests / browser (chrome, chromium) (push) Failing after 8m3s
tests / browser (firefox, firefox) (push) Failing after 12m58s
tests / browser (safari, webkit) (push) Failing after 13m8s
Tailwind left the stack in 2.0.0, but the package still carried about 330 mentions of it. What the guard's Tailwind detection protected — a class that compiles to nothing — is now protected by a check that does not care where a dead class came from. DesignGuard: about 500 lines of Tailwind tables, scales, palettes and "2.0.0 replacement" hints give way to one check — a class a view or PHP file writes that neither the application's stylesheets nor the package's own declare. It catches a utility of any framework, a typo and a class whose rules were deleted alike, so it also found two classes ReStride draws nothing with. A stylesheet has to be in reach for it: the `.css` files among the scanned paths, or what the `missingStylesheets()` entry imports. The class reader no longer mistakes an array index for a class list (`$block['base']`), and it reads the array a class helper is given, where it read nothing before. The package's own three Tailwind self-guards go with it. Only their one unique check stays, as a test of its own: every `matchMedia` width in resources/js is an M3 breakpoint. The pagination views are `material.blade.php` and `simple-material.blade.php`; only Laravel's and Livewire's default theme names ever made them `tailwind`. The provider sets `Paginator`'s default views and switches `livewire.pagination_theme` to `material` when it is still Livewire's own default, so no application can forget the config; a theme an application chose, and a component's own `$paginationTheme` or `paginationView()`, still win. The rest is prose: the layer-order guidance for an application that still builds Tailwind, the Tailwind wording in the README, the Boost guidelines and the development skill, and about 25 "this used to be a Tailwind utility" comments, along with every "plan step NN" pointer into a gitignored folder. The reset keeps its credit, and NOTICE now carries it too. Feature suite 1159 passed, Chrome browser suite 299 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
179 lines
7.7 KiB
PHP
179 lines
7.7 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>
|
|
<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, 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="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'],
|
|
]);
|