e2e flake: phone-shell's now-playing click is intercepted by the cover placeholder #150

Closed
opened 2026-08-20 02:11:29 +00:00 by logan · 2 comments
Collaborator

Report

e2e/specs/phone-shell.spec.ts:115opens the full-screen now
playing, and comes back
— failed in CI on both engines in one run,
and passed on both when the same commit was re-run with no change.
Run 17192 on PR #149: attempt 1 red on chromium and webkit (each with
its automatic retry), attempt 2 green on both.

Error: locator.click: Test timeout of 30000ms exceeded.
  - waiting for getByTestId('open-now-playing')
    - locator resolved to <button class="expand" aria-label="Open now playing" ...>
    - element is visible, enabled and stable
    - <wa-icon rotate="0" name="music" library="default" aria-hidden="true">
      from <div class="cover-art">…</div> subtree intercepts pointer events
    - retrying click action    (58 attempts, 30s)

So the button is found, visible, enabled and stable, and Playwright
then refuses to click it for the whole timeout because the cover art's
placeholder icon is what is under the pointer.

Findings

  • .expand is position: absolute; inset: 0 inside
    .cover-art-wrapper and is a previous sibling of .cover-art, so
    it wins the hit test by being the only positioned box. Checked
    against the running app at 390px: elementFromPoint at the button's
    centre returns button.expand both with an <img> and with the
    placeholder markup swapped in, so plain stacking is not the
    explanation
    and the naive reading of the log is wrong.
  • Which leaves something time-dependent about the moment of the click.
    Two candidates, neither confirmed: wa-icon is a custom element and
    its upgrade replaces its own contents, so there is a window in which
    the box under the pointer is being rebuilt; and .cover-art carries
    @mouseenter/@mouseleave opening the cover-preview wa-popup,
    which Playwright's own pointer movement triggers on the way to the
    click.
  • Not reproducible locally: 10/10 on phone-shell.spec.ts alone and
    green in three full-suite runs, on a freshly built seed
    (make sandbox-seed NAME=default) as well as an older one.
  • Both engines failing together points at app state rather than at an
    engine, and CI's chromium and webkit passes share one app — but
    the chromium pass is the first one, so this is not the
    leftover-state shape #146 and the config-section .header fault
    have.

Why it matters more than one red run. The failure costs a full
e2e job — ~10 min on a capacity-1 runner, with the 30s timeout paid
four times — and it reads exactly like a regression in whatever is
being reviewed. It cost a cycle on #149 for that reason.

Direction

Establish the mechanism before changing the spec: drive the click at
390px against a track with no cover art (the fixture's Extremes
album), with the popup's mouseenter disabled, and see which of the
two candidates it is. If it is the preview popup, the fix is on the
component — a preview that opens under the pointer on a phone, where
there is no hover, is wrong independently of the test. If it is the
icon's upgrade, .cover-art { pointer-events: none } on the phone is
the honest statement: it is decoration, and the button over it is the
only thing there to click.

**Report** `e2e/specs/phone-shell.spec.ts:115` — *opens the full-screen now playing, and comes back* — failed in CI on **both engines in one run**, and passed on **both** when the same commit was re-run with no change. Run 17192 on PR #149: attempt 1 red on chromium and webkit (each with its automatic retry), attempt 2 green on both. ``` Error: locator.click: Test timeout of 30000ms exceeded. - waiting for getByTestId('open-now-playing') - locator resolved to <button class="expand" aria-label="Open now playing" ...> - element is visible, enabled and stable - <wa-icon rotate="0" name="music" library="default" aria-hidden="true"> from <div class="cover-art">…</div> subtree intercepts pointer events - retrying click action (58 attempts, 30s) ``` So the button is found, visible, enabled and stable, and Playwright then refuses to click it for the whole timeout because the cover art's **placeholder icon** is what is under the pointer. **Findings** - `.expand` is `position: absolute; inset: 0` inside `.cover-art-wrapper` and is a *previous* sibling of `.cover-art`, so it wins the hit test by being the only positioned box. Checked against the running app at 390px: `elementFromPoint` at the button's centre returns `button.expand` both with an `<img>` and with the placeholder markup swapped in, so **plain stacking is not the explanation** and the naive reading of the log is wrong. - Which leaves something time-dependent about the moment of the click. Two candidates, neither confirmed: `wa-icon` is a custom element and its upgrade replaces its own contents, so there is a window in which the box under the pointer is being rebuilt; and `.cover-art` carries `@mouseenter`/`@mouseleave` opening the `cover-preview` `wa-popup`, which Playwright's own pointer movement triggers on the way to the click. - Not reproducible locally: 10/10 on `phone-shell.spec.ts` alone and green in three full-suite runs, on a freshly built seed (`make sandbox-seed NAME=default`) as well as an older one. - Both engines failing together points at app state rather than at an engine, and CI's chromium and webkit passes share **one app** — but the chromium pass is the *first* one, so this is not the leftover-state shape #146 and the `config-section .header` fault have. **Why it matters more than one red run.** The failure costs a full `e2e` job — ~10 min on a capacity-1 runner, with the 30s timeout paid four times — and it reads exactly like a regression in whatever is being reviewed. It cost a cycle on #149 for that reason. **Direction** Establish the mechanism before changing the spec: drive the click at 390px against a track with **no** cover art (the fixture's *Extremes* album), with the popup's `mouseenter` disabled, and see which of the two candidates it is. If it is the preview popup, the fix is on the component — a preview that opens under the pointer on a phone, where there is no hover, is wrong independently of the test. If it is the icon's upgrade, `.cover-art { pointer-events: none }` on the phone is the honest statement: it is decoration, and the button over it is the only thing there to click.
logan added the Area/Player
Reviewed
Confirmed
1
Priority
Medium
3
Kind/Testing
labels 2026-08-20 02:11:29 +00:00
Author
Collaborator

