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
135 lines
8.4 KiB
PHP
135 lines
8.4 KiB
PHP
<?php
|
|
|
|
use Livewire\Component;
|
|
use Livewire\Livewire;
|
|
use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
|
|
|
|
it('draws a choice as connected radios', function () {
|
|
$html = (string) $this->blade(<<<'BLADE'
|
|
<x-group label="Theme" name="theme" x-model="theme" :options="[
|
|
['id' => 'light', 'name' => 'Light', 'icon' => 'light_mode'],
|
|
['id' => 'dark', 'name' => 'Dark', 'disabled' => true],
|
|
]" />
|
|
BLADE);
|
|
|
|
expect($html)
|
|
->toMatch('/<fieldset data-md-group="data-md-group" data-md-size="sm" data-md-variant="tonal" data-md-shape="round">/')
|
|
->toContain('<legend data-md-group-legend>Theme</legend>')
|
|
->toContain('<div data-md-button-group="connected" data-md-size="sm" data-md-shape="round">')
|
|
->toMatch('/<label data-md-group-segment class="md-state-layer\s+md-touch-target\s*">/')
|
|
->toContain('type="radio"')
|
|
->toContain('name="theme"')
|
|
->toContain('x-model="theme"')
|
|
->toContain('value="light"')
|
|
->toContain('disabled')
|
|
->toContain('<span data-md-group-label>Light</span>')
|
|
->and(substr_count($html, '<svg'))->toBe(1);
|
|
});
|
|
|
|
it('squares a choice\'s ends, and still rounds the chosen segment', function () {
|
|
$options = [['id' => 'a', 'name' => 'A']];
|
|
|
|
expect((string) $this->blade('<x-group shape="square" name="x" :$options />', ['options' => $options]))->toContain('data-md-button-group="connected" data-md-size="sm" data-md-shape="square"')
|
|
->and((string) $this->blade('<x-group name="x" :$options />', ['options' => $options]))->toContain('data-md-shape="round"')
|
|
->and(ComponentStylesheet::read('button-group')->declarations("[data-md-button-group][data-md-shape='square']"))->toBe(['--md-button-group-outer' => 'var(--md-button-group-inner)'])
|
|
->and(ComponentStylesheet::read('button-group')->declarations("[data-md-button-group='connected'] > :is([aria-pressed='true'], :has(:checked))"))->toBe(['--md-button-group-corner' => 'var(--md-button-group-full)'])
|
|
->and(ComponentStylesheet::read('group')->imports())->toBe(['./icon.css', './button-group.css']);
|
|
});
|
|
|
|
it('keeps a 48px target and a 48px minimum width on the smallest connected segments, from the foundation\'s class', function () {
|
|
$css = ComponentStylesheet::read('group');
|
|
$options = [['id' => 'a', 'name' => 'A']];
|
|
|
|
expect($css->declarations("[data-md-group]:is([data-md-size='xs'], [data-md-size='sm']) [data-md-group-segment]"))->toBe(['min-inline-size' => 'var(--md-sys-measurement-space600)'])
|
|
->and($css->has("[data-md-group]:is([data-md-size='xs'], [data-md-size='sm']) [data-md-group-segment]::after"))->toBeFalse()
|
|
->and($css->declarations('[data-md-group-segment]'))->toMatchArray(['min-inline-size' => '0'])
|
|
->and(ComponentStylesheet::read('button-group')->declarations("[data-md-button-group='connected']:is([data-md-size='xs'], [data-md-size='sm']) > *"))->toBe(['min-inline-size' => 'var(--md-sys-measurement-space600)'])
|
|
->and((string) $this->blade('<x-group size="xs" name="x" :$options />', ['options' => $options]))
|
|
->toMatch('/class="md-state-layer\s+md-touch-target\s*"/')
|
|
->and((string) $this->blade('<x-group size="md" name="x" :$options />', ['options' => $options]))
|
|
->toMatch('/class="md-state-layer\s*"/')
|
|
->not->toContain('md-touch-target');
|
|
});
|
|
|
|
it('sizes its segments as M3\'s buttons', function (string $size, string $height, string $padding, string $type, int $icon) {
|
|
$html = (string) $this->blade("<x-group size=\"{$size}\" name=\"x\" :options=\"[['id' => 'a', 'name' => 'A', 'icon' => 'home']]\" />");
|
|
|
|
expect($html)->toContain("data-md-size=\"{$size}\"")->toContain("--md-icon-size: {$icon}px")
|
|
->and(ComponentStylesheet::read('group')->declarations("[data-md-group][data-md-size='{$size}'] [data-md-group-segment]"))->toMatchArray([
|
|
'block-size' => $height,
|
|
'padding-inline' => $padding,
|
|
'font' => "var(--md-sys-typescale-{$type})",
|
|
]);
|
|
})->with([
|
|
'xs' => ['xs', '32px', 'var(--md-sys-measurement-space200)', 'label-lg', 20],
|
|
'sm' => ['sm', '40px', 'var(--md-sys-measurement-space200)', 'label-lg', 20],
|
|
'md' => ['md', '56px', 'var(--md-sys-measurement-space300)', 'title-md', 24],
|
|
'lg' => ['lg', '96px', 'var(--md-sys-measurement-space600)', 'headline-sm', 32],
|
|
'xl' => ['xl', '136px', 'var(--md-sys-measurement-space800)', 'headline-lg', 40],
|
|
]);
|
|
|
|
it('draws several choices as checkboxes, in the toggle button\'s colours', function () {
|
|
$css = ComponentStylesheet::read('group');
|
|
|
|
expect((string) $this->blade('<x-group name="days" multiple variant="outlined" :options="[[\'id\' => \'mon\', \'name\' => \'Mon\']]" />'))
|
|
->toContain('type="checkbox"')
|
|
->toContain('name="days[]"')
|
|
->toContain('data-md-variant="outlined"')
|
|
->toContain('data-md-multiple')
|
|
->and($css->declarations('[data-md-group-segment]:has(:checked)'))->toBe(['--md-group-container' => 'var(--md-sys-color-secondary)', '--md-group-label' => 'var(--md-sys-color-on-secondary)'])
|
|
->and($css->declarations("[data-md-group][data-md-variant='filled'] [data-md-group-segment]:has(:checked)"))->toBe(['--md-group-container' => 'var(--md-sys-color-primary)', '--md-group-label' => 'var(--md-sys-color-on-primary)'])
|
|
->and($css->declarations("[data-md-group][data-md-variant='outlined'] [data-md-group-segment]:has(:checked)"))->toBe(['--md-group-container' => 'var(--md-sys-color-inverse-surface)', '--md-group-label' => 'var(--md-sys-color-inverse-on-surface)', 'border-color' => 'transparent'])
|
|
->and($css->declarations('[data-md-group-segment]:has(:disabled)'))->toMatchArray(['cursor' => 'not-allowed'])
|
|
->and($css->declarations('[data-md-group-segment]:has(:disabled)::before'))->toBe(['display' => 'none'])
|
|
->and($css->declarations('[data-md-group-segment]:has(:focus-visible)'))->toBe(['outline' => '3px solid var(--md-sys-color-secondary)', 'outline-offset' => '2px'])
|
|
->and($css->declarations('[data-md-group-segment]:has(:focus-visible)::before'))->toBe(['opacity' => 'var(--md-sys-state-focus-state-layer-opacity)'])
|
|
->and($css->has('[data-md-group-segment]::before'))->toBeFalse()
|
|
->and((string) $this->blade('<x-group name="x" variant="glass" :options="[]" />'))->toContain('data-md-variant="tonal"');
|
|
});
|
|
|
|
it('binds to a Livewire property and shows its validation message', function () {
|
|
$component = new class extends Component
|
|
{
|
|
public string $theme = 'dark';
|
|
|
|
public function save(): void
|
|
{
|
|
$this->validate(['theme' => 'in:light'], ['theme.in' => 'Pick light.']);
|
|
}
|
|
|
|
public function render(): string
|
|
{
|
|
return <<<'BLADE'
|
|
<div>
|
|
<x-group wire:model="theme" hint="How it looks" :options="[['id' => 'light', 'name' => 'Light'], ['id' => 'dark', 'name' => 'Dark']]" />
|
|
</div>
|
|
BLADE;
|
|
}
|
|
};
|
|
|
|
Livewire::test($component)
|
|
->assertSeeHtml('wire:model="theme"')
|
|
->assertSee('How it looks')
|
|
->set('theme', 'light')
|
|
->assertSet('theme', 'light')
|
|
->set('theme', 'dark')
|
|
->call('save')
|
|
->assertSee('Pick light.')
|
|
->assertDontSee('How it looks');
|
|
});
|
|
|
|
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="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="md-ink-warning" :$options />', ['options' => $options]))
|
|
->toContain('<p data-md-group-error>Pick a terrain.</p>')
|
|
->not->toContain('No elevation data here')
|
|
->not->toContain('md-ink-warning');
|
|
});
|