Files
buzz/desktop/tests/e2e/workflow-title-stability.spec.ts
Logan Johnson 091670ff77 fix(e2e): close bootstrap checker receiver gaps
Preserve unsafe bootstrap events through supported parenthesized forms and grant loop bootstrap credit only when syntax proves an enumerable runtime iteration.

Co-authored-by: Larry <larry@buzz.local>
Co-authored-by: Brother Darryl <darryl@buzz.local>
Signed-off-by: Logan Johnson <loganj@squareup.com>
2026-08-21 00:34:00 +00:00

282 lines
9.8 KiB
TypeScript

import {
expect,
test,
type Locator,
type Page,
bootstrapE2ePage,
} from "../helpers/test";
import { installMockBridge } from "../helpers/bridge";
import { waitForAnimations } from "../helpers/animations";
test.beforeEach(async ({ page }) => {
await installMockBridge(page);
});
const GENERATED_NAME = /^[a-z]+-[a-z]+-[a-z]+$/;
async function navigateToWorkflows(page: Page) {
await bootstrapE2ePage(page, "/");
await page.getByTestId("open-workflows-view").click();
await expect(page.getByTestId("workflows-view")).toBeVisible();
}
function readWorkflowName(dialog: Locator) {
return dialog
.getByRole("button", { name: "Edit workflow name" })
.evaluate((button) =>
(button.previousElementSibling?.textContent ?? "").trim(),
);
}
async function selectWorkflowChannel(page: Page, dialog: Locator) {
const channelList = page.getByTestId("channel-combobox-list");
if (!(await channelList.isVisible())) {
await dialog.getByRole("combobox", { name: "Channel" }).click();
}
await expect(channelList).toBeVisible();
await waitForAnimations(page);
await channelList
.getByRole("option", { name: "agents", exact: true })
.click();
}
async function openTriggerPane(dialog: Locator) {
await dialog.getByRole("button", { name: /^Trigger: / }).click();
await expect(
dialog.getByRole("button", { name: "Trigger event" }),
).toBeVisible();
}
async function openStepPane(dialog: Locator, index: number) {
await dialog
.getByRole("button", { name: new RegExp(`^Step ${index}: `) })
.click();
await expect(dialog.getByTestId("workflow-node-inspector")).toContainText(
`Step ${index}`,
);
}
/**
* Inserts a step right after the trigger. The insertion control only becomes
* interactive on hover once the trigger is no longer the terminal node.
*/
async function addStepAfterTrigger(page: Page, dialog: Locator) {
const ingress = dialog.getByTestId("workflow-node-ingress").first();
await ingress.hover();
await ingress.getByRole("button", { name: "Add step", exact: true }).click();
await page.getByRole("menuitem", { name: "Send Message" }).click();
}
async function createNamedWorkflow(page: Page, name: string) {
await page.getByRole("button", { name: "Create Workflow" }).click();
const dialog = page.getByRole("dialog", { name: "Create workflow" });
await expect(dialog).toBeVisible();
await selectWorkflowChannel(page, dialog);
await dialog.getByRole("button", { name: "Edit workflow name" }).click();
await dialog.getByRole("textbox", { name: "Workflow name" }).fill(name);
await dialog.getByRole("button", { name: "Save workflow name" }).click();
await dialog.getByRole("button", { name: "Add first step" }).click();
await dialog.getByLabel("Message text").fill("Workflow notification");
await dialog.getByRole("button", { name: "Create workflow" }).click();
await expect(page.getByTestId("workflows-view")).toContainText(name);
}
function workflowCard(page: Page, name: string) {
return page
.locator('[data-testid^="workflow-card-"]')
.filter({ hasText: name })
.first();
}
test("keeps the generated name while moving between trigger and step panes", async ({
page,
}) => {
await navigateToWorkflows(page);
await page.getByRole("button", { name: "Create Workflow" }).click();
const dialog = page.getByRole("dialog", { name: "Create workflow" });
await selectWorkflowChannel(page, dialog);
await expect.poll(() => readWorkflowName(dialog)).toMatch(GENERATED_NAME);
const generatedName = await readWorkflowName(dialog);
// Adding the first step lands on the step pane while the step is still
// incomplete — the header must keep showing the workflow name.
await dialog.getByRole("button", { name: "Add first step" }).click();
await expect(dialog.getByTestId("workflow-node-inspector")).toContainText(
"Step 1",
);
expect(await readWorkflowName(dialog)).toBe(generatedName);
await expect(
dialog.getByRole("button", { name: "Edit workflow name" }),
).toBeEnabled();
await openTriggerPane(dialog);
expect(await readWorkflowName(dialog)).toBe(generatedName);
await openStepPane(dialog, 1);
expect(await readWorkflowName(dialog)).toBe(generatedName);
// A second, still-empty step must not clear the name either.
await addStepAfterTrigger(page, dialog);
await expect(dialog.getByTestId("workflow-node-inspector")).toContainText(
"Step 1",
);
expect(await readWorkflowName(dialog)).toBe(generatedName);
await openStepPane(dialog, 2);
expect(await readWorkflowName(dialog)).toBe(generatedName);
});
test("renames stay visible across pane navigation before the step is filled in", async ({
page,
}) => {
const name = `renamed_workflow_${Date.now()}`;
await navigateToWorkflows(page);
await page.getByRole("button", { name: "Create Workflow" }).click();
const dialog = page.getByRole("dialog", { name: "Create workflow" });
await selectWorkflowChannel(page, dialog);
await dialog.getByRole("button", { name: "Add first step" }).click();
await expect(dialog.getByTestId("workflow-node-inspector")).toContainText(
"Step 1",
);
await dialog.getByRole("button", { name: "Edit workflow name" }).click();
await dialog.getByRole("textbox", { name: "Workflow name" }).fill(name);
await dialog.getByRole("button", { name: "Save workflow name" }).click();
expect(await readWorkflowName(dialog)).toBe(name);
await openTriggerPane(dialog);
expect(await readWorkflowName(dialog)).toBe(name);
await openStepPane(dialog, 1);
expect(await readWorkflowName(dialog)).toBe(name);
// Completing the step re-serializes the definition from the builder's form
// state, which must have picked the rename up rather than writing back the
// generated name it was holding when the rename happened.
await dialog.getByLabel("Message text").fill("Workflow notification");
expect(await readWorkflowName(dialog)).toBe(name);
await dialog.getByRole("button", { name: "Create workflow" }).click();
await expect(page.getByTestId("workflows-view")).toContainText(name);
});
test("keeps a header disable across a later form edit", async ({ page }) => {
const name = `disabled_workflow_${Date.now()}`;
await navigateToWorkflows(page);
await createNamedWorkflow(page, name);
await workflowCard(page, name)
.getByRole("button", { name: "Workflow actions" })
.click();
await page.getByRole("menuitem", { name: "Edit" }).click();
const dialog = page.getByRole("dialog", { name: "Edit workflow" });
await expect(dialog).toBeVisible();
const enableItem = page.getByRole("menuitemcheckbox", { name: "Enable" });
// The open menu aria-hides the dialog behind it, so it has to be dismissed
// before the dialog can be addressed again.
const closeActionsMenu = async () => {
await page.keyboard.press("Escape");
await expect(enableItem).not.toBeVisible();
};
// Add an empty step so the definition stops validating, then disable from the
// header. Completing the step re-serializes the definition from the builder's
// form state, which must carry the disable rather than the enabled state it
// held before the header wrote it.
await addStepAfterTrigger(page, dialog);
await expect(dialog.getByTestId("workflow-node-inspector")).toContainText(
"Step 1",
);
await dialog.getByRole("button", { name: "Workflow actions" }).click();
await expect(enableItem).toHaveAttribute("aria-checked", "true");
await enableItem.click();
await expect(enableItem).toHaveAttribute("aria-checked", "false");
await closeActionsMenu();
await dialog.getByLabel("Message text").fill("Updated notification");
await dialog.getByRole("button", { name: "Workflow actions" }).click();
await expect(enableItem).toHaveAttribute("aria-checked", "false");
await closeActionsMenu();
await dialog.getByRole("button", { name: "Save changes" }).click();
await expect(workflowCard(page, name)).toContainText("disabled");
});
test("keeps the saved name while editing an existing workflow", async ({
page,
}) => {
const name = `edit_title_${Date.now()}`;
await navigateToWorkflows(page);
await createNamedWorkflow(page, name);
await workflowCard(page, name)
.getByRole("button", { name: "Workflow actions" })
.click();
await page.getByRole("menuitem", { name: "Edit" }).click();
const dialog = page.getByRole("dialog", { name: "Edit workflow" });
await expect(dialog).toBeVisible();
expect(await readWorkflowName(dialog)).toBe(name);
await openTriggerPane(dialog);
expect(await readWorkflowName(dialog)).toBe(name);
await openStepPane(dialog, 1);
expect(await readWorkflowName(dialog)).toBe(name);
await addStepAfterTrigger(page, dialog);
await expect(dialog.getByTestId("workflow-node-inspector")).toContainText(
"Step 1",
);
expect(await readWorkflowName(dialog)).toBe(name);
await expect(
dialog.getByRole("button", { name: "Edit workflow name" }),
).toBeEnabled();
await openStepPane(dialog, 2);
expect(await readWorkflowName(dialog)).toBe(name);
});
test("keeps the copy name while duplicating an existing workflow", async ({
page,
}) => {
const name = `dup_title_${Date.now()}`;
await navigateToWorkflows(page);
await createNamedWorkflow(page, name);
await workflowCard(page, name)
.getByRole("button", { name: "Workflow actions" })
.click();
await page.getByRole("menuitem", { name: "Duplicate" }).click();
const dialog = page.getByRole("dialog", { name: "Duplicate workflow" });
await expect(dialog).toBeVisible();
expect(await readWorkflowName(dialog)).toBe(`${name} (copy)`);
await openStepPane(dialog, 1);
expect(await readWorkflowName(dialog)).toBe(`${name} (copy)`);
await addStepAfterTrigger(page, dialog);
await expect(dialog.getByTestId("workflow-node-inspector")).toContainText(
"Step 1",
);
expect(await readWorkflowName(dialog)).toBe(`${name} (copy)`);
await openTriggerPane(dialog);
expect(await readWorkflowName(dialog)).toBe(`${name} (copy)`);
});