BangSo/apps/web/e2e/group-chats.spec.ts

255 lines
12 KiB
TypeScript
Raw Permalink Normal View History

2026-09-01 16:51:19 +00:00
import { expect, test } from "@playwright/test";
import {
activeBotId,
captureScreenshot,
completeOnboarding,
openNewBot,
rpc,
signup,
} from "./helpers";
async function createBot(page: import("@playwright/test").Page, name: string) {
const botList = page.locator("aside").first();
await openNewBot(page);
await expect(page.getByText("New bot", { exact: true })).toBeVisible();
await page.locator("label:has-text('Name') input").fill(name);
await page.getByRole("button", { name: "Create", exact: true }).click();
await expect(botList.getByRole("button", { name: new RegExp(`^${name}`) })).toBeVisible();
await expect(page.getByRole("combobox", { name: `Message ${name}` })).toBeVisible();
await page.waitForURL(/\/app\/[^/]+$/);
return activeBotId(page);
}
test("create group from + and see two bots in one transcript", async ({ page }, testInfo) => {
const stamp = Date.now();
await signup(page, `group-${stamp}@rakazo.test`, "password12", "Group E2E");
await completeOnboarding(page);
await page.goto("/app");
await page.waitForURL(/\/app\/[^/]+$/);
const researcherId = await createBot(page, "Researcher");
const writerId = await createBot(page, "Research Writer");
await page.getByTitle("Create").click();
await page.getByRole("button", { name: "New group" }).click();
await page.locator("label:has-text('Name') input").fill("Draft team");
const panel = page.getByTestId("side-panel");
await panel.getByRole("button", { name: "Researcher" }).click();
await panel.getByRole("button", { name: "Research Writer" }).click();
await captureScreenshot(page, testInfo, "group-creation");
await page.route("**/rpc/groups/create", async (route) => route.abort("failed"));
await page.getByRole("button", { name: "Create group", exact: true }).click();
await expect(panel.getByRole("alert")).toHaveText("Failed to fetch");
await expect(page.getByRole("button", { name: "Create group", exact: true })).toBeEnabled();
await page.unroute("**/rpc/groups/create");
await page.getByRole("button", { name: "Create group", exact: true }).click();
await page.waitForURL(/\/app\/g\/[^/]+$/);
const groupUrl = page.url();
const draftGroupId = new URL(groupUrl).pathname.split("/").at(-1)!;
const reviewGroup = await rpc<{ id: string }>(page, "groups/create", {
name: "Review team",
botIds: [researcherId, writerId],
});
await rpc(page, "voice/connect", {
provider: "scripted",
apiKey: "fake-group-voice-key",
});
await page.reload();
await expect(page).toHaveURL(groupUrl);
await expect(page.getByRole("combobox", { name: "Message Draft team" })).toBeVisible();
const groups = await rpc<
Array<{
id: string;
members: Array<{ botId: string; name: string; color: string; status?: string }>;
}>
>(page, "groups/list", {});
const groupSnapshot = await rpc<{
members?: Array<{ botId: string; name: string; color: string; status?: string }>;
}>(page, "threads/get", { groupId: draftGroupId });
await page.route("**/rpc/groups/list", async (route) => {
await route.fulfill({
contentType: "application/json",
body: JSON.stringify({
json: groups.map((group) => ({
...group,
members: group.members.map((member, index) => ({
...member,
status: index === 0 ? "running" : "idle",
})),
})),
}),
});
});
await page.route("**/rpc/threads/get", async (route) => {
await route.fulfill({
contentType: "application/json",
body: JSON.stringify({
json: {
...groupSnapshot,
members: groupSnapshot.members?.map((member, index) => ({
...member,
status: index === 0 ? "running" : "idle",
})),
},
}),
});
});
await page.reload();
// Anchor ^ so Now/Recent activity rows ("Bot · Draft team, …") do not match.
const groupAvatar = page
.locator("aside")
.first()
.getByRole("button", { name: /^Draft team/ })
.locator(".rakazo-group-avatar");
await expect(groupAvatar).toBeVisible();
await expect(groupAvatar.locator(".rakazo-bot-avatar")).toHaveCount(2);
const workingAvatar = groupAvatar.locator('[data-working="true"]');
await expect(workingAvatar).toHaveCount(1);
await expect(workingAvatar.locator("svg")).toHaveCSS("animation-name", "rakazo-avatar-spin");
await captureScreenshot(page, testInfo, "group-avatar-active");
await page.unroute("**/rpc/groups/list");
await page.unroute("**/rpc/threads/get");
await page.reload();
await page.getByTestId("bot-settings-trigger").click();
const desktopSettings = page.getByTestId("side-panel");
const groupName = desktopSettings.locator("label:has-text('Name') input");
await groupName.fill("Unsaved Draft team name");
const sidebar = page.locator("aside").first();
await sidebar.getByRole("button", { name: /^Review team/ }).click();
await expect(groupName).toHaveValue("Review team");
await sidebar.getByRole("button", { name: /^Draft team/ }).click();
await expect(groupName).toHaveValue("Draft team");
await page.route("**/rpc/groups/update", async (route) => route.abort("failed"));
await desktopSettings.getByRole("button", { name: "Save", exact: true }).click();
await expect(desktopSettings.getByRole("alert")).toHaveText("Failed to fetch");
await expect(desktopSettings.getByRole("button", { name: "Save", exact: true })).toBeEnabled();
await page.unroute("**/rpc/groups/update");
await desktopSettings.getByRole("button", { name: "Save", exact: true }).click();
await page
.getByRole("combobox", { name: "Message Draft team" })
.fill("@Researcher unfinished draft");
await sidebar.getByRole("button", { name: /^Review team/ }).click();
await expect(page.getByRole("combobox", { name: "Message Review team" })).toHaveValue("");
await sidebar.getByRole("button", { name: /^Draft team/ }).click();
await expect(page.getByRole("combobox", { name: "Message Draft team" })).toHaveValue("");
const composer = page.getByRole("combobox", { name: "Message Draft team" });
await composer.fill("@Res");
await captureScreenshot(page, testInfo, "group-mention-picker");
await page.getByRole("option", { name: "@Research Writer", exact: true }).click();
await expect(
page.getByTestId("mention-chip").filter({ hasText: "Research Writer" }),
).toBeVisible();
await composer.fill("turn the sources into a draft. @Res");
await page.getByRole("option", { name: "@Researcher", exact: true }).click();
await expect(page.getByTestId("mention-chip").filter({ hasText: "Researcher" })).toBeVisible();
await composer.fill(`${await composer.inputValue()}gather sources.`);
await composer.press("Enter");
await expect(page.getByTestId("transcript")).toContainText(/handled|on it|gather/i, {
timeout: 60_000,
});
const transcript = page.getByTestId("transcript");
await expect(transcript.getByText("Researcher", { exact: true }).first()).toBeVisible();
await expect(transcript.getByText("Research Writer", { exact: true }).first()).toBeVisible();
const researcherReply = transcript.getByText("Researcher", { exact: true }).first().locator("..");
const [speechRequest] = await Promise.all([
page.waitForRequest(
(request) => request.url().includes("/api/voice/speak") && request.method() === "POST",
),
researcherReply.getByRole("button", { name: "Speak this reply" }).click(),
]);
expect(speechRequest.postDataJSON()).toMatchObject({ botId: researcherId });
await captureScreenshot(page, testInfo, "group-transcript");
await composer.fill("@Res");
await page.getByRole("option", { name: "@Research Writer", exact: true }).click();
await expect(
page.getByTestId("mention-chip").filter({ hasText: "Research Writer" }),
).toBeVisible();
await composer.fill("ask me which city to use");
await composer.press("Enter");
// threads/get / member status can observe waiting_input before realtime paints the ask card.
await expect(page.getByRole("button", { name: /Research Writer waiting_input/ })).toBeVisible({
timeout: 60_000,
});
const cityAsk = page.locator("p").filter({ hasText: /^Which city should I use\?$/ });
if ((await cityAsk.count()) === 0) {
await page.reload({ waitUntil: "domcontentloaded" });
await expect(page.getByRole("combobox", { name: "Message Draft team" })).toBeVisible({
timeout: 15_000,
});
}
await expect(cityAsk).toBeVisible({ timeout: 15_000 });
await page.getByRole("button", { name: "Edit first" }).click();
await page.getByRole("textbox", { name: "Answer" }).fill("Paris");
await page.getByRole("button", { name: "Send answer" }).click();
await expect(page.getByText("Answered: Paris", { exact: true })).toBeVisible({ timeout: 30_000 });
const firstMessage = transcript.locator("[data-message-id]").first();
await firstMessage.hover();
const replyButton = firstMessage.getByRole("button", { name: "Reply" });
await expect(replyButton).toBeVisible();
await replyButton.click();
await expect(page.getByTestId("reply-chip")).toContainText(/Replying to/);
await page.getByRole("button", { name: "Cancel reply" }).click();
let releaseReviewSnapshot!: () => void;
let sawReviewSnapshot!: () => void;
const reviewSnapshotReleased = new Promise<void>((resolve) => {
releaseReviewSnapshot = resolve;
});
const reviewSnapshotIntercepted = new Promise<void>((resolve) => {
sawReviewSnapshot = resolve;
});
await page.route("**/rpc/threads/get", async (route) => {
if (route.request().postData()?.includes(reviewGroup.id) !== true) {
await route.continue();
return;
}
sawReviewSnapshot();
await reviewSnapshotReleased;
await route.continue();
});
await sidebar.getByRole("button", { name: /^Review team/ }).click();
await reviewSnapshotIntercepted;
await expect(page).toHaveURL(new RegExp(`/app/g/${reviewGroup.id}$`));
await expect(page.getByTestId("transcript")).not.toContainText("Answered: Paris");
releaseReviewSnapshot();
await expect(page.getByRole("combobox", { name: "Message Review team" })).toBeVisible();
await page.unroute("**/rpc/threads/get");
await sidebar.getByRole("button", { name: /^Draft team/ }).click();
await expect(page.getByText("Answered: Paris", { exact: true })).toBeVisible();
await composer.fill(
"@Researcher write path notes/group-preview.md and attach it to the thread says # Group artifact",
);
await composer.press("Enter");
const groupMarkdown = page.getByRole("button", { name: "Preview group-preview.md" });
await expect(groupMarkdown).toBeVisible({ timeout: 30_000 });
await groupMarkdown.click();
const markdownDialog = page.getByRole("dialog", { name: "group-preview.md" });
await expect(markdownDialog.getByRole("heading", { name: "Group artifact" })).toBeVisible();
await markdownDialog.getByRole("button", { name: "Close preview" }).click();
await page.setViewportSize({ width: 390, height: 844 });
await expect(page.getByRole("button", { name: "Open navigation" })).toBeVisible();
expect((await transcript.boundingBox())?.width).toBeGreaterThan(350);
await page.getByRole("button", { name: "Open navigation" }).click();
await expect(page.getByRole("button", { name: "Close navigation" })).toBeVisible();
await page.getByRole("button", { name: "Close navigation" }).click();
await page.getByTestId("bot-settings-trigger").click();
const settings = page.getByTestId("side-panel");
await expect(settings).toHaveAttribute("data-panel", "group-settings");
expect((await settings.boundingBox())?.width).toBeLessThanOrEqual(390);
await captureScreenshot(page, testInfo, "group-settings-mobile");
await rpc(page, "groups/remove", { groupId: reviewGroup.id });
await page.goto(`/app/g/${reviewGroup.id}`);
await page.waitForURL(/\/app\/(?!g\/)[^/]+$/);
await expect(page.getByRole("combobox", { name: /Message/ })).toBeVisible();
});