forked from molecule-ai/molecule-core
Issue #1138: Add Playwright E2E for context-menu → delete confirm flow. The unit test (ContextMenu.keyboard.test.tsx) only exercises the store setter — it can't catch the portal/race bug from PR #1133 where the portal-rendered ConfirmDialog was closed by the menu's outside-click handler before onConfirm fired. This E2E test covers: - Right-click workspace node → context menu opens - Click Delete → ConfirmDialog appears (not swallowed) - Click Confirm → dialog closes, node disappears, DELETE /workspaces/:id fires - Click Cancel → dialog closes, node remains Requires: platform on :8080, canvas on :3000. Closes #1138. Co-authored-by: Molecule AI Core-UIUX <core-uiux@agents.moleculesai.app>
131 lines
5.1 KiB
TypeScript
131 lines
5.1 KiB
TypeScript
import { test, expect } from "@playwright/test";
|
|
|
|
/**
|
|
* Playwright E2E for context-menu → delete confirm flow.
|
|
* Regression test for the portal/race bug fixed in PR #1133:
|
|
* clicking "Delete" in the context menu did nothing because the
|
|
* portal-rendered ConfirmDialog was closed by the menu's outside-click
|
|
* handler before onConfirm could fire.
|
|
*
|
|
* The fix hoists dialog state to the canvas store via `setPendingDelete`,
|
|
* which survives ContextMenu unmount. This test exercises the full
|
|
* interaction in a real browser environment.
|
|
*
|
|
* Requires: platform on :8080, canvas on :3000.
|
|
*/
|
|
const API = process.env.E2E_API_URL ?? "http://localhost:8080";
|
|
|
|
test.describe("Context Menu → Delete Confirm", () => {
|
|
test.beforeEach(async ({ request }) => {
|
|
// Ensure at least one workspace exists so the menu can be triggered
|
|
const res = await request.get(`${API}/workspaces`);
|
|
const workspaces = (await res.json()) as Array<{ id: string; name: string }>;
|
|
if (workspaces.length === 0) {
|
|
test.skip("No workspaces on canvas — cannot test context menu");
|
|
}
|
|
});
|
|
|
|
test("Delete button opens ConfirmDialog and clicking Confirm deletes the workspace", async ({
|
|
page,
|
|
request,
|
|
}) => {
|
|
// 1. Create a workspace to delete (leaf node — no children, no cascade)
|
|
const create = await request.post(`${API}/workspaces`, {
|
|
data: { name: "E2E Delete Test", tier: 1, runtime: "claude-code" },
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
const workspace = (await create.json()) as { id: string; name: string };
|
|
const wsId = workspace.id;
|
|
|
|
// Register so the node appears online on the canvas
|
|
await request.post(`${API}/registry/register`, {
|
|
data: {
|
|
id: wsId,
|
|
url: `http://localhost:9999`,
|
|
agent_card: { name: "E2E Delete Test", skills: [] },
|
|
},
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
|
|
// 2. Open the canvas and wait for the workspace node
|
|
await page.goto("/", { waitUntil: "networkidle" });
|
|
await page.waitForTimeout(2000); // allow WS to appear
|
|
|
|
// Find the workspace node on the canvas
|
|
const node = page.locator(`.react-flow__node`).filter({ hasText: "E2E Delete Test" }).first();
|
|
await expect(node).toBeVisible({ timeout: 10000 });
|
|
|
|
// 3. Right-click to open context menu
|
|
await node.click({ button: "right" });
|
|
const menu = page.locator('[role="menu"]').first();
|
|
await expect(menu).toBeVisible({ timeout: 3000 });
|
|
await expect(menu).toHaveAttribute("aria-label", /E2E Delete Test/i);
|
|
|
|
// 4. Click "Delete" — should open the ConfirmDialog (not close silently)
|
|
const deleteBtn = menu.getByRole("menuitem").filter({ hasText: /Delete/i });
|
|
await expect(deleteBtn).toBeVisible();
|
|
await deleteBtn.click();
|
|
|
|
// 5. ConfirmDialog should appear (portal renders into document.body)
|
|
const dialog = page.locator('[role="dialog"]');
|
|
await expect(dialog).toBeVisible({ timeout: 3000 });
|
|
await expect(dialog).toContainText(/delete/i);
|
|
await expect(dialog.getByRole("button", { name: /confirm|delete/i })).toBeVisible();
|
|
|
|
// 6. Click Confirm — workspace should be deleted
|
|
await dialog.getByRole("button", { name: /confirm|delete/i }).first().click();
|
|
|
|
// 7. Dialog should close
|
|
await expect(dialog).not.toBeVisible({ timeout: 3000 });
|
|
|
|
// 8. Node should disappear from canvas
|
|
await expect(
|
|
page.locator(`.react-flow__node`).filter({ hasText: "E2E Delete Test" })
|
|
).not.toBeVisible({ timeout: 5000 });
|
|
|
|
// 9. API confirms workspace is gone
|
|
const getRes = await request.get(`${API}/workspaces/${wsId}`);
|
|
expect(getRes.status()).toBeGreaterThanOrEqual(400); // 404 or similar
|
|
});
|
|
|
|
test("Cancel closes the dialog and the workspace remains", async ({ page, request }) => {
|
|
const res = await request.get(`${API}/workspaces`);
|
|
const workspaces = (await res.json()) as Array<{ id: string; name: string }>;
|
|
if (workspaces.length === 0) {
|
|
test.skip("No workspaces");
|
|
}
|
|
|
|
const ws = workspaces[0];
|
|
|
|
// Register if not already
|
|
await request.post(`${API}/registry/register`, {
|
|
data: { id: ws.id, url: `http://localhost:9999`, agent_card: { name: ws.name, skills: [] } },
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
|
|
await page.goto("/", { waitUntil: "networkidle" });
|
|
await page.waitForTimeout(2000);
|
|
|
|
const node = page.locator(`.react-flow__node`).filter({ hasText: ws.name }).first();
|
|
await node.click({ button: "right" });
|
|
|
|
const menu = page.locator('[role="menu"]').first();
|
|
await expect(menu).toBeVisible();
|
|
|
|
// Get workspace name before we click Delete (can't easily look it up after)
|
|
const wsName = ws.name;
|
|
|
|
await menu.getByRole("menuitem").filter({ hasText: /Delete/i }).click();
|
|
const dialog = page.locator('[role="dialog"]');
|
|
await expect(dialog).toBeVisible({ timeout: 3000 });
|
|
|
|
// Cancel
|
|
await dialog.getByRole("button", { name: /cancel/i }).first().click();
|
|
await expect(dialog).not.toBeVisible({ timeout: 3000 });
|
|
|
|
// Node still on canvas
|
|
await expect(
|
|
page.locator(`.react-flow__node`).filter({ hasText: wsName }).first()
|
|
).toBeVisible({ timeout: 5000 });
|
|
});
|
|
}); |