* 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>
320 lines
12 KiB
TypeScript
320 lines
12 KiB
TypeScript
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");
|
||
});
|