import { expect, type Locator, test } from "@playwright/test"; import { captureScreenshot, completeOnboarding, openNewSpace, routePatchRpc, rpc, signup, unterhaltungenOnly, } from "./helpers"; function projectSpace(projekte: Locator, name: string) { return projekte.locator('[data-sidebar-group^="project-space:"]').filter({ hasText: name, exact: true, }); } test("Projekte + opens new project dialog instead of group form", async ({ page }) => { const stamp = Date.now(); await signup(page, `proj-create-${stamp}@rakazo.test`, "password12", "Project Create"); await completeOnboarding(page); await page.goto("/app"); await page.waitForURL(/\/app\/[^/]+$/); const projekte = page.locator(".sidebar-projekte"); await projekte.getByTestId("create-project-trigger").click(); await expect(page.getByRole("dialog", { name: "New project" })).toBeVisible(); await expect(page.getByRole("button", { name: "Create project", exact: true })).toBeVisible(); await expect(page.getByTestId("side-panel")).not.toHaveAttribute("data-panel", "create-group"); await expect(page.getByText("New group", { exact: true })).toHaveCount(0); const dialog = page.getByRole("dialog", { name: "New project" }); await dialog.getByLabel("Name").fill("Customer project"); await dialog.getByRole("button", { name: "Create project", exact: true }).click(); await expect(page).not.toHaveURL(/\/onboarding/); await page.waitForURL(/\/app/); await page.waitForLoadState("networkidle"); await expect(projekte.getByText("Customer project", { exact: true })).toBeVisible({ timeout: 30_000, }); await expect(unterhaltungenOnly(page).getByText("Customer project", { exact: true })).toHaveCount( 0, ); await expect(page.getByTestId("project-start-main")).toBeVisible(); await expect(page.getByTestId("project-start-main")).toHaveText(/Starten/); }); for (const owner of [true, false]) { const role = owner ? "owner" : "member"; const title = owner ? "new project dialog offers a host folder to the deployment owner" : "new project dialog offers no host folder to a member"; test(title, async ({ page }, testInfo) => { await signup(page, `proj-folder-${role}-${Date.now()}@rakazo.test`, "password12", "Folder"); await completeOnboarding(page); // Whoever signs up first owns the deployment, which depends on spec order. await routePatchRpc(page, "bootstrap", (json) => ({ ...json, me: { ...(json.me as Record), isDeploymentOwner: owner }, })); await page.goto("/app"); await page.waitForURL(/\/app\/[^/]+$/); await page.locator(".sidebar-projekte").getByTestId("create-project-trigger").click(); const dialog = page.getByRole("dialog", { name: "New project" }); await expect(dialog.getByLabel("Name")).toBeVisible(); await expect(dialog.getByLabel("Project folder")).toHaveCount(owner ? 1 : 0); await captureScreenshot(page, testInfo, `new-project-${role}`); }); } test("spaces stay invisible by default and chat creation requires approval", async ({ page, }, testInfo) => { const stamp = Date.now(); await signup(page, `spaces-${stamp}@rakazo.test`, "password12", "Space Owner"); await completeOnboarding(page); const sidebar = page.locator("aside").first(); const personalSpaceId = await page.evaluate(() => window.localStorage.getItem("rakazo:space-id")); expect(personalSpaceId).toBeTruthy(); await expect(sidebar.getByText("Personal", { exact: true })).toHaveCount(0); await expect(sidebar.getByRole("button", { name: /^Chief/ })).toHaveCount(1); await captureScreenshot(page, testInfo, "single-space-sidebar"); await openNewSpace(page); const dialog = page.getByRole("dialog", { name: "New space" }); await expect(dialog.getByLabel("Name")).toBeVisible(); await dialog.getByLabel("Name").fill("Customer support"); await captureScreenshot(page, testInfo, "new-space-dialog"); await dialog.getByRole("button", { name: "Cancel" }).click(); const composer = page.getByRole("combobox", { name: "Message" }); await composer.fill("Create a space named Customer support"); await composer.press("Enter"); await expect(page.getByRole("button", { name: "Create space", exact: true })).toBeVisible({ timeout: 15_000, }); await expect(page.getByRole("button", { name: "Cancel", exact: true })).toBeVisible(); await expect(page.getByRole("button", { name: "Always allow this tool" })).toHaveCount(0); await expect(sidebar.getByText("Customer support", { exact: true })).toHaveCount(0); await captureScreenshot(page, testInfo, "create-space-chat-approval"); await page.getByRole("button", { name: "Create space", exact: true }).click(); await expect(page.getByText("Created", { exact: true })).toBeVisible(); const projekte = page.locator(".sidebar-projekte"); await expect(projekte.getByText("Customer support", { exact: true })).toBeVisible({ timeout: 15_000, }); await expect(unterhaltungenOnly(page).getByText("Customer support", { exact: true })).toHaveCount( 0, ); await expect(unterhaltungenOnly(page).getByRole("button", { name: /^Chief/ })).toHaveCount(1); const supportSpace = projekte.locator('[data-sidebar-group^="project-space:"]').filter({ hasText: "Customer support", }); const supportSpaceGroup = await supportSpace.getAttribute("data-sidebar-group"); const supportSpaceId = supportSpaceGroup?.match(/^project-space:([^:]+)/)?.[1]; expect(supportSpaceId).toBeTruthy(); await supportSpace.getByRole("button", { name: "Select Customer support" }).click(); await page.waitForURL(/\/app(?:\/)?$/); await expect .poll(() => page.evaluate(() => window.localStorage.getItem("rakazo:space-id"))) .toBe(supportSpaceId); // The main conversation stays the first entry while another space is open. await expect(projekte.getByText("Customer support", { exact: true })).toBeVisible(); await expect(unterhaltungenOnly(page).getByRole("button", { name: /^Chief/ })).toHaveCount(1); await expect(projekte.getByRole("button", { name: /^Chief/ })).toHaveCount(0); await expect(projekte.getByRole("button", { name: "Starten", exact: true })).toBeVisible(); await captureScreenshot(page, testInfo, "spaces-sidebar"); await page.evaluate((id) => { window.localStorage.setItem("rakazo:space-id", id!); }, personalSpaceId); await page.reload(); await page.waitForURL(/\/app\/[^/]+$/); await expect .poll(() => page.evaluate(() => window.localStorage.getItem("rakazo:space-id"))) .toBe(personalSpaceId); await expect(projekte.getByText("Customer support", { exact: true })).toBeVisible(); }); test("a new space opens empty and can be deleted from its menu", async ({ page }, testInfo) => { const stamp = Date.now(); await signup(page, `spaces-delete-${stamp}@rakazo.test`, "password12", "Space Owner"); await completeOnboarding(page); const sidebar = page.locator("aside").first(); await openNewSpace(page); const dialog = page.getByRole("dialog", { name: "New space" }); await dialog.getByLabel("Name").fill("Temporary"); await dialog.getByRole("button", { name: "Create space", exact: true }).click(); // Onboarding runs once per user: a new space opens directly, with no bot of its own. await page.waitForURL(/\/app(?:\/)?$/); const projekte = page.locator(".sidebar-projekte"); await expect(projekte.getByText("Temporary", { exact: true })).toBeVisible(); await expect(unterhaltungenOnly(page).getByRole("button", { name: /^Chief/ })).toHaveCount(1); await expect(projekte.getByRole("button", { name: /^Chief/ })).toHaveCount(0); await expect(projekte.getByRole("button", { name: "Starten", exact: true })).toBeVisible(); expect(await rpc(page, "bots/list", {})).toEqual([]); await captureScreenshot(page, testInfo, "new-space-empty"); // The empty space stays selectable instead of trapping the user. await expect(projekte.getByRole("button", { name: "Select Temporary" }).first()).toBeVisible(); // Deletion is discoverable through the visible Space actions menu. await projectSpace(projekte, "Temporary") .getByRole("button", { name: "Actions for Temporary" }) .click(); await captureScreenshot(page, testInfo, "space-actions-menu"); await expect(page.getByRole("menuitem", { name: "Delete space" })).toBeVisible(); await page.getByRole("menuitem", { name: "Delete space" }).click(); const deleteSpaceDialog = page.getByRole("alertdialog", { name: "Delete Temporary?" }); await expect(deleteSpaceDialog).toBeVisible(); await captureScreenshot(page, testInfo, "delete-space-dialog"); await deleteSpaceDialog.getByRole("button", { name: "Delete", exact: true }).click(); await expect(sidebar.getByText("Temporary", { exact: true })).toHaveCount(0); await expect(page).not.toHaveURL(/\/onboarding/); await expect(sidebar.getByRole("button", { name: /^Chief/ })).toHaveCount(1); // A stale onboarding URL recovers without duplicating the bot. await page.goto("/onboarding"); await page.waitForURL(/\/app/); await expect(page).not.toHaveURL(/\/onboarding/); await expect(sidebar.getByRole("button", { name: /^Chief/ })).toHaveCount(1); }); test("deleting the last bot in a space stays in the app after first use", async ({ page }) => { const stamp = Date.now(); await signup(page, `spaces-empty-${stamp}@rakazo.test`, "password12", "Space Owner"); await completeOnboarding(page); const sidebar = page.locator("aside").first(); // The main bot cannot be archived, so the archived section comes from a specialist. const helper = await rpc<{ id: string }>(page, "bots/create", { name: "Helper", title: "", description: "", notifyOnFinish: true, computerMode: "team", }); await rpc(page, "bots/archive", { botId: helper.id }); await page.reload({ waitUntil: "domcontentloaded" }); await expect(sidebar.getByRole("button", { name: /^Archived/ })).toBeVisible({ timeout: 30_000, }); await openNewSpace(page); const dialog = page.getByRole("dialog", { name: "New space" }); await dialog.getByLabel("Name").fill("Side"); await dialog.getByRole("button", { name: "Create space", exact: true }).click(); await page.waitForURL(/\/app(?:\/)?$/); const projekte = page.locator(".sidebar-projekte"); await expect(projekte.getByText("Side", { exact: true })).toBeVisible(); // Delete the only bot in the new space: the app must stay put instead of // forcing onboarding again. const sideBot = await rpc<{ id: string }>(page, "bots/create", { name: "Side bot", title: "", description: "", notifyOnFinish: true, computerMode: "team", }); await rpc(page, "bots/remove", { botId: sideBot.id, deleteMemories: false }); await expect(page).not.toHaveURL(/\/onboarding/); await page.goto("/app"); await expect(page).not.toHaveURL(/\/onboarding/); await expect(projekte.getByRole("button", { name: "Select Side" }).first()).toBeVisible(); await expect(page.getByTestId("project-start-main")).toBeVisible(); await projectSpace(projekte, "Side").getByRole("button", { name: "Actions for Side" }).click(); await expect(page.getByRole("menuitem", { name: "Delete space" })).toBeVisible(); await page.getByRole("menuitem", { name: "Delete space" }).click(); await page .getByRole("alertdialog", { name: "Delete Side?" }) .getByRole("button", { name: "Delete", exact: true }) .click(); await expect(sidebar.getByText("Side", { exact: true })).toHaveCount(0); const archivedToggle = sidebar.getByRole("button", { name: /^Archived/ }); await expect(archivedToggle).toBeVisible({ timeout: 30_000 }); await archivedToggle.click(); await expect(sidebar.getByText("Helper", { exact: true })).toBeVisible(); });