Say what stood in the way when a click never lands

The two full-screen range picker tests give up after the full timeout on
the runner's Firefox, and nowhere else. Playwright waits for a click
target to be visible, still, enabled and on top, and reports none of
those when it gives up — so the failure says only that 45 seconds
passed.

Both tests now name the element the browser would hit at the toggle's own
centre before they press it (`hitTarget()`), so a covered or undrawn
control fails with what covered it rather than with a timeout. The run
also keeps the screenshots Playwright writes for a failure, as an
artifact: a page that only misbehaves there is otherwise invisible from a
workstation where all three engines pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
surtic86
2026-09-18 06:14:31 +02:00
co-authored by Claude Opus 5
parent 1b38b39a47
commit dd76e79877
2 changed files with 51 additions and 5 deletions
+12
View File
@@ -102,3 +102,15 @@ jobs:
run: vendor/bin/pest --testsuite=Browser --browser ${{ matrix.browser }} run: vendor/bin/pest --testsuite=Browser --browser ${{ matrix.browser }}
env: env:
BROWSER_TIMEOUT: 45000 BROWSER_TIMEOUT: 45000
# What the runner saw when an assertion or an action gave up. A failure that does not
# reproduce on a workstation is otherwise only a stack trace: the page itself says whether an
# element was covered, unstyled or never drawn.
- name: Keep the screenshots of what failed
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: screenshots-${{ matrix.browser }}
path: tests/Browser/Screenshots
if-no-files-found: ignore
retention-days: 7
+39 -5
View File
@@ -503,6 +503,34 @@ it('keeps each picker\'s own settings inside a page\'s outer x-data scope', func
->assertScript('! (\'min\' in Alpine.$data(document.querySelector(\'[x-data="{ step: 1 }"]\')))'); ->assertScript('! (\'min\' in Alpine.$data(document.querySelector(\'[x-data="{ step: 1 }"]\')))');
}); });
/**
* What the browser would hit at the centre of `$selector`, as `tag#id[attributes]`, and whether the
* element itself is the answer. Playwright waits for a click target to be visible, still and on top,
* and gives up with nothing but a timeout when it never is; this says what stood in the way, on a
* runner where a layout that works elsewhere does not.
*/
function hitTarget(string $selector): string
{
return <<<JS
(() => {
const target = document.querySelector("{$selector}")
if (! target) return 'nothing matches {$selector}'
const box = target.getBoundingClientRect()
const top = document.elementFromPoint(box.left + box.width / 2, box.top + box.height / 2)
if (top === null) return `outside the viewport: \${JSON.stringify(box)}`
if (target === top || target.contains(top)) return 'the target'
const name = (element) => element.tagName.toLowerCase() + (element.id ? '#' + element.id : '')
+ [...element.attributes].filter((a) => a.name.startsWith('data-md')).map((a) => `[\${a.name}]`).join('')
return `\${name(top)} covers \${name(target)}`
})()
JS;
}
it('opens the range picker full screen below 600px and grows its month list both ways by keyboard paging', function () { it('opens the range picker full screen below 600px and grows its month list both ways by keyboard paging', function () {
$picker = "document.querySelector('#trip-field-picker')"; $picker = "document.querySelector('#trip-field-picker')";
$scope = "Alpine.\$data(document.querySelector('[data-md-datepicker]:has(#trip-field)'))"; $scope = "Alpine.\$data(document.querySelector('[data-md-datepicker]:has(#trip-field)'))";
@@ -512,8 +540,11 @@ it('opens the range picker full screen below 600px and grows its month list both
// Explicit, not implicit: the resize's own reflow can still be under way when the click // Explicit, not implicit: the resize's own reflow can still be under way when the click
// that follows fires, which is what made this time out on a loaded runner — the toggle's // that follows fires, which is what made this time out on a loaded runner — the toggle's
// box kept moving under the click rather than the click itself being slow. // box kept moving under the click rather than the click itself being slow.
->assertScript('window.innerWidth === 390 && document.documentElement.getAnimations({ subtree: true }).length === 0') ->assertScript('window.innerWidth === 390 && document.documentElement.getAnimations({ subtree: true }).length === 0');
->click('[aria-controls="trip-field-picker"][data-md-datepicker-toggle]')
expect($page->script(hitTarget('[aria-controls=\"trip-field-picker\"][data-md-datepicker-toggle]')))->toBe('the target');
$page->click('[aria-controls="trip-field-picker"][data-md-datepicker-toggle]')
->assertScript("{$picker}.dataset.mdPresentation === 'full'") ->assertScript("{$picker}.dataset.mdPresentation === 'full'")
->assertScript("{$picker}.matches(':modal')") ->assertScript("{$picker}.matches(':modal')")
->assertScript("document.querySelector('#trip-field-picker [data-md-datepicker-app-bar]').offsetHeight > 0") ->assertScript("document.querySelector('#trip-field-picker [data-md-datepicker-app-bar]').offsetHeight > 0")
@@ -557,10 +588,13 @@ it('opens the range picker full screen below 600px and grows its month list both
it('closes the full-screen range picker from its app bar close button without keeping the draft', function () { it('closes the full-screen range picker from its app bar close button without keeping the draft', function () {
$picker = "document.querySelector('#trip-field-picker')"; $picker = "document.querySelector('#trip-field-picker')";
dateProbe() $page = dateProbe()
->resize(390, 800) ->resize(390, 800)
->assertScript('window.innerWidth === 390 && document.documentElement.getAnimations({ subtree: true }).length === 0') ->assertScript('window.innerWidth === 390 && document.documentElement.getAnimations({ subtree: true }).length === 0');
->click('[aria-controls="trip-field-picker"][data-md-datepicker-toggle]')
expect($page->script(hitTarget('[aria-controls=\"trip-field-picker\"][data-md-datepicker-toggle]')))->toBe('the target');
$page->click('[aria-controls="trip-field-picker"][data-md-datepicker-toggle]')
->click(day('trip-field', '2026-09-20')) ->click(day('trip-field', '2026-09-20'))
->click('#trip-field-picker [data-md-datepicker-close]') ->click('#trip-field-picker [data-md-datepicker-close]')
->assertScript("! {$picker}.open") ->assertScript("! {$picker}.open")