Review leftover (plan step 36): none of the three foundation/shared stylesheets was in a group dataset, so their shape, token and breakpoint rules went unchecked. icon and shape have views of their own and sit in components.css's first block, so they join ActionStylesheetsTest's dataset outright, with that file's block check narrowed to look for them there instead of the actions block. selection.css has no view (checkbox, radio and toggle each import it) and components.css never imports it directly, so it follows navigation-item.css's pattern in InputStylesheetsTest: a second, stylesheet-only dataset alongside the view-based one. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
216 lines
11 KiB
PHP
216 lines
11 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), 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
|
|
* "Inputs, selection and data" block of components.css.
|
|
*
|
|
* `pagination` is not in this dataset: its four views live in `resources/views/pagination/**`, not
|
|
* `resources/views/components/`, so the two checks that read a single `resources/views/components/
|
|
* {name}.blade.php` file do not apply to it. It gets the three checks that do not depend on that
|
|
* path below, and its own small versions of the other two.
|
|
*/
|
|
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. Nothing renders it directly and components.css never imports
|
|
* it either — only checkbox.css, radio.css and toggle.css do — 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('imports the stylesheet of every component its view renders', function (string $name) {
|
|
preg_match_all('/<x-livewire-material::([a-z-]+)/', File::get(__DIR__."/../../../resources/views/components/{$name}.blade.php"), $tags);
|
|
|
|
$rendered = collect($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('input components');
|
|
|
|
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 components.css', function (string $name) {
|
|
$components = File::get(__DIR__.'/../../../resources/css/components.css');
|
|
$block = substr($components, (int) strpos($components, '/* Inputs, selection and data */'));
|
|
$block = substr($block, 0, (int) strpos($block, '/* Containment */'));
|
|
|
|
expect($block)->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 components.css for pagination', function () {
|
|
$components = File::get(__DIR__.'/../../../resources/css/components.css');
|
|
$block = substr($components, (int) strpos($components, '/* Inputs, selection and data */'));
|
|
$block = substr($block, 0, (int) strpos($block, '/* Containment */'));
|
|
|
|
expect($block)->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()
|
|
->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');
|