Files
livewire-material/tests/Feature/Components/FieldTest.php
T
Andreas Reinhold / reiniandClaude Fable 5.1 0074f9bd5c Finish the keyboards, and say a state once
Shift+M and Shift+Y reach the month and year dropdowns, which is M3's
date picker keyboard table; Home and End go to the ends of an open
combobox list, and stay the field's own caret keys when it is closed. The
password reveal drops aria-pressed and keeps only its flipping label, so
a screen reader hears the state once rather than twice and inverted. The
radio's `inline` now says that M3 cautions against a row and wants an
option chosen on load.

Plan step 20, findings IN-22, IN-23, IN-28 and IN-29.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:13:00 +02:00

166 lines
7.3 KiB
PHP

<?php
it('draws an outlined field with a notched label unless told otherwise', function () {
$html = (string) $this->blade('<x-input label="Share name" />');
expect($html)
->toContain('data-variant="outlined"')
->toContain('<legend><span>Share name</span></legend>')
->toMatch('/<label for="(field-[0-9a-f]{12})" class="field-label">Share name<\/label>/')
->toContain('placeholder=" "')
->toContain('class="field-control"');
});
it('draws a filled field on request, or by the application\'s default', function () {
expect((string) $this->blade('<x-input label="Name" variant="filled" />'))->toContain('data-variant="filled"');
config(['livewire-material.fields.variant' => 'filled']);
expect((string) $this->blade('<x-input label="Name" />'))->toContain('data-variant="filled"')
->and((string) $this->blade('<x-input label="Name" variant="outlined" />'))->toContain('data-variant="outlined"')
->and((string) $this->blade('<x-input label="Name" variant="glass" />'))->toContain('data-variant="filled"');
});
it('ties the label, the hint and the control together', function () {
$html = (string) $this->blade('<x-input id="share-name" label="Share name" hint="Recipients see this name" wire:model="name" />');
expect($html)
->toContain('<label for="share-name" class="field-label">')
->toContain('id="share-name"')
->toContain('aria-describedby="share-name-support"')
->toContain('<p id="share-name-support" class="field-support">Recipients see this name</p>')
->toContain('wire:model="name"')
->not->toContain('aria-invalid');
});
it('replaces the hint with the errors for its model', function () {
$html = (string) $this->withViewErrors(['email' => ['Enter a valid email address.']])
->blade('<x-input id="email" label="Email" hint="We never share it" wire:model.live="email" />');
expect($html)
->toContain('data-invalid')
->toContain('aria-invalid="true"')
->toContain('aria-describedby="email-support"')
->toContain('Enter a valid email address.')
->not->toContain('We never share it');
});
it('pairs the error colours with a trailing error icon', function () {
$errors = ['email' => ['Enter a valid email address.']];
expect((string) $this->withViewErrors($errors)->blade('<x-input id="email" label="Email" wire:model="email" />'))
->toContain('field-trailing field-error')
->toContain('aria-label="Error"')
->and((string) $this->withViewErrors($errors)->blade('<x-input id="email" label="Email" size="xs" wire:model="email" />'))
->not->toContain('field-error')
->and((string) $this->withViewErrors($errors)->blade('<x-input id="email" label="Email" icon-right="info" wire:model="email" />'))
->not->toContain('field-error')
->and((string) $this->blade('<x-input id="email" label="Email" />'))
->not->toContain('field-error');
});
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 />');
expect($html)
->toContain('class="field w-40"')
->toContain('data-size="sm"')
->toContain('data-mono')
->toContain('data-readonly')
->toContain('<span class="field-affix">CHF</span>')
->toContain('<span class="field-affix">per month</span>')
->toContain('type="number"')
->toContain('min="0"')
->and(substr_count($html, '<svg'))->toBe(2);
});
it('offers to clear and to copy the value', function () {
$html = (string) $this->blade('<x-input label="Link" clearable copyable />');
expect($html)
->toContain('data-field-clear')
->toContain('aria-label="Clear"')
->toContain('data-field-copy')
->toContain('navigator.clipboard.writeText')
->toContain('aria-label="Copy"');
});
it('lets a password be shown and hidden again', function () {
$html = (string) $this->blade('<x-password label="Password" autocomplete="current-password" type="text" />');
expect($html)
->toContain('type="password"')
->not->toContain('type="text"')
->toContain('x-bind:type="shown ? \'text\' : \'password\'"')
->toContain('data-field-reveal')
->toContain('aria-label="Show password"')
// The flipping label already says the state; aria-pressed would say it again, inverted.
->not->toContain('aria-pressed')
->toContain('autocomplete="current-password"');
});
it('grows a textarea from its rows up to its maximum, or leaves it to be resized', function () {
expect((string) $this->blade('<x-textarea label="Message" rows="2" max-rows="6" />'))
->toContain('rows="2"')
->toContain('data-autogrow')
->toContain('--field-rows: 2;')
->toContain('--field-max-rows: 6;')
->and((string) $this->blade('<x-textarea label="Message" :autogrow="false" />'))
->toContain('rows="3"')
->not->toContain('data-autogrow');
});
it('draws a native select whose label always floats', function () {
$html = (string) $this->blade(<<<'BLADE'
<x-select label="Expires" wire:model="hours" placeholder="Choose…" option-value="hours" option-label="label" :options="[
['hours' => 1, 'label' => '1 hour'],
['hours' => 720, 'label' => '30 days', 'disabled' => true],
]" />
BLADE);
expect($html)
->toContain('data-floated')
->toContain('<select')
->toContain('wire:model="hours"')
->toContain('<option value="">Choose…</option>')
->toContain('<option value="1" >1 hour</option>')
->toContain('<option value="720" disabled>30 days</option>')
->toContain('field-arrow');
});
it('takes a select\'s options from its slot as well', function () {
expect((string) $this->blade('<x-select label="Size"><option value="s">Small</option></x-select>'))
->toContain('<option value="s">Small</option>');
});
it('collects a file field\'s own errors and each file\'s', function () {
$html = (string) $this->withViewErrors([
'photos' => ['Choose at most 10 photos.'],
'photos.0' => ['photo.jpg is larger than 2 GB.'],
])->blade('<x-file label="Photos" wire:model="photos" multiple />');
expect($html)
->toContain('type="file"')
->toContain('multiple')
->toContain('data-floated')
->toContain('Choose at most 10 photos.')
->toContain('photo.jpg is larger than 2 GB.');
});
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-form>
BLADE);
expect($html)
->toContain('<form class="grid grid-cols-1 auto-rows-min gap-4" wire:submit="save">')
->toContain('role="separator"')
->toContain('justify-between')
->toContain('<button>Save</button>')
->and((string) $this->blade('<x-form><x-slot:actions><button>Save</button></x-slot:actions></x-form>'))
->not->toContain('role="separator"');
});