Action/Input/Containment/NavigationStylesheetsTest.php each carried the same "imports the stylesheet of every component its view renders" check on their own group's dataset. StylesheetsTest.php now runs it once, over every resources/views/components/*.blade.php backed by a stylesheet of its own (icon and shape included, no longer needing a group to run in at all; tabs/tab's shared stylesheet, N-16, still handled) — a new component gets the check by existing, not by being added to the right group's array too. Each group file keeps its own dataset for the checks that stay group-shaped (shape, tokens, breakpoints, the block it imports from). Plan step 42 (Phase F), Part A. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
196 lines
9.6 KiB
PHP
196 lines
9.6 KiB
PHP
<?php
|
|
|
|
use Illuminate\Support\Facades\File;
|
|
use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
|
|
use NoNameWeb\LivewireMaterial\Tests\Support\ViewClasses;
|
|
|
|
/**
|
|
* The inputs, selection and data components, 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 the "Inputs, selection and data" block of all.css.
|
|
* Every component stylesheet importing the stylesheets its view renders is
|
|
* tests/Feature/StylesheetsTest.php's now, folded into one dataset over every component view (step
|
|
* 42) rather than kept once per group.
|
|
*
|
|
* `pagination` is not in this dataset: its four views live in `resources/views/pagination/**`, not
|
|
* `resources/views/components/`, so the check that reads a single `resources/views/components/
|
|
* {name}.blade.php` file does not apply to it. It gets the two checks that do not depend on that
|
|
* path below, and its own small version of the third.
|
|
*/
|
|
function inputComponents(): array
|
|
{
|
|
return [
|
|
'form',
|
|
'field',
|
|
'input',
|
|
'password',
|
|
'textarea',
|
|
'select',
|
|
'file',
|
|
'checkbox',
|
|
'radio',
|
|
'toggle',
|
|
'chip',
|
|
'chip-set',
|
|
'choices',
|
|
'slider',
|
|
'search',
|
|
'table',
|
|
'sort-header',
|
|
'datepicker',
|
|
'timepicker',
|
|
];
|
|
}
|
|
|
|
dataset('input components', inputComponents());
|
|
|
|
/**
|
|
* The checks that read a stylesheet alone also run on the one no view renders by itself:
|
|
* selection.css, what the checkbox, radio and switch share (its own file header names them), which
|
|
* each of their stylesheets imports too. Nothing renders it directly, so without this its shape
|
|
* and token rules would go unchecked, the way navigation-item.css did before
|
|
* NavigationStylesheetsTest.php picked it up.
|
|
*/
|
|
dataset('input stylesheets', [...inputComponents(), 'selection']);
|
|
|
|
/**
|
|
* Fails unless every media query in the stylesheet writes its lengths in px, at one of M3's four
|
|
* breakpoints. The one exception is a viewport *height* in a query keyed on `orientation`: the time
|
|
* picker lies its dial down by device orientation and height, which M3 explicitly does not make a
|
|
* breakpoint (docs/reference/m3/components-navigation-selection-inputs.md § Time pickers/Behaviour),
|
|
* so that height may be any px. A width, in any query, is still a breakpoint. `rem` and `em` would
|
|
* grow with the reader's text size, which neither should.
|
|
*/
|
|
function assertBreakpointsInPx(ComponentStylesheet $css, string $label): void
|
|
{
|
|
foreach ($css->mediaQueries() as $query) {
|
|
$orientation = preg_match('/\(\s*orientation\s*:/', $query) === 1;
|
|
|
|
preg_match_all('/\(([^()]*)\)/', $query, $features);
|
|
|
|
foreach ($features[1] as $feature) {
|
|
preg_match_all('/(\d*\.?\d+)(px|rem|em)\b/', $feature, $lengths, PREG_SET_ORDER);
|
|
$height = preg_match('/(?:^|[\s:<>=])(?:min-|max-)?height\b/', $feature) === 1 && ! str_contains($feature, 'width');
|
|
|
|
foreach ($lengths as [, $number, $unit]) {
|
|
expect($unit)->toBe('px', "{$label}: {$query}");
|
|
|
|
if (! ($orientation && $height)) {
|
|
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, type is a type style, motion is a spring — and
|
|
* breakpoints are px, at one of M3's four (an orientation query's height aside; see
|
|
* assertBreakpointsInPx()).
|
|
*
|
|
* One named exception, in field.css alone: its autofill workaround (`transition: background-color
|
|
* 604800s, color 604800s`) delays Chrome's autofill background for a week, which is not a spring and
|
|
* is not meant to be one — a `var(--md-sys-motion-*)` duration there would visibly repaint on load.
|
|
*/
|
|
function assertTokensAndPxBreakpoints(ComponentStylesheet $css, string $label): void
|
|
{
|
|
$source = (string) preg_replace('~/\*.*?\*/~s', '', $css->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)/');
|
|
|
|
// A box-shadow is `none`, an elevation level, or a ring: `[inset] 0 0 0 <n>px` in a colour
|
|
// role (or a role mixed toward transparent, a disabled ring) — a day's or a year's "current"
|
|
// outline, a focused time field's edge. Never a hand-made shadow.
|
|
$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");
|
|
}
|
|
|
|
assertBreakpointsInPx($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('input stylesheets');
|
|
|
|
it('writes no class list into the view but the interaction and text classes', function (string $name) {
|
|
expect(ViewClasses::violations(File::get(__DIR__."/../../../resources/views/components/{$name}.blade.php")))->toBe([]);
|
|
})->with('input components');
|
|
|
|
it('takes its values from the tokens and its breakpoints in px', function (string $name) {
|
|
assertTokensAndPxBreakpoints(ComponentStylesheet::read($name), "{$name}.css");
|
|
})->with('input stylesheets');
|
|
|
|
it('is imported from the inputs, selection and data block of all.css', function (string $name) {
|
|
expect(allCssBlock('Inputs, selection and data'))->toContain("@import './components/{$name}.css';");
|
|
})->with('input components');
|
|
|
|
// ---- pagination: its views live outside resources/views/components/ --------------------------
|
|
|
|
it('draws pagination from a stylesheet shaped like every package stylesheet', function () {
|
|
$css = ComponentStylesheet::read('pagination');
|
|
|
|
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('pagination')).'/'.$import))->toBeTrue("pagination.css imports {$import}, which does not exist");
|
|
}
|
|
});
|
|
|
|
it('takes pagination\'s values from the tokens and its breakpoints in px', function () {
|
|
assertTokensAndPxBreakpoints(ComponentStylesheet::read('pagination'), 'pagination.css');
|
|
});
|
|
|
|
it('is imported from the inputs, selection and data block of all.css for pagination', function () {
|
|
expect(allCssBlock('Inputs, selection and data'))->toContain("@import './components/pagination.css';");
|
|
});
|
|
|
|
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');
|