evOo-local/apps/web/e2e/spaces.spec.ts
cynarAI 6ace14c3c8
Projects: one sidebar row with icon and color, coordinator overview (#190)
Each project is now a single sidebar row with its own icon and color that
opens the project's coordinator conversation. The whole row drags to
reorder. The row menu gains Customize for name, icon and color. Inside
the coordinator conversation, an overview lists the project's agent
threads that need the user, are running, or recently finished.


Claude-Session: https://claude.ai/code/session_01Tgvrj34NMnaqvtgvruYQZZ

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-10 03:32:40 +02:00

248 lines
12 KiB
TypeScript

import { expect, test } from "@playwright/test";
import {
captureScreenshot,
completeOnboarding,
openNewSpace,
projectRow,
routePatchRpc,
rpc,
signup,
unterhaltungenOnly,
} from "./helpers";
test("Projects + opens new project dialog instead of group form", async ({ page }, testInfo) => {
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/);
// The new project opens its conversation with the project's main agent.
await page.waitForURL(/\/app\/g\/[^/]+$/, { timeout: 30_000 });
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(projectRow(projekte, "Customer project")).toBeVisible();
await expect(page.getByTestId("project-start-main")).toHaveCount(0);
await captureScreenshot(page, testInfo, "new-project-main-conversation");
});
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<string, unknown>), 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 = projectRow(projekte, "Customer support");
const supportSpaceId = (await supportSpace.getAttribute("data-testid"))?.replace(
/^project-row-/,
"",
);
expect(supportSpaceId).toBeTruthy();
// A project created from chat comes with its own main agent, reached through its row.
await supportSpace.click();
await page.waitForURL(/\/app\/g\/[^/]+$/);
await expect
.poll(() => page.evaluate(() => window.localStorage.getItem("rakazo:space-id")))
.toBe(supportSpaceId);
// The home 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: "Start", exact: true })).toHaveCount(0);
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 its main conversation 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 in the conversation with its
// own main agent, its only bot.
await page.waitForURL(/\/app\/g\/[^/]+$/, { timeout: 30_000 });
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(projectRow(projekte, "Temporary")).toBeVisible();
await expect(projekte.getByRole("button", { name: "Start", exact: true })).toHaveCount(0);
expect(await rpc<Array<{ kind: string }>>(page, "bots/list", {})).toEqual([
expect.objectContaining({ kind: "main" }),
]);
await captureScreenshot(page, testInfo, "new-space-main-conversation");
// Deletion is discoverable through the visible Space actions menu.
await projectRow(projekte, "Temporary").hover();
await projekte.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/);
// The project's main agent went with it.
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\/g\/[^/]+$/, { timeout: 30_000 });
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/);
// The project's main conversation stays, so the project opens there.
await page.waitForURL(/\/app\/g\/[^/]+$/, { timeout: 30_000 });
await expect(projectRow(projekte, "Side")).toBeVisible();
await projectRow(projekte, "Side").hover();
await projekte.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();
});