evOo-local/apps/web/e2e/bot-organization.spec.ts
Claude f3e438444a
Put voice in the composer's send slot until there is something to send
An empty draft shows the mic where the send arrow sits; it opens the
voice conversation as the separate Voice button did. As soon as the draft
holds text, a chip or an attachment, the slot turns back into Send. This
frees the width the extra Voice pill took on small screens.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XcoERsCTbvjqDzE8qC82Wo
2026-10-05 08:52:06 +00:00

316 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");
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");
});