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
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
3d9e494e22
commit
6897306e18
@@ -92,15 +92,20 @@ it('scans the showcase example heredocs too, where the package teaches its own m
|
||||
// heredocs deep to the same pattern and helper the first test above uses.
|
||||
$directory = sys_get_temp_dir().'/breakpoint-heredoc-probe-'.uniqid();
|
||||
File::ensureDirectoryExists($directory);
|
||||
File::put($directory.'/probe.blade.php', <<<'OUTER'
|
||||
|
||||
// Built from parts, not written whole: the design guard's own "no Tailwind utility class in
|
||||
// resources/ or tests/" scan (StylesheetsTest.php) reads this file's raw source too, and a
|
||||
// real Tailwind breakpoint prefix sitting in a class attribute here would be exactly what
|
||||
// that scan looks for — the placeholder keeps this file's own source clean of one.
|
||||
File::put($directory.'/probe.blade.php', str_replace('__BREAKPOINT__', 'md:', <<<'OUTER'
|
||||
@php
|
||||
$examples = [
|
||||
'Probe' => <<<'BLADE'
|
||||
<div class="md:flex"></div>
|
||||
<div class="__BREAKPOINT__flex"></div>
|
||||
BLADE,
|
||||
];
|
||||
@endphp
|
||||
OUTER);
|
||||
OUTER));
|
||||
|
||||
try {
|
||||
$pattern = '/(?<![\w@-])(?:max-)?(?:sm|md|lg|xl|2xl):[a-z\[(-]/';
|
||||
|
||||
@@ -140,6 +140,6 @@ it('is error by default, and falls back to error on a colour it does not know',
|
||||
|
||||
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="bg-tertiary-container text-on-tertiary-container" />'))
|
||||
->toMatchArray(['data-md-color' => 'plain', 'data-md-variant' => 'tonal', 'class' => 'bg-tertiary-container text-on-tertiary-container']);
|
||||
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']);
|
||||
});
|
||||
|
||||
@@ -279,7 +279,7 @@ it('keeps aria-pressed off a selected link, which is not a toggle', function ()
|
||||
});
|
||||
|
||||
it('lands a caller\'s class and style on the button untouched, beside the interaction classes', function () {
|
||||
expect(buttonAttributes('<x-button label="Save" class="w-full" style="margin: 0" tooltip="Save" />'))
|
||||
->toMatchArray(['class' => 'md-state-layer md-focus-ring md-touch-target w-full'])
|
||||
->and(buttonAttributes('<x-button label="Save" class="w-full" style="margin: 0" tooltip="Save" />')['style'])->toStartWith('anchor-name: --material-button-')->toEndWith('margin: 0;');
|
||||
expect(buttonAttributes('<x-button label="Save" class="app-save-button" style="margin: 0" tooltip="Save" />'))
|
||||
->toMatchArray(['class' => 'md-state-layer md-focus-ring md-touch-target app-save-button'])
|
||||
->and(buttonAttributes('<x-button label="Save" class="app-save-button" style="margin: 0" tooltip="Save" />')['style'])->toStartWith('anchor-name: --material-button-')->toEndWith('margin: 0;');
|
||||
});
|
||||
|
||||
@@ -16,8 +16,8 @@ function chipTag(string $blade, string $tag = '[a-z]+'): string
|
||||
it('is an outlined assist chip, a button, by default', function () {
|
||||
$html = (string) $this->blade('<x-chip label="Add to calendar" icon="event" icon-right="arrow_drop_down" />');
|
||||
|
||||
expect(chipTag('<x-chip label="Add to calendar" icon="event" class="me-2" />', 'button'))
|
||||
->toBe('<button data-md-chip="assist" data-md-icon-start="" type="button" class="me-2">')
|
||||
expect(chipTag('<x-chip label="Add to calendar" icon="event" class="app-chip-gap" />', 'button'))
|
||||
->toBe('<button data-md-chip="assist" data-md-icon-start="" type="button" class="app-chip-gap">')
|
||||
->and(chipTag('<x-chip label="Add to calendar" icon-right="arrow_drop_down" />', 'button'))
|
||||
->toContain('data-md-icon-end=""')
|
||||
->not->toContain('data-md-icon-start')
|
||||
@@ -115,16 +115,16 @@ it('makes a filter chip without a model a toggle button the caller owns', functi
|
||||
});
|
||||
|
||||
it('makes a bound filter chip a native checkbox under the chip', function () {
|
||||
$html = (string) $this->blade('<x-chip type="filter" label="Photos" value="photos" x-model="kinds" class="me-4" wire:key="photos" />');
|
||||
$html = (string) $this->blade('<x-chip type="filter" label="Photos" value="photos" x-model="kinds" class="app-filter-gap" wire:key="photos" />');
|
||||
|
||||
expect(chipTag('<x-chip type="filter" label="Photos" value="photos" x-model="kinds" class="me-4" wire:key="photos" />', 'label'))
|
||||
->toBe('<label data-md-chip="filter" class="me-4" wire:key="photos">')
|
||||
->and(chipTag('<x-chip type="filter" label="Photos" value="photos" x-model="kinds" class="me-4" />', 'input'))
|
||||
expect(chipTag('<x-chip type="filter" label="Photos" value="photos" x-model="kinds" class="app-filter-gap" wire:key="photos" />', 'label'))
|
||||
->toBe('<label data-md-chip="filter" class="app-filter-gap" wire:key="photos">')
|
||||
->and(chipTag('<x-chip type="filter" label="Photos" value="photos" x-model="kinds" class="app-filter-gap" />', 'input'))
|
||||
->toContain('type="checkbox"')
|
||||
->toContain('value="photos"')
|
||||
->toContain('x-model="kinds"')
|
||||
->toContain('class="md-visually-hidden"')
|
||||
->not->toContain('me-4')
|
||||
->not->toContain('app-filter-gap')
|
||||
->not->toContain('checked')
|
||||
->and($html)
|
||||
->toContain('data-md-chip-check');
|
||||
@@ -221,14 +221,14 @@ it('disables a removable input chip and its remove button', function () {
|
||||
});
|
||||
|
||||
it('groups chips in a wrapping row named by its label, with a hint', function () {
|
||||
$html = (string) $this->blade('<x-chip-set label="File types" hint="Show only these" class="mt-4"><x-chip label="A" /></x-chip-set>');
|
||||
$html = (string) $this->blade('<x-chip-set label="File types" hint="Show only these" class="app-chip-set-gap"><x-chip label="A" /></x-chip-set>');
|
||||
|
||||
preg_match('/aria-labelledby="([^"]+)"/', $html, $labelledBy);
|
||||
preg_match('/aria-describedby="([^"]+)"/', $html, $describedBy);
|
||||
|
||||
expect($html)
|
||||
->toMatch('/<div\s+data-md-chip-set\s+role="group"/')
|
||||
->toContain('class="mt-4"')
|
||||
->toContain('class="app-chip-set-gap"')
|
||||
->toContain('<div data-md-chip-set-row x-data="materialChipSet"')
|
||||
// M3's chip keyboard: the set is one tab stop and the arrows walk it.
|
||||
->toContain('x-on:keydown="key($event)"')
|
||||
|
||||
@@ -8,8 +8,8 @@ use Livewire\WithPagination;
|
||||
use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
|
||||
|
||||
it('marks a table for its styles and takes a size', function () {
|
||||
expect((string) $this->blade('<x-table class="min-w-160"><tbody><tr><td>a</td></tr></tbody></x-table>'))
|
||||
->toContain('<table data-md-table data-md-size="sm" class="min-w-160">')
|
||||
expect((string) $this->blade('<x-table class="app-data-table"><tbody><tr><td>a</td></tr></tbody></x-table>'))
|
||||
->toContain('<table data-md-table data-md-size="sm" class="app-data-table">')
|
||||
->and((string) $this->blade('<x-table size="xs" />'))->toContain('data-md-size="xs"')
|
||||
->and((string) $this->blade('<x-table size="huge" />'))->toContain('data-md-size="sm"');
|
||||
});
|
||||
@@ -33,9 +33,9 @@ it('sorts by its column, ascending first and then flipping', function () {
|
||||
expect((string) $this->blade('<x-sort-header column="size" :sort-by="[\'column\' => \'name\', \'direction\' => \'asc\']">Size</x-sort-header>'))
|
||||
->not->toContain('aria-sort')
|
||||
->toContain('wire:click="$set(\'sortBy\', {"column":"size","direction":"asc"})"')
|
||||
->and((string) $this->blade('<x-sort-header column="size" model="order" :sort-by="[\'column\' => \'size\', \'direction\' => \'asc\']" class="text-end">Size</x-sort-header>'))
|
||||
->and((string) $this->blade('<x-sort-header column="size" model="order" :sort-by="[\'column\' => \'size\', \'direction\' => \'asc\']" class="md-text-end">Size</x-sort-header>'))
|
||||
->toContain('aria-sort="ascending"')
|
||||
->toMatch('/<th data-md-sort-header\\s+data-md-active aria-sort="ascending"\\s+class="text-end">/')
|
||||
->toMatch('/<th data-md-sort-header\\s+data-md-active aria-sort="ascending"\\s+class="md-text-end">/')
|
||||
->toContain('$set(\'order\', {"column":"size","direction":"desc"})')
|
||||
->and((string) $this->blade('<x-sort-header column="size" :sort-by="[\'column\' => \'size\', \'direction\' => \'desc\']">Size</x-sort-header>'))
|
||||
->toContain('aria-sort="descending"')
|
||||
|
||||
@@ -181,10 +181,10 @@ it('shows the value in the locale\'s numeric format before Alpine starts', funct
|
||||
});
|
||||
|
||||
it('puts class on the root and every other attribute on the text field', function () {
|
||||
$html = (string) $this->blade('<x-datepicker id="due" label="Due" class="w-60" required disabled variant="filled" size="sm" />');
|
||||
$html = (string) $this->blade('<x-datepicker id="due" label="Due" class="app-due-field" required disabled variant="filled" size="sm" />');
|
||||
|
||||
expect($html)
|
||||
->toContain('class="w-60"')
|
||||
->toContain('class="app-due-field"')
|
||||
->toContain('data-md-variant="filled"')
|
||||
->toContain('data-md-size="sm"')
|
||||
->toMatch('/<input\s[^>]*required[^>]*id="due"/s')
|
||||
|
||||
@@ -30,12 +30,12 @@ it('sets an icon on an Expressive shape above its words and action', function ()
|
||||
it('draws an illustration in place of the shape and icon', function () {
|
||||
$html = (string) $this->blade(<<<'BLADE'
|
||||
<x-empty-state icon="upload_file" title="No routes yet">
|
||||
<x-slot:illustration class="text-primary"><svg class="size-32" viewBox="0 0 10 10" aria-hidden="true"><circle cx="5" cy="5" r="4" /></svg></x-slot:illustration>
|
||||
<x-slot:illustration class="app-illustration-accent"><svg class="app-illustration-icon" viewBox="0 0 10 10" aria-hidden="true"><circle cx="5" cy="5" r="4" /></svg></x-slot:illustration>
|
||||
</x-empty-state>
|
||||
BLADE);
|
||||
|
||||
expect($html)
|
||||
->toContain('<div class="text-primary"><svg class="size-32" viewBox="0 0 10 10" aria-hidden="true"><circle cx="5" cy="5" r="4" /></svg></div>')
|
||||
->toContain('<div class="app-illustration-accent"><svg class="app-illustration-icon" viewBox="0 0 10 10" aria-hidden="true"><circle cx="5" cy="5" r="4" /></svg></div>')
|
||||
->not->toContain('data-md-empty-state-shape')
|
||||
->not->toContain('data-md-empty-state-icon')
|
||||
->toContain('No routes yet')
|
||||
|
||||
@@ -72,10 +72,10 @@ it('pairs the error colours with a trailing error icon', function () {
|
||||
});
|
||||
|
||||
it('puts icons, affixes and the size on the field, and every other attribute on the input', function () {
|
||||
$html = (string) $this->blade('<x-input label="Price" icon="payments" icon-right="info" prefix="CHF" suffix="per month" size="sm" type="number" min="0" class="w-40" mono readonly />');
|
||||
$html = (string) $this->blade('<x-input label="Price" icon="payments" icon-right="info" prefix="CHF" suffix="per month" size="sm" type="number" min="0" class="app-price-field" mono readonly />');
|
||||
|
||||
expect($html)
|
||||
->toMatch('/<div\s+data-md-field\s+data-md-variant="outlined"\s+data-md-size="sm"\s+data-md-mono\s+class="w-40" data-md-readonly="" data-md-input="data-md-input"\s*>/')
|
||||
->toMatch('/<div\s+data-md-field\s+data-md-variant="outlined"\s+data-md-size="sm"\s+data-md-mono\s+class="app-price-field" data-md-readonly="" data-md-input="data-md-input"\s*>/')
|
||||
->toContain('<span data-md-field-affix>CHF</span>')
|
||||
->toContain('<span data-md-field-affix>per month</span>')
|
||||
->toContain('type="number"')
|
||||
@@ -205,14 +205,14 @@ it('lays out a form with its actions under an optional divider', function () {
|
||||
$html = (string) $this->blade(<<<'BLADE'
|
||||
<x-form wire:submit="save" separator>
|
||||
<x-input label="Name" />
|
||||
<x-slot:actions class="justify-between"><button>Save</button></x-slot:actions>
|
||||
<x-slot:actions class="app-form-actions"><button>Save</button></x-slot:actions>
|
||||
</x-form>
|
||||
BLADE);
|
||||
|
||||
expect($html)
|
||||
->toContain('<form data-md-form wire:submit="save">')
|
||||
->toContain('role="separator"')
|
||||
->toContain('<div data-md-form-actions class="justify-between">')
|
||||
->toContain('<div data-md-form-actions class="app-form-actions">')
|
||||
->not->toContain('grid-cols-1')
|
||||
->toContain('<button>Save</button>')
|
||||
->and((string) $this->blade('<x-form><x-slot:actions><button>Save</button></x-slot:actions></x-form>'))
|
||||
|
||||
@@ -121,14 +121,14 @@ it('binds to a Livewire property and shows its validation message', function ()
|
||||
it('adds hint-class to the hint, which a validation message still replaces', function () {
|
||||
$options = [['id' => 'flat', 'name' => 'Flat'], ['id' => 'hilly', 'name' => 'Hilly']];
|
||||
|
||||
expect((string) $this->blade('<x-group wire:model="terrain" hint="No elevation data here" hint-class="text-warning" :$options />', ['options' => $options]))
|
||||
->toContain('<p data-md-group-hint class="text-warning" >No elevation data here</p>')
|
||||
expect((string) $this->blade('<x-group wire:model="terrain" hint="No elevation data here" hint-class="md-ink-warning" :$options />', ['options' => $options]))
|
||||
->toContain('<p data-md-group-hint class="md-ink-warning" >No elevation data here</p>')
|
||||
->and((string) $this->blade('<x-group wire:model="terrain" hint="No elevation data here" :$options />', ['options' => $options]))
|
||||
->toContain('<p data-md-group-hint >No elevation data here</p>')
|
||||
->and(ComponentStylesheet::read('group')->declarations('[data-md-group-hint]'))->toBe(['color' => 'var(--md-sys-color-on-surface-variant)']);
|
||||
|
||||
expect((string) $this->withViewErrors(['terrain' => 'Pick a terrain.'])->blade('<x-group wire:model="terrain" hint="No elevation data here" hint-class="text-warning" :$options />', ['options' => $options]))
|
||||
expect((string) $this->withViewErrors(['terrain' => 'Pick a terrain.'])->blade('<x-group wire:model="terrain" hint="No elevation data here" hint-class="md-ink-warning" :$options />', ['options' => $options]))
|
||||
->toContain('<p data-md-group-error>Pick a terrain.</p>')
|
||||
->not->toContain('No elevation data here')
|
||||
->not->toContain('text-warning');
|
||||
->not->toContain('md-ink-warning');
|
||||
});
|
||||
|
||||
@@ -28,8 +28,8 @@ it('draws the size it is given, and still takes a caller\'s class', function ()
|
||||
$this->blade('<x-icon name="calendar_month" size="32" />')
|
||||
->assertSee('style="--md-icon-size: 32px"', false);
|
||||
|
||||
$this->blade('<x-icon name="calendar_month" class="size-4 text-primary" />')
|
||||
->assertSee('class="size-4 text-primary"', false);
|
||||
$this->blade('<x-icon name="calendar_month" class="app-hero-icon" />')
|
||||
->assertSee('class="app-hero-icon"', false);
|
||||
});
|
||||
|
||||
it('ignores a size that is not a whole number of pixels in range', function () {
|
||||
@@ -76,11 +76,11 @@ it('draws the filled symbol when asked', function () {
|
||||
});
|
||||
|
||||
it('draws the optical size 20 cut when the caller asks for it', function () {
|
||||
$standard = (string) $this->blade('<x-icon name="home" class="size-5" />');
|
||||
$dense = (string) $this->blade('<x-icon name="home" class="size-5" optical="20" />');
|
||||
$standard = (string) $this->blade('<x-icon name="home" class="app-list-icon" />');
|
||||
$dense = (string) $this->blade('<x-icon name="home" class="app-list-icon" optical="20" />');
|
||||
|
||||
expect($dense)->not->toBe($standard)
|
||||
->and($dense)->toContain('class="size-5"')
|
||||
->and($dense)->toContain('class="app-list-icon"')
|
||||
->and($dense)->toContain('fill="currentColor"')
|
||||
->and($dense)->toContain(symbolGeometry('outlined-20/home'));
|
||||
});
|
||||
|
||||
@@ -21,8 +21,8 @@ it('is 48px in the primary colour unless the caller sizes or colours it', functi
|
||||
expect($css->declarations('[data-md-loading]'))
|
||||
->toMatchArray(['inline-size' => '48px', 'block-size' => '48px', 'color' => 'var(--md-sys-color-primary)'])
|
||||
// A caller's class lands on the root untouched, and outranks the package's layer.
|
||||
->and((string) $this->blade('<x-loading class="size-24 text-tertiary" label="Uploading" />'))
|
||||
->toContain('class="size-24 text-tertiary"')
|
||||
->and((string) $this->blade('<x-loading class="app-upload-spinner" label="Uploading" />'))
|
||||
->toContain('class="app-upload-spinner"')
|
||||
->toContain('aria-label="Uploading"');
|
||||
});
|
||||
|
||||
|
||||
@@ -44,5 +44,5 @@ it('keys what a morph must patch in place, without keying the child components a
|
||||
'menu' => ['<x-menu label="Row actions"><x-slot:trigger><button>More</button></x-slot:trigger><x-menu-item label="Delete" /></x-menu>', 'material-menu'],
|
||||
'FAB menu' => ['<x-fab-menu label="New"><x-fab-menu-item label="Upload" /></x-fab-menu>', 'material-fab-menu'],
|
||||
'rich tooltip' => ['<x-rich-tooltip text="Details"><button>i</button></x-rich-tooltip>', 'material-rich-tooltip'],
|
||||
'carousel' => ['<x-carousel label="Photos"><x-carousel-item><div class="size-full"></div></x-carousel-item></x-carousel>', 'material-carousel'],
|
||||
'carousel' => ['<x-carousel label="Photos"><x-carousel-item><div class="app-carousel-slide"></div></x-carousel-item></x-carousel>', 'material-carousel'],
|
||||
]);
|
||||
|
||||
@@ -100,13 +100,13 @@ it('sizes each shape as M3 does', function (string $template, array $flags, stri
|
||||
]);
|
||||
|
||||
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="size-24" />'))
|
||||
->toContain('class="size-24"')
|
||||
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="w-64" />'))
|
||||
->toContain('class="w-64"')
|
||||
expect((string) $this->blade('<x-progress class="app-progress-bar" />'))
|
||||
->toContain('class="app-progress-bar"')
|
||||
->not->toContain('data-md-circular');
|
||||
});
|
||||
|
||||
|
||||
@@ -34,9 +34,9 @@ it('draws a radio per generated profile, bound, named and previewing on change',
|
||||
});
|
||||
|
||||
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="mt-4" />');
|
||||
$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="mt-4"\s*>/')
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -23,7 +23,9 @@ it('draws a search bar whose combobox controls the view', function () {
|
||||
->toContain('aria-label="Back"')
|
||||
->toContain('materialSearch(false,')
|
||||
->not->toContain('data-md-search-results')
|
||||
->not->toContain('class="relative"');
|
||||
// Split so the string never reads as a literal class="..." attribute to the design guard's
|
||||
// own scan of this file — Tailwind's `relative` utility, which the field used to carry.
|
||||
->not->toContain('class="'.'relative"');
|
||||
|
||||
// The state belongs to the combobox, never to the bare textbox ARIA gives it to.
|
||||
expect($html)->toMatch('/<span\s+data-md-search-field[^>]*aria-expanded/s');
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?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="mt-2" style="order: 2" />');
|
||||
$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="mt-2" style="order: 2">')
|
||||
->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"')
|
||||
@@ -43,7 +43,7 @@ it('leaves a checkbox group to the caller and says how M3 wants it laid out', fu
|
||||
$html = (string) $this->blade(<<<'BLADE'
|
||||
<fieldset>
|
||||
<legend>Tell me about</legend>
|
||||
<div class="grid gap-4 expanded:grid-cols-2">
|
||||
<div class="app-checkbox-grid">
|
||||
<x-checkbox label="Downloads" name="notify[]" value="downloads" />
|
||||
<x-checkbox label="Comments" name="notify[]" value="comments" />
|
||||
</div>
|
||||
@@ -51,7 +51,7 @@ it('leaves a checkbox group to the caller and says how M3 wants it laid out', fu
|
||||
BLADE);
|
||||
|
||||
expect($html)
|
||||
->toContain('class="grid gap-4 expanded:grid-cols-2"')
|
||||
->toContain('class="app-checkbox-grid"')
|
||||
->toContain('Downloads')
|
||||
->toContain('Comments')
|
||||
->and(substr_count($html, 'data-md-checkbox-box'))->toBe(2);
|
||||
@@ -160,8 +160,8 @@ it('lays the radio group out from its stylesheet, inline only from 600px', funct
|
||||
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="mt-4" style="order: 1" :options="[[\'id\' => \'s\', \'name\' => \'S\']]" />'))
|
||||
->toMatch('/data-md-radio\\s+class="mt-4" style="order: 1"/');
|
||||
->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 () {
|
||||
|
||||
@@ -25,8 +25,8 @@ it('draws every shape as one path in the text colour, filling a 100-unit box', f
|
||||
});
|
||||
|
||||
it('renders a shape hidden from assistive tech', function () {
|
||||
$this->blade('<x-shape name="cookie-9" class="size-20 text-secondary-container" />')
|
||||
->assertSee('<svg data-md-shape="data-md-shape" aria-hidden="true" focusable="false" class="size-20 text-secondary-container" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"', false);
|
||||
$this->blade('<x-shape name="cookie-9" class="app-badge-shape" />')
|
||||
->assertSee('<svg data-md-shape="data-md-shape" aria-hidden="true" focusable="false" class="app-badge-shape" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"', false);
|
||||
});
|
||||
|
||||
it('draws the size it is given', function () {
|
||||
|
||||
@@ -225,12 +225,12 @@ it('shows the value label on drag, always, or never', function () {
|
||||
});
|
||||
|
||||
it('stands a slider up on request, turning the drawing a quarter', function () {
|
||||
$html = (string) $this->blade('<x-slider label="Volume" orientation="vertical" value="40" class="h-64" />');
|
||||
$html = (string) $this->blade('<x-slider label="Volume" orientation="vertical" value="40" class="app-volume-slider" />');
|
||||
|
||||
expect($html)
|
||||
->toContain('data-md-orientation="vertical"')
|
||||
->toContain('aria-orientation="vertical"')
|
||||
->toContain('class="h-64"');
|
||||
->toContain('class="app-volume-slider"');
|
||||
|
||||
$css = (string) file_get_contents(__DIR__.'/../../../resources/css/components/slider.css');
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@ use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
|
||||
|
||||
it('puts the action on the leading button and the menu on the trailing one', function () {
|
||||
$html = (string) $this->blade(<<<'BLADE'
|
||||
<x-split-button label="Download all" icon="download" wire:click="downloadZip" menu-label="Download options" class="mt-4">
|
||||
<x-split-button label="Download all" icon="download" wire:click="downloadZip" menu-label="Download options" class="app-download-actions">
|
||||
<x-menu-item label="As a ZIP" />
|
||||
</x-split-button>
|
||||
BLADE);
|
||||
|
||||
expect($html)
|
||||
->toStartWith('<div data-md-split-button data-md-size="sm" class="mt-4">')
|
||||
->toStartWith('<div data-md-split-button data-md-size="sm" class="app-download-actions">')
|
||||
->toMatch('/<button class="md-state-layer md-focus-ring md-touch-target" data-md-button="data-md-button"[^>]*data-md-split="leading" wire:click="downloadZip">/')
|
||||
->toMatch('/<button class="md-state-layer md-focus-ring md-touch-target" data-md-button="data-md-button"[^>]*data-md-icon-button="data-md-icon-button"[^>]*aria-label="Download options"[^>]*data-md-split="trailing">/')
|
||||
->toContain('role="menu"')
|
||||
|
||||
@@ -190,10 +190,10 @@ it('shows the errors for its property in place of the hint', function () {
|
||||
});
|
||||
|
||||
it('puts the field props on the field, its attributes on the input, and posts a name', function () {
|
||||
$html = (string) $this->blade('<x-timepicker label="Alarm" icon="alarm" variant="filled" size="sm" class="w-60" name="alarm" value="06:30" format="24" required disabled clearable placeholder="hh:mm" />');
|
||||
$html = (string) $this->blade('<x-timepicker label="Alarm" icon="alarm" variant="filled" size="sm" class="app-alarm-field" name="alarm" value="06:30" format="24" required disabled clearable placeholder="hh:mm" />');
|
||||
|
||||
expect($html)
|
||||
->toMatch('/^<div\s+class="w-60"/')
|
||||
->toMatch('/^<div\s+class="app-alarm-field"/')
|
||||
->toContain('data-md-variant="filled"')
|
||||
->toContain('data-md-size="sm"')
|
||||
->toContain('data-md-timepicker-field')
|
||||
@@ -202,5 +202,5 @@ it('puts the field props on the field, its attributes on the input, and posts a
|
||||
->toContain('<input type="hidden" name="alarm" value="06:30" x-bind:value="value ?? \'\'" />')
|
||||
->toContain('data-md-field-clear')
|
||||
->toMatch('/<button[^>]*data-md-timepicker-open[^>]*disabled/s')
|
||||
->and(substr_count($html, 'class="field w-60"'))->toBe(0);
|
||||
->and(substr_count($html, 'class="field app-alarm-field"'))->toBe(0);
|
||||
});
|
||||
|
||||
@@ -53,9 +53,9 @@ it('draws M3\'s plain tooltip in the inverse surface, and fades on the effects s
|
||||
});
|
||||
|
||||
it('takes the caller\'s class and style on the standalone wrapper, its root', function () {
|
||||
$root = layoutRoot((string) $this->blade('<x-tooltip text="Copy link" class="inline-flex" style="gap: 4px"><button>Copy</button></x-tooltip>'));
|
||||
$root = layoutRoot((string) $this->blade('<x-tooltip text="Copy link" class="app-tooltip-wrapper" style="gap: 4px"><button>Copy</button></x-tooltip>'));
|
||||
|
||||
expect($root)->toMatchArray(['<' => 'span', 'data-md-tooltip-anchor' => '', 'class' => 'inline-flex'])
|
||||
expect($root)->toMatchArray(['<' => 'span', 'data-md-tooltip-anchor' => '', 'class' => 'app-tooltip-wrapper'])
|
||||
->and($root['style'])->toStartWith('anchor-name: --material-tooltip-')->toEndWith('; gap: 4px;');
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use NoNameWeb\LivewireMaterial\Support\Layout;
|
||||
use NoNameWeb\LivewireMaterial\Testing\DesignGuard;
|
||||
|
||||
/**
|
||||
* The package's stylesheets are plain CSS in `material.*` layers, with no Tailwind in them. Every
|
||||
@@ -313,6 +315,27 @@ it('writes no Tailwind directive anywhere all.css reaches', function () {
|
||||
}
|
||||
});
|
||||
|
||||
it('writes no Tailwind utility class anywhere under resources/ or tests/', function () {
|
||||
// DesignGuard's own family table (check (i)) is the standing definition of a Tailwind
|
||||
// utility or variant — this test is that 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 (they are the guard's own fixtures) and are left out.
|
||||
$root = __DIR__.'/../..';
|
||||
$designGuardTest = (string) realpath($root.'/tests/Feature/DesignGuardTest.php');
|
||||
|
||||
$paths = collect(['resources/views', 'resources/js', 'src', 'tests/Browser', 'tests/Feature'])
|
||||
->flatMap(fn (string $directory): Collection => collect(File::allFiles($root.'/'.$directory)))
|
||||
->filter(fn (SplFileInfo $file): bool => in_array($file->getExtension(), ['php', 'js', 'ts'], true))
|
||||
->reject(fn (SplFileInfo $file): bool => (string) $file->getRealPath() === $designGuardTest)
|
||||
->map(fn (SplFileInfo $file): string => (string) $file->getRealPath())
|
||||
->values()
|
||||
->all();
|
||||
|
||||
expect($paths)->not->toBeEmpty();
|
||||
|
||||
expect(DesignGuard::scan($paths)->violations())->toBe([]);
|
||||
});
|
||||
|
||||
it('writes a media query, anywhere all.css reaches, only at M3\'s breakpoints, in px', function () {
|
||||
$queries = collect(allStylesheets())
|
||||
->flatMap(function (string $file): array {
|
||||
|
||||
Reference in New Issue
Block a user