Mechanism found, and it is not a race — it is the playing track.

Reproduced deterministically against the running app at 390px: play a
track with no cover art and the button is unclickable.

track: "01 Tone A"   hasImg: false   hasIcon: true
elementFromPoint(centre of .expand) -> wa-icon      <-- not the button

Play a track with art and the same probe returns button.expand. So
the earlier reading — an upgrade window, or the preview popup opening
under the pointer — was wrong on both counts. wa-icon is
position: static, z-index: auto, and it still wins, because .expand
is position: absolute with z-index: auto and the placeholder is a
later sibling: they tie, and the later one paints on top.

So the "flake" is which track happens to be playing, which is the same
family as #156 — the fixture library deliberately contains tracks with
no art, phone-shell.spec.ts starts the first row of the track list,
and that order is the scan's.

This is a user-facing bug, not a test defect. On a phone, the only
way into the full-screen now-playing view is that button, and it does
not work whenever the current track has no artwork. It has nothing to do
with the fixture: any library has untagged files.

Verified fix: z-index: 1 on .expand restores button.expand as the
hit-test result. That is preferable to pointer-events: none on the
art, which would also take the cover-preview mouseenter with it, and
to reordering the DOM, which would put the button after the thing it
covers and rely on the tie going the other way.

Raising the priority: it costs a CI cycle on an unrelated PR roughly
one run in three, and it is an unreachable control on the platform it
affects.

**Mechanism found, and it is not a race — it is the playing track.** Reproduced deterministically against the running app at 390px: play a track with **no cover art** and the button is unclickable. ``` track: "01 Tone A" hasImg: false hasIcon: true elementFromPoint(centre of .expand) -> wa-icon <-- not the button ``` Play a track *with* art and the same probe returns `button.expand`. So the earlier reading — an upgrade window, or the preview popup opening under the pointer — was wrong on both counts. `wa-icon` is `position: static, z-index: auto`, and it still wins, because `.expand` is `position: absolute` with `z-index: auto` and the placeholder is a *later* sibling: they tie, and the later one paints on top. So the "flake" is which track happens to be playing, which is the same family as #156 — the fixture library deliberately contains tracks with no art, `phone-shell.spec.ts` starts the *first* row of the track list, and that order is the scan's. **This is a user-facing bug, not a test defect.** On a phone, the only way into the full-screen now-playing view is that button, and it does not work whenever the current track has no artwork. It has nothing to do with the fixture: any library has untagged files. Verified fix: `z-index: 1` on `.expand` restores `button.expand` as the hit-test result. That is preferable to `pointer-events: none` on the art, which would also take the cover-preview `mouseenter` with it, and to reordering the DOM, which would put the button after the thing it covers and rely on the tie going the other way. Raising the priority: it costs a CI cycle on an unrelated PR roughly one run in three, and it is an unreachable control on the platform it affects.
logan added
Priority
High
2
and removed
Priority
Medium
3
labels 2026-08-20 05:03:02 +00:00
logan self-assigned this 2026-08-20 05:03:10 +00:00
logan added the
Status
In Progress
label 2026-08-20 05:03:10 +00:00
Author
Collaborator

Claiming this. Branch: fix/150-expand-button-under-the-art.

Mechanism and reproduction are in the comment above: the placeholder
icon and the button tie on paint order and the later sibling wins, so
the control is unreachable whenever the playing track has no artwork.
Fix is the z-index, plus a spec that plays a track chosen for having
no art rather than whichever one the scan inserted first.

Claiming this. Branch: `fix/150-expand-button-under-the-art`. Mechanism and reproduction are in the comment above: the placeholder icon and the button tie on paint order and the later sibling wins, so the control is unreachable whenever the playing track has no artwork. Fix is the z-index, plus a spec that plays a track *chosen* for having no art rather than whichever one the scan inserted first.
logan closed this issue 2026-08-20 05:47:47 +00:00
gitea-actions bot removed the
Status
In Progress
label 2026-08-20 05:47:55 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: yonlu/yellowjacket#150