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>
138 lines
6.9 KiB
PHP
138 lines
6.9 KiB
PHP
<?php
|
|
|
|
use Illuminate\Support\Facades\File;
|
|
use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
|
|
|
|
beforeEach(function () {
|
|
$this->path = sys_get_temp_dir().'/scheme-picker-'.uniqid().'.json';
|
|
File::put($this->path, json_encode([
|
|
'default' => 'indigo',
|
|
'profiles' => [
|
|
'indigo' => ['label' => 'Indigo', 'light' => ['primary' => '#4f46e5', 'secondary' => '#5b5d72', 'tertiary' => '#77536d'], 'dark' => ['primary' => '#c0c1ff', 'secondary' => '#c4c5dd', 'tertiary' => '#e6bad7']],
|
|
'teal' => ['label' => 'Teal', 'light' => ['primary' => '#00897b', 'secondary' => '#4a635f', 'tertiary' => '#456179'], 'dark' => ['primary' => '#80cbc4', 'secondary' => '#b1ccc6', 'tertiary' => '#adcae5']],
|
|
],
|
|
]));
|
|
config(['livewire-material.scheme' => $this->path]);
|
|
});
|
|
|
|
afterEach(function () {
|
|
File::delete($this->path);
|
|
});
|
|
|
|
it('draws a radio per generated profile, bound, named and previewing on change', function () {
|
|
$html = (string) $this->blade('<x-scheme-picker label="Colour profile" hint="Applies after saving" wire:model="colorProfile" />');
|
|
|
|
expect($html)
|
|
->toContain('<legend data-md-scheme-picker-legend class="md-type-label-lg md-ink-variant">Colour profile</legend>')
|
|
->toContain('Applies after saving')
|
|
->toContain('data-md-scheme-picker-option="indigo"')
|
|
->toContain('data-md-scheme-picker-option="teal"')
|
|
->toContain('class="md-visually-hidden"')
|
|
->toMatch('/<input\s+class="md-visually-hidden"\s+wire:model="colorProfile"\s+type="radio"\s+name="colorProfile"\s+value="teal"/')
|
|
->toContain('x-on:change="$store.theme.previewScheme($event.target.value)"')
|
|
->toContain('>Teal</span>')
|
|
->toContain('style="--swatch-light: #00897b; --swatch-dark: #80cbc4"');
|
|
});
|
|
|
|
it('puts its other attributes on the group and the binding on the radios', function () {
|
|
$html = (string) $this->blade('<x-scheme-picker wire:model="colorProfile" data-test="color-profile" class="app-scheme-picker-gap" />');
|
|
|
|
expect($html)->toMatch('/<fieldset\s+x-data="\{ swatches: [^"]+\}"\s+data-md-scheme-picker\s+data-test="color-profile" class="app-scheme-picker-gap"\s*>/')
|
|
->and(substr_count($html, 'wire:model="colorProfile"'))->toBe(2)
|
|
->and(substr_count($html, 'data-test="color-profile"'))->toBe(1);
|
|
});
|
|
|
|
it('lays the swatch cards out 2 up below medium and 4 up from it', function () {
|
|
$css = ComponentStylesheet::read('scheme-picker');
|
|
|
|
expect($css->declarations('[data-md-scheme-picker-options]'))
|
|
->toBe([
|
|
'display' => 'grid',
|
|
'grid-template-columns' => 'repeat(2, 1fr)',
|
|
'gap' => 'var(--md-sys-measurement-space100)',
|
|
])
|
|
->and($css->declarations('[data-md-scheme-picker-options]', ['@media (width >= 600px)']))
|
|
->toBe(['grid-template-columns' => 'repeat(4, 1fr)']);
|
|
});
|
|
|
|
it('marks the chosen card through the radio inside, read with :has()', function () {
|
|
$css = ComponentStylesheet::read('scheme-picker');
|
|
|
|
expect($css->declarations('[data-md-scheme-picker-option]:has(:checked)'))
|
|
->toBe([
|
|
'border-color' => 'transparent',
|
|
'background-color' => 'var(--md-sys-color-secondary-container)',
|
|
'color' => 'var(--md-sys-color-on-secondary-container)',
|
|
])
|
|
->and($css->declarations('[data-md-scheme-picker-option]:has(:checked) [data-md-scheme-picker-swatch]'))
|
|
->toBe(['box-shadow' => '0 0 0 2px var(--md-sys-color-secondary-container)'])
|
|
->and($css->declarations('[data-md-scheme-picker-option]:has(:checked) [data-md-scheme-picker-check]'))
|
|
->toBe(['display' => 'block']);
|
|
});
|
|
|
|
it('swaps a swatch to its dark colour under [data-theme=dark], never by class', function () {
|
|
$css = ComponentStylesheet::read('scheme-picker');
|
|
|
|
expect($css->declarations('[data-md-scheme-picker-swatch]'))
|
|
->toHaveKey('background-color', 'var(--swatch-light)')
|
|
->and($css->declarations("[data-theme='dark'] [data-md-scheme-picker-swatch]"))
|
|
->toBe(['background-color' => 'var(--swatch-dark)']);
|
|
});
|
|
|
|
it('swaps the dots to the contrast level on screen', function () {
|
|
File::put($this->path, json_encode([
|
|
'default' => 'indigo',
|
|
'profiles' => [
|
|
'indigo' => [
|
|
'label' => 'Indigo',
|
|
'light' => ['primary' => '#4f46e5', 'secondary' => '#5b5d72', 'tertiary' => '#77536d'],
|
|
'dark' => ['primary' => '#c0c1ff', 'secondary' => '#c4c5dd', 'tertiary' => '#e6bad7'],
|
|
'contrast' => ['high' => ['light' => ['primary' => '#241bb0'], 'dark' => ['primary' => '#eeeaff']]],
|
|
],
|
|
],
|
|
]));
|
|
|
|
$html = (string) $this->blade('<x-scheme-picker wire:model="colorProfile" />');
|
|
|
|
expect($html)
|
|
// Standard is the inline pair, so a dot is painted before Alpine runs.
|
|
->toContain('style="--swatch-light: #4f46e5; --swatch-dark: #c0c1ff"')
|
|
->toContain('\u0022standard\u0022:{\u0022light\u0022:{\u0022primary\u0022:\u0022#4f46e5\u0022')
|
|
->toContain('\u0022high\u0022:{\u0022light\u0022:{\u0022primary\u0022:\u0022#241bb0\u0022')
|
|
// A level the file does not carry repeats the standard colours rather than none.
|
|
->toContain('\u0022medium\u0022:{\u0022light\u0022:{\u0022primary\u0022:\u0022#4f46e5\u0022')
|
|
->toContain('swatches[\'indigo\'][$store.theme.resolvedContrast].light[\'primary\']')
|
|
->toContain('swatches[\'indigo\'][$store.theme.resolvedContrast].dark[\'tertiary\']');
|
|
});
|
|
|
|
it('keeps its inline styles to the scheme file\'s checked colours', function () {
|
|
File::put($this->path, json_encode([
|
|
'default' => 'indigo',
|
|
'profiles' => ['indigo' => ['label' => 'Indigo', 'light' => ['primary' => 'red; background: url(x)'], 'dark' => []]],
|
|
]));
|
|
|
|
$html = (string) $this->blade('<x-scheme-picker wire:model="colorProfile" />');
|
|
|
|
// Only the swatches' own style: the check icon's --md-icon-size is a size prop, not a colour
|
|
// the scheme file supplied.
|
|
preg_match_all('/data-md-scheme-picker-swatch\s+style="([^"]*)"/', $html, $styles);
|
|
|
|
expect($styles[1])->not->toBeEmpty()
|
|
->each->toMatch('/^--swatch-light: #[0-9a-fA-F]{6}; --swatch-dark: #[0-9a-fA-F]{6}$/')
|
|
// The colours Alpine swaps in are the same filtered ones, never the file's own text.
|
|
->and($html)->not->toContain('url(');
|
|
});
|
|
|
|
it('shows a validation message for the bound property instead of the hint', function () {
|
|
$this->withViewErrors(['colorProfile' => ['Choose one of the colour profiles.']])
|
|
->blade('<x-scheme-picker wire:model="colorProfile" hint="Applies after saving" />')
|
|
->assertSee('Choose one of the colour profiles.')
|
|
->assertDontSee('Applies after saving');
|
|
});
|
|
|
|
it('renders nothing for a single scheme', function () {
|
|
File::put($this->path, json_encode(['light' => ['primary' => '#123456'], 'dark' => []]));
|
|
|
|
expect(trim((string) $this->blade('<x-scheme-picker wire:model="colorProfile" />')))->toBe('');
|
|
});
|