*/ function declarations(string $css, string $selector): array { preg_match_all('/(--[\w-]+):\s*([^;]+);/', Str::of($css)->after($selector)->before('}')->toString(), $matches); return array_combine($matches[1], $matches[2]); } it('ships a default scheme that declares every role in both themes', function () { $scheme = json_decode(File::get(packageCss('tokens/scheme.json')), true); $css = File::get(packageCss('tokens/scheme.css')); $light = declarations($css, "[data-theme='light']"); $dark = declarations($css, "[data-theme='dark']"); expect(array_keys($light))->toEqual(array_keys($dark)) ->and(count($light))->toBe(count($scheme['light'])) ->and(Str::of($css)->after("[data-theme='light']")->before('}')->toString())->toContain('color-scheme: light;') ->and(Str::of($css)->after("[data-theme='dark']")->before('}')->toString())->toContain('color-scheme: dark;'); foreach ($scheme['light'] as $role => $hex) { expect($light["--md-sys-color-{$role}"])->toBe($hex) ->and($dark["--md-sys-color-{$role}"])->toBe($scheme['dark'][$role]); } }); it('ships M3\'s medium and high contrast levels beside the standard one', function () { $scheme = json_decode(File::get(packageCss('tokens/scheme.json')), true); $css = File::get(packageCss('tokens/scheme.css')); foreach (['medium', 'high'] as $level) { foreach (['light', 'dark'] as $theme) { $declared = declarations($css, "[data-contrast='{$level}'] [data-theme='{$theme}'] {"); expect(array_keys($declared))->toEqual(array_map(fn (string $role): string => "--md-sys-color-{$role}", array_keys($scheme['light']))); foreach ($scheme['contrast'][$level][$theme] as $role => $hex) { expect($declared["--md-sys-color-{$role}"])->toBe($hex); } } } }); it('turns every role into a Tailwind colour', function () { $roles = array_keys(json_decode(File::get(packageCss('tokens/scheme.json')), true)['light']); $theme = File::get(packageCss('tokens/theme.css')); foreach ($roles as $role) { expect($theme)->toContain("--color-{$role}: var(--md-sys-color-{$role});"); } }); it('resolves colour utilities on the element, so a nested data-theme repaints them', function () { $theme = File::get(packageCss('tokens/theme.css')); preg_match_all('/@theme inline \{(.*?)\}/s', $theme, $inline); expect(implode("\n", $inline[1])) ->toContain('--color-primary: var(--md-sys-color-primary);') ->toContain('--color-body: var(--md-sys-color-on-surface-variant);') ->not->toMatch('/--[\w-]+:\s*#/'); }); it('leaves the choice of theme and of contrast to the head script, never to a media query', function () { foreach (File::allFiles(packageCss()) as $file) { expect($file->getContents()) ->not->toContain('prefers-color-scheme') ->not->toContain('prefers-contrast'); } }); it('reads every safe-area inset through a variable that can replace it', function () { $files = collect([packageCss(), __DIR__.'/../../resources/views', __DIR__.'/../../resources/js']) ->flatMap(fn (string $path): array => File::allFiles($path)); $insets = 0; foreach ($files as $file) { preg_match_all('/env\(safe-area-inset-(top|bottom|left|right)\)/', $file->getContents(), $matches, PREG_OFFSET_CAPTURE); foreach ($matches[1] as [$side, $offset]) { $insets++; expect(substr($file->getContents(), 0, $offset - strlen('env(safe-area-inset-'))) ->toMatch("/var\\(--material-safe-{$side},\\s?$/", "{$file->getRelativePathname()} reads safe-area-inset-{$side} directly"); } } expect($insets)->toBeGreaterThan(10) ->and(File::get(packageCss('components/app-bar.css')))->toContain('padding-top: var(--material-safe-top, env(safe-area-inset-top));'); }); it('makes every motion token instant under reduced motion', function () { $motion = File::get(packageCss('tokens/motion.css')); $reduced = Str::of($motion)->after('prefers-reduced-motion: reduce')->toString(); preg_match_all('/(--md-sys-motion-[\w-]*duration[\w-]*):/', Str::before($motion, '@media'), $durations); expect($durations[1])->not->toBeEmpty(); foreach ($durations[1] as $duration) { expect($reduced)->toContain("{$duration}: 0ms;"); } });