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 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('/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 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('/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');