test(desktop): fix the two smoke E2E failures blocking PR #6852

Desktop Smoke E2E shards 3 and 4 were red on this branch's CI while the
branch touches neither spec. Both failures are pre-existing races in the
specs themselves, so fix them where they are.

Shard 3 — "profile hover uses the channel hover surface" sampled the
channel row's background straight after hover(), which lands mid
`transition-colors` fade (rgba(0, 0, 0, 0.027) on the failing run). The
profile card then settles on the real hover colour, rgba(0, 0, 0, 0.04),
so toHaveCSS could never match and all three attempts failed. Poll the
reference element until two consecutive reads agree on a value that has
moved off its resting colour. Reproduced locally at 1-in-5 before the
change; 12 consecutive runs green after.

Shard 4 — "round-trips and reopens structured message-text conditions"
failed two ways. The CI page snapshot shows the reopened dialog in YAML
mode holding `filter: str_ends_with(trigger_text, "Workflow
notification")` and a textless send_message step: addMessageStep filled
"Message text" before the inspector handed over from the trigger to the
new step, so the step body overwrote the trigger condition. That
definition no longer parses as a form, getInitialEditorMode falls back
to YAML, and the trigger controls the test waits on never render — a
30s test timeout. Wait for the step inspector's own Action control
before filling, and pin the saved YAML right after create so any
recurrence fails there instead of stalling.

The remaining attempt measured the three Match operator buttons with
sequential boundingBox() calls while the inspector was still animating
in (Framer Motion, width 0 → 26rem and x 24 → 0 over 240ms), reporting
the second button 44px left of the first. That animation is driven off
requestAnimationFrame, so document.getAnimations() is empty and
waitForAnimations cannot see it; read the boxes from one frame via
evaluateAll and poll until the geometry stops moving.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
This commit is contained in:
Matt Toohey
2026-08-28 17:17:10 +10:00
parent 554a7a5103
commit 01ea7e5824

View File

@@ -48,6 +48,45 @@ async function selectTrigger(
await page.getByRole("menuitem", { name: trigger, exact: true }).click();
}
/**
* Read the first `count` boxes matched by `locator` from a single frame, once
* their geometry has stopped moving.
*
* The node inspector animates in over 240ms (width 0 → 26rem, x 24 → 0) under
* Framer Motion, which drives that off requestAnimationFrame rather than
* WAAPI — so `document.getAnimations()` stays empty and `waitForAnimations`
* cannot see it. Sequential `boundingBox()` calls sample different frames of
* that slide and can report a later control to the left of an earlier one.
*/
async function settledBoxes(
locator: import("@playwright/test").Locator,
count: number,
): Promise<Array<{ x: number; y: number }>> {
const read = () =>
locator.evaluateAll(
(elements, limit) =>
elements.slice(0, limit).map((element) => {
const rect = element.getBoundingClientRect();
return { x: rect.x, y: rect.y };
}),
count,
);
let previous = JSON.stringify(await read());
let current = previous;
await expect
.poll(async () => {
const boxes = await read();
current = JSON.stringify(boxes);
const stable = boxes.length === count && current === previous;
previous = current;
return stable;
})
.toBe(true);
return JSON.parse(current) as Array<{ x: number; y: number }>;
}
async function openTriggerInspector(
dialog: import("@playwright/test").Locator,
) {
@@ -64,6 +103,11 @@ async function addMessageStep(
) {
await dialog.getByRole("button", { name: "Add step", exact: true }).click();
await page.getByRole("menuitem", { name: "Send Message" }).click();
// Target the step's own textarea by id: a message-text trigger condition
// also labels its input "Message text", and filling by label before the
// inspector hands over from the trigger to the new step typed the step body
// into the trigger condition, leaving the step textless and the workflow
// unparseable as a form on reopen.
await dialog
.locator('textarea[id^="wf-step-"][id$="-text"]')
.fill("Workflow notification");
@@ -292,17 +336,11 @@ test("round-trips and reopens structured message-text conditions", async ({
await waitForAnimations(page);
const matchControls = dialog.getByRole("group", { name: "Match" });
const operatorButtons = matchControls.getByRole("button");
const firstOperatorBox = await operatorButtons.nth(0).boundingBox();
const secondOperatorBox = await operatorButtons.nth(1).boundingBox();
const thirdOperatorBox = await operatorButtons.nth(2).boundingBox();
expect(firstOperatorBox).not.toBeNull();
expect(secondOperatorBox).not.toBeNull();
expect(thirdOperatorBox).not.toBeNull();
expect(secondOperatorBox?.x).toBeGreaterThan(firstOperatorBox?.x ?? 0);
expect(
Math.abs((secondOperatorBox?.y ?? 0) - (firstOperatorBox?.y ?? 0)),
).toBeLessThan(1);
expect(thirdOperatorBox?.y).toBeGreaterThan(firstOperatorBox?.y ?? 0);
const [firstOperatorBox, secondOperatorBox, thirdOperatorBox] =
await settledBoxes(operatorButtons, 3);
expect(secondOperatorBox.x).toBeGreaterThan(firstOperatorBox.x);
expect(Math.abs(secondOperatorBox.y - firstOperatorBox.y)).toBeLessThan(1);
expect(thirdOperatorBox.y).toBeGreaterThan(firstOperatorBox.y);
await waitForAnimations(page);
await matchControls.screenshot({
path: "test-results/workflow-message-condition-operators.png",
@@ -319,6 +357,22 @@ test("round-trips and reopens structured message-text conditions", async ({
await addMessageStep(page, dialog);
await createEnabled(page, dialog);
// Pin the saved definition before reopening. A step body that lands in the
// trigger condition leaves the step textless, which makes the definition
// unparseable as a form — the dialog then reopens in YAML mode and the
// assertions below stall on trigger controls that never render.
const created = parseYaml(
(await page.evaluate(() => {
const call = [...(window.__BUZZ_E2E_COMMAND_PAYLOADS__ ?? [])]
.reverse()
.find((candidate) => candidate.command === "create_workflow");
return (call?.payload as { yamlDefinition?: string } | undefined)
?.yamlDefinition;
})) ?? "",
);
expect(created.trigger.filter).toBe(expression);
expect(created.steps[0].text).toBe("Workflow notification");
const reopened = await reopenWorkflow(page, name);
await openTriggerInspector(reopened);
await expect(