` and `` share one stylesheet (tabs.css) and one * commit, so `navigationViews()` maps the 'tabs' entry onto both blade views; every other entry is * one view, one stylesheet, as usual. */ dataset('navigation components', [ 'app-bar', ]); /** * The blade view(s) a dataset name's checks read. One file for every name but 'tabs', whose panel * lives in a second view, tab.blade.php, sharing tabs.css with tabs.blade.php. * * @return list */ function navigationViews(string $name): array { return $name === 'tabs' ? ['tabs', 'tab'] : [$name]; } /** * Fails unless every media query in the stylesheet writes its lengths in px, at one of M3's four * breakpoints. `rem` and `em` would grow with the reader's text size, which a breakpoint must not. */ function assertNavigationBreakpointsInPx(ComponentStylesheet $css, string $label): void { foreach ($css->mediaQueries() as $query) { preg_match_all('/\(([^()]*)\)/', $query, $features); foreach ($features[1] as $feature) { preg_match_all('/(\d*\.?\d+)(px|rem|em)\b/', $feature, $lengths, PREG_SET_ORDER); foreach ($lengths as [, $number, $unit]) { expect($unit)->toBe('px', "{$label}: {$query}"); 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 or a ring, type is a type style, and breakpoints * are px, at one of M3's four. */ function assertNavigationTokensAndPxBreakpoints(ComponentStylesheet $css, string $label): void { $source = (string) preg_replace('~/\*.*?\*/~s', '', $css->css); 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. $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"); } assertNavigationBreakpointsInPx($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('navigation components'); it('imports the stylesheet of every component its view renders', function (string $name) { $rendered = collect(navigationViews($name)) ->flatMap(function (string $view): array { 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('navigation components'); it('writes no class list into the view but the interaction and text classes', function (string $name) { foreach (navigationViews($name) as $view) { expect(ViewClasses::violations(File::get(__DIR__."/../../../resources/views/components/{$view}.blade.php")))->toBe([]); } })->with('navigation components'); it('takes its values from the tokens and its breakpoints in px', function (string $name) { assertNavigationTokensAndPxBreakpoints(ComponentStylesheet::read($name), "{$name}.css"); })->with('navigation components'); it('is imported from the navigation block of components.css', function (string $name) { $components = File::get(__DIR__.'/../../../resources/css/components.css'); $block = substr($components, (int) strpos($components, '/* Navigation */')); expect($block)->toContain("@import './components/{$name}.css';"); })->with('navigation components'); it('scopes every element-wide selector to a data-md hook', function (string $name) { // `all.css` bundles every component stylesheet into every page, so a bare `html`, `body`, // `dialog` or `:root` rule would restyle the application's own pages. $source = (string) preg_replace('~/\*.*?\*/~s', '', ComponentStylesheet::read($name)->css); expect($source)->not->toMatch('/(?:^|[,{};]\s*)(?:html|body|dialog|:root)(?![\w-])(?!\[data-md-|:has\(> \[data-md-)/m'); })->with('navigation components');