Cut duplicated and speculative code across the package
An over-engineering audit of the whole tree, applied in five reviewed batches. Behaviour stays the same except where UPGRADE.md says otherwise. PHP: the showcase and error-page stylesheets are prebuilt into resources/dist by bin/stylesheets.mjs, through Vite's own postcss-import (first occurrence kept, the order an application's build gives), instead of Stylesheets::bundle() inlining imports on every request; only the import walk DesignGuard needs stays. SchemeStylesheet::withProfiles() replaces three copies of the scheme-plus-profiles loop, material:scheme leaves spec and contrast checks to the node script that already made them, and the error page's scheme cache, the hashed view namespace, the translations path with no lang/ folder and DesignGuard's 1.x-name hints are gone. JS: the androidx shape port progress.js and both bin scripts each carried lives once in resources/js/shapes.js (the generated SVGs are unchanged); util.js holds ringIndex(), ms(), reopenGuard() and remember(), which were written out several times; listeners are released through AbortController; tooltip.js's hoverPopover() serves the rich tooltip too. CSS: every rule for an element inside the navigation rail queries `--md-navigation-rail-value` instead of repeating the seven collapsed conditions under five media branches; badge, alert, progress, slider and button read one non-inheriting colour-role table (components/color.css); the dialog chrome, the submenu's popover chrome, the chip's state layer and touch target, and the visually-hidden inputs use the shared rules they copied; foundation/tokens.css is folded into foundation.css. Views: Support\Field and Support\Link replace the error-key, bound-value and link-attribute blocks copied into the fields and link components; the timepicker period group, the menu filter and the showcase head are partials; the datepicker's steppers and entry fields are loops; component docblocks no longer restate SKILL.md. Tests and tooling: one dataset-driven ComponentStylesheetsTest replaces four per-group files, DesignGuardTest and the layout-component tests use datasets, browser tests share one ready() helper, CSS parsing lives in ComponentStylesheet alone. docs/audits and the finding IDs citing it are removed, as are pestphp/pest-plugin-laravel, the unused composer scripts and check:font; the lint job runs in the feature job, which now installs node packages so the prebuilt-stylesheet staleness test runs in CI. Feature suite 1177 passed, Chrome browser suite 299 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
471d927e64
commit
247c596c3a
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\File;
|
||||
use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
|
||||
use NoNameWeb\LivewireMaterial\Tests\Support\ViewClasses;
|
||||
|
||||
/**
|
||||
* Every component and layout-shared stylesheet, rewritten without Tailwind (plan step 36): 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, the plain imports and freedom from Tailwind 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/tailwind',
|
||||
'pagination/laravel/simple-tailwind',
|
||||
'pagination/livewire/tailwind',
|
||||
'pagination/livewire/simple-tailwind',
|
||||
]);
|
||||
|
||||
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');
|
||||
Reference in New Issue
Block a user