tests/e2e/flows-delete-selection.spec.ts
Rama main · réplica de lectura del repositorio original.
import { expect, test } from "@playwright/test";
import { enableScopedE2EAuth, scopedPath } from "./helpers/auth";
import type { Route } from "@playwright/test";
const connectedUser = {
id: "00000000-0000-4000-8000-000000000001",
email: "alex@example.com",
username: "alex",
name: "Alex",
avatar_url: "https://example.com/avatar.png",
github_connected: true,
github_app_connected: true,
github_app_available: true,
github_connection_mode: "app" as const,
};
async function fulfillJson(route: Route, data: unknown, status = 200) {
await route.fulfill({
status,
contentType: "application/json",
body: JSON.stringify(data),
});
}
function buildFlow(input: {
id: string;
name: string;
status: "active" | "inactive";
}) {
return {
id: input.id,
installation_id: 101,
name: input.name,
description: "",
notes: "",
source_kind: "github",
status: input.status,
last_run_status: null,
published_version_id: null,
published_version: null,
draft_graph: {
nodes: [
{
id: "start",
type: "start",
position: { x: 0, y: 160 },
data: { label: "Start", config: { event: "pr_opened", filter: {} } },
},
],
edges: [],
viewport: { x: 0, y: 0, zoom: 1 },
},
};
}
test("deleting the selected workflow selects another workflow even when the list revalidation returns stale data", async ({
page,
}) => {
await enableScopedE2EAuth(page);
const allFlows = [
buildFlow({ id: "flow-1", name: "Live Review", status: "active" }),
buildFlow({ id: "flow-2", name: "Paused Merger", status: "inactive" }),
];
let flows = [...allFlows];
// Simulates SWR deduping the post-delete mutate() into a revalidation that
// started before the DELETE landed: the first list fetch after deletion
// still contains the deleted flow.
let staleListReadsRemaining = 0;
await page.route("**/api/auth/user", (route) =>
fulfillJson(route, { user: connectedUser })
);
await page.route("**/api/settings", (route) =>
fulfillJson(route, { default_model: "minimax/minimax-m2.5", theme: "dark" })
);
await page.route("**/api/models", (route) =>
fulfillJson(route, { models: [], catalog: [] })
);
await page.route("**/api/agents", (route) => fulfillJson(route, []));
await page.route("**/api/github/installations", (route) =>
fulfillJson(route, [
{
id: "inst-1",
installation_id: 101,
account_login: "webrenew",
repositories: [{ id: "repo-1", full_name: "webrenew/blackbox" }],
},
])
);
await page.route("**/api/flows", (route) => {
const stale = staleListReadsRemaining > 0;
if (stale) staleListReadsRemaining -= 1;
return fulfillJson(route, stale ? allFlows : flows);
});
await page.route("**/api/flows/flow-1", async (route) => {
if (route.request().method() === "DELETE") {
flows = flows.filter((flow) => flow.id !== "flow-1");
staleListReadsRemaining = 1;
await fulfillJson(route, { success: true });
return;
}
const flow = allFlows.find((candidate) => candidate.id === "flow-1");
await fulfillJson(route, flow ?? {}, flow ? 200 : 404);
});
await page.route("**/api/flows/flow-2", (route) =>
fulfillJson(
route,
allFlows.find((flow) => flow.id === "flow-2")
)
);
await page.route("**/api/flows/*/runs?limit=12", (route) =>
fulfillJson(route, { runs: [] })
);
await page.setViewportSize({ width: 1680, height: 900 });
await page.goto(scopedPath("automations"));
await page.waitForLoadState("networkidle");
await expect(page.getByLabel("Select workflow")).toHaveAttribute(
"data-value",
"flow-1"
);
page.on("dialog", (dialog) => void dialog.accept());
await page.getByRole("button", { name: "Delete flow" }).click();
await expect(
page
.locator('[data-variant="info"]')
.filter({ hasText: "Workflow deleted" })
).toBeVisible();
await expect(page.getByLabel("Select workflow")).toHaveAttribute(
"data-value",
"flow-2"
);
});
test("deleting the selected workflow selects another workflow", async ({
page,
}) => {
await enableScopedE2EAuth(page);
let flows = [
buildFlow({ id: "flow-1", name: "Live Review", status: "active" }),
buildFlow({ id: "flow-2", name: "Paused Merger", status: "inactive" }),
];
await page.route("**/api/auth/user", (route) =>
fulfillJson(route, { user: connectedUser })
);
await page.route("**/api/settings", (route) =>
fulfillJson(route, { default_model: "minimax/minimax-m2.5", theme: "dark" })
);
await page.route("**/api/models", (route) =>
fulfillJson(route, { models: [], catalog: [] })
);
await page.route("**/api/agents", (route) => fulfillJson(route, []));
await page.route("**/api/github/installations", (route) =>
fulfillJson(route, [
{
id: "inst-1",
installation_id: 101,
account_login: "webrenew",
repositories: [{ id: "repo-1", full_name: "webrenew/blackbox" }],
},
])
);
await page.route("**/api/flows", (route) => fulfillJson(route, flows));
await page.route("**/api/flows/flow-1", async (route) => {
if (route.request().method() === "DELETE") {
flows = flows.filter((flow) => flow.id !== "flow-1");
await fulfillJson(route, { success: true });
return;
}
const flow = flows.find((candidate) => candidate.id === "flow-1");
await fulfillJson(route, flow ?? {}, flow ? 200 : 404);
});
await page.route("**/api/flows/flow-2", (route) =>
fulfillJson(
route,
flows.find((flow) => flow.id === "flow-2")
)
);
await page.route("**/api/flows/*/runs?limit=12", (route) =>
fulfillJson(route, { runs: [] })
);
await page.setViewportSize({ width: 1680, height: 900 });
await page.goto(scopedPath("automations"));
await page.waitForLoadState("networkidle");
// flow-1 is selected by default (first visible flow)
await expect(page.getByLabel("Select workflow")).toHaveAttribute(
"data-value",
"flow-1"
);
page.on("dialog", (dialog) => void dialog.accept());
await page.getByRole("button", { name: "Delete flow" }).click();
await expect(
page
.locator('[data-variant="info"]')
.filter({ hasText: "Workflow deleted" })
).toBeVisible();
// After deletion, the selection should switch to the remaining flow (or be
// cleared) rather than still pointing at the deleted flow.
await expect(page.getByLabel("Select workflow")).not.toHaveAttribute(
"data-value",
"flow-1"
);
});