Plan step 32. inputs.md IN-01 moved `aria-expanded` from the raw input to the `role="combobox"` wrapper around it, and IN-09 put a scrim under the docked view, so a press outside lands on the scrim and Playwright could never press the text beneath it. The tests read the wrapper and press the scrim. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
171 lines
7.0 KiB
PHP
171 lines
7.0 KiB
PHP
<?php
|
|
|
|
use Illuminate\Support\Facades\Blade;
|
|
use Illuminate\Support\Facades\Route;
|
|
use Livewire\Component;
|
|
use Livewire\Livewire;
|
|
|
|
class PickProbe extends Component
|
|
{
|
|
/** @var list<int> */
|
|
public array $days = [2];
|
|
|
|
public ?string $zone = 'Europe/Zurich';
|
|
|
|
public string $query = '';
|
|
|
|
public function render(): string
|
|
{
|
|
return <<<'BLADE'
|
|
<div class="grid max-w-md gap-6 p-4">
|
|
<p>days: <span id="days">{{ json_encode($days) }}</span></p>
|
|
<p>zone: <span id="zone">{{ $zone }}</span></p>
|
|
<p>query: <span id="query">{{ $query }}</span></p>
|
|
|
|
<x-choices label="Days" wire:model.live="days" :options="[['id' => 1, 'name' => 'Mon'], ['id' => 2, 'name' => 'Tue'], ['id' => 3, 'name' => 'Wed']]" />
|
|
|
|
<div id="clip" class="h-24 overflow-hidden rounded-corner-md bg-surface-container p-2">
|
|
<x-choices id="zone-field" label="Time zone" searchable wire:model.live="zone" :options="[['id' => 'Europe/Berlin', 'name' => 'Berlin'], ['id' => 'Europe/Zurich', 'name' => 'Zurich'], ['id' => 'UTC', 'name' => 'UTC', 'disabled' => true]]" />
|
|
</div>
|
|
|
|
<x-search id="find" wire:model.live="query" placeholder="Search files">
|
|
@foreach (array_filter(['holiday.zip', 'contract.pdf', 'review.mp4'], fn ($file) => $query === '' || str_contains($file, $query)) as $file)
|
|
<button type="button" wire:key="result-{{ $file }}" class="block w-full px-4 py-3 text-start">{{ $file }}</button>
|
|
@endforeach
|
|
<x-slot:empty>No files match.</x-slot:empty>
|
|
</x-search>
|
|
|
|
</div>
|
|
BLADE;
|
|
}
|
|
}
|
|
|
|
function pickProbe()
|
|
{
|
|
Livewire::component('pick-probe', PickProbe::class);
|
|
|
|
Route::middleware('web')->get('/pick-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:pick-probe />
|
|
@livewireScripts
|
|
</body>
|
|
</html>
|
|
BLADE));
|
|
|
|
return visit('/pick-probe')->waitForEvent('networkidle')
|
|
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
|
|
}
|
|
|
|
it('keeps the values of filter chips as the property\'s own type', function () {
|
|
pickProbe()
|
|
->assertAttribute('button[aria-pressed="true"]', 'aria-pressed', 'true')
|
|
->click('button:has-text("Mon")')
|
|
->assertSeeIn('#days', '[2,1]')
|
|
->click('button:has-text("Tue")')
|
|
->assertSeeIn('#days', '[1]')
|
|
->assertScript("[...document.querySelectorAll('[aria-pressed=\"true\"]')].map((chip) => chip.textContent.trim()).join() === 'Mon'");
|
|
});
|
|
|
|
it('filters a searchable choice as it is typed and chooses from the keyboard', function () {
|
|
$list = "document.querySelector('#zone-field-list')";
|
|
|
|
$page = pickProbe()
|
|
->assertValue('#zone-field', 'Zurich')
|
|
->click('#zone-field')
|
|
->assertScript("{$list}.matches(':popover-open')")
|
|
->assertAttribute('#zone-field', 'aria-expanded', 'true');
|
|
|
|
$page->type('#zone-field', 'ber')
|
|
->assertScript("{$list}.querySelectorAll('[role=\"option\"]').length === 1");
|
|
|
|
$page->keys('#zone-field', 'Enter')
|
|
->assertSeeIn('#zone', 'Europe/Berlin')
|
|
->assertValue('#zone-field', 'Berlin')
|
|
->assertScript("! {$list}.matches(':popover-open')");
|
|
});
|
|
|
|
it('puts a searchable choice back on Escape and never chooses a disabled option', function () {
|
|
$page = pickProbe()->click('#zone-field')->type('#zone-field', 'ber');
|
|
|
|
$page->keys('#zone-field', 'Escape')
|
|
->assertValue('#zone-field', 'Zurich')
|
|
->assertSeeIn('#zone', 'Europe/Zurich');
|
|
|
|
$page->click('#zone-field')->type('#zone-field', 'UTC')->keys('#zone-field', 'Enter')
|
|
->assertSeeIn('#zone', 'Europe/Zurich');
|
|
});
|
|
|
|
it('opens a searchable choice\'s list above a container that clips', function () {
|
|
pickProbe()
|
|
->click('#zone-field')
|
|
->assertScript(<<<'JS'
|
|
(() => {
|
|
const list = document.querySelector('#zone-field-list');
|
|
const clip = document.querySelector('#clip').getBoundingClientRect();
|
|
const box = list.getBoundingClientRect();
|
|
const probe = document.elementFromPoint(box.left + box.width / 2, box.bottom - 8);
|
|
|
|
return box.bottom > clip.bottom && list.contains(probe);
|
|
})()
|
|
JS);
|
|
});
|
|
|
|
it('opens the search view with the results Livewire renders for the query', function () {
|
|
$view = "document.querySelector('#find-view')";
|
|
|
|
// The combobox is the wrapper around the input, which carries `aria-expanded` (inputs.md IN-01).
|
|
$page = pickProbe()
|
|
->click('#find')
|
|
->assertScript("getComputedStyle({$view}).display !== 'none'")
|
|
->assertAttribute('[role="combobox"]:has(> #find)', 'aria-expanded', 'true');
|
|
|
|
$page->type('#find', 'con')
|
|
->assertSeeIn('#query', 'con')
|
|
->assertScript("[...{$view}.querySelectorAll('button')].map((button) => button.textContent.trim()).join() === 'contract.pdf'");
|
|
|
|
$page->keys('#find', 'ArrowDown')
|
|
->assertScript("document.activeElement.textContent.trim() === 'contract.pdf'");
|
|
|
|
$page->keys(':focus', 'Escape')
|
|
->assertScript("getComputedStyle({$view}).display === 'none'")
|
|
->assertScript("document.activeElement.id === 'find'")
|
|
->assertAttribute('[role="combobox"]:has(> #find)', 'aria-expanded', 'false');
|
|
});
|
|
|
|
it('says so when nothing matches, and closes on a press outside or a chosen result', function () {
|
|
$view = "document.querySelector('#find-view')";
|
|
|
|
$page = pickProbe()
|
|
->click('#find')
|
|
->type('#find', 'zzz')
|
|
->assertSeeIn('#find-view', 'No files match.');
|
|
|
|
// The docked view opens over a scrim that covers the rest of the page (inputs.md IN-09), so a
|
|
// press outside the view lands on the scrim.
|
|
$page->click('[data-search-scrim]')
|
|
->assertScript("getComputedStyle({$view}).display === 'none'");
|
|
|
|
$page->click('#find')
|
|
->clear('#find')
|
|
->assertScript("{$view}.querySelectorAll('button').length === 3")
|
|
->click('#find-view button:has-text("review.mp4")')
|
|
->assertScript("getComputedStyle({$view}).display === 'none'");
|
|
});
|
|
|
|
it('takes the whole screen on a compact window, with a back arrow', function () {
|
|
$page = pickProbe()->resize(400, 800);
|
|
|
|
$page->click('#find')
|
|
->assertScript("document.querySelector('[data-search]').hasAttribute('data-full-screen')")
|
|
->assertScript("(() => { const box = document.querySelector('#find-view').getBoundingClientRect(); return box.top === 0 && box.width === 400; })()")
|
|
->click('[data-search-back]')
|
|
->assertScript("! document.querySelector('[data-search]').hasAttribute('data-open')");
|
|
});
|