From 8da8b1fa20edcde7707a5cca95767db7e8b89c03 Mon Sep 17 00:00:00 2001 From: surtic86 Date: Fri, 18 Sep 2026 06:14:31 +0200 Subject: [PATCH] Say what stood in the way when a click never lands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .github/workflows/tests.yml | 12 +++++++++ tests/Browser/DatepickerTest.php | 44 ++++++++++++++++++++++++++++---- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index db76ff5c..e53586e6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -102,3 +102,15 @@ jobs: run: vendor/bin/pest --testsuite=Browser --browser ${{ matrix.browser }} env: 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 diff --git a/tests/Browser/DatepickerTest.php b/tests/Browser/DatepickerTest.php index cc333663..828093fc 100644 --- a/tests/Browser/DatepickerTest.php +++ b/tests/Browser/DatepickerTest.php @@ -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 }"]\')))'); }); +/** + * 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 << { + 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 () { $picker = "document.querySelector('#trip-field-picker')"; $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 // 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. - ->assertScript('window.innerWidth === 390 && document.documentElement.getAnimations({ subtree: true }).length === 0') - ->click('[aria-controls="trip-field-picker"][data-md-datepicker-toggle]') + ->assertScript('window.innerWidth === 390 && document.documentElement.getAnimations({ subtree: true }).length === 0'); + + 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}.matches(':modal')") ->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 () { $picker = "document.querySelector('#trip-field-picker')"; - dateProbe() + $page = dateProbe() ->resize(390, 800) - ->assertScript('window.innerWidth === 390 && document.documentElement.getAnimations({ subtree: true }).length === 0') - ->click('[aria-controls="trip-field-picker"][data-md-datepicker-toggle]') + ->assertScript('window.innerWidth === 390 && document.documentElement.getAnimations({ subtree: true }).length === 0'); + + 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('#trip-field-picker [data-md-datepicker-close]') ->assertScript("! {$picker}.open")