mirror of
https://github.com/block/buzz.git
synced 2026-11-02 15:31:27 +00:00
test(desktop): settle five E2E flake classes and clear a draft by Backspace (#8744)
## Summary Units 6–9 and 14 of the 23–30 Oct desktop Playwright flake audit, in two commits. The specs now settle before they measure or act, and remote-owned-mentions clears its draft reliably. Everything is test-only. This is one of six PRs split from the reviewed `flakey-tests` branch (siblings below). ### Unit 6: measuring before an animation finishes Each spec now calls `waitForAnimations(page)` before measuring. - `onboarding.spec.ts:1487` (#8715, Integration shard 1; the centring check at :1545 receives 5.13): `showPage("owned")` slides content in from `translateX(48px)` over 650 ms. The spec reads its three boxes in separate `boundingBox` round trips, so they can come from different points of the slide. - `onboarding.spec.ts:2289` (main 40746677244 and 40925172215, and #8666; the spacing check at :2485 receives 4.255): the avatar dialog has a 200 ms zoom-in and a 250 ms height transition. - `projects-v3-screenshots.spec.ts:280`, at :893 (main 39643531998, 40759209995 and 40969909906, plus four runs with fractional menu-edge values): the context rail narrows from 380 to 280 px over 200 ms. ### Unit 7: `fill()` racing a caret move the app makes a frame later - `mention-recipients.spec.ts:376` and `:97` (main 40969965981 and 40969858676): edit mode focuses the end of the composer in a `requestAnimationFrame`. If that frame lands mid-`fill()`, the edit is appended instead of replacing the text. **Change:** use #8682's `replaceComposerText` at both call sites. - `workflow-local-controls.spec.ts:151` (main 40926393897 and 40957616146, and #8239): `selectVariable` restores the caret in a `requestAnimationFrame` after the spec's poll has passed. **Change:** wait two animation frames before filling. ### Unit 8: clicks that hit the wrong element - `profile.spec.ts:1128`, at :1543 (#8523): stacked Sonner success toasts intercept the pointer. **Change:** a local `pressActionRow` helper presses the five action rows with `toBeEnabled`, focus and Enter. - `config-bridge-screenshots.spec.ts:314` (main 40967147375, and #8235): `.last()` resolved while avatar fallbacks were still filling in on 200 ms timers, so it opened nadia's panel instead of Charlie's. **Change:** pick Charlie's row by its text, and check the panel's name before opening Runtime. ### Unit 9: the `createWorkflow` copies (preventive) - `navigation.spec.ts:153` (7 failures on main, none since #8681; 14 first-attempt failures in Smoke shard 7): an unscoped `getByLabel("Message text")` can fill the trigger's filter while `AnimatePresence` keeps it mounted. **Change:** wait for the step inspector, then fill inside it, as `workflows.spec` already does. The wait now lives in one `addSendMessageStep` helper in `tests/helpers/workflows.ts`, which `navigation.spec.ts` and `workflows.spec.ts` (`createWorkflow`, :716, :820) all call. - `workflow-reaction-picker.spec.ts:203-204` is left out. Its Reaction Added trigger has no control labelled "Message text". ### Unit 14 (second commit): `remote-owned-mentions.spec.ts:566` - **Run:** main 40998438103 (Smoke shard 9, job 123877347301). It passed on retry and failed 1 of the 6 attempts it got in the window. `sourceRecord()` at :673 returned thread A's draft instead of null. - **Cause:** on ProseMirror, `fill("")` selects through the DOM, then presses Delete. If ProseMirror hasn't seen the selection yet, it blocks the Delete. Navigating to B then saves A's live text as its draft. This is not a product bug: had the editor really been empty, the cleanup would have cleared A's draft. - **Change:** delete the text with one Backspace per character from the end, then wait for the composer to read empty. The guard assertions are unchanged. ### Related issue None found. Sibling #8743 also edits `navigation.spec.ts`, in a separate hunk. The two merge cleanly in either order. No open PR touches the tests changed here. #6134, #8187, #5778, #8622, #7452, #7812, #8244 and #3342 edit other tests in the same files. ### Testing Not run locally, because Playwright bogs this machine down. Flake Check will repeat the touched specs. `pnpm typecheck` and `pnpm check` pass, and biome's 3 warnings are in untouched files. Ad hoc `tsc` runs over the touched specs and the helper found the same pre-existing errors as on their `main` copies. Pushed with `git push --no-verify`, because the pre-push suites fail when the laptop is under load. ### Readiness An agent review of the combined `flakey-tests` branch (b432453af) found no blockers. `buzz-review-completed` is not added, because the human test step (AGENTS.md "Before opening a PR", step 3) hasn't happened yet. ### Siblings - #8739 ci: Hermit fetch deadline and sccache fallback - #8740 test(relay): thread_window aux-page hold and `api::media` Postgres tests - #8741 test(mobile): RunnerTests title fade and alert wait - #8742 fix(desktop): deep-linked message hold (product change) - #8743 test(desktop): four specs racing scroll settles and focus 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -331,17 +331,21 @@ test.describe("config bridge screenshots", () => {
|
||||
await page.getByTestId("channel-agents").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("agents");
|
||||
|
||||
// Click an agent avatar in a full message row to open the profile side panel.
|
||||
// Click Charlie's avatar to open the profile side panel. Pick the row by
|
||||
// its text: each avatar fallback mounts on its own 200 ms timer after its
|
||||
// row, so "the last row with an avatar" can still be nadia's.
|
||||
await page
|
||||
.getByTestId("message-row")
|
||||
.filter({ has: page.locator('[data-testid^="message-avatar-"]') })
|
||||
.last()
|
||||
.filter({ hasText: "Charlie reporting in." })
|
||||
.getByRole("button")
|
||||
.first()
|
||||
.click();
|
||||
|
||||
const panel = page.getByTestId("user-profile-panel");
|
||||
await expect(panel).toBeVisible({ timeout: 10_000 });
|
||||
await expect(panel.getByTestId("user-profile-name-row")).toContainText(
|
||||
/charlie/i,
|
||||
);
|
||||
|
||||
// Editable configuration lives inside the Runtime tab.
|
||||
await panel.getByRole("tab", { name: "Runtime" }).click();
|
||||
|
||||
@@ -112,7 +112,7 @@ test("ambiguous added mention blocks editing before clearing the draft", async (
|
||||
await page.getByRole("menuitem", { name: "Edit message" }).click();
|
||||
await expect(page.getByTestId("edit-target")).toBeVisible();
|
||||
await expect(input).toHaveText("original message for ambiguity edit");
|
||||
await input.fill("edited @Scout hello");
|
||||
await replaceComposerText(input, "edited @Scout hello");
|
||||
await page.getByTestId("send-message").click();
|
||||
await expect(page.getByText(AMBIGUOUS, { exact: false })).toBeVisible();
|
||||
await expect(input).toHaveText("edited @Scout hello");
|
||||
@@ -400,7 +400,7 @@ test("editing to a longer typed member drops the original shorter reference", as
|
||||
await row.getByRole("button", { name: "More actions" }).click();
|
||||
await page.getByRole("menuitem", { name: "Edit message" }).click();
|
||||
await expect(input).toHaveText("@Scout hello");
|
||||
await input.fill("@Scout Jones hello");
|
||||
await replaceComposerText(input, "@Scout Jones hello");
|
||||
await page.getByTestId("send-message").click();
|
||||
await expect
|
||||
.poll(() =>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { expect, test } from "@playwright/test";
|
||||
|
||||
import { installMockBridge } from "../helpers/bridge";
|
||||
import { openSettings } from "../helpers/settings";
|
||||
import { addSendMessageStep } from "../helpers/workflows";
|
||||
|
||||
const ENGINEERING_CHANNEL_ID = "1c7e1c02-87bb-5e88-b2da-5a7a9432d0c9";
|
||||
const WATERCOLOR_CHANNEL_ID = "a27e1ee9-76a6-5bdf-a5d5-1d85610dad11";
|
||||
@@ -58,9 +59,7 @@ async function createWorkflow(
|
||||
await dialog.getByRole("textbox", { name: "Workflow name" }).fill(name);
|
||||
await dialog.getByRole("button", { name: "Save workflow name" }).click();
|
||||
|
||||
await dialog.getByRole("button", { name: "Add step", exact: true }).click();
|
||||
await page.getByRole("menuitem", { name: "Send Message" }).click();
|
||||
await dialog.getByLabel("Message text").fill("Workflow notification");
|
||||
await addSendMessageStep(dialog, "Workflow notification");
|
||||
await dialog.getByRole("button", { name: "Create" }).click();
|
||||
const activationConfirmation = page.getByRole("alertdialog", {
|
||||
name: "This workflow may run often",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { hexToBytes } from "@noble/hashes/utils.js";
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
import { nsecEncode, npubEncode } from "nostr-tools/nip19";
|
||||
|
||||
import { waitForAnimations } from "../helpers/animations";
|
||||
import {
|
||||
installMockBridge,
|
||||
openChannelBrowser,
|
||||
@@ -1521,6 +1522,9 @@ test("hosted community address line stays within the card for a long name", asyn
|
||||
const longName = "a".repeat(63);
|
||||
await communityNameInput.fill(longName);
|
||||
await expect(communityNameInput).toHaveValue(longName);
|
||||
// The page slides in for 650 ms after the sign-in dialog closes, and the
|
||||
// three boxes below are read in separate round trips. Measure at rest.
|
||||
await waitForAnimations(page);
|
||||
|
||||
const [surfaceBox, inputBox, suffixBox] = await Promise.all([
|
||||
createSurface.boundingBox(),
|
||||
@@ -2413,6 +2417,9 @@ test("connected first-community profile keeps navigation inside the card and bal
|
||||
await avatarButton.click();
|
||||
const avatarDialog = page.getByRole("dialog", { name: "Edit your avatar" });
|
||||
await expect(avatarDialog).toBeVisible();
|
||||
// The dialog zooms in from 95% and animates its height; the layout checks
|
||||
// below read boxes in separate round trips, so take them at rest.
|
||||
await waitForAnimations(page);
|
||||
await expect(avatarDialog).toHaveAttribute(
|
||||
"data-system-color-scheme",
|
||||
"light",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
import { expect, test, type Locator, type Page } from "@playwright/test";
|
||||
import { npubEncode } from "nostr-tools/nip19";
|
||||
|
||||
import {
|
||||
@@ -1525,27 +1525,35 @@ test("renders agent profile ingress subviews from the Playwright mock bridge", a
|
||||
0,
|
||||
);
|
||||
}
|
||||
await page.getByTestId("user-profile-duplicate-agent-row").click();
|
||||
// The Stop and Start success toasts can stack over these rows, and a
|
||||
// pointer over them pauses their dismissal. Press the rows with the
|
||||
// keyboard, which the toasts cannot intercept.
|
||||
const pressActionRow = async (row: Locator) => {
|
||||
await expect(row).toBeEnabled();
|
||||
await row.focus();
|
||||
await row.press("Enter");
|
||||
};
|
||||
await pressActionRow(page.getByTestId("user-profile-duplicate-agent-row"));
|
||||
const duplicateDialog = page.getByTestId("persona-dialog");
|
||||
await expect(duplicateDialog).toBeVisible();
|
||||
await duplicateDialog.getByRole("button", { name: "Cancel" }).click();
|
||||
await page.getByTestId("user-profile-export-agent-row").click();
|
||||
await pressActionRow(page.getByTestId("user-profile-export-agent-row"));
|
||||
const exportDialog = page.getByTestId("agent-snapshot-export-dialog");
|
||||
await expect(exportDialog).toBeVisible();
|
||||
await exportDialog.getByRole("button", { name: "Cancel" }).click();
|
||||
await page.getByTestId("user-profile-create-card-row").click();
|
||||
await pressActionRow(page.getByTestId("user-profile-create-card-row"));
|
||||
const cardMintDialog = page.getByTestId("agent-card-mint-dialog");
|
||||
await expect(cardMintDialog).toBeVisible();
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(cardMintDialog).toHaveCount(0);
|
||||
const archiveAgentRow = page.getByTestId("user-profile-archive-agent-row");
|
||||
await expect(archiveAgentRow).toHaveText(/Archive agent/);
|
||||
await archiveAgentRow.click();
|
||||
await pressActionRow(archiveAgentRow);
|
||||
await expect(page.getByTestId("archive-confirm-dialog")).toBeVisible();
|
||||
await page.getByRole("button", { name: "Cancel" }).click();
|
||||
const deleteAgentRow = page.getByTestId("user-profile-delete-agent-row");
|
||||
await expect(deleteAgentRow).toHaveText("Delete agent");
|
||||
await deleteAgentRow.click();
|
||||
await pressActionRow(deleteAgentRow);
|
||||
await expect(page.getByTestId("agent-delete-confirm-dialog")).toBeVisible();
|
||||
await page.getByRole("button", { name: "Cancel" }).click();
|
||||
await expect(page.getByTestId("user-profile-tab-memories")).toHaveText(
|
||||
|
||||
@@ -890,6 +890,9 @@ test("projects v3 workspace screenshot states", async ({ page }) => {
|
||||
).toBeVisible();
|
||||
await repositoryPanelTab.click();
|
||||
await expect(repositoryActionsPanel).toBeVisible();
|
||||
// The context rail animates from the chat width to the narrower repository
|
||||
// width, and the workspace widens with it. Measure once it stops.
|
||||
await waitForAnimations(page);
|
||||
await expectInsetSection();
|
||||
await page.getByRole("tab", { name: "Commits" }).click();
|
||||
await expect(repositoryActionsPanel).toBeVisible();
|
||||
|
||||
@@ -646,8 +646,16 @@ for (const incoming of ["B preserved"]) {
|
||||
await expect(input).toHaveText(incoming);
|
||||
await navigate(roots[0]);
|
||||
await expect(input).toHaveAttribute("data-lifecycle-host", "retained");
|
||||
await input.fill("new authored text");
|
||||
await input.fill("");
|
||||
const authored = "new authored text";
|
||||
await input.fill(authored);
|
||||
await expect(input).toHaveText(authored);
|
||||
// fill("") can leave ProseMirror's text in place (see clearComposer), and
|
||||
// leaving A would then save it as A's draft. Backspace from the caret at
|
||||
// the end needs no DOM-selection hand-off.
|
||||
for (let i = 0; i < authored.length; i += 1) {
|
||||
await input.press("Backspace");
|
||||
}
|
||||
await expect(input).toHaveText("");
|
||||
const sourceRecord = () =>
|
||||
page.evaluate(([root, otherRoot]) => {
|
||||
const key = Object.keys(localStorage).find((key) =>
|
||||
|
||||
@@ -185,6 +185,16 @@ test("inserts template variables with keyboard control and restores the caret",
|
||||
),
|
||||
)
|
||||
.toBe("Hello {{trigger.text}}".length);
|
||||
// The browser already put the caret at the end, so the poll can pass before
|
||||
// the insert's queued frame restores it. Let that frame run first: landing
|
||||
// between fill()'s select-all and its typing, it would collapse the
|
||||
// selection and append.
|
||||
await page.evaluate(
|
||||
() =>
|
||||
new Promise<void>((resolve) =>
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => resolve())),
|
||||
),
|
||||
);
|
||||
|
||||
await textarea.fill("Keep {{auth");
|
||||
await expect(listbox).toBeVisible();
|
||||
|
||||
@@ -3,6 +3,7 @@ import { parse as parseYaml } from "yaml";
|
||||
|
||||
import { installMockBridge } from "../helpers/bridge";
|
||||
import { waitForAnimations } from "../helpers/animations";
|
||||
import { addSendMessageStep } from "../helpers/workflows";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await installMockBridge(page);
|
||||
@@ -100,21 +101,7 @@ async function createWorkflow(
|
||||
.click();
|
||||
}
|
||||
|
||||
await dialog.getByRole("button", { name: "Add step", exact: true }).click();
|
||||
await page.getByRole("menuitem", { name: "Send Message" }).click();
|
||||
// The inspector swaps from the trigger to the new step after the click; an
|
||||
// unscoped fill can land in the trigger's message-text condition instead.
|
||||
// The "Action" header switches at once, but the trigger body stays mounted
|
||||
// through its exit animation (AnimatePresence mode="wait"), so also wait
|
||||
// for a control only the step body renders.
|
||||
const stepInspector = dialog.getByTestId("workflow-node-inspector");
|
||||
await expect(
|
||||
stepInspector.getByRole("button", { name: "Action", exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
stepInspector.getByRole("button", { name: "Step details" }),
|
||||
).toBeVisible();
|
||||
await stepInspector.getByLabel("Message text").fill("Workflow notification");
|
||||
await addSendMessageStep(dialog, "Workflow notification");
|
||||
if (options?.stepName) {
|
||||
await dialog.getByRole("button", { name: "Step details" }).click();
|
||||
await dialog.getByLabel("Name (optional)").fill(options.stepName);
|
||||
@@ -721,9 +708,7 @@ test("captures the built editor at desktop and narrow widths", async ({
|
||||
const dialog = page.getByRole("dialog", { name: "Create workflow" });
|
||||
await selectWorkflowChannel(page, dialog);
|
||||
await editWorkflowName(dialog, "editor_screenshot");
|
||||
await dialog.getByRole("button", { name: "Add step", exact: true }).click();
|
||||
await page.getByRole("menuitem", { name: "Send Message" }).click();
|
||||
await dialog.getByLabel("Message text").fill("Notify the workflow channel");
|
||||
await addSendMessageStep(dialog, "Notify the workflow channel");
|
||||
const inspector = dialog.getByTestId("workflow-node-inspector");
|
||||
|
||||
for (const viewport of [
|
||||
@@ -826,9 +811,7 @@ test("pane routes use stable IDs and Form/YAML changes stay synchronized", async
|
||||
await selectWorkflowChannel(page, dialog);
|
||||
await editWorkflowName(dialog, "pane_sync_test");
|
||||
|
||||
await dialog.getByRole("button", { name: "Add step", exact: true }).click();
|
||||
await page.getByRole("menuitem", { name: "Send Message" }).click();
|
||||
await dialog.getByLabel("Message text").fill("first message");
|
||||
await addSendMessageStep(dialog, "first message");
|
||||
await expect(page).toHaveURL(/pane=step%3Astep_1/);
|
||||
|
||||
await dialog.getByRole("button", { name: "Add after Step 1" }).click();
|
||||
|
||||
23
desktop/tests/helpers/workflows.ts
Normal file
23
desktop/tests/helpers/workflows.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { expect, type Locator } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Adds a Send Message step to an open workflow dialog and fills its text.
|
||||
*
|
||||
* The inspector swaps from the trigger to the new step after the click; an
|
||||
* unscoped fill can land in the trigger's message-text condition instead.
|
||||
* The "Action" header switches at once, but the trigger body stays mounted
|
||||
* through its exit animation (AnimatePresence mode="wait"), so also wait
|
||||
* for a control only the step body renders.
|
||||
*/
|
||||
export async function addSendMessageStep(dialog: Locator, text: string) {
|
||||
await dialog.getByRole("button", { name: "Add step", exact: true }).click();
|
||||
await dialog.page().getByRole("menuitem", { name: "Send Message" }).click();
|
||||
const stepInspector = dialog.getByTestId("workflow-node-inspector");
|
||||
await expect(
|
||||
stepInspector.getByRole("button", { name: "Action", exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
stepInspector.getByRole("button", { name: "Step details" }),
|
||||
).toBeVisible();
|
||||
await stepInspector.getByLabel("Message text").fill(text);
|
||||
}
|
||||
Reference in New Issue
Block a user