Files
livewire-material/tests/Feature/Components/ActionStylesheetsTest.php
T
Andreas Reinhold / reiniandClaude Opus 5 4ad257034e Draw the button without Tailwind
Plan step 36. <x-button> renders data-md-button with its variant, colour,
size, shape, icon-button width, selected state and compact FAB as
data-md-* attributes, and passes the glyph's size to <x-icon>.
button.css draws Button*Tokens' geometry per size, the colour roles
through custom properties that the disabled treatment replaces, the
state layer, focus ring and 48px target, and the compact FAB below
600px. Its corner rules carry no specificity (:where), so a group's
stylesheet reshapes the buttons inside it with any selector; the
split button no longer passes an empty corners prop, which is gone.

data-icon-button becomes data-md-icon-button, also in groups.css,
toolbar.css and theme-toggle's own button (navigation group), and the
AppBarTest assertions follow the new hooks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 14:50:58 +02:00

81 lines
4.1 KiB
PHP

<?php
use Illuminate\Support\Facades\File;
use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
/**
* The actions and communication components, rewritten without Tailwind (plan step 36): each view
* renders `data-md-*` attributes and no class list of its own, and each has a stylesheet in
* `material.components` that imports the stylesheets of the components its view renders, writes
* its values from the tokens and its breakpoints as px range queries, and is imported from the
* "Actions and communication" block of components.css.
*/
dataset('action components', [
'loading',
'tooltip',
'badge',
'button',
]);
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('/<x-livewire-material::([a-z-]+)/', File::get(__DIR__."/../../../resources/views/components/{$name}.blade.php"), $tags);
$rendered = collect($tags[1])->unique()
// A component still written for Tailwind has no stylesheet of its own to import yet.
->filter(fn (string $tag): bool => is_file(ComponentStylesheet::path($tag)))
->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', function (string $name) {
$view = File::get(__DIR__."/../../../resources/views/components/{$name}.blade.php");
// A class the caller hands in (`hint-class`, `icon-class`, a slot's attributes) passes through.
expect($view)->not->toMatch('/\bclass="(?!\{\{ \$)|@class\(|->class\(|toCssClasses|(?:x-bind)?:class=/');
})->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 components.css', function (string $name) {
$components = File::get(__DIR__.'/../../../resources/css/components.css');
$block = substr($components, (int) strpos($components, '/* Actions and communication */'));
$block = substr($block, 0, (int) strpos($block, '/* Inputs, selection and data */'));
expect($block)->toContain("@import './components/{$name}.css';");
})->with('action components');