mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
feat(workflows): clarify workflow setup and activation (#6470)
**Category:** new-feature **User Impact:** Workflow authors can build filtered, runtime-aware automations, understand them at a glance, and get a clear warning before turning on workflows likely to run often. **Problem:** Workflow setup exposed raw configuration without enough help composing message templates, filtering triggers, or understanding saved behavior; activation could also make a broadly triggered workflow live without explaining its likely frequency. **Solution:** Batch 3 adds local, deterministic template variables, trigger filters, and semantic summaries, then refines cards and activation around configured behavior and a risk-aware warning boundary. Scheduling remains the already-shipped implementation, advanced expressions remain lossless, and network-backed identity/message enrichment stays in Batch 4. | Message inputs | Trigger filters | | --- | --- | | Caret-aware, keyboard-accessible suggestions expose trigger-local values and safe prior-step outputs in `send_message.text`. | Structured conditions and validated manual IDs block invalid submission while preserving advanced expressions. | |  |  | | Workflow cards | Risk-aware activation | | --- | --- | | Semantic labels, channel-first hierarchy, configured reaction/action visuals, real step stacks, and compact status controls make behavior scannable. | Broad message and frequent schedule triggers explain the risk before **Turn on**; narrowly scoped triggers proceed without unnecessary ceremony. | |  |  | ## Changes <details> <summary>File changes</summary> **desktop/src/features/workflows/ui/WorkflowActionsMenu.tsx** Separates direct card status controls from secondary actions while retaining modal status actions. **desktop/src/features/workflows/ui/WorkflowCard.tsx** Adds semantic behavior, channel-first hierarchy, configured reaction/action visuals, real subsequent-step stacks, status controls, and reduced-motion-aware trigger feedback. **desktop/src/features/workflows/ui/WorkflowDialog.tsx** Warns before activating broadly triggered workflows while allowing narrowly scoped workflows to proceed directly. **desktop/src/features/workflows/ui/WorkflowFormBuilder.tsx** Connects structured trigger filters and template-aware step inputs while preserving schedules, trigger transitions, and selected YAML authority. **desktop/src/features/workflows/ui/WorkflowStepCard.tsx** Replaces generic labels with deterministic configured-step descriptions. **desktop/src/features/workflows/ui/WorkflowTemplateTextarea.tsx** Adds caret-aware variable suggestions with keyboard navigation and focus restoration. **desktop/src/features/workflows/ui/WorkflowTriggerConditions.tsx** Adds structured local filters, validated author/message IDs, and a lossless advanced-expression fallback. **desktop/src/features/workflows/ui/workflowActivationWarning.ts** and **workflowActivationWarning.test.mjs** Classify broad message and frequent schedule triggers for contextual activation warnings. **desktop/src/features/workflows/ui/workflowConditionExpression.ts** and **workflowConditionExpression.test.mjs** Model and cover parsing, serialization, validation, and advanced-expression preservation. **desktop/src/features/workflows/ui/workflowDefinition.ts** and **workflowDefinition.test.mjs** Preserve trigger/step configuration and derive deterministic card metadata across YAML round trips. **desktop/src/features/workflows/ui/workflowStepDescription.ts** and **workflowStepDescription.test.mjs** Generate and cover local step summaries. **desktop/src/features/workflows/ui/workflowTemplateVariables.ts** and **workflowTemplateVariables.test.mjs** Define and cover trigger-specific, order-bounded variables and caret insertion. **desktop/src/features/workflows/ui/workflowTriggerDescription.ts** and **workflowTriggerDescription.test.mjs** Generate and cover semantic trigger summaries without network lookups. **desktop/tests/e2e/workflow-local-controls.spec.ts** and snapshot Cover filters, IDs, advanced expressions, autocomplete, activation choices, summaries, and YAML authority. **desktop/tests/e2e/workflow-reaction-picker.spec.ts** Covers configured reaction emoji in workflow nodes and summaries. **desktop/tests/e2e/workflows.spec.ts** Covers risk-aware activation warnings, direct safe creation, duplication, and card status controls. </details> ## Reproduction steps 1. Create a message-posted workflow in **Workflows**, add a Send message step, and type `{{trig`; verify keyboard-selectable variables insert at the caret. 2. Configure message-text and manual ID filters; verify malformed IDs block submission and advanced expressions survive Form/YAML transitions. 3. Create a broad message workflow; verify **Back** persists nothing, **Keep off** saves it disabled, and **Turn on** enables it. Confirm a narrowly triggered webhook skips the warning. 4. Inspect the saved card; verify its channel, semantic behavior, configured actions/reaction, real step stack, and status are understandable without opening YAML. ## Validation Validated at exact clean head `f99503819889b95ee3c61657c5c3850aae35481e` on base `24ec6a468ec9d0d425ee58fbfc4d416412c446ad`. - Focused workflow regressions passed 59/60 locally; the only local miss was a 438-pixel macOS snapshot drift, while the checked-in Linux baseline comes from the failing CI artifact. Repository pre-push gates and E2E build/typecheck passed. - A broader 36-test smoke invocation had 31 passes and five unrelated pre-existing expectation/snapshot failures, so it is not claimed as fully green. Adversarial fixes are recorded in [round one](https://github.com/block/buzz/pull/6470#issuecomment-5366366670) and [round two](https://github.com/block/buzz/pull/6470#issuecomment-5366407535). --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz> Co-authored-by: Mongo <5398c5fd039b963ce132b3e078e7c4af097dd997517bb5e14c2682fe68c25197@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -62,6 +62,18 @@ async function createWorkflow(
|
||||
await page.getByRole("menuitem", { name: "Send Message" }).click();
|
||||
await dialog.getByLabel("Message text").fill("Workflow notification");
|
||||
await dialog.getByRole("button", { name: "Create" }).click();
|
||||
const activationConfirmation = page.getByRole("alertdialog", {
|
||||
name: "This workflow may run often",
|
||||
});
|
||||
await Promise.race([
|
||||
activationConfirmation.waitFor({ state: "visible" }),
|
||||
dialog.waitFor({ state: "hidden" }),
|
||||
]);
|
||||
if (await activationConfirmation.isVisible()) {
|
||||
await activationConfirmation
|
||||
.getByRole("button", { name: "Turn on" })
|
||||
.click();
|
||||
}
|
||||
await expect(dialog).not.toBeVisible();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { parse as parseYaml } from "yaml";
|
||||
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
||||
|
||||
import { waitForAnimations } from "../helpers/animations";
|
||||
import { installMockBridge } from "../helpers/bridge";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -39,11 +40,10 @@ async function selectTrigger(
|
||||
|
||||
async function openTriggerInspector(
|
||||
dialog: import("@playwright/test").Locator,
|
||||
trigger: string,
|
||||
) {
|
||||
const menu = dialog.getByRole("button", { name: "Trigger event" });
|
||||
if (!(await menu.isVisible())) {
|
||||
await dialog.getByRole("button", { name: `Trigger: ${trigger}` }).click();
|
||||
await dialog.getByRole("button", { name: /^Trigger:/ }).click();
|
||||
}
|
||||
await expect(menu).toBeVisible();
|
||||
}
|
||||
@@ -57,6 +57,19 @@ async function addMessageStep(
|
||||
await dialog.getByLabel("Message text").fill("Workflow notification");
|
||||
}
|
||||
|
||||
async function createEnabled(
|
||||
page: import("@playwright/test").Page,
|
||||
dialog: import("@playwright/test").Locator,
|
||||
) {
|
||||
await dialog.getByRole("button", { name: "Create" }).click();
|
||||
const confirmation = page.getByRole("alertdialog", {
|
||||
name: "This workflow may run often",
|
||||
});
|
||||
if (await confirmation.isVisible()) {
|
||||
await confirmation.getByRole("button", { name: "Turn on" }).click();
|
||||
}
|
||||
}
|
||||
|
||||
async function reopenWorkflow(
|
||||
page: import("@playwright/test").Page,
|
||||
name: string,
|
||||
@@ -69,6 +82,111 @@ async function reopenWorkflow(
|
||||
return page.getByRole("dialog", { name: "Edit workflow" });
|
||||
}
|
||||
|
||||
test("confirms activation after create and preserves a safe disabled path", async ({
|
||||
page,
|
||||
}) => {
|
||||
const name = `activation_confirmation_${Date.now()}`;
|
||||
const dialog = await openCreateWorkflow(page, name);
|
||||
await addMessageStep(page, dialog);
|
||||
|
||||
await expect(
|
||||
dialog.getByRole("switch", { name: "Enable workflow" }),
|
||||
).toHaveCount(0);
|
||||
await dialog.getByRole("button", { name: "Create" }).click();
|
||||
const confirmation = page.getByRole("alertdialog", {
|
||||
name: "This workflow may run often",
|
||||
});
|
||||
await expect(confirmation).toBeVisible();
|
||||
await expect(
|
||||
confirmation.getByRole("button", { name: "Back" }),
|
||||
).toBeFocused();
|
||||
|
||||
await confirmation.getByRole("button", { name: "Back" }).click();
|
||||
await expect(confirmation).toBeHidden();
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(
|
||||
() =>
|
||||
(window.__BUZZ_E2E_COMMAND_PAYLOADS__ ?? []).filter(
|
||||
(call) => call.command === "create_workflow",
|
||||
).length,
|
||||
),
|
||||
)
|
||||
.toBe(0);
|
||||
|
||||
await dialog.getByRole("button", { name: "Create" }).click();
|
||||
await confirmation.getByRole("button", { name: "Keep off" }).click();
|
||||
await expect(dialog).toBeHidden();
|
||||
|
||||
const yaml = 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(parseYaml(yaml ?? "").enabled).toBe(false);
|
||||
const card = page
|
||||
.locator('[data-testid^="workflow-card-"]')
|
||||
.filter({ hasText: name })
|
||||
.first();
|
||||
await expect(
|
||||
card.getByRole("switch", { name: "Enable workflow" }),
|
||||
).not.toBeChecked();
|
||||
});
|
||||
|
||||
test("inserts template variables with keyboard control and restores the caret", async ({
|
||||
page,
|
||||
}) => {
|
||||
const dialog = await openCreateWorkflow(page, "template_variables_keyboard");
|
||||
await dialog.getByRole("button", { name: "Add step", exact: true }).click();
|
||||
await page.getByRole("menuitem", { name: "Send Message" }).click();
|
||||
|
||||
const textarea = dialog.getByLabel("Message text");
|
||||
const listbox = page.getByRole("listbox");
|
||||
await textarea.fill("Hello {{trig");
|
||||
await expect(listbox).toBeVisible();
|
||||
await expect(listbox.getByRole("option")).toHaveCount(5);
|
||||
await waitForAnimations(page);
|
||||
expect(await page.locator("body").screenshot()).toMatchSnapshot(
|
||||
"workflow-template-variable-autocomplete.png",
|
||||
);
|
||||
|
||||
await textarea.press("ArrowUp");
|
||||
await expect(textarea).toHaveAttribute(
|
||||
"aria-activedescendant",
|
||||
/-variables-option-4$/,
|
||||
);
|
||||
await textarea.press("ArrowDown");
|
||||
await expect(textarea).toHaveAttribute(
|
||||
"aria-activedescendant",
|
||||
/-variables-option-0$/,
|
||||
);
|
||||
await textarea.press("Enter");
|
||||
await expect(textarea).toHaveValue("Hello {{trigger.text}}");
|
||||
await expect(textarea).toBeFocused();
|
||||
await expect
|
||||
.poll(() =>
|
||||
textarea.evaluate((element) =>
|
||||
element instanceof HTMLTextAreaElement ? element.selectionStart : -1,
|
||||
),
|
||||
)
|
||||
.toBe("Hello {{trigger.text}}".length);
|
||||
|
||||
await textarea.fill("Keep {{auth");
|
||||
await expect(listbox).toBeVisible();
|
||||
await textarea.press("Escape");
|
||||
await expect(listbox).toBeHidden();
|
||||
await expect(textarea).toBeFocused();
|
||||
await expect(textarea).toHaveValue("Keep {{auth");
|
||||
|
||||
await textarea.fill("By {{auth");
|
||||
await textarea.press("Tab");
|
||||
await expect(textarea).toHaveValue("By {{trigger.author}}");
|
||||
await expect(textarea).toBeFocused();
|
||||
});
|
||||
|
||||
test("round-trips schedule presets and saves a custom UTC cron", async ({
|
||||
page,
|
||||
}) => {
|
||||
@@ -86,7 +204,7 @@ test("round-trips schedule presets and saves a custom UTC cron", async ({
|
||||
expect(definition.trigger).toEqual({ on: "schedule", interval: "15m" });
|
||||
|
||||
await dialog.getByRole("tab", { name: "Form" }).click();
|
||||
await openTriggerInspector(dialog, "Schedule");
|
||||
await openTriggerInspector(dialog);
|
||||
await expect(
|
||||
dialog.getByRole("radio", { name: "Every 15 minutes" }),
|
||||
).toBeChecked();
|
||||
@@ -114,7 +232,7 @@ test("round-trips schedule presets and saves a custom UTC cron", async ({
|
||||
cron: "5 */2 * * 2-4",
|
||||
});
|
||||
await dialog.getByRole("tab", { name: "Form" }).click();
|
||||
await openTriggerInspector(dialog, "Schedule");
|
||||
await openTriggerInspector(dialog);
|
||||
await expect(
|
||||
dialog.getByRole("radio", { name: "Custom cron" }),
|
||||
).toBeChecked();
|
||||
@@ -123,9 +241,9 @@ test("round-trips schedule presets and saves a custom UTC cron", async ({
|
||||
).toHaveValue("2-4");
|
||||
|
||||
await addMessageStep(page, dialog);
|
||||
await dialog.getByRole("button", { name: "Create" }).click();
|
||||
await createEnabled(page, dialog);
|
||||
const reopened = await reopenWorkflow(page, name);
|
||||
await openTriggerInspector(reopened, "Schedule");
|
||||
await openTriggerInspector(reopened);
|
||||
await expect(
|
||||
reopened.getByRole("radio", { name: "Custom cron" }),
|
||||
).toBeChecked();
|
||||
@@ -145,17 +263,41 @@ test("round-trips and reopens structured message-text conditions", async ({
|
||||
const expression = 'str_ends_with(trigger_text, "deploy \\"buzz\\"\\\\path")';
|
||||
const dialog = await openCreateWorkflow(page, name);
|
||||
|
||||
await dialog.getByText("ends with", { exact: true }).click();
|
||||
await dialog.getByLabel("Text to match").fill(text);
|
||||
await dialog
|
||||
.getByRole("group", { name: "Match" })
|
||||
.getByRole("button", {
|
||||
name: "ends with",
|
||||
})
|
||||
.click();
|
||||
await dialog.getByLabel("Message text").fill(text);
|
||||
await dialog.getByRole("tab", { name: "YAML" }).click();
|
||||
const yamlEditor = dialog.getByRole("textbox", { name: "Workflow YAML" });
|
||||
const definition = parseYaml(await yamlEditor.inputValue());
|
||||
expect(definition.trigger.filter).toBe(expression);
|
||||
|
||||
await dialog.getByRole("tab", { name: "Form" }).click();
|
||||
await openTriggerInspector(dialog, "Message Posted");
|
||||
await expect(dialog.getByText("ends with", { exact: true })).toBeVisible();
|
||||
await expect(dialog.getByLabel("Text to match")).toHaveValue(text);
|
||||
await openTriggerInspector(dialog);
|
||||
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);
|
||||
await waitForAnimations(page);
|
||||
await matchControls.screenshot({
|
||||
path: "test-results/workflow-message-condition-operators.png",
|
||||
});
|
||||
await expect(
|
||||
matchControls.getByRole("button", { name: "ends with" }),
|
||||
).toHaveAttribute("aria-pressed", "true");
|
||||
await expect(dialog.getByLabel("Message text")).toHaveValue(text);
|
||||
await dialog.getByRole("tab", { name: "Advanced" }).click();
|
||||
await expect(dialog.getByLabel("Advanced expression")).toHaveValue(
|
||||
expression,
|
||||
@@ -163,11 +305,196 @@ test("round-trips and reopens structured message-text conditions", async ({
|
||||
await dialog.getByRole("tab", { name: "Basic" }).click();
|
||||
|
||||
await addMessageStep(page, dialog);
|
||||
await dialog.getByRole("button", { name: "Create" }).click();
|
||||
await createEnabled(page, dialog);
|
||||
const reopened = await reopenWorkflow(page, name);
|
||||
await openTriggerInspector(reopened, "Message Posted");
|
||||
await expect(reopened.getByText("ends with", { exact: true })).toBeVisible();
|
||||
await expect(reopened.getByLabel("Text to match")).toHaveValue(text);
|
||||
await openTriggerInspector(reopened);
|
||||
await expect(
|
||||
reopened
|
||||
.getByRole("group", { name: "Match" })
|
||||
.getByRole("button", { name: "ends with" }),
|
||||
).toHaveAttribute("aria-pressed", "true");
|
||||
await expect(reopened.getByLabel("Message text")).toHaveValue(text);
|
||||
});
|
||||
|
||||
test("renders deterministic trigger, step, and workflow-card summaries", async ({
|
||||
page,
|
||||
}) => {
|
||||
const name = `semantic_summaries_${Date.now()}`;
|
||||
await page.addInitScript((workflowName) => {
|
||||
const positions: number[] = [];
|
||||
Object.defineProperty(window, "__WORKFLOW_METADATA_POSITIONS__", {
|
||||
configurable: true,
|
||||
value: positions,
|
||||
});
|
||||
|
||||
const findTarget = () => {
|
||||
const target = Array.from(
|
||||
document.querySelectorAll<HTMLElement>(
|
||||
'[data-testid="workflow-card-name"]',
|
||||
),
|
||||
).find((element) => element.textContent === workflowName);
|
||||
if (!target) {
|
||||
requestAnimationFrame(findTarget);
|
||||
return;
|
||||
}
|
||||
|
||||
let remainingFrames = 12;
|
||||
const sample = () => {
|
||||
positions.push(target.getBoundingClientRect().y);
|
||||
remainingFrames -= 1;
|
||||
if (remainingFrames > 0) requestAnimationFrame(sample);
|
||||
};
|
||||
sample();
|
||||
};
|
||||
requestAnimationFrame(findTarget);
|
||||
}, name);
|
||||
const dialog = await openCreateWorkflow(page, name);
|
||||
|
||||
await dialog.getByLabel("Message text").fill("deploy");
|
||||
const triggerNode = dialog.getByRole("button", {
|
||||
name: "Trigger: Message contains “deploy”",
|
||||
});
|
||||
await expect(triggerNode).toContainText("Trigger");
|
||||
await expect(triggerNode).toContainText("Message contains “deploy”");
|
||||
|
||||
await addMessageStep(page, dialog);
|
||||
await dialog.getByRole("tab", { name: "YAML" }).click();
|
||||
const yamlEditor = dialog.getByRole("textbox", { name: "Workflow YAML" });
|
||||
const definition = parseYaml(await yamlEditor.inputValue());
|
||||
definition.steps[0].channel = "94a444a4-c0a3-5966-ab05-530c6ddc2301";
|
||||
await yamlEditor.fill(stringifyYaml(definition));
|
||||
await dialog.getByRole("tab", { name: "Form" }).click();
|
||||
const stepNode = dialog.getByRole("button", {
|
||||
name: "Step 1: “Workflow notification” in #agents",
|
||||
});
|
||||
await expect(stepNode).toContainText("Send Message");
|
||||
await expect(stepNode).toContainText("“Workflow notification” in #agents");
|
||||
await createEnabled(page, dialog);
|
||||
|
||||
const card = page
|
||||
.locator('[data-testid^="workflow-card-"]')
|
||||
.filter({ hasText: name })
|
||||
.first();
|
||||
const semanticLabel = card.getByTestId("workflow-card-semantic-label");
|
||||
const workflowName = card.getByTestId("workflow-card-name");
|
||||
const channelName = card.getByTestId("workflow-card-channel");
|
||||
await expect(card.getByTestId("workflow-card-trigger-summary")).toHaveCount(
|
||||
0,
|
||||
);
|
||||
await expect(semanticLabel).toHaveText(
|
||||
"When a message contains “deploy”, send “Workflow notification”",
|
||||
);
|
||||
await expect(workflowName).toHaveText(name);
|
||||
await expect(channelName).toHaveText("#agents");
|
||||
const semanticBox = await semanticLabel.boundingBox();
|
||||
const nameBox = await workflowName.boundingBox();
|
||||
const channelBox = await channelName.boundingBox();
|
||||
expect(semanticBox).not.toBeNull();
|
||||
expect(nameBox).not.toBeNull();
|
||||
expect(channelBox).not.toBeNull();
|
||||
expect(semanticBox?.y).toBeLessThan(nameBox?.y ?? 0);
|
||||
expect(channelBox?.y).toBeLessThan(nameBox?.y ?? 0);
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(
|
||||
() =>
|
||||
(
|
||||
window as typeof window & {
|
||||
__WORKFLOW_METADATA_POSITIONS__?: number[];
|
||||
}
|
||||
).__WORKFLOW_METADATA_POSITIONS__?.length ?? 0,
|
||||
),
|
||||
)
|
||||
.toBe(12);
|
||||
const metadataPositions = await page.evaluate(
|
||||
() =>
|
||||
(
|
||||
window as typeof window & {
|
||||
__WORKFLOW_METADATA_POSITIONS__?: number[];
|
||||
}
|
||||
).__WORKFLOW_METADATA_POSITIONS__ ?? [],
|
||||
);
|
||||
expect(
|
||||
Math.max(...metadataPositions) - Math.min(...metadataPositions),
|
||||
).toBeLessThan(0.5);
|
||||
});
|
||||
|
||||
test("round-trips manual author and reaction message IDs through save and reopen", async ({
|
||||
page,
|
||||
}) => {
|
||||
const name = `manual_trigger_ids_${Date.now()}`;
|
||||
const author = "a".repeat(64);
|
||||
const messageId = "b".repeat(64);
|
||||
const dialog = await openCreateWorkflow(page, name);
|
||||
await selectTrigger(page, dialog, "Reaction Added");
|
||||
await addMessageStep(page, dialog);
|
||||
await dialog.getByRole("button", { name: /^Trigger:/ }).click();
|
||||
await openTriggerInspector(dialog);
|
||||
|
||||
await dialog.getByRole("button", { name: "Author pubkey" }).click();
|
||||
await dialog.getByLabel("Author pubkey").fill("not-hex");
|
||||
await expect(
|
||||
dialog.getByText("Enter a 64-character hex pubkey."),
|
||||
).toBeVisible();
|
||||
await expect(dialog.getByRole("button", { name: "Create" })).toBeDisabled();
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(
|
||||
() =>
|
||||
(window.__BUZZ_E2E_COMMAND_PAYLOADS__ ?? []).filter(
|
||||
(call) => call.command === "create_workflow",
|
||||
).length,
|
||||
),
|
||||
)
|
||||
.toBe(0);
|
||||
await dialog.getByLabel("Author pubkey").fill(author);
|
||||
await expect(dialog.getByRole("button", { name: "Create" })).toBeEnabled();
|
||||
await dialog.getByLabel("Author pubkey").fill("still-not-hex");
|
||||
await expect(dialog.getByRole("button", { name: "Create" })).toBeDisabled();
|
||||
await dialog.getByRole("tab", { name: "YAML" }).click();
|
||||
const correctionEditor = dialog.getByRole("textbox", {
|
||||
name: "Workflow YAML",
|
||||
});
|
||||
const correctedDefinition = parseYaml(await correctionEditor.inputValue());
|
||||
delete correctedDefinition.trigger.filter;
|
||||
await correctionEditor.fill(stringifyYaml(correctedDefinition));
|
||||
await expect(dialog.getByRole("button", { name: "Create" })).toBeEnabled();
|
||||
await dialog.getByRole("tab", { name: "Form" }).click();
|
||||
await openTriggerInspector(dialog);
|
||||
await dialog.getByRole("button", { name: "Author pubkey" }).click();
|
||||
await expect(dialog.getByLabel("Author pubkey")).toHaveValue("");
|
||||
await dialog.getByLabel("Author pubkey").fill(author);
|
||||
await dialog
|
||||
.getByRole("group", { name: "Match" })
|
||||
.getByRole("button", { name: "is not", exact: true })
|
||||
.click();
|
||||
|
||||
await dialog.getByRole("button", { name: "Message event ID" }).click();
|
||||
await dialog.getByLabel("Message event ID").fill(messageId);
|
||||
|
||||
await dialog.getByRole("tab", { name: "YAML" }).click();
|
||||
const yamlEditor = dialog.getByRole("textbox", { name: "Workflow YAML" });
|
||||
let definition = parseYaml(await yamlEditor.inputValue());
|
||||
expect(definition.trigger).toEqual({
|
||||
on: "reaction_added",
|
||||
filter: `trigger_author != "${author}" && trigger_message_id == "${messageId}"`,
|
||||
});
|
||||
|
||||
await dialog.getByRole("tab", { name: "Form" }).click();
|
||||
await openTriggerInspector(dialog);
|
||||
await createEnabled(page, dialog);
|
||||
|
||||
const reopened = await reopenWorkflow(page, name);
|
||||
await openTriggerInspector(reopened);
|
||||
await reopened.getByRole("button", { name: "Author pubkey" }).click();
|
||||
await expect(reopened.getByLabel("Author pubkey")).toHaveValue(author);
|
||||
await reopened.getByRole("button", { name: "Message event ID" }).click();
|
||||
await expect(reopened.getByLabel("Message event ID")).toHaveValue(messageId);
|
||||
await reopened.getByRole("tab", { name: "YAML" }).click();
|
||||
definition = parseYaml(
|
||||
await reopened.getByRole("textbox", { name: "Workflow YAML" }).inputValue(),
|
||||
);
|
||||
expect(definition.trigger.filter).toContain(messageId);
|
||||
});
|
||||
|
||||
test("hides and clears message-text step conditions for schedule triggers", async ({
|
||||
@@ -181,9 +508,9 @@ test("hides and clears message-text step conditions for schedule triggers", asyn
|
||||
await dialog.getByRole("button", { name: "Run controls" }).click();
|
||||
await dialog.getByLabel("Text to match").fill("deploy");
|
||||
|
||||
await dialog.getByRole("button", { name: "Trigger: Message Posted" }).click();
|
||||
await dialog.getByRole("button", { name: /^Trigger:/ }).click();
|
||||
await selectTrigger(page, dialog, "Schedule");
|
||||
await dialog.getByRole("button", { name: "Step 1: Send Message" }).click();
|
||||
await dialog.getByRole("button", { name: /^Step 1:/ }).click();
|
||||
await dialog.getByRole("button", { name: "Run controls" }).click();
|
||||
|
||||
await expect(dialog.getByLabel("Text to match")).toHaveCount(0);
|
||||
@@ -212,7 +539,7 @@ test("keeps advanced and malformed definitions lossless", async ({ page }) => {
|
||||
),
|
||||
);
|
||||
await dialog.getByRole("tab", { name: "Form" }).click();
|
||||
await openTriggerInspector(dialog, "Message Posted");
|
||||
await openTriggerInspector(dialog);
|
||||
await expect(dialog.getByRole("tab", { name: "Advanced" })).toHaveAttribute(
|
||||
"data-state",
|
||||
"active",
|
||||
@@ -223,7 +550,9 @@ test("keeps advanced and malformed definitions lossless", async ({ page }) => {
|
||||
dialog.getByText(/advanced expression is active/i),
|
||||
).toBeVisible();
|
||||
|
||||
await dialog.getByRole("switch", { name: "Enable workflow" }).click();
|
||||
await expect(
|
||||
dialog.getByRole("switch", { name: "Enable workflow" }),
|
||||
).toHaveCount(0);
|
||||
await dialog.getByRole("tab", { name: "YAML" }).click();
|
||||
expect(parseYaml(await yamlEditor.inputValue()).trigger.filter).toBe(
|
||||
advanced,
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
@@ -73,15 +73,60 @@ test("reaction trigger uses the emoji picker instead of a free-text field", asyn
|
||||
await expect(
|
||||
dialog.getByRole("button", { name: "Clear trigger emoji" }),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
dialog
|
||||
.getByRole("button", { name: "Trigger: Reaction Added" })
|
||||
.getByTestId("workflow-node-icon")
|
||||
.locator("svg"),
|
||||
).toHaveCount(1);
|
||||
|
||||
await pickEmoji(page, dialog, "rocket", EMOJI);
|
||||
|
||||
const triggerNode = dialog.getByRole("button", {
|
||||
name: "Trigger: 🚀 reaction added",
|
||||
});
|
||||
await expect(triggerNode.getByTestId("workflow-node-icon")).toHaveText(EMOJI);
|
||||
await expect(triggerNode).toContainText("Reaction added");
|
||||
await expect(triggerNode).not.toContainText(`${EMOJI} reaction added`);
|
||||
await expect(
|
||||
triggerNode.getByTestId("workflow-node-icon").locator("svg"),
|
||||
).toHaveCount(0);
|
||||
await waitForAnimations(page);
|
||||
await triggerNode.screenshot({
|
||||
path: "test-results/workflow-reaction-trigger-node.png",
|
||||
});
|
||||
|
||||
// Selecting closes the popover and shows the chosen reaction by name.
|
||||
await expect(page.locator("em-emoji-picker")).toHaveCount(0);
|
||||
await expect(pickerButton).toContainText(EMOJI_SHORTCODE);
|
||||
await expect(pickerButton).not.toContainText("Choose a reaction");
|
||||
});
|
||||
|
||||
test("add-reaction step replaces its sequence number with the configured emoji", async ({
|
||||
page,
|
||||
}) => {
|
||||
const dialog = await openReactionTrigger(page);
|
||||
await dialog.getByRole("tab", { name: "YAML" }).click();
|
||||
const yamlEditor = dialog.getByRole("textbox", { name: "Workflow YAML" });
|
||||
await yamlEditor.fill(
|
||||
(await yamlEditor.inputValue()).replace(
|
||||
"steps: []",
|
||||
"steps:\n - id: step_1\n action: add_reaction\n emoji: ✅",
|
||||
),
|
||||
);
|
||||
|
||||
await dialog.getByRole("tab", { name: "Form" }).click();
|
||||
const stepNode = dialog.getByRole("button", { name: "Step 1: ✅" });
|
||||
await expect(stepNode.getByTestId("workflow-node-icon")).toHaveText("✅");
|
||||
await expect(stepNode).toContainText("Add Reaction");
|
||||
await expect(stepNode).not.toContainText("✅✅");
|
||||
await expect(stepNode.getByTestId("workflow-node-icon")).not.toHaveText("1");
|
||||
await waitForAnimations(page);
|
||||
await stepNode.screenshot({
|
||||
path: "test-results/workflow-add-reaction-step-node.png",
|
||||
});
|
||||
});
|
||||
|
||||
test("picked reaction persists into canonical YAML and clears back out", async ({
|
||||
page,
|
||||
}) => {
|
||||
@@ -92,15 +137,17 @@ test("picked reaction persists into canonical YAML and clears back out", async (
|
||||
const yamlEditor = dialog.getByRole("textbox", { name: "Workflow YAML" });
|
||||
const yaml = await yamlEditor.inputValue();
|
||||
expect(yaml).toContain("on: reaction_added");
|
||||
// Stored verbatim as the reaction content the executor compares against.
|
||||
expect(yaml).toContain(`emoji: ${EMOJI}`);
|
||||
// Stored verbatim in the local filter expression the executor compares.
|
||||
expect(yaml).toContain(`trigger_emoji == "${EMOJI}"`);
|
||||
|
||||
// Clearing the optional filter removes the key entirely rather than
|
||||
// writing an empty string, so the trigger matches any reaction again.
|
||||
await dialog.getByRole("tab", { name: "Form" }).click();
|
||||
// Switching editor modes drops the inspector selection, so re-open the
|
||||
// trigger node before reaching for its fields.
|
||||
await dialog.getByRole("button", { name: "Trigger: Reaction Added" }).click();
|
||||
await dialog
|
||||
.getByRole("button", { name: `Trigger: ${EMOJI} reaction added` })
|
||||
.click();
|
||||
await dialog.getByRole("button", { name: "Clear trigger emoji" }).click();
|
||||
await expect(
|
||||
dialog.getByRole("button", { name: "Choose trigger emoji" }),
|
||||
@@ -109,7 +156,7 @@ test("picked reaction persists into canonical YAML and clears back out", async (
|
||||
await dialog.getByRole("tab", { name: "YAML" }).click();
|
||||
const clearedYaml = await yamlEditor.inputValue();
|
||||
expect(clearedYaml).toContain("on: reaction_added");
|
||||
expect(clearedYaml).not.toContain("emoji:");
|
||||
expect(clearedYaml).not.toContain("trigger_emoji");
|
||||
});
|
||||
|
||||
test("legacy free-text emoji values survive a trip through the picker", async ({
|
||||
@@ -164,7 +211,7 @@ test("saved reaction filter round-trips back into the picker", async ({
|
||||
const editDialog = page.getByRole("dialog", { name: "Edit workflow" });
|
||||
await expect(editDialog).toBeVisible();
|
||||
await editDialog
|
||||
.getByRole("button", { name: "Trigger: Reaction Added" })
|
||||
.getByRole("button", { name: `Trigger: ${EMOJI} reaction added` })
|
||||
.click();
|
||||
|
||||
// Reopening rehydrates the stored glyph into the picker, not an empty field.
|
||||
|
||||
@@ -62,6 +62,20 @@ async function addStepAfterTrigger(page: Page, dialog: Locator) {
|
||||
await page.getByRole("menuitem", { name: "Send Message" }).click();
|
||||
}
|
||||
|
||||
async function finishRiskyActivationIfShown(page: Page, dialog: Locator) {
|
||||
const confirmation = page.getByRole("alertdialog", {
|
||||
name: "This workflow may run often",
|
||||
});
|
||||
await Promise.race([
|
||||
confirmation.waitFor({ state: "visible" }),
|
||||
dialog.waitFor({ state: "hidden" }),
|
||||
]);
|
||||
if (await confirmation.isVisible()) {
|
||||
await confirmation.getByRole("button", { name: "Turn on" }).click();
|
||||
}
|
||||
await expect(dialog).toBeHidden();
|
||||
}
|
||||
|
||||
async function createNamedWorkflow(page: Page, name: string) {
|
||||
await page.getByRole("button", { name: "Create Workflow" }).click();
|
||||
const dialog = page.getByRole("dialog", { name: "Create workflow" });
|
||||
@@ -73,8 +87,17 @@ async function createNamedWorkflow(page: Page, name: string) {
|
||||
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("tab", { name: "YAML" }).click();
|
||||
const yamlEditor = dialog.getByRole("textbox", { name: "Workflow YAML" });
|
||||
const yaml = await yamlEditor.inputValue();
|
||||
await yamlEditor.fill(
|
||||
yaml.replace(
|
||||
" action: send_message",
|
||||
' action: send_message\n text: "Workflow notification"',
|
||||
),
|
||||
);
|
||||
await dialog.getByRole("button", { name: "Create workflow" }).click();
|
||||
await finishRiskyActivationIfShown(page, dialog);
|
||||
|
||||
await expect(page.getByTestId("workflows-view")).toContainText(name);
|
||||
}
|
||||
@@ -154,10 +177,14 @@ test("renames stay visible across pane navigation before the step is filled in",
|
||||
// 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");
|
||||
await dialog
|
||||
.getByTestId("workflow-node-inspector")
|
||||
.getByLabel("Message text")
|
||||
.fill("Workflow notification");
|
||||
expect(await readWorkflowName(dialog)).toBe(name);
|
||||
|
||||
await dialog.getByRole("button", { name: "Create workflow" }).click();
|
||||
await finishRiskyActivationIfShown(page, dialog);
|
||||
await expect(page.getByTestId("workflows-view")).toContainText(name);
|
||||
});
|
||||
|
||||
@@ -174,6 +201,7 @@ test("keeps a header disable across a later form edit", async ({ page }) => {
|
||||
|
||||
const dialog = page.getByRole("dialog", { name: "Edit workflow" });
|
||||
await expect(dialog).toBeVisible();
|
||||
await dialog.getByRole("tab", { name: "Form" }).click();
|
||||
|
||||
const enableItem = page.getByRole("menuitemcheckbox", { name: "Enable" });
|
||||
// The open menu aria-hides the dialog behind it, so it has to be dismissed
|
||||
@@ -223,6 +251,7 @@ test("keeps the saved name while editing an existing workflow", async ({
|
||||
|
||||
const dialog = page.getByRole("dialog", { name: "Edit workflow" });
|
||||
await expect(dialog).toBeVisible();
|
||||
await dialog.getByRole("tab", { name: "Form" }).click();
|
||||
expect(await readWorkflowName(dialog)).toBe(name);
|
||||
|
||||
await openTriggerPane(dialog);
|
||||
@@ -259,6 +288,7 @@ test("keeps the copy name while duplicating an existing workflow", async ({
|
||||
|
||||
const dialog = page.getByRole("dialog", { name: "Duplicate workflow" });
|
||||
await expect(dialog).toBeVisible();
|
||||
await dialog.getByRole("tab", { name: "Form" }).click();
|
||||
expect(await readWorkflowName(dialog)).toBe(`${name} (copy)`);
|
||||
|
||||
await openStepPane(dialog, 1);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { parse as parseYaml } from "yaml";
|
||||
|
||||
import { installMockBridge } from "../helpers/bridge";
|
||||
import { waitForAnimations } from "../helpers/animations";
|
||||
@@ -74,9 +75,6 @@ async function createWorkflow(
|
||||
);
|
||||
await dialog.getByRole("tab", { name: "Form" }).click();
|
||||
}
|
||||
if (options?.enabled === false) {
|
||||
await dialog.getByRole("switch", { name: "Enable" }).click();
|
||||
}
|
||||
if (options?.trigger) {
|
||||
await dialog
|
||||
.getByRole("button", { name: "Trigger: Message Posted" })
|
||||
@@ -116,6 +114,16 @@ async function createWorkflow(
|
||||
}
|
||||
|
||||
await dialog.getByRole("button", { name: "Create" }).click();
|
||||
if (!options?.trigger || options.trigger === "message_posted") {
|
||||
const activationConfirmation = page.getByRole("alertdialog", {
|
||||
name: "This workflow may run often",
|
||||
});
|
||||
await activationConfirmation
|
||||
.getByRole("button", {
|
||||
name: options?.enabled === false ? "Keep off" : "Turn on",
|
||||
})
|
||||
.click();
|
||||
}
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Create Workflow" }),
|
||||
@@ -131,6 +139,27 @@ test("navigates to workflows view and shows the empty create tile", async ({
|
||||
await expect(page.locator('[data-testid^="workflow-card-"]')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("creates a narrowly triggered workflow without an activation warning", async ({
|
||||
page,
|
||||
}) => {
|
||||
await navigateToWorkflows(page);
|
||||
await createWorkflow(page, `safe_webhook_${Date.now()}`, {
|
||||
trigger: "webhook",
|
||||
});
|
||||
|
||||
await expect(
|
||||
page.getByTestId("workflow-activation-confirmation"),
|
||||
).toHaveCount(0);
|
||||
const yaml = 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(parseYaml(yaml ?? "").enabled).not.toBe(false);
|
||||
});
|
||||
|
||||
test("creation reveals the trigger pane only after a one-shot channel pick", async ({
|
||||
page,
|
||||
}) => {
|
||||
@@ -211,6 +240,10 @@ test("accepts the generated name through the first form mutation", async ({
|
||||
|
||||
await dialog.getByRole("button", { name: "Add first step" }).click();
|
||||
await dialog.getByRole("button", { name: "Create workflow" }).click();
|
||||
const activationConfirmation = page.getByRole("alertdialog", {
|
||||
name: "This workflow may run often",
|
||||
});
|
||||
await activationConfirmation.getByRole("button", { name: "Turn on" }).click();
|
||||
|
||||
await expect(page.getByTestId("workflows-view")).toContainText(generatedName);
|
||||
});
|
||||
@@ -249,9 +282,9 @@ test("shows executable guidance for diff trigger conditions", async ({
|
||||
.getByRole("menuitem", { name: "Diff Posted", exact: true })
|
||||
.click();
|
||||
|
||||
await expect(dialog.getByLabel("Condition (optional)")).toHaveAttribute(
|
||||
await expect(dialog.getByLabel("Diff text")).toHaveAttribute(
|
||||
"placeholder",
|
||||
'e.g. str_contains(trigger_text, "deploy")',
|
||||
"e.g. deploy",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -300,11 +333,9 @@ test("captures workflow library across responsive viewports", async ({
|
||||
|
||||
test("captures disabled diff workflows in the list UI", async ({ page }) => {
|
||||
const workflowName = `diff_workflow_${Date.now()}`;
|
||||
const description = "Watches diff events for src/ changes";
|
||||
|
||||
await navigateToWorkflows(page);
|
||||
await createWorkflow(page, workflowName, {
|
||||
description,
|
||||
enabled: false,
|
||||
trigger: "diff_posted",
|
||||
stepName: "Notify reviewers",
|
||||
@@ -316,13 +347,18 @@ test("captures disabled diff workflows in the list UI", async ({ page }) => {
|
||||
.locator('[data-testid^="workflow-card-"]')
|
||||
.filter({ hasText: workflowName })
|
||||
.first();
|
||||
await expect(card.getByText("Diff Posted", { exact: true })).toBeVisible();
|
||||
await expect(card.locator("h3")).toHaveText(workflowName);
|
||||
await expect(card.getByText(description, { exact: true })).toBeVisible();
|
||||
await expect(card).toContainText("disabled");
|
||||
await expect(card.locator("h3")).toHaveText(
|
||||
"When a diff is posted, send “Workflow notification”",
|
||||
);
|
||||
await expect(card).toContainText(workflowName);
|
||||
await expect(
|
||||
card.getByRole("switch", { name: "Disable workflow" }),
|
||||
).toBeChecked();
|
||||
});
|
||||
|
||||
test("enables and disables a workflow from its card menu", async ({ page }) => {
|
||||
test("enables and disables a workflow from its card status toggle", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workflowName = `toggle_workflow_${Date.now()}`;
|
||||
|
||||
await navigateToWorkflows(page);
|
||||
@@ -333,26 +369,32 @@ test("enables and disables a workflow from its card menu", async ({ page }) => {
|
||||
.locator('[data-testid^="workflow-card-"]')
|
||||
.filter({ hasText: workflowName })
|
||||
.first();
|
||||
const workflowActions = () =>
|
||||
workflowCard().getByRole("button", { name: "Workflow actions" });
|
||||
|
||||
const enableItem = page.getByRole("menuitemcheckbox", { name: "Enable" });
|
||||
const disable = workflowCard().getByRole("switch", {
|
||||
name: "Disable workflow",
|
||||
});
|
||||
await expect(disable).toBeChecked();
|
||||
await disable.click();
|
||||
const enable = workflowCard().getByRole("switch", {
|
||||
name: "Enable workflow",
|
||||
});
|
||||
await expect(enable).not.toBeChecked();
|
||||
await enable.click();
|
||||
const activationConfirmation = page.getByRole("alertdialog", {
|
||||
name: "This workflow may run often",
|
||||
});
|
||||
await expect(activationConfirmation).toBeVisible();
|
||||
await activationConfirmation.getByRole("button", { name: "Turn on" }).click();
|
||||
await expect(
|
||||
workflowCard().getByRole("switch", { name: "Disable workflow" }),
|
||||
).toBeChecked();
|
||||
|
||||
await workflowActions().click();
|
||||
await expect(enableItem).toHaveAttribute("aria-checked", "true");
|
||||
await expect(enableItem.locator("button")).toHaveCount(0);
|
||||
await workflowCard()
|
||||
.getByRole("button", { name: "Workflow actions" })
|
||||
.click();
|
||||
await expect(
|
||||
enableItem.getByTestId("workflow-enabled-switch-visual"),
|
||||
).toHaveAttribute("aria-hidden", "true");
|
||||
await enableItem.click();
|
||||
await expect(
|
||||
workflowCard().getByText("disabled", { exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
await enableItem.click();
|
||||
await expect(
|
||||
workflowCard().getByText("active", { exact: true }),
|
||||
).toBeVisible();
|
||||
page.getByRole("menuitemcheckbox", { name: "Enable" }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("rejects a stale card toggle without overwriting a newer edit", async ({
|
||||
@@ -395,8 +437,7 @@ test("rejects a stale card toggle without overwriting a newer edit", async ({
|
||||
});
|
||||
}, workflowName);
|
||||
|
||||
await workflowCard.getByRole("button", { name: "Workflow actions" }).click();
|
||||
await page.getByRole("menuitemcheckbox", { name: "Enable" }).click();
|
||||
await workflowCard.getByRole("switch", { name: "Disable workflow" }).click();
|
||||
|
||||
await expect(
|
||||
page
|
||||
@@ -436,14 +477,15 @@ test("reports a rejected workflow status change", async ({ page }) => {
|
||||
.locator('[data-testid^="workflow-card-"]')
|
||||
.filter({ hasText: workflowName })
|
||||
.first();
|
||||
await workflowCard.getByRole("button", { name: "Workflow actions" }).click();
|
||||
await page.getByRole("menuitemcheckbox", { name: "Enable" }).click();
|
||||
await workflowCard.getByRole("switch", { name: "Disable workflow" }).click();
|
||||
|
||||
const errorToast = page
|
||||
.locator("[data-sonner-toast][data-removed='false']")
|
||||
.filter({ hasText: "Couldn’t change workflow status" });
|
||||
await expect(errorToast).toContainText("relay refused the update");
|
||||
await expect(workflowCard.getByText("active", { exact: true })).toBeVisible();
|
||||
await expect(
|
||||
workflowCard.getByRole("switch", { name: "Disable workflow" }),
|
||||
).toBeChecked();
|
||||
});
|
||||
|
||||
test("shows the webhook secret dialog after saving a webhook workflow", async ({
|
||||
@@ -528,8 +570,37 @@ test("duplicates a workflow", async ({ page }) => {
|
||||
const dialog = page.getByRole("dialog", { name: "Duplicate workflow" });
|
||||
await selectWorkflowChannel(page, dialog);
|
||||
await dialog.getByRole("button", { name: "Create copy" }).click();
|
||||
const activationConfirmation = page.getByRole("alertdialog", {
|
||||
name: "This workflow may run often",
|
||||
});
|
||||
await expect(activationConfirmation).toBeVisible();
|
||||
await activationConfirmation.getByRole("button", { name: "Back" }).click();
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(
|
||||
() =>
|
||||
(window.__BUZZ_E2E_COMMAND_PAYLOADS__ ?? []).filter(
|
||||
(call) => call.command === "create_workflow",
|
||||
).length,
|
||||
),
|
||||
)
|
||||
.toBe(1);
|
||||
await dialog.getByRole("button", { name: "Create copy" }).click();
|
||||
await activationConfirmation
|
||||
.getByRole("button", { name: "Keep off" })
|
||||
.click();
|
||||
await expect(page.getByRole("dialog")).not.toBeVisible();
|
||||
|
||||
const copiedYaml = await page.evaluate(() => {
|
||||
const calls = (window.__BUZZ_E2E_COMMAND_PAYLOADS__ ?? []).filter(
|
||||
(call) => call.command === "create_workflow",
|
||||
);
|
||||
return (calls.at(-1)?.payload as { yamlDefinition?: string } | undefined)
|
||||
?.yamlDefinition;
|
||||
});
|
||||
expect(parseYaml(copiedYaml ?? "").enabled).toBe(false);
|
||||
|
||||
// Both the original and copy should exist
|
||||
await expect(page.getByTestId("workflows-view")).toContainText(originalName);
|
||||
});
|
||||
@@ -742,7 +813,7 @@ test("pane routes use stable IDs and Form/YAML changes stay synchronized", async
|
||||
await dialog.getByLabel("Duration").fill("5m");
|
||||
await expect(page).toHaveURL(/pane=step%3Astep_2/);
|
||||
|
||||
await dialog.getByRole("button", { name: "Step 1: Send Message" }).click();
|
||||
await dialog.getByRole("button", { name: /^Step 1:/ }).click();
|
||||
await expect(page).toHaveURL(/pane=step%3Astep_1/);
|
||||
await dialog
|
||||
.getByTestId("workflow-node-inspector")
|
||||
@@ -757,7 +828,7 @@ test("pane routes use stable IDs and Form/YAML changes stay synchronized", async
|
||||
const yaml = await yamlEditor.inputValue();
|
||||
await yamlEditor.fill(yaml.replace("duration: 5m", "duration: 10m"));
|
||||
await dialog.getByRole("tab", { name: "Form" }).click();
|
||||
await dialog.getByRole("button", { name: "Step 1: Delay" }).click();
|
||||
await dialog.getByRole("button", { name: /^Step 1:/ }).click();
|
||||
await expect(dialog.getByLabel("Duration")).toHaveValue("10m");
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user