Plan step 36. <x-split-button> renders data-md-split-button and data-md-size on the pair and data-md-split on its halves. split-button.css draws SplitButton*Tokens' shape — full outer corners, inner corners that grow under the finger, the trailing half round while its menu is open — the narrower leading padding and 48px trailing half at xs and sm, and the chevron's own size, nudge and turn on the standard scheme. groups.css is empty and leaves tailwind.css. The actions browser test follows data-md-split and data-md-button-group, and gains the owed tests for a connected segment's 48px edge and a button group's single-required and multi selection. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
85 lines
4.4 KiB
PHP
85 lines
4.4 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',
|
|
'group',
|
|
'button-group',
|
|
'split-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; 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', 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('/(?<![\w-])class="(?!\{\{ \$)|@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');
|