Files
livewire-material/tests/Feature/Components/NavigationStylesheetsTest.php
T
Andreas Reinhold / reiniandClaude Opus 5 cbd058f948 Check navigation-item.css's shape like every navigation stylesheet
Plan step 36, navigation review. The shared indicator and state-layer
stylesheet has no view of its own, so it was in no group dataset, and
since 9c134b2d the layout stylesheet tests skip the component files
scaffold.css reaches it through. Its layer, Tailwind, token, scoping
and components.css checks now run with the navigation components'.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-15 01:11:12 +02:00

165 lines
8.1 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
* components.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 components.css', function (string $name) {
$components = File::get(__DIR__.'/../../../resources/css/components.css');
$block = substr($components, (int) strpos($components, '/* Navigation */'));
expect($block)->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');