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

173 lines
9.2 KiB
PHP

<?php
it('draws a checkbox whose row is its label', function () {
$html = (string) $this->blade('<x-checkbox id="notify" label="Notify me" hint="On every download" wire:model="notify" class="app-field-spacing" style="order: 2" />');
expect($html)
->toContain('<div data-md-checkbox class="app-field-spacing" style="order: 2">')
->toMatch('/<label for="notify" data-md-selection-row\s*>/')
->toContain('<span data-md-checkbox-box >')
->toContain('type="checkbox"')
->toContain('wire:model="notify"')
->toContain('data-md-checkbox-check')
->toContain('data-md-checkbox-mixed')
->toContain('<span data-md-selection-label>Notify me</span>')
->toContain('<span data-md-selection-hint>On every download</span>')
->not->toContain('data-md-indeterminate')
->not->toContain('min-w-0');
});
it('fills the box with an 18px tick from the 20 optical cut', function () {
expect((string) $this->blade('<x-checkbox label="Notify me" />'))
->toContain((string) $this->blade('<x-icon name="check" size="18" data-md-checkbox-check />'))
->toContain((string) $this->blade('<x-icon name="remove" size="18" data-md-checkbox-mixed />'))
->toContain('--md-icon-size: 18px');
});
it('marks a checkbox that is partly ticked', function () {
expect((string) $this->blade('<x-checkbox label="Select all" indeterminate />'))->toContain('data-md-indeterminate');
});
it('gives a selection control without a label a 48px target', function () {
expect((string) $this->blade('<x-checkbox aria-label="Select all" />'))->toMatch('/<span data-md-checkbox-box\s+class="md-touch-target"\s*>/')
->and((string) $this->blade('<x-toggle aria-label="Public link" />'))->toMatch('/<span data-md-switch\\s+class="md-touch-target"\\s*>/')
->and((string) $this->blade('<x-radio :options="[[\'id\' => \'a\', \'name\' => \'\']]" />'))->toMatch('/<span data-md-radio-button\\s+class="md-touch-target"\\s*>/');
// A labelled control is pressed anywhere along its row, so it needs no extra target.
expect((string) $this->blade('<x-checkbox label="Notify me" />'))->not->toContain('md-touch-target')
->and((string) $this->blade('<x-toggle label="Public link" />'))->not->toContain('md-touch-target')
->and((string) $this->blade('<x-radio :options="[[\'id\' => \'a\', \'name\' => \'A\']]" />'))->not->toContain('md-touch-target');
});
it('leaves a checkbox group to the caller and says how M3 wants it laid out', function () {
$html = (string) $this->blade(<<<'BLADE'
<fieldset>
<legend>Tell me about</legend>
<div class="app-checkbox-grid">
<x-checkbox label="Downloads" name="notify[]" value="downloads" />
<x-checkbox label="Comments" name="notify[]" value="comments" />
</div>
</fieldset>
BLADE);
expect($html)
->toContain('class="app-checkbox-grid"')
->toContain('Downloads')
->toContain('Comments')
->and(substr_count($html, 'data-md-checkbox-box'))->toBe(2);
// The rule is guidance, not markup, so the component's header has to carry it.
expect(file_get_contents(__DIR__.'/../../../resources/views/components/checkbox.blade.php'))
->toContain('expanded')
->toContain('contained region');
});
it('puts a checkbox at the end of its row on request', function () {
expect((string) $this->blade('<x-checkbox label="Show" right />'))->toMatch('/<label for="check-[0-9a-f]{12}" data-md-selection-row\s+data-md-right\s*>/')
->and((string) $this->blade('<x-checkbox label="Show" />'))->not->toContain('data-md-right');
});
it('shows a checkbox\'s errors', function () {
$html = (string) $this->withViewErrors(['terms' => ['Accept the terms to continue.']])
->blade('<x-checkbox id="terms" label="I accept the terms" wire:model="terms" />');
expect($html)
->toContain('data-md-invalid')
->toContain('<div id="terms-support" data-md-selection-support role="alert">')
->toContain('aria-invalid="true"')
->toContain('aria-describedby="terms-support"')
->toContain('Accept the terms to continue.');
});
it('draws radio buttons in a fieldset named by its question', function () {
$html = (string) $this->blade(<<<'BLADE'
<x-radio label="Who can open it" wire:model.live="audience" :options="[
['id' => 'anyone', 'name' => 'Anyone with the link'],
['id' => 'password', 'name' => 'Anyone with the password', 'hint' => 'Share it separately'],
['id' => 'team', 'name' => 'My team', 'disabled' => true],
]" />
BLADE);
expect($html)
->toMatch('/<fieldset\\s+data-md-radio\\s/')
->toContain('<legend data-md-radio-legend>Who can open it</legend>')
->toContain('<div data-md-radio-options>')
->toContain('<span data-md-selection-hint>Share it separately</span>')
->not->toContain('class=')
->and(substr_count($html, 'type="radio"'))->toBe(3)
->and(substr_count($html, 'name="audience"'))->toBe(3)
->and(substr_count($html, 'wire:model.live="audience"'))->toBe(3)
->and($html)->toContain('value="password"')
->toContain('Share it separately')
->toMatch('/value="team"\s+disabled/');
});
it('names unbound radios after their group and checks the given value', function () {
$html = (string) $this->blade('<x-radio name="theme" value="dark" inline :options="[[\'id\' => \'light\', \'name\' => \'Light\'], [\'id\' => \'dark\', \'name\' => \'Dark\']]" />');
expect($html)
->toMatch('/<fieldset\\s+data-md-radio\\s+data-md-inline\\s/')
->and(substr_count($html, 'name="theme"'))->toBe(2)
->and($html)->toMatch('/value="dark"\s+checked/')
->not->toMatch('/value="light"\s+checked/');
});
it('shows the errors of a radio group', function () {
expect((string) $this->withViewErrors(['audience' => ['Choose who can open it.']])
->blade('<x-radio wire:model="audience" :options="[[\'id\' => \'a\', \'name\' => \'A\']]" />'))
->toContain('data-md-invalid')
->toMatch('/<div id="radio-[0-9a-f]{12}-support" data-md-selection-support role="alert">/')
->toContain('Choose who can open it.');
});
it('draws a switch on a checkbox with the switch role', function () {
$html = (string) $this->blade('<x-toggle id="public" label="Public link" wire:model.live="public" right />');
expect($html)
->toMatch('/<div data-md-toggle\s*>/')
->toMatch('/<label for="public" data-md-selection-row\\s+data-md-right\\s*>/')
->toContain('data-md-switch')
->toContain('role="switch"')
->toContain('type="checkbox"')
->toContain('wire:model.live="public"')
->toContain('data-md-switch-handle')
->not->toContain('class=')
->not->toContain('data-md-icons');
});
it('puts icons on a switch\'s handle', function () {
expect((string) $this->blade('<x-toggle label="Wi-Fi" icons />'))->toContain('data-md-icons="both"')->toContain('data-md-switch-on')->toContain('data-md-switch-off')
->and((string) $this->blade('<x-toggle label="Wi-Fi" icons />'))->toContain('--md-icon-size: 16px')
->and((string) $this->blade('<x-toggle label="Wi-Fi" icons="selected" />'))->toContain('data-md-icons="selected"');
});
it('draws the checkbox from its stylesheet, on the row the selection controls share', function () {
$css = (string) file_get_contents(__DIR__.'/../../../resources/css/components/checkbox.css');
expect($css)->toContain("@import './selection.css';")
->toContain("@import './icon.css';")
->toContain('@layer material.components')
->toMatch('/\\[data-md-checkbox-box\\] \\{[^}]*width: 18px;[^}]*height: 18px;/')
->and((string) file_get_contents(__DIR__.'/../../../resources/css/components/selection.css'))
->toContain('[data-md-selection-row]')
->toContain('@layer material.components')
->and((string) file_get_contents(__DIR__.'/../../../resources/css/all.css'))->toContain("@import './components/checkbox.css';");
});
it('lays the radio group out from its stylesheet, inline only from 600px', function () {
$css = (string) file_get_contents(__DIR__.'/../../../resources/css/components/radio.css');
expect($css)->toContain("@import './selection.css';")
->toMatch('/@media \\(width >= 600px\\) \\{\\s*\\[data-md-radio\\]\\[data-md-inline\\] \\[data-md-radio-options\\] \\{\\s*display: flex;/')
->and((string) file_get_contents(__DIR__.'/../../../resources/css/all.css'))->toContain("@import './components/radio.css';")
->and((string) $this->blade('<x-radio name="size" class="app-field-spacing" style="order: 1" :options="[[\'id\' => \'s\', \'name\' => \'S\']]" />'))
->toMatch('/data-md-radio\\s+class="app-field-spacing" style="order: 1"/');
});
it('draws the switch from its stylesheet', function () {
expect((string) file_get_contents(__DIR__.'/../../../resources/css/components/toggle.css'))
->toContain("@import './selection.css';")
->toMatch('/\\[data-md-switch\\] \\{[^}]*width: 52px;[^}]*height: 32px;/')
->and((string) file_get_contents(__DIR__.'/../../../resources/css/all.css'))->toContain("@import './components/toggle.css';");
});