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>
137 lines
7.3 KiB
PHP
137 lines
7.3 KiB
PHP
<?php
|
|
|
|
use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
|
|
|
|
/**
|
|
* The attributes of the rendered badge, by name.
|
|
*
|
|
* @return array<string, string>
|
|
*/
|
|
function badgeAttributes(string $blade): array
|
|
{
|
|
return layoutRoot((string) test()->blade($blade), 'span');
|
|
}
|
|
|
|
it('is M3\'s small badge without a value, hidden from screen readers', function () {
|
|
expect(badgeAttributes('<x-badge />'))->toBe([
|
|
'data-md-badge' => 'data-md-badge',
|
|
'data-md-dot' => 'data-md-dot',
|
|
'data-md-color' => 'error',
|
|
'aria-hidden' => 'true',
|
|
])
|
|
->and(ComponentStylesheet::read('badge')->declarations('[data-md-badge][data-md-dot]'))->toBe([
|
|
'inline-size' => '6px',
|
|
'block-size' => '6px',
|
|
'border-radius' => 'var(--md-sys-shape-corner-full)',
|
|
]);
|
|
});
|
|
|
|
it('is M3\'s large badge with a count, capped by max', function () {
|
|
$css = ComponentStylesheet::read('badge');
|
|
|
|
expect((string) $this->blade('<x-badge value="4" />'))->toContain('>4</span>')->not->toContain('data-md-dot')->not->toContain('data-md-variant')
|
|
->and((string) $this->blade('<x-badge value="1204" max="999" />'))->toContain('>999+</span>')
|
|
->and((string) $this->blade('<x-badge value="12" max="999" color="primary" />'))->toContain('>12</span>')->toContain('data-md-color="primary"')
|
|
->and($css->declarations('[data-md-badge]:not([data-md-dot], [data-md-variant])'))->toMatchArray([
|
|
'min-inline-size' => '16px',
|
|
'block-size' => '16px',
|
|
'border-radius' => 'var(--md-sys-shape-corner-full)',
|
|
'padding-inline' => 'var(--md-sys-measurement-space50)',
|
|
'font' => 'var(--md-sys-typescale-label-sm)',
|
|
'font-variant-numeric' => 'tabular-nums',
|
|
]);
|
|
});
|
|
|
|
it('pins itself to the corner of an icon when floating', function () {
|
|
$css = ComponentStylesheet::read('badge');
|
|
|
|
expect((string) $this->blade('<x-badge floating />'))->toContain('data-md-floating')
|
|
->and(badgeAttributes('<x-badge value="3" floating label="3 new messages" />'))
|
|
->toMatchArray(['data-md-floating' => 'data-md-floating', 'aria-label' => '3 new messages'])
|
|
->not->toHaveKey('aria-hidden')
|
|
->and($css->declarations('[data-md-badge][data-md-floating]'))->toBe([
|
|
'position' => 'absolute',
|
|
'inset-block-start' => 'calc(-1 * var(--md-sys-measurement-space25))',
|
|
'inset-inline-start' => 'calc(100% - 12px)',
|
|
])
|
|
->and($css->declarations('[data-md-badge][data-md-floating][data-md-dot]'))->toBe(['inset-block-start' => '0', 'inset-inline' => 'auto 0']);
|
|
});
|
|
|
|
it('draws a status label in a container or an outline, spoken like any label', function () {
|
|
$css = ComponentStylesheet::read('badge');
|
|
|
|
expect(badgeAttributes('<x-badge value="Active" tone="success" tonal />'))
|
|
->toBe(['data-md-badge' => 'data-md-badge', 'data-md-variant' => 'tonal', 'data-md-color' => 'success'])
|
|
->and(badgeAttributes('<x-badge value="Pro" outline />'))
|
|
->toBe(['data-md-badge' => 'data-md-badge', 'data-md-variant' => 'outline', 'data-md-color' => 'error'])
|
|
->and($css->declarations('[data-md-badge][data-md-variant]'))->toMatchArray([
|
|
'gap' => 'var(--md-sys-measurement-space50)',
|
|
'block-size' => '24px',
|
|
'border-radius' => 'var(--md-sys-shape-corner-sm)',
|
|
'padding-inline' => 'var(--md-sys-measurement-space100)',
|
|
'font' => 'var(--md-sys-typescale-label-md)',
|
|
])
|
|
->and($css->declarations("[data-md-badge]:not([data-md-color='plain'])[data-md-variant='tonal']"))->toBe([
|
|
'background-color' => 'var(--md-badge-container)',
|
|
'color' => 'var(--md-badge-on-container)',
|
|
])
|
|
// The outline role, not outline-variant: a badge needs 3:1 against what is behind it.
|
|
->and($css->declarations("[data-md-badge]:not([data-md-color='plain'])[data-md-variant='outline']"))->toBe([
|
|
'border-color' => 'var(--md-sys-color-outline)',
|
|
'background-color' => 'transparent',
|
|
'color' => 'var(--md-sys-color-on-surface-variant)',
|
|
])
|
|
->and($css->declarations("[data-md-badge][data-md-variant='outline']"))->toBe(['border' => '1px solid']);
|
|
});
|
|
|
|
it('resolves solid over tonal, and either over outline', function () {
|
|
expect(badgeAttributes('<x-badge value="Built in" solid tonal outline color="primary" />'))->toMatchArray(['data-md-variant' => 'solid', 'data-md-color' => 'primary'])
|
|
->and(badgeAttributes('<x-badge value="Built in" tonal outline />'))->toMatchArray(['data-md-variant' => 'tonal'])
|
|
->and(badgeAttributes('<x-badge solid />'))->toMatchArray(['data-md-dot' => 'data-md-dot'])->not->toHaveKey('data-md-variant')
|
|
->and((string) $this->blade('<x-badge value="Built in" solid color="primary" />'))->toContain('>Built in<')->not->toContain('aria-hidden');
|
|
});
|
|
|
|
it('renders its slot as HTML, and is a dot while the slot holds nothing but comments', function () {
|
|
$html = (string) $this->blade('<x-badge tonal><x-icon name="bolt" size="12" /> Pro</x-badge>');
|
|
|
|
expect($html)
|
|
->toContain('data-md-variant="tonal"')
|
|
->toContain('<svg')
|
|
->toContain(' Pro</span>')
|
|
->not->toContain('<svg')
|
|
->and((string) $this->blade('<x-badge value="<b>4</b>" />'))->toContain('<b>4</b>')
|
|
->and((string) $this->blade("<x-badge tonal>\n <!-- nothing yet -->\n</x-badge>"))->toContain('data-md-dot')
|
|
->and((string) $this->blade('<x-badge max="99">120</x-badge>'))->toContain('>99+</span>');
|
|
});
|
|
|
|
it('paints neutral from its own pair of roles, the one hue the shared table has none for', function () {
|
|
expect(ComponentStylesheet::read('badge')->declarations("[data-md-badge][data-md-color='neutral']"))->toBe([
|
|
'--md-badge-color' => 'var(--md-sys-color-on-surface-variant)',
|
|
'--md-badge-on-color' => 'var(--md-sys-color-surface)',
|
|
'--md-badge-container' => 'var(--md-sys-color-surface-container-high)',
|
|
'--md-badge-on-container' => 'var(--md-sys-color-on-surface-variant)',
|
|
]);
|
|
});
|
|
|
|
it('is error by default, and falls back to error on a colour it does not know', function () {
|
|
$css = ComponentStylesheet::read('badge');
|
|
|
|
// Every other hue comes from the shared colour-role table (color.css, ColorTest.php): badge.css
|
|
// only has to give its own prefixed variables a fallback for when `data-md-color` is absent.
|
|
expect($css->declarations('[data-md-badge]'))->toMatchArray([
|
|
'--md-badge-color' => 'var(--md-color, var(--md-sys-color-error))',
|
|
'--md-badge-on-color' => 'var(--md-on-color, var(--md-sys-color-on-error))',
|
|
])
|
|
->and($css->declarations("[data-md-badge]:not([data-md-color='plain'])"))->toBe([
|
|
'background-color' => 'var(--md-badge-color)',
|
|
'color' => 'var(--md-badge-on-color)',
|
|
])
|
|
->and(badgeAttributes('<x-badge value="3" color="sport-run" />'))->toMatchArray(['data-md-color' => 'error']);
|
|
});
|
|
|
|
it('leaves a plain badge, and every colour, to the caller\'s CSS', function () {
|
|
// `plain` matches none of the colour rules; a caller's class lands on the root untouched.
|
|
expect(badgeAttributes('<x-badge value="Run" color="plain" tonal class="app-badge-accent" />'))
|
|
->toMatchArray(['data-md-color' => 'plain', 'data-md-variant' => 'tonal', 'class' => 'app-badge-accent']);
|
|
});
|