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>
131 lines
6.8 KiB
PHP
131 lines
6.8 KiB
PHP
<?php
|
|
|
|
use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
|
|
|
|
it('is a named, determinate progressbar with its value', function () {
|
|
expect((string) $this->blade('<x-progress value="42" />'))
|
|
->toContain('data-md-progress')
|
|
->toContain('role="progressbar"')
|
|
->toContain('aria-label="Progress"')
|
|
->toContain('aria-valuemin="0"')
|
|
->toContain('aria-valuemax="100"')
|
|
->toContain('aria-valuenow="42"')
|
|
->toContain('data-md-value="42"')
|
|
->toContain('data-md-color="primary"')
|
|
->toContain('x-data="materialProgress"');
|
|
});
|
|
|
|
it('leaves out aria-valuenow while indeterminate', function () {
|
|
expect((string) $this->blade('<x-progress label="Uploading" />'))
|
|
->toContain('role="progressbar"')
|
|
->toContain('aria-label="Uploading"')
|
|
->not->toContain('aria-valuenow')
|
|
->not->toContain('data-md-value');
|
|
});
|
|
|
|
it('says nothing where something else already does', function () {
|
|
expect((string) $this->blade('<x-progress value="10" :label="false" />'))
|
|
->toContain('aria-hidden="true"')
|
|
->not->toContain('role="progressbar"')
|
|
->not->toContain('aria-label')
|
|
->not->toContain('aria-valuenow');
|
|
});
|
|
|
|
it('clamps the value into its range', function (string $template, string $now, string $max) {
|
|
expect((string) $this->blade($template))
|
|
->toContain("aria-valuenow=\"{$now}\"")
|
|
->toContain("aria-valuemax=\"{$max}\"")
|
|
->toContain("data-md-value=\"{$now}\"");
|
|
})->with([
|
|
'below zero' => ['<x-progress :value="-5" />', '0', '100'],
|
|
'above the maximum' => ['<x-progress :value="250" />', '100', '100'],
|
|
'a custom maximum' => ['<x-progress :value="3.5" :max="5" />', '3.5', '5'],
|
|
'a maximum that cannot be one' => ['<x-progress :value="30" :max="0" />', '30', '100'],
|
|
]);
|
|
|
|
it('draws the first frame on the server, at the value', function () {
|
|
expect((string) $this->blade('<x-progress value="42" />'))
|
|
->toContain('<svg x="42%" overflow="visible">')
|
|
->toContain('x2="42%"')
|
|
->toContain('<circle cx="100%"')
|
|
->toContain('wire:ignore');
|
|
|
|
expect((string) $this->blade('<x-progress value="25" circular />'))
|
|
->toContain('viewBox="0 0 40 40"')
|
|
->toContain('<path d="M20 2A18 18 0 0 1 38 20" stroke="currentColor" />')
|
|
->toContain('<path d="M36.579 27.01A18 18 0 1 1 12.99 3.421" />');
|
|
});
|
|
|
|
it('draws the active indicator in the colour and the track in its container', function (string $color) {
|
|
// Every hue but primary comes straight off the shared colour-role table (color.css,
|
|
// ColorTest.php): `stroke` reads `--md-container`, which is the hue's own container for
|
|
// every one of them.
|
|
expect((string) $this->blade('<x-progress value="50" :color="$color" />', ['color' => $color]))
|
|
->toContain("data-md-color=\"{$color}\"");
|
|
})->with(['secondary', 'tertiary', 'error']);
|
|
|
|
it('falls back to primary for an unknown colour, secondary-container track included', function () {
|
|
$css = ComponentStylesheet::read('progress');
|
|
|
|
expect((string) $this->blade('<x-progress value="50" color="purple" />'))->toContain('data-md-color="primary"')
|
|
->and($css->declarations('[data-md-progress]'))->toMatchArray([
|
|
'color' => 'var(--md-color, var(--md-sys-color-primary))',
|
|
'stroke' => 'var(--md-container, var(--md-sys-color-secondary-container))',
|
|
])
|
|
// Unlike every other hue, primary keeps a track of its own: the shared table's `primary`
|
|
// entry is plain primary-container, which progress does not want.
|
|
->and($css->declarations("[data-md-progress][data-md-color='primary']"))->toBe([
|
|
'stroke' => 'var(--md-sys-color-secondary-container)',
|
|
]);
|
|
});
|
|
|
|
it('sizes each shape as M3 does', function (string $template, array $flags, string $selector, array $declarations) {
|
|
$html = (string) $this->blade($template);
|
|
|
|
foreach (['data-md-circular', 'data-md-wavy', 'data-md-thick'] as $flag) {
|
|
in_array($flag, $flags, true)
|
|
? expect($html)->toContain($flag)
|
|
: expect($html)->not->toContain($flag);
|
|
}
|
|
|
|
expect(ComponentStylesheet::read('progress')->declarations($selector))->toMatchArray($declarations);
|
|
})->with([
|
|
'linear' => ['<x-progress />', [], '[data-md-progress]:not([data-md-circular])', ['height' => '4px']],
|
|
'linear thick' => ['<x-progress thick />', ['data-md-thick'], '[data-md-progress]:not([data-md-circular])[data-md-thick]', ['height' => '8px']],
|
|
'linear wavy' => ['<x-progress wavy />', ['data-md-wavy'], '[data-md-progress]:not([data-md-circular])[data-md-wavy]', ['height' => '10px']],
|
|
'linear wavy thick' => ['<x-progress wavy thick />', ['data-md-wavy', 'data-md-thick'], '[data-md-progress]:not([data-md-circular])[data-md-wavy][data-md-thick]', ['height' => '14px']],
|
|
'circular' => ['<x-progress circular />', ['data-md-circular'], '[data-md-progress][data-md-circular]', ['width' => '40px', 'height' => '40px']],
|
|
'circular thick' => ['<x-progress circular thick />', ['data-md-circular', 'data-md-thick'], '[data-md-progress][data-md-circular][data-md-thick]', ['width' => '44px', 'height' => '44px']],
|
|
'circular wavy' => ['<x-progress circular wavy />', ['data-md-circular', 'data-md-wavy'], '[data-md-progress][data-md-circular][data-md-wavy]', ['width' => '48px', 'height' => '48px']],
|
|
'circular wavy thick' => ['<x-progress circular wavy thick />', ['data-md-circular', 'data-md-wavy', 'data-md-thick'], '[data-md-progress][data-md-circular][data-md-wavy][data-md-thick]', ['width' => '52px', 'height' => '52px']],
|
|
]);
|
|
|
|
it('takes the caller\'s size instead of its own, its class landing on the root untouched', function () {
|
|
expect((string) $this->blade('<x-progress circular wavy class="app-upload-progress" />'))
|
|
->toContain('class="app-upload-progress"')
|
|
->toContain('data-md-circular')
|
|
->toContain('data-md-wavy');
|
|
|
|
expect((string) $this->blade('<x-progress class="app-progress-bar" />'))
|
|
->toContain('class="app-progress-bar"')
|
|
->not->toContain('data-md-circular');
|
|
});
|
|
|
|
it('mirrors the linear indicator in a right-to-left document, unconditionally', function () {
|
|
$css = ComponentStylesheet::read('progress');
|
|
|
|
expect($css->declarations("[data-md-progress]:not([data-md-circular]):is([dir='rtl'], [dir='rtl'] *)"))->toBe(['rotate' => '180deg'])
|
|
->and($css->has("[data-md-progress][data-md-circular]:is([dir='rtl'], [dir='rtl'] *)"))->toBeFalse();
|
|
});
|
|
|
|
it('follows a value bound in Alpine', function () {
|
|
$html = (string) $this->blade('<x-progress bind="upload.progress" max="1" />');
|
|
|
|
expect($html)
|
|
->toContain('x-bind:data-md-value="upload.progress"')
|
|
->toContain('x-bind:aria-valuenow="((v) => v === null')
|
|
->toContain('Math.min(Math.max(Number(v), 0), 1))(upload.progress)')
|
|
->toContain('data-md-max="1"')
|
|
->not->toContain(' data-md-value=');
|
|
});
|