Files
livewire-material/tests/Feature/TokensTest.php
T
Andreas Reinhold / reiniandClaude Fable 5.1 4263982369 Give success, warning and info the 2025 spec and three contrast levels
The three state colours are DynamicColors on their own tonal palette now, built
exactly as Google builds error/on-error/error-container/on-error-container in both
specs, so the scheme's contrast level, dark tones, spec version and platform reach
them as they reach every other role; --harmonize (and a profile's harmonize) pulls
each source towards the seed, off by default. material:scheme also writes M3's
medium (0.5) and high (1.0) levels for both themes and every profile, keyed on
data-contrast and never on a media query, and --contrast now moves the standard
block alone. Scheme::load() takes the level; the mail theme stays on standard.
Plan: docs/plans/material-3-alignment.md, steps 6 and 7 (core C1, C2, C15).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 05:30:48 +02:00

119 lines
4.6 KiB
PHP

<?php
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
function packageCss(string $path = ''): string
{
return __DIR__.'/../../resources/css'.($path === '' ? '' : '/'.$path);
}
/**
* The custom properties one selector's block declares.
*
* @return array<string, string>
*/
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;");
}
});