Files
pad/web/e2e/lib/boardClickProbe.ts
T
xarmian d8a44bb5ee test(e2e): record the geometry when a board-card click fails (BUG-3553 probe) (#2049)
* test(e2e): record the geometry when a board-card click fails (BUG-3553 probe)

pane-controller :583 and pane-content-link-anchors :290 fail on CI with
another card intercepting the hit point, and then "not stable" until the
timeout. It has not reproduced locally (a CI-shaped desktop shard 1/2 at
2 workers passed clean), so the mechanism has to be read from a failing
CI run. clickBoardCard clicks as before; on failure it records the target's
box over ten frames, its lane index, the column's scroll, neighbours'
content-visibility state, transforms, and the element under the hit
point, attaches it, and rethrows. No wait is added.

Claude-Session: https://claude.ai/code/session_01FAVXA5mJ5BNhKmv95jxQkQ

* test(e2e): BUG-2871 spec opens its own parent on a retry; its click is probed (BUG-3553)

On #2049's first run the spec's first attempt clicked the parent card and
no pane opened (the BUG-3553 family), and its retry then opened the FIRST
attempt's parent: the locator matched the title prefix and took .first().
It now selects the card by the seeded slug. With --repeat-each=3 on one
server the old locator fails exactly as CI did; the new one passes 3/3.

Claude-Session: https://claude.ai/code/session_01FAVXA5mJ5BNhKmv95jxQkQ
2026-10-11 02:21:30 -04:00

75 lines
3.6 KiB
TypeScript

import { test, type Locator } from '@playwright/test';
/**
* BUG-3553 diagnostic: click a board card, and if the click fails, record the
* geometry that explains why, then fail exactly as before. It adds NO wait and
* changes nothing about a click that succeeds.
*
* The failure it is for (pane-controller :583, pane-content-link-anchors
* :290): after scrolling, another card intercepts the hit point, and the
* target then reads "not stable" until the test times out. The trace shows a
* static screen and a column scrollTop far too small for the target's place
* in the lane, so the target's box at hit-test time is not where normal flow
* puts it. The fix depends on which mechanism moves it; this records the
* candidates (content-visibility state, transforms, scroll, the element under
* the hit point, the box across frames) from the failing run itself, because
* it has not reproduced locally.
*
* The click gets a bounded timeout, so the probe still has time to run inside
* the test's 30s budget.
*/
export async function clickBoardCard(card: Locator, timeout = 12_000): Promise<void> {
const testInfo = test.info();
try {
await card.click({ timeout });
} catch (err) {
const probe = await card
.evaluate(async (el) => {
const frame = () => new Promise<void>((r) => requestAnimationFrame(() => r()));
const rect = (e: Element) => {
const b = e.getBoundingClientRect();
return { x: Math.round(b.x), y: Math.round(b.y), w: Math.round(b.width), h: Math.round(b.height) };
};
const boxes: ReturnType<typeof rect>[] = [];
for (let i = 0; i < 10; i++) {
boxes.push(rect(el));
await frame();
}
const wrapper = el.closest('.card-wrapper');
const column = el.closest('.column-cards') as HTMLElement | null;
const wrappers = column ? Array.from(column.querySelectorAll(':scope > .card-wrapper')) : [];
const at = wrapper ? wrappers.indexOf(wrapper) : -1;
const b = el.getBoundingClientRect();
const hit = document.elementFromPoint(b.x + b.width / 2, b.y + b.height / 2);
const transforms: string[] = [];
for (let n: Element | null = el; n && n !== document.body; n = n.parentElement) {
const t = getComputedStyle(n).transform;
if (t && t !== 'none') transforms.push(`${n.className}: ${t}`);
}
const cv = (e: Element | null) =>
e && 'checkVisibility' in e ? (e as Element & { checkVisibility(o: object): boolean }).checkVisibility({ contentVisibilityAuto: true }) : null;
const neighbour = (i: number) => (wrappers[i] ? { i, rect: rect(wrappers[i]), rendered: cv(wrappers[i]) } : null);
return {
viewport: { w: innerWidth, h: innerHeight },
boxesOverTenFrames: boxes,
wrapper: wrapper ? { rect: rect(wrapper), offsetTop: (wrapper as HTMLElement).offsetTop, rendered: cv(wrapper) } : null,
indexInLane: at,
laneLength: wrappers.length,
column: column
? { scrollTop: column.scrollTop, scrollHeight: column.scrollHeight, clientHeight: column.clientHeight, rect: rect(column) }
: null,
neighbours: [neighbour(at - 2), neighbour(at - 1), neighbour(at + 1)],
hitAtCentre: hit ? { tag: hit.tagName, cls: String(hit.className), text: (hit.textContent ?? '').trim().slice(0, 60), insideTarget: el.contains(hit) } : null,
transforms,
activeAnimations: document.getAnimations().length,
url: location.pathname + location.search,
};
})
.catch((e) => ({ probeFailed: String(e) }));
const body = JSON.stringify(probe, null, 1);
console.log(`BUG-3553 board-click probe (${testInfo.title}):\n${body}`);
await testInfo.attach('bug-3553-board-click-probe', { body, contentType: 'application/json' });
throw err;
}
}