Files
Andreas Reinhold / reiniandClaude Sonnet 5 6897306e18 Prove no Tailwind utility class remains in resources/ or tests/
StylesheetsTest.php's new test runs DesignGuard's own family table (check
(i)) over resources/views, resources/js, src, tests/Browser and
tests/Feature — the guard turned on the package's own source, so the two
never drift apart. tests/Feature/DesignGuardTest.php and
tests/Fixtures/design-guard/ carry Tailwind on purpose and are left out.

The scan found Tailwind-shaped class strings used as caller classes in 21
test files (a component test proving a caller's class lands on the root or
a caller's size wins, mostly), left over from before Tailwind cleared the
whole stack: replaced with neutral application-style names (`app-hero-icon`)
or, where one already carries the right meaning, an `md-*` class
(`md-text-end`, `md-ink-warning`). BreakpointsTest.php's own heredoc probe —
which needs a genuine Tailwind-shaped breakpoint prefix to prove its
detection works — now builds that one string from a placeholder at runtime,
so its own source stays clean for this same scan.

Plan step 42 (Phase F), Part A.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-15 09:32:36 +02:00

146 lines
8.0 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
{
preg_match('/<span\b([^>]*)>/', (string) test()->blade($blade), $tag);
preg_match_all('/([\w:-]+)="([^"]*)"/', $tag[1] ?? '', $pairs, PREG_SET_ORDER);
return collect($pairs)->mapWithKeys(fn (array $pair): array => [$pair[1] => $pair[2]])->all();
}
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('&lt;svg')
->and((string) $this->blade('<x-badge value="<b>4</b>" />'))->toContain('&lt;b&gt;4&lt;/b&gt;')
->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 each colour from its pair of roles, and neutral without a hue', function (string $color, array $roles) {
expect(ComponentStylesheet::read('badge')->declarations("[data-md-badge][data-md-color='{$color}']"))->toBe([
'--md-badge-color' => "var(--md-sys-color-{$roles[0]})",
'--md-badge-on-color' => "var(--md-sys-color-{$roles[1]})",
'--md-badge-container' => "var(--md-sys-color-{$roles[2]})",
'--md-badge-on-container' => "var(--md-sys-color-{$roles[3]})",
]);
})->with([
'primary' => ['primary', ['primary', 'on-primary', 'primary-container', 'on-primary-container']],
'secondary' => ['secondary', ['secondary', 'on-secondary', 'secondary-container', 'on-secondary-container']],
'tertiary' => ['tertiary', ['tertiary', 'on-tertiary', 'tertiary-container', 'on-tertiary-container']],
'success' => ['success', ['success', 'on-success', 'success-container', 'on-success-container']],
'warning' => ['warning', ['warning', 'on-warning', 'warning-container', 'on-warning-container']],
'info' => ['info', ['info', 'on-info', 'info-container', 'on-info-container']],
'neutral' => ['neutral', ['on-surface-variant', 'surface', 'surface-container-high', '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');
expect($css->declarations('[data-md-badge]'))->toMatchArray([
'--md-badge-color' => 'var(--md-sys-color-error)',
'--md-badge-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']);
});