tests / feature (8.4) (push) Successful in 1m50s
tests / feature (8.5) (push) Successful in 1m54s
tests / browser (chrome, chromium) (push) Successful in 7m52s
tests / browser (firefox, firefox) (push) Successful in 11m56s
tests / browser (safari, webkit) (push) Successful in 12m35s
The browser plugin runs every call on a page again when its first attempt takes over a second, and on the runner a press can. For most presses that costs nothing. For two kinds it breaks the test: a press that opens a dialog, a sheet, a full-screen view or a modal rail over its own trigger, whose second press can never land, and a press that changes state a second one would change again — a menu trigger, a toggle, a chip, a range picker's day, a paging key, a Save. The bottom sheet with preset heights timed out on WebKit that way after the date picker had on Firefox. Every such press in the browser suite now goes through pressOnce(), 253 of them across sixteen files, not only the ones the runner happened to catch; focus moves inside an open view, Escape, links and plain "set" actions keep the retry, which is harmless for them. Two samples that were still racing the machine: - The standard side sheet's exit is caught half-way with its motion stretched, as every other mid-exit sample is, and fullSpeed() takes the stretch off again before the test times a reopen against the real exit. Under load on Linux WebKit it had failed two runs in five; it passes ten in ten. - The switch-and-checkbox row measures its widths once, so it now waits for the resize to land and the brand face to load before it does. Browser 300 passed on Firefox and WebKitGTK in a Linux container held to two busy cores, and on Chrome, Firefox and WebKit on macOS. Feature 1159 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
183 lines
8.2 KiB
PHP
183 lines
8.2 KiB
PHP
<?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 style="padding: var(--md-sys-measurement-space200);">
|
||
<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>
|
||
<link rel="icon" href="data:,">
|
||
<x-theme-script />
|
||
@vite(config('livewire-material.showcase.vite'))
|
||
@livewireStyles
|
||
</head>
|
||
<body style="background-color: var(--md-sys-color-surface);">
|
||
<livewire:data-probe />
|
||
@livewireScripts
|
||
</body>
|
||
</html>
|
||
BLADE));
|
||
|
||
return ready(visit('/data-probe'));
|
||
}
|
||
|
||
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'");
|
||
|
||
// A second press on the same header flips its direction again: pressOnce(), tests/Pest.php.
|
||
pressOnce($page)->click('#by-name button');
|
||
|
||
$page->assertAttribute('#by-name', 'aria-sort', 'descending')
|
||
->assertScript("{$first} === 'file-25.zip'");
|
||
|
||
pressOnce($page)->click('#by-size button');
|
||
|
||
$page->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('1–10 of 25')
|
||
->assertAttribute('[data-md-pagination] [aria-current="page"]', 'aria-current', 'page')
|
||
->click('[data-md-pagination] button[aria-label="Go to page 3"]')
|
||
->assertSee('21–25 of 25')
|
||
->assertSeeIn('[data-md-pagination] [aria-current="page"]', '3');
|
||
|
||
// A relative paging key a retry would page twice: pressOnce(), tests/Pest.php.
|
||
pressOnce($page)->click('[data-md-pagination] button[aria-label="Previous"]');
|
||
|
||
$page->assertSee('11–20 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'");
|
||
});
|
||
|
||
it('keeps a selected table row\'s fill under the hover and focus tint', function () {
|
||
$page = ready(visit('/material/data'));
|
||
|
||
$selected = "document.querySelector('#data tr[data-md-list-row][aria-selected=\"true\"]')";
|
||
$plain = "document.querySelector('#data tr[data-md-list-row]:not([aria-selected=\"true\"])')";
|
||
|
||
$restBackground = $page->script("getComputedStyle({$selected}).backgroundColor");
|
||
|
||
$page->hover('#data tr[data-md-list-row][aria-selected="true"]');
|
||
$selectedHoverBackground = $page->script("getComputedStyle({$selected}).backgroundColor");
|
||
|
||
$page->hover('#data tr[data-md-list-row]:not([aria-selected="true"]) >> nth=0');
|
||
$plainHoverBackground = $page->script("getComputedStyle({$plain}).backgroundColor");
|
||
|
||
// The hover tint mixes over the row's own fill (`--md-list-row-fill`, table.css/list-item.css):
|
||
// the selected row's secondary-container stays under it, so its hover colour differs from both
|
||
// its own resting colour and a plain row's hover, which has no fill to keep.
|
||
expect($selectedHoverBackground)->not->toBe($restBackground)
|
||
->and($selectedHoverBackground)->not->toBe($plainHoverBackground);
|
||
|
||
$page->script("{$selected}.querySelector('[data-md-list-open]').focus()");
|
||
|
||
$selectedFocusBackground = $page->script("getComputedStyle({$selected}).backgroundColor");
|
||
|
||
expect($selectedFocusBackground)->not->toBe($restBackground)
|
||
->and($selectedFocusBackground)->not->toBe($selectedHoverBackground);
|
||
});
|