Press once whatever a second press would open over or undo
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>
This commit is contained in:
surtic86
2026-09-18 16:15:25 +02:00
co-authored by Claude Opus 5
parent ad230e0f48
commit ebdc2ef2e1
17 changed files with 1170 additions and 578 deletions
+147 -73
View File
@@ -73,10 +73,13 @@ function supportText(string $id): string
}
it('opens the dial on the hour, as the bound time says', function () {
timeProbe()
->assertValue('#meeting', '9:30 AM')
->click('#meeting')
->assertScript("document.querySelector('#meeting-dialog').open")
$page = timeProbe()
->assertValue('#meeting', '9:30 AM');
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
pressOnce($page)->click('#meeting');
$page->assertScript("document.querySelector('#meeting-dialog').open")
->assertAttribute('#meeting', 'aria-expanded', 'true')
->assertSeeIn('#meeting-title', 'Select time')
->assertAttribute('#meeting-dialog [data-md-timepicker-box="hour"]', 'aria-pressed', 'true')
@@ -90,9 +93,12 @@ it('opens the dial on the hour, as the bound time says', function () {
it('writes the hour and the minute pressed on the dial to Livewire on OK', function () {
$dial = inPicker('meeting', '[data-md-timepicker-dial]');
$page = timeProbe()
->click('#meeting')
->click('#meeting-dialog [data-md-timepicker-label="hour12"][data-md-value="3"]')
$page = timeProbe();
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
pressOnce($page)->click('#meeting');
$page->click('#meeting-dialog [data-md-timepicker-label="hour12"][data-md-value="3"]')
->assertSeeIn('#meeting-dialog [data-md-timepicker-box="hour"]', '03')
->assertAttribute('#meeting-dialog [data-md-timepicker-dial]', 'data-md-view', 'minute')
->assertAttribute('#meeting-dialog [data-md-timepicker-box="minute"]', 'aria-pressed', 'true')
@@ -108,34 +114,49 @@ it('writes the hour and the minute pressed on the dial to Livewire on OK', funct
$page->click('#meeting-dialog [data-md-timepicker-label="minute"][data-md-value="45"]')
->assertSeeIn('#meeting-dialog [data-md-timepicker-box="minute"]', '45')
->assertSeeIn('#meeting-value', '09:30')
->click('#meeting-dialog [data-md-timepicker-confirm]')
->assertSeeIn('#meeting-value', '03:45')
->assertSeeIn('#meeting-value', '09:30');
// The confirm button closes the dialog: pressOnce(), tests/Pest.php.
pressOnce($page)->click('#meeting-dialog [data-md-timepicker-confirm]');
$page->assertSeeIn('#meeting-value', '03:45')
->assertValue('#meeting', '3:45 AM')
->assertScript("! document.querySelector('#meeting-dialog').open")
->assertAttribute('#meeting', 'aria-expanded', 'false');
});
it('leaves the time alone when the picker is cancelled', function () {
timeProbe()
->click('#meeting')
->click('#meeting-dialog [data-md-timepicker-label="hour12"][data-md-value="7"]')
->assertSeeIn('#meeting-dialog [data-md-timepicker-box="hour"]', '07')
->click('#meeting-dialog [data-md-timepicker-cancel]')
->assertScript("! document.querySelector('#meeting-dialog').open")
$page = timeProbe();
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
pressOnce($page)->click('#meeting');
$page->click('#meeting-dialog [data-md-timepicker-label="hour12"][data-md-value="7"]')
->assertSeeIn('#meeting-dialog [data-md-timepicker-box="hour"]', '07');
// The cancel button closes the dialog: pressOnce(), tests/Pest.php.
pressOnce($page)->click('#meeting-dialog [data-md-timepicker-cancel]');
$page->assertScript("! document.querySelector('#meeting-dialog').open")
->assertSeeIn('#meeting-value', '09:30')
->assertValue('#meeting', '9:30 AM');
});
it('turns three o\'clock into 15 with PM', function () {
timeProbe()
->click('#meeting')
->click('#meeting-dialog [data-md-timepicker-display] [data-md-timepicker-period-option="pm"]')
$page = timeProbe();
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
pressOnce($page)->click('#meeting');
$page->click('#meeting-dialog [data-md-timepicker-display] [data-md-timepicker-period-option="pm"]')
->assertAttribute('#meeting-dialog [data-md-timepicker-display] [data-md-timepicker-period-option="pm"]', 'aria-checked', 'true')
->click('#meeting-dialog [data-md-timepicker-label="hour12"][data-md-value="3"]')
->click('#meeting-dialog [data-md-timepicker-label="minute"][data-md-value="15"]')
->click('#meeting-dialog [data-md-timepicker-confirm]')
->assertSeeIn('#meeting-value', '15:15')
->click('#meeting-dialog [data-md-timepicker-label="minute"][data-md-value="15"]');
// The confirm button closes the dialog: pressOnce(), tests/Pest.php.
pressOnce($page)->click('#meeting-dialog [data-md-timepicker-confirm]');
$page->assertSeeIn('#meeting-value', '15:15')
->assertValue('#meeting', '3:15 PM');
});
@@ -150,9 +171,12 @@ it('draws a German dial with 24 hours, 12 to 23 on the inner ring', function ()
})()
JS;
$page = timeProbe()
->click('#alarm')
->assertAttribute('#alarm-dialog [data-md-timepicker-dial]', 'data-md-cycle', '24')
$page = timeProbe();
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
pressOnce($page)->click('#alarm');
$page->assertAttribute('#alarm-dialog [data-md-timepicker-dial]', 'data-md-cycle', '24')
->assertScript("getComputedStyle(document.querySelector('#alarm-dialog [data-md-timepicker-period]')).display === 'none'")
->assertScript("{$dial}.querySelector('[data-md-timepicker-label=\"hour24\"][data-md-value=\"0\"]').textContent.trim() === '00'")
->assertScript($radius('0').' === 101')
@@ -164,24 +188,33 @@ it('draws a German dial with 24 hours, 12 to 23 on the inner ring', function ()
$page->click('#alarm-dialog [data-md-timepicker-label="hour24"][data-md-value="15"]')
->assertSeeIn('#alarm-dialog [data-md-timepicker-box="hour"]', '15')
->assertAttribute('#alarm-dialog [data-md-timepicker-dial]', 'data-md-view', 'minute')
->click('#alarm-dialog [data-md-timepicker-label="minute"][data-md-value="0"]')
->click('#alarm-dialog [data-md-timepicker-confirm]')
->assertSeeIn('#alarm-value', '15:00')
->click('#alarm-dialog [data-md-timepicker-label="minute"][data-md-value="0"]');
// The confirm button closes the dialog: pressOnce(), tests/Pest.php.
pressOnce($page)->click('#alarm-dialog [data-md-timepicker-confirm]');
$page->assertSeeIn('#alarm-value', '15:00')
->assertValue('#alarm', '15:00');
// The outer ring is the morning.
$page->click('#alarm')
->click('#alarm-dialog [data-md-timepicker-label="hour24"][data-md-value="3"]')
pressOnce($page)->click('#alarm');
$page->click('#alarm-dialog [data-md-timepicker-label="hour24"][data-md-value="3"]')
->assertSeeIn('#alarm-dialog [data-md-timepicker-box="hour"]', '03')
->click('#alarm-dialog [data-md-timepicker-label="minute"][data-md-value="0"]')
->click('#alarm-dialog [data-md-timepicker-confirm]')
->assertSeeIn('#alarm-value', '03:00');
->click('#alarm-dialog [data-md-timepicker-label="minute"][data-md-value="0"]');
pressOnce($page)->click('#alarm-dialog [data-md-timepicker-confirm]');
$page->assertSeeIn('#alarm-value', '03:00');
});
it('follows a drag round the dial and settles on the nearest hour', function () {
$dial = inPicker('meeting', '[data-md-timepicker-dial]');
$page = timeProbe()->click('#meeting');
$page = timeProbe();
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
pressOnce($page)->click('#meeting');
// Pointer events built in the page's own realm, from twelve o'clock round to just past four.
$send = fn (string $type, int $degrees): string => <<<JS
@@ -216,8 +249,10 @@ it('changes the focused hour and minute with the arrow keys and confirms with En
$page->script("document.querySelector('#meeting').focus()");
$page->keys('#meeting', 'Enter')
->assertScript('document.activeElement === '.inPicker('meeting', '[data-md-timepicker-dial]'))
// Enter on the field opens the dialog, same as clicking it: pressOnce(), tests/Pest.php.
pressOnce($page)->keys('#meeting', 'Enter');
$page->assertScript('document.activeElement === '.inPicker('meeting', '[data-md-timepicker-dial]'))
->keys(':focus', 'ArrowUp')
->assertAttribute('#meeting-dialog [data-md-timepicker-dial]', 'aria-valuenow', '10')
->assertSeeIn('#meeting-dialog [data-md-timepicker-box="hour"]', '10')
@@ -233,16 +268,22 @@ it('changes the focused hour and minute with the arrow keys and confirms with En
$page->keys(':focus', ['ArrowDown', 'ArrowLeft'])
->assertAttribute('#meeting-dialog [data-md-timepicker-dial]', 'aria-valuenow', '28')
->assertAttribute('#meeting-dialog [data-md-timepicker-dial]', 'aria-valuetext', '28 minutes')
->keys(':focus', 'Enter')
->assertScript("! document.querySelector('#meeting-dialog').open")
->assertAttribute('#meeting-dialog [data-md-timepicker-dial]', 'aria-valuetext', '28 minutes');
// Enter on the dial confirms and closes the dialog: pressOnce(), tests/Pest.php.
pressOnce($page)->keys(':focus', 'Enter');
$page->assertScript("! document.querySelector('#meeting-dialog').open")
->assertSeeIn('#meeting-value', '10:28');
});
it('takes a typed time in the input variant and says what is wrong with it', function () {
$page = timeProbe()
->click('#meeting')
->click('#meeting-dialog [data-md-timepicker-mode="input"]')
$page = timeProbe();
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
pressOnce($page)->click('#meeting');
$page->click('#meeting-dialog [data-md-timepicker-mode="input"]')
->assertSeeIn('#meeting-title', 'Enter time')
->assertScript("document.activeElement.id === 'meeting-hour'")
->assertValue('#meeting-hour', '09')
@@ -251,9 +292,13 @@ it('takes a typed time in the input variant and says what is wrong with it', fun
$page->type('#meeting-hour', '13')
->assertAttribute('#meeting-hour', 'aria-invalid', 'true')
// Word joiners keep the range on one line in the narrow column.
->assertScript(supportText('meeting-hour')." === 'Hour must be 112'")
->click('#meeting-dialog [data-md-timepicker-confirm]')
->assertScript("document.querySelector('#meeting-dialog').open")
->assertScript(supportText('meeting-hour')." === 'Hour must be 112'");
// The confirm button closes the dialog when the time is valid; here it stays open (invalid),
// but pressed once anyway: pressOnce(), tests/Pest.php.
pressOnce($page)->click('#meeting-dialog [data-md-timepicker-confirm]');
$page->assertScript("document.querySelector('#meeting-dialog').open")
->assertSeeIn('#meeting-value', '09:30');
$page->clear('#meeting-hour')
@@ -275,9 +320,12 @@ it('takes a typed time in the input variant and says what is wrong with it', fun
it('keeps to the step and the limits', function () {
$dial = inPicker('slot', '[data-md-timepicker-dial]');
$page = timeProbe()
->click('#slot')
->assertScript("{$dial}.querySelector('[data-md-timepicker-label=\"hour24\"][data-md-value=\"8\"]').hasAttribute('data-md-disabled')")
$page = timeProbe();
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
pressOnce($page)->click('#slot');
$page->assertScript("{$dial}.querySelector('[data-md-timepicker-label=\"hour24\"][data-md-value=\"8\"]').hasAttribute('data-md-disabled')")
->assertScript("! {$dial}.querySelector('[data-md-timepicker-label=\"hour24\"][data-md-value=\"9\"]').hasAttribute('data-md-disabled')")
->assertScript("{$dial}.querySelector('[data-md-timepicker-label=\"hour24\"][data-md-value=\"18\"]').hasAttribute('data-md-disabled')")
// An hour outside the limits is not taken.
@@ -299,12 +347,18 @@ it('keeps to the step and the limits', function () {
->assertSeeIn('#slot-dialog [data-md-timepicker-range-error]', 'Choose a time from 09:00 to 17:00')
->type('#slot-hour', '16')
->type('#slot-minute', '20')
->assertSeeIn('#slot-minute-support', 'Minute must be a multiple of 15')
->click('#slot-dialog [data-md-timepicker-confirm]')
->assertScript("document.activeElement.id === 'slot-minute'")
->type('#slot-minute', '45')
->click('#slot-dialog [data-md-timepicker-confirm]')
->assertSeeIn('#slot-value', '16:45');
->assertSeeIn('#slot-minute-support', 'Minute must be a multiple of 15');
// The confirm button closes the dialog when the time is valid; here it stays open (invalid),
// but pressed once anyway: pressOnce(), tests/Pest.php.
pressOnce($page)->click('#slot-dialog [data-md-timepicker-confirm]');
$page->assertScript("document.activeElement.id === 'slot-minute'")
->type('#slot-minute', '45');
pressOnce($page)->click('#slot-dialog [data-md-timepicker-confirm]');
$page->assertSeeIn('#slot-value', '16:45');
});
it('gives focus back to the field on Escape', function () {
@@ -312,8 +366,10 @@ it('gives focus back to the field on Escape', function () {
$page->script("document.querySelector('#meeting').focus()");
$page->keys('#meeting', 'Enter')
->assertScript("document.querySelector('#meeting-dialog').open")
// Enter on the field opens the dialog, same as clicking it: pressOnce(), tests/Pest.php.
pressOnce($page)->keys('#meeting', 'Enter');
$page->assertScript("document.querySelector('#meeting-dialog').open")
->keys(':focus', 'ArrowUp')
->keys(':focus', 'Escape')
->assertScript("! document.querySelector('#meeting-dialog').open")
@@ -322,9 +378,12 @@ it('gives focus back to the field on Escape', function () {
});
it('stays open with its draft through a Livewire render', function () {
$page = timeProbe()
->click('#meeting')
->click('#meeting-dialog [data-md-timepicker-label="hour12"][data-md-value="5"]')
$page = timeProbe();
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
pressOnce($page)->click('#meeting');
$page->click('#meeting-dialog [data-md-timepicker-label="hour12"][data-md-value="5"]')
->assertAttribute('#meeting-dialog [data-md-timepicker-dial]', 'data-md-view', 'minute');
$page->script('window.eval("Livewire.first().touch()")');
@@ -333,18 +392,23 @@ it('stays open with its draft through a Livewire render', function () {
->assertScript("document.querySelector('#meeting-dialog').open")
->assertSeeIn('#meeting-dialog [data-md-timepicker-box="hour"]', '05')
->assertAttribute('#meeting-dialog [data-md-timepicker-dial]', 'data-md-view', 'minute')
->click('#meeting-dialog [data-md-timepicker-label="minute"][data-md-value="10"]')
->click('#meeting-dialog [data-md-timepicker-confirm]')
->assertSeeIn('#meeting-value', '05:10');
->click('#meeting-dialog [data-md-timepicker-label="minute"][data-md-value="10"]');
pressOnce($page)->click('#meeting-dialog [data-md-timepicker-confirm]');
$page->assertSeeIn('#meeting-value', '05:10');
});
it('moves the period selector between AM and PM with arrow keys, as a radio group', function () {
$am = '#meeting-dialog [data-md-timepicker-display] [data-md-timepicker-period-option="am"]';
$pm = '#meeting-dialog [data-md-timepicker-display] [data-md-timepicker-period-option="pm"]';
$page = timeProbe()
->click('#meeting')
->assertAttribute($am, 'role', 'radio')
$page = timeProbe();
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
pressOnce($page)->click('#meeting');
$page->assertAttribute($am, 'role', 'radio')
->assertAttribute($am, 'aria-checked', 'true')
->assertAttribute($pm, 'aria-checked', 'false')
// show() moves focus to the dial from inside Alpine's $nextTick, which Livewire's bundled
@@ -356,25 +420,32 @@ it('moves the period selector between AM and PM with arrow keys, as a radio grou
$page->script("document.querySelector('{$am}').focus()");
$page->keys(':focus', 'ArrowRight')
->assertAttribute($pm, 'aria-checked', 'true')
// A two-option radio group: a second press of the same arrow cycles back: pressOnce(),
// tests/Pest.php.
pressOnce($page)->keys(':focus', 'ArrowRight');
$page->assertAttribute($pm, 'aria-checked', 'true')
->assertAttribute($am, 'aria-checked', 'false')
->assertScript("document.activeElement.dataset.mdTimepickerPeriodOption === 'pm'")
// M3's radio-in-a-list: an arrow key moves to the other option and selects it, so PM
// reaches the dial straight away — the hour turns 9 into 21 without a confirm.
->assertSeeIn('#meeting-dialog [data-md-timepicker-box="hour"]', '09');
$page->keys(':focus', 'ArrowLeft')
->assertAttribute($am, 'aria-checked', 'true')
pressOnce($page)->keys(':focus', 'ArrowLeft');
$page->assertAttribute($am, 'aria-checked', 'true')
->assertAttribute($pm, 'aria-checked', 'false')
->assertScript("document.activeElement.dataset.mdTimepickerPeriodOption === 'am'");
});
it('lies the dial on its side in a short landscape window', function () {
$page = timeProbe()
->resize(700, 400)
->click('#meeting')
->assertScript("document.querySelector('#meeting-dialog').open");
->resize(700, 400);
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
pressOnce($page)->click('#meeting');
$page->assertScript("document.querySelector('#meeting-dialog').open");
// Side by side rather than stacked: the display (216px wide) sits centred in the dial's row,
// inside its height, with the dial 36px after it, and the picker's own layout (flex column)
@@ -407,7 +478,10 @@ it('keeps the dial and the boxes their fixed pixel size at a 20px root font size
$dialWidth = "Math.round({$dial}.getBoundingClientRect().width)";
$font = "getComputedStyle({$hourBox}).fontSize";
$page = timeProbe()->click('#meeting');
$page = timeProbe();
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
pressOnce($page)->click('#meeting');
// After the dialog's entry, not during it: mid-scale the dial measures 0.95 of its own size,
// which would make the resting width this test compares against a number the CSS never sets.