Files
livewire-material/tests/Browser/FieldsTest.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

186 lines
6.9 KiB
PHP

<?php
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Route;
use Livewire\Component;
use Livewire\Livewire;
class FieldProbe extends Component
{
public string $name = '';
public string $hours = '24';
/** @var list<string> */
public array $files = [];
public string $audience = 'anyone';
public bool $public = false;
public string $message = '';
public function save(): void
{
$this->validate(['name' => 'required']);
}
public function render(): string
{
return <<<'BLADE'
<div class="grid max-w-md gap-4 p-4">
<p>name: <span id="name">{{ $name }}</span></p>
<p>hours: <span id="hours">{{ $hours }}</span></p>
<p>files: <span id="files">{{ implode(',', $files) }}</span></p>
<p>audience: <span id="audience">{{ $audience }}</span></p>
<p>public: <span id="public-state">{{ var_export($public, true) }}</span></p>
<x-input id="name-field" label="Share name" wire:model.live="name" clearable copyable />
<x-password id="password-field" label="Password" />
<x-select id="hours-field" label="Expires" wire:model.live="hours" :options="[['id' => '1', 'name' => '1 hour'], ['id' => '24', 'name' => '1 day']]" />
<x-checkbox id="all" label="All files" :indeterminate="count($files) > 0 && count($files) < 3" />
@foreach (['a', 'b', 'c'] as $file)
<x-checkbox id="file-{{ $file }}" :label="'File '.$file" value="{{ $file }}" wire:model.live="files" wire:key="file-{{ $file }}" />
@endforeach
<x-radio label="Audience" wire:model.live="audience" :options="[['id' => 'anyone', 'name' => 'Anyone'], ['id' => 'team', 'name' => 'Team']]" />
<x-toggle id="public" label="Public" wire:model.live="public" />
<x-textarea id="message" label="Message" rows="2" max-rows="4" wire:model="message" />
<x-button label="Save" wire:click="save" />
</div>
BLADE;
}
}
function fieldProbe()
{
Livewire::component('field-probe', FieldProbe::class);
Route::middleware('web')->get('/field-probe', fn () => Blade::render(<<<'BLADE'
<!DOCTYPE html>
<html>
<head>
<x-theme-script />
@vite(config('livewire-material.showcase.vite'))
@livewireStyles
</head>
<body class="bg-surface">
<livewire:field-probe />
<x-toast />
@livewireScripts
</body>
</html>
BLADE));
return visit('/field-probe')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
}
it('floats the label while the field has focus or holds a value', function () {
$label = "getComputedStyle(document.querySelector('label[for=\"name-field\"]')).fontSize";
$page = fieldProbe()->assertScript("{$label} === '16px'");
$page->click('#name-field')->assertScript("{$label} === '12px'");
$page->type('#name-field', 'Holiday')
->assertSeeIn('#name', 'Holiday');
$page->click('#password-field')->assertScript("{$label} === '12px'");
});
it('clears a field and tells Livewire', function () {
fieldProbe()
->type('#name-field', 'Holiday')
->assertSeeIn('#name', 'Holiday')
->click('[data-field-clear]')
->assertScript("document.querySelector('#name-field').value === ''")
->assertScript("document.querySelector('#name').textContent === ''")
->assertScript("document.activeElement.id === 'name-field'");
});
it('copies a field\'s value and says so', function () {
$page = fieldProbe()->type('#name-field', 'Holiday');
$page->script("window.eval(\"Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText: async (text) => { window.copied = text } } })\")");
$page->click('[data-field-copy]')
->assertScript("window.copied === 'Holiday'")
->assertSee('Copied to the clipboard');
});
it('shows and hides a password', function () {
fieldProbe()
->type('#password-field', 'secret')
->click('[data-field-reveal]')
->assertScript("document.querySelector('#password-field').type === 'text'")
->assertAttribute('[data-field-reveal]', 'aria-label', 'Hide password')
->click('[data-field-reveal]')
->assertScript("document.querySelector('#password-field').type === 'password'");
});
it('shows the server\'s error in place of the hint', function () {
fieldProbe()
->click('button:has-text("Save")')
->assertSee('The name field is required.')
->assertAttribute('#name-field', 'aria-invalid', 'true')
->assertScript("document.querySelector('#name-field').closest('.field').hasAttribute('data-invalid')");
});
it('binds a select to Livewire', function () {
fieldProbe()
->select('#hours-field', '1')
->assertSeeIn('#hours', '1');
});
it('keeps a partly ticked checkbox in step with the server', function () {
$all = "document.querySelector('#all')";
$page = fieldProbe()->assertScript("! {$all}.indeterminate");
$page->click('label[for="file-a"]')
->assertSeeIn('#files', 'a')
->assertScript("{$all}.hasAttribute('data-indeterminate') && {$all}.indeterminate");
$page->click('label[for="file-b"]')
->click('label[for="file-c"]')
->assertSeeIn('#files', 'a,b,c')
->assertScript("! {$all}.hasAttribute('data-indeterminate') && ! {$all}.indeterminate");
});
it('moves between radio buttons with the arrow keys', function () {
fieldProbe()
->keys('input[type="radio"][value="anyone"]', 'ArrowDown')
->assertSeeIn('#audience', 'team')
->assertScript("document.querySelector('input[type=\"radio\"][value=\"team\"]').checked");
});
it('turns a switch on from the keyboard and from its label', function () {
$page = fieldProbe()
->keys('#public', 'Space')
->assertSeeIn('#public-state', 'true');
$page->click('label[for="public"]')
->assertSeeIn('#public-state', 'false')
->assertAttribute('#public', 'role', 'switch');
});
it('grows a textarea with its text up to its maximum', function () {
$height = "document.querySelector('#message').getBoundingClientRect().height";
$page = fieldProbe();
$rows = $page->script("({$height})");
$page->type('#message', "one\ntwo\nthree")
->assertScript("({$height}) > {$rows}");
$three = $page->script("({$height})");
$page->type('#message', "one\ntwo\nthree\nfour\nfive\nsix\nseven")
->assertScript("Math.abs(({$height}) - ({$three} + 24)) < 2")
->assertScript("document.querySelector('#message').scrollHeight > document.querySelector('#message').clientHeight");
});