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>
210 lines
11 KiB
PHP
210 lines
11 KiB
PHP
<?php
|
|
|
|
use Illuminate\Support\Facades\File;
|
|
use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
|
|
use NoNameWeb\LivewireMaterial\Tests\Support\ViewClasses;
|
|
|
|
/**
|
|
* Every component and layout-shared stylesheet: each view renders `data-md-*` attributes and no
|
|
* class list of its own beyond the interaction and text classes (tests/Support/ViewClasses.php),
|
|
* writes its values from the tokens and its breakpoints as px range queries, and is imported from
|
|
* its own block of all.css. This used to be four near-copies of the same checks, one per group
|
|
* (Action/Containment/Input/Navigation), plus a fifth in ErrorPagesTest.php for the error layout —
|
|
* folded into this one file (plan cleanup) because the checks never differed in substance, only in
|
|
* which names they ran over and, for the token check, which box-shadow shape a group's own values
|
|
* may take (see $boxShadow below). `icon` and `shape` (no group's view renders them exclusively)
|
|
* and `color` (its own ColorTest.php already covers it, the shared colour-role table every
|
|
* hue-taking component imports) keep their separate treatment.
|
|
*
|
|
* Four names in the dataset are not a component view of their own: `selection` (what checkbox,
|
|
* radio and toggle share), `navigation-item` (what the bar and the rail's items share), `pagination`
|
|
* (four views under resources/views/pagination/, not resources/views/components/) and `error-page`
|
|
* (the framework's error layout, not a component at all). The stylesheet-only checks below (shape,
|
|
* tokens, block import) still run on all four; the view-scoped ones (class list, element-wide
|
|
* selector scoping) run only where the original group already ran them — pagination's own views get
|
|
* their own small dataset further down, and error-page's view-scoped checks stay in
|
|
* ErrorPagesTest.php, which reads a view outside resources/views/components/.
|
|
*/
|
|
function componentStylesheets(): array
|
|
{
|
|
$entries = [];
|
|
|
|
foreach (['icon', 'shape'] as $name) {
|
|
$entries[$name] = ['Foundation components', 'loose'];
|
|
}
|
|
|
|
foreach ([
|
|
'loading', 'tooltip', 'badge', 'button', 'group', 'button-group', 'split-button', 'fab',
|
|
'menu-separator', 'menu-group', 'menu-item', 'menu', 'fab-menu-item', 'fab-menu',
|
|
'rich-tooltip', 'toast', 'progress', 'alert', 'stat', 'empty-state',
|
|
] as $name) {
|
|
$entries[$name] = ['Actions and communication', 'loose'];
|
|
}
|
|
|
|
foreach ([
|
|
'divider', 'collapse', 'card', 'list', 'list-item', 'modal', 'drawer', 'bottom-sheet',
|
|
'carousel-item', 'carousel',
|
|
] as $name) {
|
|
$entries[$name] = ['Containment', 'ring'];
|
|
}
|
|
|
|
$entries['error-page'] = ['Containment', 'none'];
|
|
|
|
foreach ([
|
|
'form', 'field', 'input', 'password', 'textarea', 'select', 'file', 'checkbox', 'radio',
|
|
'toggle', 'chip', 'chip-set', 'choices', 'slider', 'search', 'table', 'sort-header',
|
|
'datepicker', 'timepicker', 'selection', 'pagination',
|
|
] as $name) {
|
|
$entries[$name] = ['Inputs, selection and data', 'ring'];
|
|
}
|
|
|
|
foreach ([
|
|
'app-bar', 'toolbar', 'tabs', 'navigation-bar', 'navigation-bar-item', 'navigation-rail',
|
|
'navigation-rail-item', 'navigation-rail-section', 'section-nav', 'account-menu',
|
|
'theme-toggle', 'scheme-picker', 'navigation-item',
|
|
] as $name) {
|
|
$entries[$name] = ['Navigation', 'ring'];
|
|
}
|
|
|
|
return $entries;
|
|
}
|
|
|
|
dataset('component stylesheets', function (): Generator {
|
|
foreach (componentStylesheets() as $name => [$block, $boxShadow]) {
|
|
yield $name => [$name, $block, $boxShadow];
|
|
}
|
|
});
|
|
|
|
// The names with exactly one view of their own at resources/views/components/<name>.blade.php —
|
|
// every dataset entry above but the four stylesheet-only names (see the file header) — for the two
|
|
// checks that read that view. `tabs` renders two (tabs.blade.php and tab.blade.php).
|
|
dataset('component views', array_values(array_diff(array_keys(componentStylesheets()), ['selection', 'navigation-item', 'pagination', 'error-page'])));
|
|
|
|
/**
|
|
* The names whose stylesheet travels in every page's bundle (all.css), so a bare `html`, `body`,
|
|
* `dialog` or `:root` rule would restyle an application's own pages — containment and navigation,
|
|
* the two groups whose original tests carried this check. `[data-md-` is the exception in both: a
|
|
* rule that only applies while a package component is on the page at all. `:has([data-md-` needs
|
|
* no `>` combinator only in navigation's own stylesheets (toolbar.css's
|
|
* `:root:has([data-md-toolbar-place=…])` has none) — containment keeps the strict, `>`-only form
|
|
* its own original test held it to.
|
|
*/
|
|
dataset('scoped selectors', function (): Generator {
|
|
$names = [
|
|
...array_fill_keys(['divider', 'collapse', 'card', 'list', 'list-item', 'modal', 'drawer', 'bottom-sheet', 'carousel-item', 'carousel'], true),
|
|
...array_fill_keys(['app-bar', 'toolbar', 'tabs', 'navigation-bar', 'navigation-bar-item', 'navigation-rail', 'navigation-rail-item', 'navigation-rail-section', 'section-nav', 'account-menu', 'theme-toggle', 'scheme-picker', 'navigation-item'], false),
|
|
];
|
|
|
|
foreach ($names as $name => $strict) {
|
|
yield $name => [$name, $strict];
|
|
}
|
|
});
|
|
|
|
it('draws the component from a stylesheet shaped like every package stylesheet', function (string $name) {
|
|
$css = ComponentStylesheet::read($name);
|
|
|
|
// The header, the layer statement and the plain imports are StylesheetsTest's, checked over
|
|
// every file all.css reaches; only the block layer and the imports' existence are this
|
|
// dataset's own to check.
|
|
expect($css->blocks())->each->toBe('@layer material.components');
|
|
|
|
foreach ($css->imports() as $import) {
|
|
expect(is_file(dirname(ComponentStylesheet::path($name)).'/'.$import))->toBeTrue("{$name}.css imports {$import}, which does not exist");
|
|
}
|
|
})->with('component stylesheets');
|
|
|
|
it('writes no class list into the view but the interaction and text classes', function (string $name) {
|
|
// `tabs` renders two views of its own, tabs.blade.php and tab.blade.php; every other name here
|
|
// has exactly one.
|
|
foreach (($name === 'tabs' ? ['tabs', 'tab'] : [$name]) as $view) {
|
|
expect(ViewClasses::violations(File::get(__DIR__."/../../../resources/views/components/{$view}.blade.php")))->toBe([]);
|
|
}
|
|
})->with('component views');
|
|
|
|
/**
|
|
* `$boxShadow` is the shape a stylesheet's own box-shadow values may take: 'loose' for actions and
|
|
* communication (a component paints its own elevation through a local custom property, e.g.
|
|
* button.css's `--md-button-elevation`, not `--md-sys-elevation-N` directly, so any `var(--md-*)`
|
|
* passes); 'ring' for containment, inputs/selection/data and navigation (`none`, an elevation level,
|
|
* or a literal ring — `[inset] 0 0 0 <n>px` in a colour role, a focus or a selected day); 'none' for
|
|
* the error layout, which draws no box-shadow of its own.
|
|
*
|
|
* Breakpoints (px, at one of M3's four) are StylesheetsTest's, checked over every file all.css
|
|
* reaches. field.css's autofill workaround (`transition: background-color 604800s, color
|
|
* 604800s`) delays Chrome's autofill background for a week — not a spring, and not meant to be
|
|
* one — so it is stripped before the transition check.
|
|
*/
|
|
it('takes its values from the tokens', function (string $name, string $block, string $boxShadow) {
|
|
$label = "{$name}.css";
|
|
$source = (string) preg_replace('~/\*.*?\*/~s', '', ComponentStylesheet::read($name)->css);
|
|
|
|
if ($label === 'field.css') {
|
|
$source = str_replace('transition: background-color 604800s, color 604800s;', '', $source);
|
|
}
|
|
|
|
expect($source)->not->toMatch('/#[0-9a-f]{3,8}\b|\b(?:rgba?|hsla?|oklch|oklab|lab|lch)\(/i')
|
|
->not->toMatch('/\bfont:(?!\s*var\(--md-sys-typescale-)/')
|
|
->not->toMatch('/\btransition[a-z-]*:[^;]*(?:\d+m?s\b|\bease\b|ease-in|ease-out|cubic-bezier)/');
|
|
|
|
if ($boxShadow === 'loose') {
|
|
expect($source)->not->toMatch('/\bbox-shadow:(?!\s*(?:none|var\(--md-))/');
|
|
} elseif ($boxShadow === 'ring') {
|
|
$colour = 'var\(--md-sys-color-[a-z-]+\)|color-mix\(in srgb, var\(--md-sys-color-[a-z-]+\) [^;]+?, transparent\)';
|
|
|
|
preg_match_all('/\bbox-shadow:\s*([^;]+);/', $source, $shadows, PREG_SET_ORDER);
|
|
|
|
foreach ($shadows as [, $value]) {
|
|
expect(trim($value))->toMatch("/^(?:none|var\\(--md-sys-elevation-[0-5]\\)|(?:inset )?0 0 0 \\d+px (?:{$colour}))$/", "{$label}: box-shadow: {$value} is not none, an elevation or a ring in a colour role");
|
|
}
|
|
}
|
|
})->with('component stylesheets');
|
|
|
|
it('is imported from its own block of all.css', function (string $name, string $block) {
|
|
expect(allCssBlock($block))->toContain("@import './components/{$name}.css';");
|
|
})->with('component stylesheets');
|
|
|
|
it('scopes every element-wide selector to a data-md hook', function (string $name, bool $strict) {
|
|
$source = (string) preg_replace('~/\*.*?\*/~s', '', ComponentStylesheet::read($name)->css);
|
|
$has = $strict ? ':has\(> \[data-md-' : ':has\((?:> )?\[data-md-';
|
|
|
|
expect($source)->not->toMatch('/(?:^|[,{};]\s*)(?:html|body|dialog|:root)(?![\w-])(?!\[data-md-|'.$has.')/m');
|
|
})->with('scoped selectors');
|
|
|
|
// ---- pagination: its views live outside resources/views/components/ --------------------------
|
|
|
|
dataset('pagination views', [
|
|
'pagination/laravel/material',
|
|
'pagination/laravel/simple-material',
|
|
'pagination/livewire/material',
|
|
'pagination/livewire/simple-material',
|
|
]);
|
|
|
|
it('imports the stylesheet of every component pagination\'s views render', function (string $view) {
|
|
preg_match_all('/<x-livewire-material::([a-z-]+)/', File::get(__DIR__."/../../../resources/views/{$view}.blade.php"), $tags);
|
|
|
|
$rendered = collect($tags[1])->unique()
|
|
// A rendered tag that is not a components/ stylesheet at all (a layout component's, e.g.
|
|
// <x-pane>) has no import of its own to check here.
|
|
->filter(fn (string $tag): bool => is_file(ComponentStylesheet::path($tag)) && str_contains(File::get(ComponentStylesheet::path($tag)), '@layer material.components'))
|
|
->map(fn (string $tag): string => "./{$tag}.css")
|
|
->values()
|
|
->all();
|
|
|
|
expect(array_values(array_diff($rendered, ComponentStylesheet::read('pagination')->imports())))->toBe([]);
|
|
})->with('pagination views');
|
|
|
|
it('writes no class list into pagination\'s views but the interaction and text classes', function (string $view) {
|
|
expect(ViewClasses::violations(File::get(__DIR__."/../../../resources/views/{$view}.blade.php")))->toBe([]);
|
|
})->with('pagination views');
|
|
|
|
// ---- partials: fragments a component view @includes, not a component view of their own --------
|
|
|
|
dataset('partial views', array_map(
|
|
fn (string $file): string => 'partials/'.basename($file, '.blade.php'),
|
|
glob(__DIR__.'/../../../resources/views/partials/*.blade.php'),
|
|
));
|
|
|
|
it('writes no class list into a partial but the interaction and text classes', function (string $view) {
|
|
expect(ViewClasses::violations(File::get(__DIR__."/../../../resources/views/{$view}.blade.php")))->toBe([]);
|
|
})->with('partial views');
|