[$block, $boxShadow]) { yield $name => [$name, $block, $boxShadow]; } }); // The names with exactly one view of their own at resources/views/components/.blade.php — // every dataset entry above but the four stylesheet-only names (see the file header) — for the two // checks that read that view. `tabs` renders two (tabs.blade.php and tab.blade.php). dataset('component views', array_values(array_diff(array_keys(componentStylesheets()), ['selection', 'navigation-item', 'pagination', 'error-page']))); /** * The names whose stylesheet travels in every page's bundle (all.css), so a bare `html`, `body`, * `dialog` or `:root` rule would restyle an application's own pages — containment and navigation, * the two groups whose original tests carried this check. `[data-md-` is the exception in both: a * rule that only applies while a package component is on the page at all. `:has([data-md-` needs * no `>` combinator only in navigation's own stylesheets (toolbar.css's * `:root:has([data-md-toolbar-place=…])` has none) — containment keeps the strict, `>`-only form * its own original test held it to. */ dataset('scoped selectors', function (): Generator { $names = [ ...array_fill_keys(['divider', 'collapse', 'card', 'list', 'list-item', 'modal', 'drawer', 'bottom-sheet', 'carousel-item', 'carousel'], true), ...array_fill_keys(['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', 'navigation-item'], false), ]; foreach ($names as $name => $strict) { yield $name => [$name, $strict]; } }); it('draws the component from a stylesheet shaped like every package stylesheet', function (string $name) { $css = ComponentStylesheet::read($name); // The header, the layer statement and the plain imports are StylesheetsTest's, checked over // every file all.css reaches; only the block layer and the imports' existence are this // dataset's own to check. expect($css->blocks())->each->toBe('@layer material.components'); foreach ($css->imports() as $import) { expect(is_file(dirname(ComponentStylesheet::path($name)).'/'.$import))->toBeTrue("{$name}.css imports {$import}, which does not exist"); } })->with('component stylesheets'); it('writes no class list into the view but the interaction and text classes', function (string $name) { // `tabs` renders two views of its own, tabs.blade.php and tab.blade.php; every other name here // has exactly one. foreach (($name === 'tabs' ? ['tabs', 'tab'] : [$name]) as $view) { expect(ViewClasses::violations(File::get(__DIR__."/../../../resources/views/components/{$view}.blade.php")))->toBe([]); } })->with('component views'); /** * `$boxShadow` is the shape a stylesheet's own box-shadow values may take: 'loose' for actions and * communication (a component paints its own elevation through a local custom property, e.g. * button.css's `--md-button-elevation`, not `--md-sys-elevation-N` directly, so any `var(--md-*)` * passes); 'ring' for containment, inputs/selection/data and navigation (`none`, an elevation level, * or a literal ring — `[inset] 0 0 0 px` in a colour role, a focus or a selected day); 'none' for * the error layout, which draws no box-shadow of its own. * * Breakpoints (px, at one of M3's four) are StylesheetsTest's, checked over every file all.css * reaches. field.css's autofill workaround (`transition: background-color 604800s, color * 604800s`) delays Chrome's autofill background for a week — not a spring, and not meant to be * one — so it is stripped before the transition check. */ it('takes its values from the tokens', function (string $name, string $block, string $boxShadow) { $label = "{$name}.css"; $source = (string) preg_replace('~/\*.*?\*/~s', '', ComponentStylesheet::read($name)->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)/'); if ($boxShadow === 'loose') { expect($source)->not->toMatch('/\bbox-shadow:(?!\s*(?:none|var\(--md-))/'); } elseif ($boxShadow === 'ring') { $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"); } } })->with('component stylesheets'); it('is imported from its own block of all.css', function (string $name, string $block) { expect(allCssBlock($block))->toContain("@import './components/{$name}.css';"); })->with('component stylesheets'); it('scopes every element-wide selector to a data-md hook', function (string $name, bool $strict) { $source = (string) preg_replace('~/\*.*?\*/~s', '', ComponentStylesheet::read($name)->css); $has = $strict ? ':has\(> \[data-md-' : ':has\((?:> )?\[data-md-'; expect($source)->not->toMatch('/(?:^|[,{};]\s*)(?:html|body|dialog|:root)(?![\w-])(?!\[data-md-|'.$has.')/m'); })->with('scoped selectors'); // ---- pagination: its views live outside resources/views/components/ -------------------------- dataset('pagination views', [ 'pagination/laravel/material', 'pagination/laravel/simple-material', 'pagination/livewire/material', 'pagination/livewire/simple-material', ]); it('imports the stylesheet of every component pagination\'s views render', function (string $view) { preg_match_all('/unique() // A rendered tag that is not a components/ stylesheet at all (a layout component's, e.g. // ) 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'); // ---- partials: fragments a component view @includes, not a component view of their own -------- dataset('partial views', array_map( fn (string $file): string => 'partials/'.basename($file, '.blade.php'), glob(__DIR__.'/../../../resources/views/partials/*.blade.php'), )); it('writes no class list into a partial but the interaction and text classes', function (string $view) { expect(ViewClasses::violations(File::get(__DIR__."/../../../resources/views/{$view}.blade.php")))->toBe([]); })->with('partial views');