tests/e2e/flows-pane-trigger-accounts.spec.ts
Rama main · réplica de lectura del repositorio original.
import { expect, test } from "@playwright/test";
import { selectAppOption } from "./helpers/app-select";
import {
stubFlowsPage,
primaryModifier,
} from "./helpers/flows-pane-keyboard-fixtures";
import type { FlowNode } from "../../lib/types";
const installations = [
{
id: "inst-1",
installation_id: 101,
account_login: "webrenew",
account_type: "Organization" as const,
repositories: [{ id: "repo-1", full_name: "webrenew/blackbox" }],
},
{
id: "inst-2",
installation_id: 202,
account_login: "alex",
account_type: "User" as const,
repositories: [{ id: "repo-2", full_name: "alex/priority-project" }],
},
{
id: "inst-3",
installation_id: 303,
account_login: "Mogplex",
account_type: "Organization" as const,
repositories: [{ id: "repo-3", full_name: "Mogplex/mogplex" }],
},
];
test("a multi-account PR trigger shows every account and keeps them across edits", async ({
page,
}) => {
// flows.installation_id is 101, but the trigger routes for 101 and 202. The
// editor used to show only "webrenew" and rewrote the filter to [101] on any
// edit, silently dropping the other account.
const { getFlow } = await stubFlowsPage(page, {
installations,
startFilter: { scope: "all", installationIds: [101, 202] },
});
const savedFilter = () =>
(getFlow().draft_graph.nodes as FlowNode[]).find(
(node) => node.type === "start"
)?.data.filter;
await page.setViewportSize({ width: 1600, height: 900 });
await page.goto("/alex/workflows");
await page.waitForLoadState("networkidle");
const filterBar = page.getByTestId("flow-browser-filters");
await selectAppOption(page.getByTestId("flow-browser-account"), "202");
await expect(filterBar).toContainText("1 of 1 workflows");
await selectAppOption(page.getByTestId("flow-browser-account"), "303");
await expect(filterBar).toContainText("0 of 1 workflows");
await selectAppOption(page.getByTestId("flow-browser-account"), "all");
const startNode = page
.locator(".react-flow__node")
.filter({ hasText: "PR opened" });
await expect(startNode).toContainText("webrenew, alex · All repositories");
await startNode.click();
const account = page.getByTestId("flow-trigger-account");
await expect(account).toHaveAttribute("data-value", "101,202");
await expect(account).toContainText("webrenew, alex");
await selectAppOption(
page.getByTestId("flow-trigger-author-filter"),
"exclude_dependabot"
);
await page.keyboard.press(`${primaryModifier}+S`);
await expect.poll(savedFilter).toEqual({
scope: "all",
installationIds: [101, 202],
authorFilter: "exclude_dependabot",
});
// Unchecking the last account must not widen the trigger to every account.
await account.click();
await page.getByTestId("flow-trigger-account-option-202").click();
await expect(account).toHaveAttribute("data-value", "101");
const lastAccount = page.getByTestId("flow-trigger-account-option-101");
await expect(lastAccount.getByRole("checkbox")).toBeDisabled();
await lastAccount.click({ force: true });
await expect(account).toHaveAttribute("data-value", "101");
await expect(startNode).toContainText("webrenew · All repositories");
await page.getByTestId("flow-trigger-account-option-all").click();
await account.click();
await expect(account).toHaveAttribute("data-value", "all");
await expect(startNode).toContainText("All accounts · All repositories");
await page.keyboard.press(`${primaryModifier}+S`);
await expect.poll(savedFilter).toEqual({
scope: "all",
authorFilter: "exclude_dependabot",
});
});
test("an org-scoped PR trigger keeps its account-type scope across edits", async ({
page,
}) => {
// API-authored filters can narrow by account type. The editor must show it
// and keep it; edits used to rewrite scope to "all", widening the trigger.
const { getFlow } = await stubFlowsPage(page, {
installations,
startFilter: { scope: "org" },
});
const savedFilter = () =>
(getFlow().draft_graph.nodes as FlowNode[]).find(
(node) => node.type === "start"
)?.data.filter;
await page.setViewportSize({ width: 1600, height: 900 });
await page.goto("/alex/workflows");
await page.waitForLoadState("networkidle");
const filterBar = page.getByTestId("flow-browser-filters");
await selectAppOption(page.getByTestId("flow-browser-account"), "202");
await expect(filterBar).toContainText("0 of 1 workflows");
await selectAppOption(page.getByTestId("flow-browser-account"), "303");
await expect(filterBar).toContainText("1 of 1 workflows");
const startNode = page
.locator(".react-flow__node")
.filter({ hasText: "PR opened" });
await expect(startNode).toContainText("All organizations");
await startNode.click();
const account = page.getByTestId("flow-trigger-account");
await expect(account).toHaveAttribute("data-value", "all-org");
// The open picker must show the org scope, not an empty selection.
await account.click();
const scopeRow = page.getByTestId("flow-trigger-account-option-scope");
await expect(scopeRow).toContainText("All organizations");
await expect(scopeRow.getByRole("checkbox")).toBeChecked();
await expect(
page.getByTestId("flow-trigger-account-option-all").getByRole("checkbox")
).not.toBeChecked();
await page.keyboard.press("Escape");
await selectAppOption(
page.getByTestId("flow-trigger-author-filter"),
"humans_only"
);
await page.keyboard.press(`${primaryModifier}+S`);
await expect.poll(savedFilter).toEqual({
scope: "org",
authorFilter: "humans_only",
});
});
test("a single-repository trigger bound to a disconnected account prompts for one", async ({
page,
}) => {
await stubFlowsPage(page, {
installations,
startFilter: { scope: "all", installationIds: [999] },
});
await page.setViewportSize({ width: 1600, height: 900 });
await page.goto("/alex/workflows");
await page.waitForLoadState("networkidle");
await page
.locator(".react-flow__node")
.filter({ hasText: "PR opened" })
.click();
await selectAppOption(page.getByTestId("flow-trigger-event"), "schedule");
const account = page.getByTestId("flow-trigger-account");
await expect(account).toHaveAttribute("data-value", "");
await expect(account).toContainText("Select an account");
// The prompt is not a choice; picking it used to be a silent no-op.
await account.click();
await expect(page.locator('[role="option"][data-value=""]')).toHaveAttribute(
"aria-disabled",
"true"
);
});
test("a multi-account trigger with a disconnected account stays editable", async ({
page,
}) => {
// 999 was disconnected after the filter was saved. Unchecking 202 used to
// leave [999], which the editor rejects, so the checkbox looked stuck.
await stubFlowsPage(page, {
installations,
startFilter: { scope: "all", installationIds: [202, 999] },
});
await page.setViewportSize({ width: 1600, height: 900 });
await page.goto("/alex/workflows");
await page.waitForLoadState("networkidle");
await page
.locator(".react-flow__node")
.filter({ hasText: "PR opened" })
.click();
const account = page.getByTestId("flow-trigger-account");
await expect(account).toContainText("1 of 3 connected accounts");
await account.click();
// The disconnected account is listed, so the drop below is not a surprise.
const disconnected = page.getByTestId("flow-trigger-account-option-999");
await expect(disconnected).toContainText("Installation 999 · Disconnected");
await expect(disconnected.getByRole("checkbox")).toBeChecked();
await expect(disconnected.getByRole("checkbox")).toBeDisabled();
await expect(
page.getByTestId("flow-trigger-account-option-202").getByRole("checkbox")
).toBeDisabled();
await page.getByTestId("flow-trigger-account-option-101").click();
await expect(account).toHaveAttribute("data-value", "202,101");
await expect(disconnected).toHaveCount(0);
});
test("switching a multi-account trigger to a schedule binds one account", async ({
page,
}) => {
// Publishing rejects a schedule whose filter lists other installations, so
// the switch must not carry the GitHub trigger's account list over. The
// org scope is dropped too: it only filters GitHub deliveries.
const { getFlow } = await stubFlowsPage(page, {
installations,
startFilter: { scope: "org", installationIds: [101, 303] },
});
const savedStart = () =>
(getFlow().draft_graph.nodes as FlowNode[]).find(
(node) => node.type === "start"
)?.data;
await page.setViewportSize({ width: 1600, height: 900 });
await page.goto("/alex/workflows");
await page.waitForLoadState("networkidle");
await page
.locator(".react-flow__node")
.filter({ hasText: "PR opened" })
.click();
await selectAppOption(page.getByTestId("flow-trigger-event"), "schedule");
await expect(page.getByTestId("flow-trigger-account")).toHaveAttribute(
"data-value",
"101"
);
await page.keyboard.press(`${primaryModifier}+S`);
await expect.poll(() => savedStart()?.event).toBe("schedule");
expect(savedStart()?.filter).toBeUndefined();
});
test("re-selecting All accounts does not add an undo step", async ({
page,
}) => {
await stubFlowsPage(page, { installations });
await page.setViewportSize({ width: 1600, height: 900 });
await page.goto("/alex/workflows");
await page.waitForLoadState("networkidle");
await page
.locator(".react-flow__node")
.filter({ hasText: "PR opened" })
.click();
const account = page.getByTestId("flow-trigger-account");
await expect(account).toHaveAttribute("data-value", "all");
await account.click();
await page.getByTestId("flow-trigger-account-option-all").click();
await page.keyboard.press("Escape");
const pane = page.locator(".react-flow__pane");
const bounds = await pane.boundingBox();
if (!bounds) throw new Error("Canvas pane has no bounding box");
await pane.dispatchEvent("contextmenu", {
clientX: bounds.x + 40,
clientY: bounds.y + bounds.height - 40,
button: 2,
bubbles: true,
cancelable: true,
});
await expect(page.getByTestId("flow-context-undo")).toBeDisabled();
});