An over-engineering audit of the whole tree, applied in five reviewed batches. Behaviour stays the same except where UPGRADE.md says otherwise. PHP: the showcase and error-page stylesheets are prebuilt into resources/dist by bin/stylesheets.mjs, through Vite's own postcss-import (first occurrence kept, the order an application's build gives), instead of Stylesheets::bundle() inlining imports on every request; only the import walk DesignGuard needs stays. SchemeStylesheet::withProfiles() replaces three copies of the scheme-plus-profiles loop, material:scheme leaves spec and contrast checks to the node script that already made them, and the error page's scheme cache, the hashed view namespace, the translations path with no lang/ folder and DesignGuard's 1.x-name hints are gone. JS: the androidx shape port progress.js and both bin scripts each carried lives once in resources/js/shapes.js (the generated SVGs are unchanged); util.js holds ringIndex(), ms(), reopenGuard() and remember(), which were written out several times; listeners are released through AbortController; tooltip.js's hoverPopover() serves the rich tooltip too. CSS: every rule for an element inside the navigation rail queries `--md-navigation-rail-value` instead of repeating the seven collapsed conditions under five media branches; badge, alert, progress, slider and button read one non-inheriting colour-role table (components/color.css); the dialog chrome, the submenu's popover chrome, the chip's state layer and touch target, and the visually-hidden inputs use the shared rules they copied; foundation/tokens.css is folded into foundation.css. Views: Support\Field and Support\Link replace the error-key, bound-value and link-attribute blocks copied into the fields and link components; the timepicker period group, the menu filter and the showcase head are partials; the datepicker's steppers and entry fields are loops; component docblocks no longer restate SKILL.md. Tests and tooling: one dataset-driven ComponentStylesheetsTest replaces four per-group files, DesignGuardTest and the layout-component tests use datasets, browser tests share one ready() helper, CSS parsing lives in ComponentStylesheet alone. docs/audits and the finding IDs citing it are removed, as are pestphp/pest-plugin-laravel, the unused composer scripts and check:font; the lint job runs in the feature job, which now installs node packages so the prebuilt-stylesheet staleness test runs in CI. Feature suite 1177 passed, Chrome browser suite 299 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
127 lines
7.1 KiB
PHP
127 lines
7.1 KiB
PHP
<?php
|
|
|
|
use Livewire\Component;
|
|
use Livewire\Livewire;
|
|
use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
|
|
|
|
it('groups buttons with the spacing of their size', function (string $size, string $gap) {
|
|
$html = (string) $this->blade("<x-button-group label=\"Views\" size=\"{$size}\"><x-button label=\"Day\" /></x-button-group>");
|
|
preg_match('/<div[^>]*>/', $html, $tag);
|
|
|
|
expect($html)
|
|
->toMatch("/<div\\s+role=\"group\"\\s+aria-label=\"Views\"\\s+data-md-button-group=\"standard\"\\s+data-md-size=\"{$size}\"\\s+data-md-shape=\"round\"/")
|
|
// The group's own root writes no class; the button nested in the slot carries the
|
|
// interaction classes of its own.
|
|
->and($tag[0])->not->toContain('class=')
|
|
->and(ComponentStylesheet::read('button-group')->declarations(in_array($size, ['xs', 'sm'], true)
|
|
? "[data-md-button-group='standard'][data-md-size='{$size}']"
|
|
: "[data-md-button-group='standard']"))->toBe(['gap' => $gap]);
|
|
})->with([
|
|
'xs' => ['xs', '18px'],
|
|
'sm' => ['sm', '12px'],
|
|
'md' => ['md', 'var(--md-sys-measurement-space100)'],
|
|
'lg' => ['lg', 'var(--md-sys-measurement-space100)'],
|
|
'xl' => ['xl', 'var(--md-sys-measurement-space100)'],
|
|
]);
|
|
|
|
it('connects buttons 2px apart, with M3\'s inner corners', function () {
|
|
$css = ComponentStylesheet::read('button-group');
|
|
|
|
expect((string) $this->blade('<x-button-group connected><x-button label="Day" /></x-button-group>'))->toContain('data-md-button-group="connected"')
|
|
->and($css->declarations("[data-md-button-group='connected']"))->toBe(['gap' => 'var(--md-sys-measurement-space25)'])
|
|
->and($css->declarations("[data-md-button-group='connected'] > *"))->toMatchArray([
|
|
'--md-button-group-corner' => 'var(--md-button-group-inner)',
|
|
'border-radius' => 'var(--md-button-group-corner)',
|
|
])
|
|
->and($css->declarations("[data-md-button-group='connected'] > :active"))->toBe(['--md-button-group-corner' => 'var(--md-button-group-inner-pressed)'])
|
|
->and($css->declarations("[data-md-button-group='connected'] > :first-child"))->toBe([
|
|
'border-start-start-radius' => 'var(--md-button-group-outer)',
|
|
'border-end-start-radius' => 'var(--md-button-group-outer)',
|
|
])
|
|
// Half the height, never 9999px, which would scale the small inner corners away.
|
|
->and($css->declarations("[data-md-button-group][data-md-size='xl']"))->toMatchArray(['--md-button-group-full' => '68px', '--md-button-group-inner' => 'var(--md-sys-shape-corner-lg-increased)']);
|
|
});
|
|
|
|
it('squares a group', function () {
|
|
// M3 lists "Default shape | Round, square" as a button-group configuration; the shape covers
|
|
// every button in the group, so it need not be written on each one.
|
|
$css = ComponentStylesheet::read('button-group');
|
|
|
|
expect((string) $this->blade('<x-button-group shape="square"><x-button label="Day" /></x-button-group>'))
|
|
->toContain('data-md-shape="square"')
|
|
->and((string) $this->blade('<x-button-group connected shape="square"><x-button label="Day" /></x-button-group>'))
|
|
->toContain('data-md-button-group="connected"')
|
|
->toContain('data-md-shape="square"')
|
|
->and((string) $this->blade('<x-button-group shape="oval"><x-button label="Day" /></x-button-group>'))
|
|
->toContain('data-md-shape="round"')
|
|
->and($css->declarations("[data-md-button-group='standard'][data-md-shape='square'] > *"))->toBe(['border-radius' => 'var(--md-button-group-square)'])
|
|
->and($css->declarations("[data-md-button-group='standard'][data-md-shape='square'] > [aria-pressed='true']"))->toBe(['border-radius' => 'var(--md-button-group-full)']);
|
|
});
|
|
|
|
it('widens a pressed label button while its neighbours give way', function () {
|
|
$css = ComponentStylesheet::read('button-group');
|
|
|
|
expect($css->declarations("[data-md-button-group='standard'] > :not([data-md-icon-button]):active:not(:disabled, [aria-disabled='true'])"))
|
|
->toBe(['padding-inline' => 'calc(var(--md-button-group-pad) + var(--md-button-group-grow))'])
|
|
->and($css->declarations("[data-md-button-group='standard'] > :not([data-md-icon-button]):active:not(:disabled, [aria-disabled='true']) + :not([data-md-icon-button])"))
|
|
->toBe(['padding-inline-start' => 'calc(var(--md-button-group-pad) - var(--md-button-group-grow))'])
|
|
->and($css->declarations("[data-md-button-group][data-md-size='lg']"))->toMatchArray(['--md-button-group-pad' => 'var(--md-sys-measurement-space600)', '--md-button-group-grow' => '16px']);
|
|
});
|
|
|
|
it('takes over aria-pressed for a group that carries a selection', function () {
|
|
$html = (string) $this->blade('<x-button-group connected selection="single" required label="View"><x-button label="Day" value="day" :selected="true" /></x-button-group>');
|
|
|
|
expect($html)
|
|
->toContain('data-md-selection="single"')
|
|
->toContain('data-md-selection-required')
|
|
->toContain('multiple: false')
|
|
->toContain('required: true')
|
|
->toContain('value: null')
|
|
->toContain('x-on:click="press($event)"')
|
|
// No model: the group takes the state from the buttons' own aria-pressed and goes on.
|
|
->toContain('x-modelable="value"')
|
|
->and((string) $this->blade('<x-button-group connected selection="multi"><x-button label="Bold" value="bold" :selected="false" /></x-button-group>'))
|
|
->toContain('multiple: true')
|
|
->toContain('required: false')
|
|
->toContain('value: []')
|
|
->and((string) $this->blade('<x-button-group connected><x-button label="Day" /></x-button-group>'))
|
|
->not->toContain('data-md-selection')
|
|
->not->toContain('press($event)');
|
|
});
|
|
|
|
it('entangles a selection group with the property it binds, and keeps wire:model off the div', function () {
|
|
$component = new class extends Component
|
|
{
|
|
public string $view = 'week';
|
|
|
|
public function render(): string
|
|
{
|
|
return <<<'BLADE'
|
|
<div>
|
|
<x-button-group connected selection="single" required wire:model.live="view" label="View">
|
|
<x-button label="Day" value="day" :selected="$view === 'day'" />
|
|
<x-button label="Week" value="week" :selected="$view === 'week'" />
|
|
</x-button-group>
|
|
</div>
|
|
BLADE;
|
|
}
|
|
};
|
|
|
|
$html = Livewire::test($component)->assertSet('view', 'week')->html();
|
|
|
|
expect($html)
|
|
->toContain('data-md-selection="single"')
|
|
->toMatch('/value: window\.Livewire\.find\(/')
|
|
->and(substr_count($html, 'wire:model.live="view"'))->toBe(0);
|
|
});
|
|
|
|
it('never wraps a group onto a second line', function () {
|
|
expect(ComponentStylesheet::read('button-group')->declarations('[data-md-button-group]'))->toMatchArray(['display' => 'inline-flex'])
|
|
->not->toHaveKey('flex-wrap');
|
|
});
|
|
|
|
it('marks icon buttons, which keep their width when a group is pressed', function () {
|
|
expect((string) $this->blade('<x-button icon="format_bold" aria-label="Bold" />'))->toContain('data-md-icon-button')
|
|
->and((string) $this->blade('<x-button label="Bold" />'))->not->toContain('data-md-icon-button');
|
|
});
|