`, ``) — no group's view * renders them exclusively, so they were in no group's dataset at all. They follow the same shape, * token and view-class rules as every other component, so they run with this group's checks too; * only "is imported from the actions and communication block" needs to read the two of them from * all.css's first block ("Foundation components") instead, since that is where they sit. */ dataset('action components', [ 'icon', 'shape', 'loading', 'tooltip', 'badge', 'button', 'group', 'button-group', 'split-button', 'fab', 'menu-separator', 'menu-group', 'menu-item', 'menu', 'fab-menu-item', 'fab-menu', 'rich-tooltip', 'toast', 'progress', 'alert', 'stat', 'empty-state', ]); 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('action components'); 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('action 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('action components'); it('takes its values from the tokens and its breakpoints in px', function (string $name) { $css = ComponentStylesheet::read($name); $source = (string) preg_replace('~/\*.*?\*/~s', '', $css->css); // Colours are roles, shadows are elevation levels, type is a type style, motion is a spring. expect($source)->not->toMatch('/#[0-9a-f]{3,8}\b|\b(?:rgba?|hsla?|oklch|oklab|lab|lch)\(/i') ->not->toMatch('/\bbox-shadow:(?!\s*(?:none|var\(--md-))/') ->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)/'); foreach ($css->mediaQueries() as $query) { preg_match_all('/(\d*\.?\d+)(px|rem|em)\b/', $query, $lengths, PREG_SET_ORDER); foreach ($lengths as [, $number, $unit]) { expect($unit)->toBe('px', "{$name}.css: {$query}") ->and(in_array($number, ['600', '840', '1200', '1600'], true))->toBeTrue("{$name}.css: {$query} is not at an M3 breakpoint"); } } })->with('action components'); it('is imported from the actions and communication block of all.css, or icon and shape from the foundation block before it', function (string $name) { $block = allCssBlock(in_array($name, ['icon', 'shape'], true) ? 'Foundation components' : 'Actions and communication'); expect($block)->toContain("@import './components/{$name}.css';"); })->with('action components');