evOo-local/apps/web/e2e/bot-organization.spec.ts
cynarAI 0e1036b2ff
First-run onboarding UX: naming, locale, voices, Heute route, mobile drawer (#171)
* Fix first-run onboarding UX: naming, locale, voices, Heute route, mobile drawer

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Fix biome import order and formatting

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Fix biome formatting on OoStage and E2E specs

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Fix E2E specs for onboarding name step and Heute reload route

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Fix German onboarding heading matcher in E2E

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Fix German signup E2E, Escape menu stacking, and Heute rhythm click

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Make onboarding E2E helpers locale-aware for German UI

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Fix completeOnboarding language skip for Skip · locale label

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Use language-step Skip · locale button in completeOnboarding

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Fix German E2E: Nachricht composer, voice loading wait, Heute rhythm

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Stabilize Heute reload URL and German voice E2E assertions

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Fix Heute bootstrap redirect and German onboarding voices

When Kokoro lists no locale-matched voices, onboarding now uses offline
presets instead of showing English-only options. Shell bootstrap skips
bot redirects while the user is on /app/heute.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Relax German voice E2E name matching for voice labels

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Harden Heute route and German voice onboarding E2E

Rebased onto main with Expo SDK 57 typecheck fixes. Voice options expose
stable test ids; Shell keeps /app/heute across bootstrap and redirects
legacy /app/heute bot URLs. Heute coach E2E asserts URL after reload.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Format Shell after Heute route helper

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-06 20:15:36 +02:00

320 lines
12 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { expect, test } from "@playwright/test";
import {
activeBotId,
captureScreenshot,
completeOnboarding,
createNamedBot,
rpc,
signup,
} from "./helpers";
test("pinned bots and sidebar sections persist", async ({ page }, testInfo) => {
const stamp = Date.now();
await signup(page, `bot-organize-${stamp}@rakazo.test`, "password12", "Test User");
await completeOnboarding(page);
// The main bot is reached through the main conversation; specialists are organized here.
await createNamedBot(page, "Scout");
const sidebar = page.locator("aside").first();
const bot = sidebar.getByRole("button", { name: /^Scout/ });
await bot.click({ button: "right" });
await page.getByRole("menuitem", { name: "Pin", exact: true }).click();
await expect(sidebar.locator('[data-sidebar-group="pinned"]')).toContainText("Scout");
await captureScreenshot(page, testInfo, "pinned-bots");
await bot.click({ button: "right" });
await page.getByRole("menuitem", { name: "Unpin", exact: true }).click();
await expect(sidebar.locator('[data-sidebar-group="pinned"]')).toHaveCount(0);
await bot.click({ button: "right" });
await page.getByRole("menuitem", { name: "Move to folder", exact: true }).hover();
const moveMenu = page.getByRole("menu", { name: "Move to folder", exact: true });
await expect(moveMenu).toBeVisible();
await captureScreenshot(page, testInfo, "move-to-section-menu");
await moveMenu.getByText("New folder").click();
const dialog = page.getByRole("dialog", { name: "New section" });
await dialog.getByLabel("Name").fill("Projects");
await dialog.getByRole("button", { name: "Create" }).click();
const projects = sidebar.locator('[data-sidebar-group^="section:"]');
await expect(projects).toContainText("Projects");
await expect(projects).toContainText("Scout");
await captureScreenshot(page, testInfo, "bot-sections");
await page.reload();
await expect(projects).toContainText("Projects");
await expect(projects).toContainText("Scout");
await bot.click({ button: "right" });
await page.getByRole("menuitem", { name: "Move to folder", exact: true }).hover();
await page
.getByRole("menu", { name: "Move to folder", exact: true })
.getByRole("menuitem", { name: "Rename folder", exact: true })
.click();
const renameDialog = page.getByRole("dialog", { name: "Rename section" });
await renameDialog.getByLabel("Name").fill("Delivery");
await captureScreenshot(page, testInfo, "rename-bot-section");
await renameDialog.getByRole("button", { name: "Save" }).click();
await expect(projects).toContainText("Delivery");
await expect(projects).not.toContainText("Projects");
await bot.click({ button: "right" });
await page.getByRole("menuitem", { name: "Move to folder", exact: true }).hover();
await page
.getByRole("menu", { name: "Move to folder", exact: true })
.getByRole("menuitem", { name: "Unassigned", exact: true })
.click();
await expect(sidebar.locator('[data-sidebar-group="unassigned"]')).toContainText("Scout");
});
test("bots can be reordered by drag or keyboard and keep that order", async ({ page }) => {
const stamp = Date.now();
await signup(page, `bot-reorder-${stamp}@rakazo.test`, "password12", "Bot Order");
await completeOnboarding(page);
// The roster lists specialists; the main bot is reached through the main conversation.
const leadId = (
await rpc<{ id: string }>(page, "bots/create", {
name: "Lead",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
computerMode: "team",
})
).id;
const alpha = await rpc<{ id: string }>(page, "bots/create", {
name: "Alpha",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
computerMode: "team",
});
const beta = await rpc<{ id: string }>(page, "bots/create", {
name: "Beta",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
computerMode: "team",
});
await page.reload();
const sidebar = page.locator("aside").first();
const rows = sidebar.locator("[data-roster-bot-id]");
const order = () =>
rows.evaluateAll((items) => items.map((item) => item.getAttribute("data-roster-bot-id")));
await expect.poll(order).toEqual([leadId, alpha.id, beta.id]);
let releaseStaleList!: () => void;
let markStaleListReady!: () => void;
let markStaleListDelivered!: () => void;
const staleListReady = new Promise<void>((resolve) => {
markStaleListReady = resolve;
});
const staleListDelivered = new Promise<void>((resolve) => {
markStaleListDelivered = resolve;
});
const staleListGate = new Promise<void>((resolve) => {
releaseStaleList = resolve;
});
let interceptedList = false;
await page.route("**/rpc/spaces/list", async (route) => {
if (interceptedList) {
await route.continue();
return;
}
interceptedList = true;
const staleResponse = await route.fetch();
markStaleListReady();
await staleListGate;
await route.fulfill({ response: staleResponse });
markStaleListDelivered();
});
await page.evaluate(() => window.dispatchEvent(new Event("focus")));
await staleListReady;
const reorderSaved = page.waitForResponse(
(response) => response.url().includes("/rpc/bots/reorder") && response.ok(),
);
await sidebar
.locator(`[data-roster-bot-id="${beta.id}"]`)
.dragTo(sidebar.locator(`[data-roster-bot-id="${leadId}"]`));
await reorderSaved;
releaseStaleList();
await staleListDelivered;
await expect.poll(order).toEqual([beta.id, leadId, alpha.id]);
await page.reload();
await expect.poll(order).toEqual([beta.id, leadId, alpha.id]);
const betaRow = sidebar.locator(`[data-roster-bot-id="${beta.id}"]`);
await betaRow.focus();
await page.keyboard.press("Alt+ArrowDown");
await expect.poll(order).toEqual([leadId, beta.id, alpha.id]);
await page.reload();
await expect.poll(order).toEqual([leadId, beta.id, alpha.id]);
let releaseRejectedReorder!: () => void;
let markRejectedReorderStarted!: () => void;
const rejectedReorderStarted = new Promise<void>((resolve) => {
markRejectedReorderStarted = resolve;
});
const rejectReorder = new Promise<void>((resolve) => {
releaseRejectedReorder = resolve;
});
await page.route(
"**/rpc/bots/reorder",
async (route) => {
markRejectedReorderStarted();
await rejectReorder;
await route.fulfill({ status: 500, contentType: "application/json", body: "{}" });
},
{ times: 1 },
);
await sidebar
.locator(`[data-roster-bot-id="${alpha.id}"]`)
.dragTo(sidebar.locator(`[data-roster-bot-id="${leadId}"]`));
await rejectedReorderStarted;
const queuedReorderSaved = page.waitForResponse(
(response) => response.url().includes("/rpc/bots/reorder") && response.ok(),
);
await sidebar
.locator(`[data-roster-bot-id="${beta.id}"]`)
.dragTo(sidebar.locator(`[data-roster-bot-id="${alpha.id}"]`));
releaseRejectedReorder();
await queuedReorderSaved;
await expect.poll(order).toEqual([beta.id, alpha.id, leadId]);
await page.reload();
await expect.poll(order).toEqual([beta.id, alpha.id, leadId]);
});
test("chat composer controls are vertically centered", async ({ page }) => {
const stamp = Date.now();
await signup(page, `composer-layout-${stamp}@rakazo.test`, "password12", "Composer Layout");
await completeOnboarding(page);
const bar = page.getByTestId("composer-bar");
const centersOf = (labels: string[]) =>
bar.evaluate(
(composer, controls) =>
controls.map((label) => {
const element = composer.querySelector<HTMLElement>(`[aria-label="${label}"]`);
if (!element) throw new Error(`Missing composer control: ${label}`);
const box = element.getBoundingClientRect();
return box.top + box.height / 2;
}),
labels,
);
const idle = await centersOf(["Add", "Message", "Voice"]);
await page.getByRole("combobox", { name: "Message" }).fill("hello");
const typing = await centersOf(["Add", "Message", "Send"]);
const centers = [...idle, ...typing];
expect(Math.max(...centers) - Math.min(...centers)).toBeLessThanOrEqual(1);
});
test("group chats share every context-menu action", async ({ page }, testInfo) => {
const stamp = Date.now();
await signup(page, `group-organize-${stamp}@rakazo.test`, "password12", "Group Menu");
await completeOnboarding(page);
await page.goto("/app");
await page.waitForURL(/\/app\/[^/]+$/);
const chiefId = activeBotId(page);
const partner = await rpc<{ id: string }>(page, "bots/create", {
name: "Partner",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
computerMode: "team",
});
await rpc(page, "groups/create", {
name: "Group menu",
botIds: [chiefId, partner.id],
});
await page.reload();
const sidebar = page.locator("aside").first();
const group = sidebar.getByRole("button", { name: /^Group menu/ }).first();
await group.click({ button: "right" });
for (const action of [
"Pin",
"Move to folder",
"Mark as Unread",
"Edit Profile",
"Duplicate",
"Clear conversation",
"Archive",
"Delete",
]) {
await expect(page.getByRole("menuitem", { name: action, exact: true })).toBeVisible();
}
await captureScreenshot(page, testInfo, "group-context-menu-desktop");
await page.getByRole("menuitem", { name: "Mark as Unread", exact: true }).click();
await group.click({ button: "right" });
await page.getByRole("menuitem", { name: "Mark as Read", exact: true }).click();
await group.click({ button: "right" });
await page.getByRole("menuitem", { name: "Pin", exact: true }).click();
await expect(sidebar.locator('[data-sidebar-group="pinned"]')).toContainText("Group menu");
await group.click({ button: "right" });
await page.getByRole("menuitem", { name: "Unpin", exact: true }).click();
await group.click({ button: "right" });
await page.getByRole("menuitem", { name: "Move to folder", exact: true }).hover();
await page
.getByRole("menu", { name: "Move to folder", exact: true })
.getByRole("menuitem", { name: "New folder", exact: true })
.click();
const sectionDialog = page.getByRole("dialog", { name: "New section" });
await sectionDialog.getByLabel("Name").fill("Teams");
await sectionDialog.getByRole("button", { name: "Create" }).click();
await expect(sidebar.locator('[data-sidebar-group^="section:"]')).toContainText("Group menu");
await group.click({ button: "right" });
await page.getByRole("menuitem", { name: "Clear conversation", exact: true }).click();
await expect(
page.getByRole("alertdialog", { name: "Clear Group menu’s conversation?" }),
).toBeVisible();
await captureScreenshot(page, testInfo, "group-clear-conversation-confirmation");
await page.getByRole("button", { name: "Clear", exact: true }).click();
await expect(
page.getByRole("alertdialog", { name: "Clear Group menu’s conversation?" }),
).toHaveCount(0);
await group.click({ button: "right" });
await page.getByRole("menuitem", { name: "Duplicate", exact: true }).click();
const copy = sidebar.getByRole("button", { name: /^Group menu copy/ });
await expect(copy).toBeVisible();
await copy.click({ button: "right" });
await page.getByRole("menuitem", { name: "Delete", exact: true }).click();
await expect(page.getByRole("alertdialog", { name: "Delete Group menu copy?" })).toBeVisible();
await page.getByRole("button", { name: "Cancel" }).click();
await copy.click({ button: "right" });
await page.getByRole("menuitem", { name: "Archive", exact: true }).click();
await expect(sidebar.getByRole("button", { name: /^Group menu copy/ })).toHaveCount(0);
await expect(sidebar.getByText("Archived", { exact: true })).toBeVisible();
await page.setViewportSize({ width: 390, height: 844 });
await page.getByRole("button", { name: "Open navigation" }).click();
await group.click({ button: "right" });
await captureScreenshot(page, testInfo, "group-context-menu-mobile");
await page.keyboard.press("Escape");
if ((await page.getByRole("menu").count()) > 0) {
await page.keyboard.press("Escape");
}
await expect(page.getByRole("menu")).toHaveCount(0);
await page.setViewportSize({ width: 1280, height: 800 });
await group.click({ button: "right" });
await page.getByRole("menuitem", { name: "Edit Profile", exact: true }).click();
await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "group-settings");
});