import { expect, type Page, test } from "@playwright/test"; import type { Group } from "@rakazo/contracts"; import { captureScreenshot, routePatchRpc, rpc, signup, waitForChatComposer } from "./helpers"; test("answers a specialist's question from the main conversation", async ({ page }, testInfo) => { const stamp = Date.now(); await signup(page, `inbox-${stamp}@rakazo.test`, "password12", "Inbox"); // Bots come from RPC: this spec is about the inbox, not the onboarding flow. await page.waitForURL(/\/(onboarding|app)/, { timeout: 30_000 }); const researcher = await createBot(page, "Researcher"); await rpc(page, "threads/send", { botId: researcher.id, text: "ask me which city to use" }); await expect .poll(async () => (await activeRun(page, researcher.id))?.status ?? null, { timeout: 60_000 }) .toBe("waiting_input"); await openMainConversation(page, researcher.id); const inbox = page.getByRole("region", { name: "Needs you" }); const ask = inbox.locator("[data-kind='ask']"); await expect(ask).toBeVisible({ timeout: 30_000 }); await expect(ask).toContainText("Researcher"); await expect(ask).toContainText("Which city should I use?"); await expect(page.getByRole("combobox", { name: "Message" })).toBeInViewport(); await captureScreenshot(page, testInfo, "inbox-ask"); await ask.getByRole("button", { name: "Edit first" }).click(); await ask.getByRole("textbox", { name: "Answer" }).fill("Paris"); await ask.getByRole("button", { name: "Send answer" }).click(); await expect(ask).toHaveCount(0); await expect.poll(async () => activeRun(page, researcher.id), { timeout: 60_000 }).toBeNull(); // The answer resumed the specialist in its own thread; its reply arrives as unread. const reply = inbox.locator("[data-kind='unread']").filter({ hasText: "Researcher" }); await expect(reply).toContainText("Paris", { timeout: 30_000 }); await captureScreenshot(page, testInfo, "inbox-unread-reply"); await reply.getByRole("button").click(); await page.waitForURL(new RegExp(`/app/${researcher.id}$`)); await expect(page.getByText("Answered: Paris", { exact: true })).toBeVisible({ timeout: 30_000 }); }); async function createBot(page: Page, name: string) { return rpc<{ id: string }>(page, "bots/create", { name, title: "", description: "", notifyOnFinish: true, computerMode: "team", }); } async function activeRun(page: Page, botId: string) { const snapshot = await rpc<{ run?: { status: string } | null }>(page, "threads/get", { botId }); return snapshot.run ?? null; } /** * Opens the user's main conversation. Until the app sets one up, a group with the specialist * stands in for it and is reported to the client as the main conversation. */ async function openMainConversation(page: Page, specialistId: string) { // The shell's first load is where the app sets up the main conversation. await rpc(page, "bootstrap", {}); const groups = await rpc(page, "groups/list", {}); let main = groups.find((group) => group.kind === "main"); if (!main) { const lead = await createBot(page, "Lead"); const stand = await rpc(page, "groups/create", { name: "Main", botIds: [lead.id, specialistId], }); const asMain = (list: unknown) => Array.isArray(list) ? list.map((group) => (group?.id === stand.id ? { ...group, kind: "main" } : group)) : list; await routePatchRpc(page, "bootstrap", (json) => ({ ...json, groups: asMain(json.groups) })); await routePatchRpc(page, "spaces/list", (json) => { const current = json.current as { groups?: unknown } | undefined; return current ? { ...json, current: { ...current, groups: asMain(current.groups) } } : json; }); main = stand; } await page.goto(`/app/g/${main.id}`, { waitUntil: "domcontentloaded" }); await waitForChatComposer(page); }