BangSo/apps/web/e2e/bot-organization.spec.ts

280 lines
11 KiB
TypeScript
Raw Normal View History

2026-09-01 16:51:19 +00:00
import { expect, test } from "@playwright/test";
import { activeBotId, captureScreenshot, completeOnboarding, rpc, signup } from "./helpers";
test("pinned bots and sidebar sections persist", async ({ page }, testInfo) => {
const stamp = Date.now();
await signup(page, `bot-organize-${stamp}@rakazo.test`, "password12", "Test User");
await completeOnboarding(page);
await page.goto("/app");
await page.waitForURL(/\/app\/[^/]+$/);
const sidebar = page.locator("aside").first();
const bot = sidebar.getByRole("button", { name: /^Chief/ });
await bot.click({ button: "right" });
await page.getByRole("menuitem", { name: "Pin", exact: true }).click();
await expect(sidebar.locator('[data-sidebar-group="pinned"]')).toContainText("Chief");
await captureScreenshot(page, testInfo, "pinned-bots");
await bot.click({ button: "right" });
await page.getByRole("menuitem", { name: "Unpin", exact: true }).click();
await expect(sidebar.locator('[data-sidebar-group="pinned"]')).toHaveCount(0);
await bot.click({ button: "right" });
await page.getByRole("menuitem", { name: "Move to", exact: true }).click();
await captureScreenshot(page, testInfo, "move-to-section-menu");
await page
.getByRole("menu", { name: /Move Chief to section/ })
.getByText("New section")
.click();
const dialog = page.getByRole("dialog", { name: "New section" });
await dialog.getByLabel("Name").fill("Projects");
await dialog.getByRole("button", { name: "Create" }).click();
const projects = sidebar.locator('[data-sidebar-group^="section:"]');
await expect(projects).toContainText("Projects");
await expect(projects).toContainText("Chief");
await captureScreenshot(page, testInfo, "bot-sections");
await page.reload();
await expect(projects).toContainText("Projects");
await expect(projects).toContainText("Chief");
await bot.click({ button: "right" });
await page.getByRole("menuitem", { name: "Move to", exact: true }).click();
await page
.getByRole("menu", { name: /Move Chief to section/ })
.getByRole("menuitem", { name: "Unassigned", exact: true })
.click();
await expect(sidebar.locator('[data-sidebar-group="unassigned"]')).toContainText("Chief");
});
test("bots can be reordered by drag or keyboard and keep that order", async ({ page }) => {
const stamp = Date.now();
await signup(page, `bot-reorder-${stamp}@rakazo.test`, "password12", "Bot Order");
await completeOnboarding(page);
await page.goto("/app");
await page.waitForURL(/\/app\/[^/]+$/);
const chiefId = activeBotId(page);
const alpha = await rpc<{ id: string }>(page, "bots/create", {
name: "Alpha",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
computerMode: "team",
});
const beta = await rpc<{ id: string }>(page, "bots/create", {
name: "Beta",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
computerMode: "team",
});
await page.reload();
const sidebar = page.locator("aside").first();
const rows = sidebar.locator("[data-roster-bot-id]");
const order = () =>
rows.evaluateAll((items) => items.map((item) => item.getAttribute("data-roster-bot-id")));
await expect.poll(order).toEqual([chiefId, alpha.id, beta.id]);
let releaseStaleList!: () => void;
let markStaleListReady!: () => void;
let markStaleListDelivered!: () => void;
const staleListReady = new Promise<void>((resolve) => {
markStaleListReady = resolve;
});
const staleListDelivered = new Promise<void>((resolve) => {
markStaleListDelivered = resolve;
});
const staleListGate = new Promise<void>((resolve) => {
releaseStaleList = resolve;
});
let interceptedList = false;
await page.route("**/rpc/spaces/list", async (route) => {
if (interceptedList) {
await route.continue();
return;
}
interceptedList = true;
const staleResponse = await route.fetch();
markStaleListReady();
await staleListGate;
await route.fulfill({ response: staleResponse });
markStaleListDelivered();
});
await page.evaluate(() => window.dispatchEvent(new Event("focus")));
await staleListReady;
const reorderSaved = page.waitForResponse(
(response) => response.url().includes("/rpc/bots/reorder") && response.ok(),
);
await sidebar
.locator(`[data-roster-bot-id="${beta.id}"]`)
.dragTo(sidebar.locator(`[data-roster-bot-id="${chiefId}"]`));
await reorderSaved;
releaseStaleList();
await staleListDelivered;
await expect.poll(order).toEqual([beta.id, chiefId, alpha.id]);
await page.reload();
await expect.poll(order).toEqual([beta.id, chiefId, alpha.id]);
const betaRow = sidebar.locator(`[data-roster-bot-id="${beta.id}"]`);
await betaRow.focus();
await page.keyboard.press("Alt+ArrowDown");
await expect.poll(order).toEqual([chiefId, beta.id, alpha.id]);
await page.reload();
await expect.poll(order).toEqual([chiefId, beta.id, alpha.id]);
let releaseRejectedReorder!: () => void;
let markRejectedReorderStarted!: () => void;
const rejectedReorderStarted = new Promise<void>((resolve) => {
markRejectedReorderStarted = resolve;
});
const rejectReorder = new Promise<void>((resolve) => {
releaseRejectedReorder = resolve;
});
await page.route(
"**/rpc/bots/reorder",
async (route) => {
markRejectedReorderStarted();
await rejectReorder;
await route.fulfill({ status: 500, contentType: "application/json", body: "{}" });
},
{ times: 1 },
);
await sidebar
.locator(`[data-roster-bot-id="${alpha.id}"]`)
.dragTo(sidebar.locator(`[data-roster-bot-id="${chiefId}"]`));
await rejectedReorderStarted;
const queuedReorderSaved = page.waitForResponse(
(response) => response.url().includes("/rpc/bots/reorder") && response.ok(),
);
await sidebar
.locator(`[data-roster-bot-id="${beta.id}"]`)
.dragTo(sidebar.locator(`[data-roster-bot-id="${alpha.id}"]`));
releaseRejectedReorder();
await queuedReorderSaved;
await expect.poll(order).toEqual([beta.id, alpha.id, chiefId]);
await page.reload();
await expect.poll(order).toEqual([beta.id, alpha.id, chiefId]);
});
test("chat composer controls are vertically centered", async ({ page }) => {
const stamp = Date.now();
await signup(page, `composer-layout-${stamp}@rakazo.test`, "password12", "Composer Layout");
await completeOnboarding(page);
const centers = await page.getByTestId("composer-bar").evaluate((composer) =>
["Attach file", "Dictate", "Message Chief", "Send"].map((label) => {
const element = composer.querySelector<HTMLElement>(`[aria-label="${label}"]`);
if (!element) throw new Error(`Missing composer control: ${label}`);
const box = element.getBoundingClientRect();
return box.top + box.height / 2;
}),
);
expect(Math.max(...centers) - Math.min(...centers)).toBeLessThanOrEqual(1);
});
test("group chats share every context-menu action", async ({ page }, testInfo) => {
const stamp = Date.now();
await signup(page, `group-organize-${stamp}@rakazo.test`, "password12", "Group Menu");
await completeOnboarding(page);
await page.goto("/app");
await page.waitForURL(/\/app\/[^/]+$/);
const chiefId = activeBotId(page);
const partner = await rpc<{ id: string }>(page, "bots/create", {
name: "Partner",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
computerMode: "team",
});
await rpc(page, "groups/create", {
name: "Group menu",
botIds: [chiefId, partner.id],
});
await page.reload();
const sidebar = page.locator("aside").first();
const group = sidebar.getByRole("button", { name: /^Group menu/ });
await group.click({ button: "right" });
for (const action of [
"Pin",
"Move to",
"Mark as Unread",
"Edit Profile",
"Duplicate",
"Clear conversation",
"Archive",
"Delete",
]) {
await expect(page.getByRole("menuitem", { name: action, exact: true })).toBeVisible();
}
await captureScreenshot(page, testInfo, "group-context-menu-desktop");
await page.getByRole("menuitem", { name: "Mark as Unread", exact: true }).click();
await group.click({ button: "right" });
await page.getByRole("menuitem", { name: "Mark as Read", exact: true }).click();
await group.click({ button: "right" });
await page.getByRole("menuitem", { name: "Pin", exact: true }).click();
await expect(sidebar.locator('[data-sidebar-group="pinned"]')).toContainText("Group menu");
await group.click({ button: "right" });
await page.getByRole("menuitem", { name: "Unpin", exact: true }).click();
await group.click({ button: "right" });
await page.getByRole("menuitem", { name: "Move to", exact: true }).click();
await page
.getByRole("menu", { name: /Move Group menu to section/ })
.getByRole("menuitem", { name: "New section", exact: true })
.click();
const sectionDialog = page.getByRole("dialog", { name: "New section" });
await sectionDialog.getByLabel("Name").fill("Teams");
await sectionDialog.getByRole("button", { name: "Create" }).click();
await expect(sidebar.locator('[data-sidebar-group^="section:"]')).toContainText("Group menu");
await group.click({ button: "right" });
await page.getByRole("menuitem", { name: "Clear conversation", exact: true }).click();
await expect(
page.getByRole("alertdialog", { name: "Clear Group menus conversation?" }),
).toBeVisible();
await page.getByRole("button", { name: "Clear", exact: true }).click();
await expect(
page.getByRole("alertdialog", { name: "Clear Group menus conversation?" }),
).toHaveCount(0);
await group.click({ button: "right" });
await page.getByRole("menuitem", { name: "Duplicate", exact: true }).click();
const copy = sidebar.getByRole("button", { name: /^Group menu copy/ });
await expect(copy).toBeVisible();
await copy.click({ button: "right" });
await page.getByRole("menuitem", { name: "Delete", exact: true }).click();
await expect(page.getByRole("alertdialog", { name: "Delete Group menu copy?" })).toBeVisible();
await page.getByRole("button", { name: "Cancel" }).click();
await copy.click({ button: "right" });
await page.getByRole("menuitem", { name: "Archive", exact: true }).click();
await expect(sidebar.getByRole("button", { name: /^Group menu copy/ })).toHaveCount(0);
await expect(sidebar.getByText("Archived", { exact: true })).toBeVisible();
await page.setViewportSize({ width: 390, height: 844 });
await page.getByRole("button", { name: "Open navigation" }).click();
await group.click({ button: "right" });
await captureScreenshot(page, testInfo, "group-context-menu-mobile");
await page.keyboard.press("Escape");
await page.setViewportSize({ width: 1280, height: 800 });
await group.click({ button: "right" });
await page.getByRole("menuitem", { name: "Edit Profile", exact: true }).click();
await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "group-settings");
});