Files
livewire-material/tests/Browser/DataTest.php
T
Andreas Reinhold / reiniandClaude Sonnet 5 a29a147f1a Cover six actions/communication behaviours the browser suite missed
Plan step 36's handoff listed six behaviours no browser test exercised
yet:
- FieldsTest: <x-select>'s open list uses the customizable picker
  (appearance: base-select) where the browser supports it, drawn as
  M3's 48px-row menu.
- CommunicationTest: a two-line snackbar's action stays on the
  title's row from 600px, only wrapping under it below that
  (toast.css's [data-md-toast-wrap]).
- ActionsTest: a disabled menu item reached by the keyboard withholds
  its state layer (interaction.css's [aria-disabled="true"] rule).
- DataTest: a paginator's page step shows md-focus-ring's outline on
  keyboard focus, alongside its already-tested 48px target.
- CommunicationTest: a transient rich tooltip stays open 1.5s after
  focus leaves rather than closing at once (rich-tooltip.js's
  LEAVE_GRACE_MS).
- ActionsTest: an xs or sm group segment takes a press at its 48px
  edge, past its drawn size, now that the target comes from the
  shared md-touch-target class rather than a rule of its own.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 18:40:43 +02:00

150 lines
6.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
use Illuminate\Contracts\View\View;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Route;
use Livewire\Component;
use Livewire\Livewire;
use Livewire\WithPagination;
class DataProbe extends Component
{
use WithPagination;
/** @var array{column: string, direction: string} */
public array $sortBy = ['column' => 'name', 'direction' => 'asc'];
public function render(): View
{
$files = collect(range(1, 25))->map(fn (int $n): array => ['name' => sprintf('file-%02d.zip', $n), 'size' => $n * 3 % 17]);
$files = $files->sortBy($this->sortBy['column'], SORT_REGULAR, $this->sortBy['direction'] === 'desc')->values();
$page = $this->getPage();
return view('probe::data', [
'files' => new LengthAwarePaginator($files->forPage($page, 10)->values(), $files->count(), 10, $page),
]);
}
}
function dataProbe()
{
$views = sys_get_temp_dir().'/livewire-material-data-probe';
@mkdir($views);
file_put_contents($views.'/data.blade.php', <<<'BLADE'
<div class="p-4">
<x-table>
<thead>
<tr>
<x-sort-header column="name" :sort-by="$sortBy" id="by-name">Name</x-sort-header>
<x-sort-header column="size" :sort-by="$sortBy" id="by-size">Size</x-sort-header>
</tr>
</thead>
<tbody>
@foreach ($files as $file)
<tr wire:key="file-{{ $file['name'] }}"><td>{{ $file['name'] }}</td><td>{{ $file['size'] }}</td></tr>
@endforeach
</tbody>
</x-table>
{{ $files->links() }}
<x-table dense id="dense-table">
<tbody>
<tr><td>dense-a.zip</td><td>1</td></tr>
<tr aria-selected="true"><td>dense-b.zip</td><td>2</td></tr>
</tbody>
</x-table>
</div>
BLADE);
app('view')->addNamespace('probe', $views);
Livewire::component('data-probe', DataProbe::class);
Route::middleware('web')->get('/data-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:data-probe />
@livewireScripts
</body>
</html>
BLADE));
return visit('/data-probe')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
}
it('sorts by a column and flips the direction on a second press', function () {
$first = "document.querySelector('tbody tr td').textContent.trim()";
$page = dataProbe()
->assertAttribute('#by-name', 'aria-sort', 'ascending')
->assertScript("{$first} === 'file-01.zip'");
$page->click('#by-name button')
->assertAttribute('#by-name', 'aria-sort', 'descending')
->assertScript("{$first} === 'file-25.zip'");
$page->click('#by-size button')
->assertAttribute('#by-size', 'aria-sort', 'ascending')
->assertScript("! document.querySelector('#by-name').hasAttribute('aria-sort')");
});
it('pages through Livewire results and marks the current page', function () {
$page = dataProbe()
->assertSee('110 of 25')
->assertAttribute('[data-md-pagination] [aria-current="page"]', 'aria-current', 'page')
->click('[data-md-pagination] button[aria-label="Go to page 3"]')
->assertSee('2125 of 25')
->assertSeeIn('[data-md-pagination] [aria-current="page"]', '3');
$page->click('[data-md-pagination] button[aria-label="Previous"]')
->assertSee('1120 of 25');
$page->resize(400, 800)
->assertSee('Page 2 of 3');
});
it('draws 52px rows, and 36px ones only in a table that asks to be dense', function () {
$row = fn (string $table): string => "document.querySelector('{$table} tbody tr').getBoundingClientRect().height";
dataProbe()
->assertScript("Math.abs(({$row('[data-md-table]:not([data-md-dense])')}) - 52) <= 1")
->assertScript("Math.abs(({$row('#dense-table')}) - 36) <= 1")
->assertScript("getComputedStyle(document.querySelector('#dense-table tr[aria-selected=\"true\"]')).backgroundColor !== 'rgba(0, 0, 0, 0)'")
->assertScript("getComputedStyle(document.querySelector('[data-md-table] thead th')).borderBottomWidth === '1px'");
});
it('lets the sort button be pressed anywhere in its 48px target', function () {
dataProbe()
->assertScript("(() => { const button = document.querySelector('#by-size [data-md-sort-header-button]'); const after = getComputedStyle(button, '::after'); return button.getBoundingClientRect().height < 48 && after.minHeight === '48px' && after.minWidth === '48px'; })()")
->assertScript("(() => { const button = document.querySelector('#by-size [data-md-sort-header-button]'); const box = button.getBoundingClientRect(); const probe = document.elementFromPoint(box.left + box.width / 2, box.top + box.height / 2 - 22); return probe === button; })()");
});
it('draws each page step 40px and lets it be pressed anywhere in its 48px target', function () {
dataProbe()
->assertScript("(() => { const step = document.querySelector('[data-md-pagination] button[aria-label=\"Go to page 2\"]'); const box = step.getBoundingClientRect(); return box.width === 40 && box.height === 40 && document.elementFromPoint(box.left + box.width / 2, box.top - 3) === step; })()")
->assertScript("getComputedStyle(document.querySelector('[data-md-pagination] [aria-current=\"page\"]')).backgroundColor !== 'rgba(0, 0, 0, 0)'");
});
it('shows a page step\'s keyboard focus ring, from md-focus-ring', function () {
$step = "document.querySelector('[data-md-pagination] button[aria-label=\"Go to page 2\"]')";
$page = dataProbe();
// A key press first puts the browser in keyboard modality, as a keyboard user would be —
// Firefox only counts a scripted focus as :focus-visible after one.
$page->keys('#by-name [data-md-sort-header-button]', 'Tab');
$page->script("{$step}.focus()");
$page->assertScript("document.activeElement === {$step}")
->assertScript("getComputedStyle({$step}).outlineStyle === 'solid'")
->assertScript("getComputedStyle({$step}).outlineWidth === '3px'");
});