Plan step 37: all.css replaces the two interim import lists with one entry for an application that wants everything — the foundation, every layout stylesheet and every component stylesheet, grouped under the same block comments components.css used, plus a Layout block. It also directly imports the three files nothing imported by name before (layout/spacing.css, layout/visibility.css, components/selection.css), so every file under components/ and layout/ is now one @import away. Every test that read a block of components.css or layout.css now reads the matching block of all.css through one shared helper (allCssBlock(), in tests/Pest.php so it loads for any test run) instead of repeating the same substr() search in each file. StylesheetsTest.php's shape, Tailwind-free and breakpoint checks, previously run twice (once for the foundation, once for the layout tree), now run once over the whole tree all.css reaches, since every component and layout stylesheet is plain CSS after step 36; a new test asserts all.css imports everything under components/ and layout/ exactly once. The Workbench imports all.css in place of layout.css and components.css. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
162 lines
7.9 KiB
PHP
162 lines
7.9 KiB
PHP
<?php
|
|
|
|
use Illuminate\Support\Facades\File;
|
|
use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
|
|
use NoNameWeb\LivewireMaterial\Tests\Support\ViewClasses;
|
|
|
|
/**
|
|
* The navigation components, rewritten without Tailwind (plan step 36, the last audit group): each
|
|
* view renders `data-md-*` attributes and no class list of its own beyond the interaction and text
|
|
* classes (tests/Support/ViewClasses.php), and each has a stylesheet in `material.components` that
|
|
* imports the stylesheets of the components its view renders, writes its values from the tokens
|
|
* and its breakpoints as px range queries, and is imported from the "Navigation" block of
|
|
* all.css.
|
|
*
|
|
* The dataset grows by one name per component commit, the same rule ContainmentStylesheetsTest.php
|
|
* and the earlier groups follow. `<x-tabs>` and `<x-tab>` share one stylesheet (tabs.css) and one
|
|
* commit, so `navigationViews()` maps the 'tabs' entry onto both blade views; every other entry is
|
|
* one view, one stylesheet, as usual. `navigation-bar` rejoined the dataset with the scaffold's own
|
|
* rewrite: its one rule reading `--material-bottom-bar` stayed unlayered only while `<x-scaffold>`
|
|
* published that variable through a Tailwind utility, which no layered rule could outrank; now
|
|
* scaffold.css sets it in `material.layout`, which this file's `material.components` always beats,
|
|
* so the whole stylesheet fits one `@layer material.components` block like every other entry here.
|
|
* `section-nav` renders `<x-tabs>`'s hooks rather than `<x-tabs>` itself (it reuses its stylesheet
|
|
* whole, tabs.css, N-16), so `navigationViews()` need not special-case it the way it does 'tabs'.
|
|
*/
|
|
function navigationComponents(): array
|
|
{
|
|
return [
|
|
'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',
|
|
];
|
|
}
|
|
|
|
dataset('navigation components', navigationComponents());
|
|
|
|
/**
|
|
* The checks that read a stylesheet alone also run on the one no view renders by itself:
|
|
* navigation-item.css, the indicator fill and state layer the bar's and the rail's items share and
|
|
* both import. Nothing else reaches it — the layout stylesheet tests skip component files they
|
|
* import — so without this its layer, tokens and scoping would go unchecked.
|
|
*/
|
|
dataset('navigation stylesheets', [...navigationComponents(), 'navigation-item']);
|
|
|
|
/**
|
|
* The blade view(s) a dataset name's checks read. One file for every name but 'tabs', whose panel
|
|
* lives in a second view, tab.blade.php, sharing tabs.css with tabs.blade.php.
|
|
*
|
|
* @return list<string>
|
|
*/
|
|
function navigationViews(string $name): array
|
|
{
|
|
return $name === 'tabs' ? ['tabs', 'tab'] : [$name];
|
|
}
|
|
|
|
/**
|
|
* Fails unless every media query in the stylesheet writes its lengths in px, at one of M3's four
|
|
* breakpoints. `rem` and `em` would grow with the reader's text size, which a breakpoint must not.
|
|
*/
|
|
function assertNavigationBreakpointsInPx(ComponentStylesheet $css, string $label): void
|
|
{
|
|
foreach ($css->mediaQueries() as $query) {
|
|
preg_match_all('/\(([^()]*)\)/', $query, $features);
|
|
|
|
foreach ($features[1] as $feature) {
|
|
preg_match_all('/(\d*\.?\d+)(px|rem|em)\b/', $feature, $lengths, PREG_SET_ORDER);
|
|
|
|
foreach ($lengths as [, $number, $unit]) {
|
|
expect($unit)->toBe('px', "{$label}: {$query}");
|
|
expect(in_array($number, ['600', '840', '1200', '1600'], true))->toBeTrue("{$label}: {$query} is not at an M3 breakpoint");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Colours are roles, shadows are elevation levels or a ring, type is a type style, and breakpoints
|
|
* are px, at one of M3's four.
|
|
*/
|
|
function assertNavigationTokensAndPxBreakpoints(ComponentStylesheet $css, string $label): void
|
|
{
|
|
$source = (string) preg_replace('~/\*.*?\*/~s', '', $css->css);
|
|
|
|
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)/');
|
|
|
|
// A box-shadow is `none`, an elevation level, or a ring: `[inset] 0 0 0 <n>px` in a colour role.
|
|
$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");
|
|
}
|
|
|
|
assertNavigationBreakpointsInPx($css, $label);
|
|
}
|
|
|
|
it('draws the component from a stylesheet shaped like every package stylesheet', function (string $name) {
|
|
$css = ComponentStylesheet::read($name);
|
|
|
|
expect($css->css)->toStartWith('/*')
|
|
->and($css->statements()[0] ?? null)->toBe('@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;')
|
|
->and(array_slice($css->statements(), 1))->each->toMatch('/^@import \'\.\/[a-z-]+\.css\';$/')
|
|
->and($css->blocks())->each->toBe('@layer material.components')
|
|
->and($css->css)->not->toMatch('/@(?:tailwind|theme|utility|variant|custom-variant|apply|source|config|plugin|reference)\b|--(?:theme|spacing|alpha)\(|\btheme\(/');
|
|
|
|
foreach ($css->imports() as $import) {
|
|
expect(is_file(dirname(ComponentStylesheet::path($name)).'/'.$import))->toBeTrue("{$name}.css imports {$import}, which does not exist");
|
|
}
|
|
})->with('navigation stylesheets');
|
|
|
|
it('imports the stylesheet of every component its view renders', function (string $name) {
|
|
$rendered = collect(navigationViews($name))
|
|
->flatMap(function (string $view): array {
|
|
preg_match_all('/<x-livewire-material::([a-z-]+)/', File::get(__DIR__."/../../../resources/views/components/{$view}.blade.php"), $tags);
|
|
|
|
return $tags[1];
|
|
})
|
|
->unique()
|
|
// A component still written for Tailwind has no stylesheet of its own to import yet; its
|
|
// old stylesheet, if it has one, lives in tailwind.css without the layer statement.
|
|
->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($name)->imports())))->toBe([]);
|
|
})->with('navigation components');
|
|
|
|
it('writes no class list into the view but the interaction and text classes', function (string $name) {
|
|
foreach (navigationViews($name) as $view) {
|
|
expect(ViewClasses::violations(File::get(__DIR__."/../../../resources/views/components/{$view}.blade.php")))->toBe([]);
|
|
}
|
|
})->with('navigation components');
|
|
|
|
it('takes its values from the tokens and its breakpoints in px', function (string $name) {
|
|
assertNavigationTokensAndPxBreakpoints(ComponentStylesheet::read($name), "{$name}.css");
|
|
})->with('navigation stylesheets');
|
|
|
|
it('is imported from the navigation block of all.css', function (string $name) {
|
|
expect(allCssBlock('Navigation'))->toContain("@import './components/{$name}.css';");
|
|
})->with('navigation stylesheets');
|
|
|
|
it('scopes every element-wide selector to a data-md hook', function (string $name) {
|
|
// `all.css` bundles every component stylesheet into every page, so a bare `html`, `body`,
|
|
// `dialog` or `:root` rule would restyle the application's own pages.
|
|
$source = (string) preg_replace('~/\*.*?\*/~s', '', ComponentStylesheet::read($name)->css);
|
|
|
|
expect($source)->not->toMatch('/(?:^|[,{};]\s*)(?:html|body|dialog|:root)(?![\w-])(?!\[data-md-|:has\(> \[data-md-)/m');
|
|
})->with('navigation stylesheets');
|