Fix BarsTest's overflow selector and reopen timing, add reviewer coverage

The overflow menu's trigger selector matched its own popover's menu
items too (they sit inside the same wrapper), so a click resolved to
5 elements instead of 1 — scoped to [data-md-menu-trigger]. Escape's
close is the browser's own light dismiss (menu.js), so a click right
after it needs to be past the 250ms reopen guard, as the same pattern
already is elsewhere (ActionsTest.php); the reopen test now waits.

Also adds the reviewer's requested coverage: the section nav's list
items share the row's width equally regardless of label length, and a
plain wire:model tab switches its panel locally without reaching the
server per click while wire:model.live does, one request that also
flushes the deferred property along with it (plan step 36).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-15 01:37:12 +02:00
co-authored by Claude Sonnet 5
parent bd2bb07106
commit 98f861c124
+58 -4
View File
@@ -9,6 +9,17 @@ class BarsProbe extends Component
{ {
public string $tab = 'files'; public string $tab = 'files';
public string $deferredTab = 'files';
public int $deferredUpdates = 0;
// Only runs while the server processes a request that carries $deferredTab along — never on
// the click alone, since a plain wire:model (no `.live`) only changes it locally until then.
public function updatedDeferredTab(): void
{
$this->deferredUpdates++;
}
public function render(): string public function render(): string
{ {
return <<<'BLADE' return <<<'BLADE'
@@ -27,6 +38,13 @@ class BarsProbe extends Component
<x-tab name="activity">Two downloads.</x-tab> <x-tab name="activity">Two downloads.</x-tab>
</x-tabs> </x-tabs>
<p>deferred tab: <span id="deferred-tab">{{ $deferredTab }}</span>, updates: <span id="deferred-updates">{{ $deferredUpdates }}</span></p>
<x-tabs id="deferred" wire:model="deferredTab" :tabs="[['name' => 'files', 'label' => 'Files'], ['name' => 'people', 'label' => 'People']]">
<x-tab name="files">Deferred files.</x-tab>
<x-tab name="people">Deferred people.</x-tab>
</x-tabs>
<x-toolbar label="Formatting"> <x-toolbar label="Formatting">
<button type="button" id="bold">B</button> <button type="button" id="bold">B</button>
<button type="button" id="italic">I</button> <button type="button" id="italic">I</button>
@@ -36,7 +54,7 @@ class BarsProbe extends Component
<x-theme-toggle mode="cycle" id="cycle" /> <x-theme-toggle mode="cycle" id="cycle" />
<x-theme-toggle mode="picker" /> <x-theme-toggle mode="picker" />
<x-section-nav :items="[['title' => 'Profile', 'url' => '#profile', 'active' => true], ['title' => 'Security', 'url' => '#security']]" no-wire-navigate /> <x-section-nav :items="[['title' => 'Profile', 'url' => '#profile', 'active' => true], ['title' => 'Security', 'url' => '#security'], ['title' => 'Advanced notification settings', 'url' => '#notifications']]" no-wire-navigate />
<div style="height: 200vh"></div> <div style="height: 200vh"></div>
</div> </div>
@@ -89,6 +107,30 @@ it('moves between tabs with the arrow keys, skipping disabled ones, and tells Li
->assertSeeIn('#tab', 'activity'); ->assertSeeIn('#tab', 'activity');
}); });
it('sends a request per click with wire:model.live, but nothing until then with plain wire:model', function () {
// updatedDeferredTab() on the probe only runs while the server processes a request that
// carries $deferredTab with it — never from the click alone — so its counter (not a browser
// network hook, which a plain property commit does not reliably fire) tells whether a click
// reached the server at all.
$page = barsProbe();
// Plain wire:model is entangled locally: the click switches the panel and its aria-selected at
// once, but the property itself only reaches the server on the next request from anything else.
$page->click('#deferred-people')
->assertScript("document.querySelector('#deferred-people').getAttribute('aria-selected') === 'true'")
->assertSee('Deferred people.')
->assertSeeIn('#deferred-tab', 'files')
->assertSeeIn('#deferred-updates', '0');
// wire:model.live sends its own request, which carries the deferred property above along with
// it — the one request updates both.
$page->click('#share-people')
->assertSeeIn('#tab', 'people')
->assertSeeIn('#deferred-tab', 'people')
->assertSeeIn('#deferred-updates', '1')
->assertNoJavaScriptErrors();
});
it('collapses a medium app bar into its row as the page scrolls, and fades its small title in', function () { it('collapses a medium app bar into its row as the page scrolls, and fades its small title in', function () {
$headline = "getComputedStyle(document.querySelector('#bar [data-md-app-bar-row] [data-md-app-bar-headline]')).opacity"; $headline = "getComputedStyle(document.querySelector('#bar [data-md-app-bar-row] [data-md-app-bar-headline]')).opacity";
@@ -146,6 +188,15 @@ it('turns section tabs into a picker on a phone', function () {
->assertScript("! document.querySelector('[data-md-section-nav-picker] [role=\"menuitem\"][href=\"#security\"]').hasAttribute('aria-current')"); ->assertScript("! document.querySelector('[data-md-section-nav-picker] [role=\"menuitem\"][href=\"#security\"]').hasAttribute('aria-current')");
}); });
it('shares the row width equally between the section nav\'s items, whatever their label', function () {
$widths = "[...document.querySelectorAll('[data-md-section-nav] [data-md-tabs-bar] > li')].map((li) => Math.round(li.getBoundingClientRect().width))";
barsProbe()
->assertScript("{$widths}.length === 3")
->assertScript("new Set({$widths}).size === 1")
->assertNoJavaScriptErrors();
});
class AppBarOverflowProbe extends Component class AppBarOverflowProbe extends Component
{ {
public int $stars = 0; public int $stars = 0;
@@ -217,7 +268,7 @@ it('shows the app bar overflow at exactly 599 and 600px', function () {
it('moves the keyboard through an app bar\'s overflow menu, and closes it once the width no longer shows it', function () { it('moves the keyboard through an app bar\'s overflow menu, and closes it once the width no longer shows it', function () {
$page = appBarOverflowProbe()->resize(599, 800); $page = appBarOverflowProbe()->resize(599, 800);
$page->click('#bar [data-md-app-bar-overflow="compact"] button') $page->click('#bar [data-md-app-bar-overflow="compact"] [data-md-menu-trigger] button')
->assertScript("document.querySelector('#bar [data-md-app-bar-overflow=\"compact\"] [data-md-menu-popover]').matches(':popover-open')") ->assertScript("document.querySelector('#bar [data-md-app-bar-overflow=\"compact\"] [data-md-menu-popover]').matches(':popover-open')")
->keys(':focus', 'ArrowDown') ->keys(':focus', 'ArrowDown')
->assertScript("document.activeElement.textContent.includes('Star')") ->assertScript("document.activeElement.textContent.includes('Star')")
@@ -226,9 +277,12 @@ it('moves the keyboard through an app bar\'s overflow menu, and closes it once t
->keys(':focus', 'Escape') ->keys(':focus', 'Escape')
->assertScript("! document.querySelector('#bar [data-md-app-bar-overflow=\"compact\"] [data-md-menu-popover]').matches(':popover-open')"); ->assertScript("! document.querySelector('#bar [data-md-app-bar-overflow=\"compact\"] [data-md-menu-popover]').matches(':popover-open')");
// Past the reopen guard of the Escape above (menu.js), so the click below is on its own.
$page->wait(0.3);
// Reopen it, then resize across 600px: the compact menu's trigger is no longer shown, so the // Reopen it, then resize across 600px: the compact menu's trigger is no longer shown, so the
// menu it opened closes rather than staying open anchored to nothing. // menu it opened closes rather than staying open anchored to nothing.
$page->click('#bar [data-md-app-bar-overflow="compact"] button') $page->click('#bar [data-md-app-bar-overflow="compact"] [data-md-menu-trigger] button')
->assertScript("document.querySelector('#bar [data-md-app-bar-overflow=\"compact\"] [data-md-menu-popover]').matches(':popover-open')") ->assertScript("document.querySelector('#bar [data-md-app-bar-overflow=\"compact\"] [data-md-menu-popover]').matches(':popover-open')")
->resize(900, 800) ->resize(900, 800)
->assertScript("! document.querySelector('#bar [data-md-app-bar-overflow=\"compact\"] [data-md-menu-popover]').matches(':popover-open')") ->assertScript("! document.querySelector('#bar [data-md-app-bar-overflow=\"compact\"] [data-md-menu-popover]').matches(':popover-open')")
@@ -242,7 +296,7 @@ it('runs a wire:click action from either the icon button or its overflow menu it
->assertSeeIn('#stars', '1'); ->assertSeeIn('#stars', '1');
$page->resize(599, 800) $page->resize(599, 800)
->click('#bar [data-md-app-bar-overflow="compact"] button') ->click('#bar [data-md-app-bar-overflow="compact"] [data-md-menu-trigger] button')
->click('[role="menuitem"]:has-text("Star")') ->click('[role="menuitem"]:has-text("Star")')
->assertSeeIn('#stars', '2'); ->assertSeeIn('#stars', '2');
}); });