The consent-first scan replaces the automatic project creation that main fixed in #116; main's git checkout fix and sidebar classes are kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
248 lines
12 KiB
TypeScript
248 lines
12 KiB
TypeScript
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("Projects + 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(/Start/);
|
|
});
|
|
|
|
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 = 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: "Start", 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: "Start", exact: true })).toBeVisible();
|
|
expect(await rpc<unknown[]>(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();
|
|
});
|