The Atelier replaces Avatar Studio: pick a species, colour, eyes, glasses, hats, neckwear, arms and temperament, with every option drawn as your own Oo wearing it. Onboarding's "Design your Oo" now offers species looks per vibe and opens the Atelier for the rest; mobile bot settings carry the same editor instead of a bare colour row, so picking a colour there no longer throws away the rest of the look. Claude-Session: https://claude.ai/code/session_01EKuME5CgL1ddGGN7LGvtBj Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
245 lines
12 KiB
TypeScript
245 lines
12 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import {
|
|
captureScreenshot,
|
|
completeOnboarding,
|
|
createBotFromPicker,
|
|
openNewBot,
|
|
signup,
|
|
} from "./helpers";
|
|
|
|
test("bot creation, editing, and deletion persist", async ({ page }, testInfo) => {
|
|
const stamp = Date.now();
|
|
await signup(page, `bot-crud-${stamp}@rakazo.test`, "password12", "Bot CRUD");
|
|
await completeOnboarding(page);
|
|
await page.goto("/app");
|
|
await page.waitForURL(/\/app\/[^/]+$/);
|
|
|
|
const botList = page.locator("aside").first();
|
|
await expect(botList.getByRole("button", { name: /^Chief/ })).toBeVisible();
|
|
|
|
let createFailed = false;
|
|
let resolveCreateAborted!: () => void;
|
|
const createAborted = new Promise<void>((resolve) => {
|
|
resolveCreateAborted = resolve;
|
|
});
|
|
await page.route("**/rpc/bots/create", async (route) => {
|
|
createFailed = true;
|
|
await route.abort("failed");
|
|
resolveCreateAborted();
|
|
});
|
|
await openNewBot(page);
|
|
const form = page.getByTestId("create-bot-form");
|
|
await form.locator("label:has-text('Name') input").fill("New Bot");
|
|
await form.getByRole("button", { name: "Create", exact: true }).click();
|
|
await createAborted;
|
|
// Failed create keeps the form open on the current bot chat.
|
|
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
|
|
await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "create");
|
|
await expect(page.getByTestId("create-bot-error")).toBeVisible();
|
|
expect(createFailed).toBe(true);
|
|
await page.unroute("**/rpc/bots/create");
|
|
await page.getByRole("button", { name: "Cancel new bot" }).click();
|
|
await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "closed");
|
|
|
|
let failedPostCreateRefresh = false;
|
|
await page.route("**/rpc/spaces/list", async (route) => {
|
|
if (failedPostCreateRefresh) {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
failedPostCreateRefresh = true;
|
|
await route.abort("failed");
|
|
});
|
|
await createBotFromPicker(page);
|
|
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
|
|
await expect.poll(() => failedPostCreateRefresh).toBe(true);
|
|
await page.unroute("**/rpc/spaces/list");
|
|
await expect(botList.getByRole("button", { name: /^New Bot/ })).toBeVisible();
|
|
await page.waitForURL(/\/app\/[^/]+$/);
|
|
const deletedBotPath = new URL(page.url()).pathname;
|
|
await page.evaluate(() => window.dispatchEvent(new Event("focus")));
|
|
await page.waitForTimeout(500);
|
|
expect(new URL(page.url()).pathname).toBe(deletedBotPath);
|
|
await captureScreenshot(page, testInfo, "27-created-bot");
|
|
|
|
await page.locator("main").getByRole("button", { name: "New Bot", exact: true }).click();
|
|
await expect(page.getByText("Settings", { exact: true })).toBeVisible();
|
|
const nameInput = page.locator("label:has-text('Name') input");
|
|
const titleInput = page.locator("label:has-text('Title') input");
|
|
const descriptionInput = page.locator("label:has-text('Description') textarea");
|
|
const longTitle = `Market researcher ${"and source verifier ".repeat(9)}`;
|
|
const normalizedLongTitle = longTitle.trim();
|
|
expect(longTitle.length).toBeGreaterThan(160);
|
|
await nameInput.fill("Researcher");
|
|
await titleInput.fill(longTitle);
|
|
await descriptionInput.fill("Finds reliable sources and turns them into concise briefs.");
|
|
await page.getByTestId("oo-atelier-trigger").click();
|
|
const studio = page.getByTestId("oo-atelier");
|
|
await expect(studio).toBeVisible();
|
|
await expect(studio.getByTestId("oo-atelier-body")).toBeVisible();
|
|
await studio.getByRole("button", { name: "Color #EAB308" }).click();
|
|
await studio.getByRole("button", { name: "Done", exact: true }).click();
|
|
await expect(studio).toBeHidden();
|
|
await page.getByRole("button", { name: "Save", exact: true }).click();
|
|
await expect(botList.getByRole("button", { name: /^Researcher/ })).toBeVisible();
|
|
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
|
|
|
|
await page.locator("main").getByRole("button", { name: "Researcher", exact: true }).click();
|
|
await expect(nameInput).toHaveValue("Researcher");
|
|
await expect(titleInput).toHaveValue(normalizedLongTitle);
|
|
await expect(descriptionInput).toHaveValue(
|
|
"Finds reliable sources and turns them into concise briefs.",
|
|
);
|
|
const settings = page.getByTestId("bot-settings");
|
|
await settings.getByTestId("oo-atelier-trigger").click();
|
|
await expect(studio).toBeVisible();
|
|
await expect(studio.getByRole("button", { name: "Color #EAB308" })).toHaveClass(/ring-2/);
|
|
await studio.getByRole("button", { name: "Done", exact: true }).click();
|
|
const modelSelect = settings.locator("label:has-text('Model') select");
|
|
const teamComputer = settings.getByRole("button", { name: "Team" });
|
|
const openWork = settings.getByTestId("bot-scratchpad");
|
|
await expect(teamComputer).toBeHidden();
|
|
await expect(modelSelect).toBeHidden();
|
|
await expect(openWork).toBeHidden();
|
|
await expect(settings.getByRole("button", { name: "Save", exact: true })).toBeVisible();
|
|
await expect(settings.getByRole("button", { name: "Recover computer" })).toHaveCount(0);
|
|
await expect(settings.getByRole("button", { name: "Reset computer" })).toHaveCount(0);
|
|
await expect(settings.getByRole("button", { name: "Update computer" })).toHaveCount(0);
|
|
await captureScreenshot(page, testInfo, "27a-settings-panel");
|
|
await settings.getByText("Advanced", { exact: true }).click();
|
|
await expect(teamComputer).toBeVisible();
|
|
await expect(openWork).toBeVisible();
|
|
await expect(modelSelect).toBeVisible();
|
|
await expect(modelSelect).toContainText("Space default");
|
|
await captureScreenshot(page, testInfo, "27a-bot-settings-model");
|
|
await page.getByRole("button", { name: "Show computer" }).click();
|
|
const sidePanel = page.getByTestId("side-panel");
|
|
await expect(sidePanel).toHaveAttribute("data-panel", "computer");
|
|
await expect(page.getByRole("button", { name: "Show settings" })).toBeVisible();
|
|
// Overlay may flash during boot or never appear (already ready/asleep/stopped). Assert panel
|
|
// chrome, then wait until any overlay has cleared — avoid Locator.or() strict-mode multi-hits.
|
|
const bootOverlay = page.getByText(/Booting up .* computer/);
|
|
await expect(sidePanel.getByTestId("computer-preview")).toBeVisible();
|
|
await expect(bootOverlay).toBeHidden();
|
|
await expect(sidePanel.getByText("Teach a task")).toHaveCount(0);
|
|
await expect(sidePanel.getByTestId("teach-start-button")).toHaveCount(0);
|
|
await expect(sidePanel.getByRole("button", { name: "Recover computer" })).toHaveCount(0);
|
|
await expect(sidePanel.getByRole("button", { name: "Reset computer" })).toHaveCount(0);
|
|
await expect(sidePanel.getByRole("button", { name: "Update computer" })).toHaveCount(0);
|
|
await expect(sidePanel.getByRole("button", { name: "Take control" })).toHaveCount(0);
|
|
await expect(sidePanel.getByTestId("computer-more-button")).toHaveCount(0);
|
|
await captureScreenshot(page, testInfo, "27b-computer-panel");
|
|
await page.getByRole("button", { name: "Show settings" }).click();
|
|
|
|
await nameInput.fill("Atlas");
|
|
await titleInput.fill("Research lead");
|
|
await descriptionInput.fill("Builds durable, source-backed research briefs.");
|
|
await page.getByRole("button", { name: "Save", exact: true }).click();
|
|
await expect(botList.getByRole("button", { name: /^Atlas/ })).toBeVisible();
|
|
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
|
|
await captureScreenshot(page, testInfo, "28-edited-bot-profile");
|
|
|
|
await page.reload();
|
|
await expect(botList.getByRole("button", { name: /^Atlas/ })).toBeVisible();
|
|
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
|
|
await page.locator("main").getByRole("button", { name: "Atlas", exact: true }).click();
|
|
await expect(nameInput).toHaveValue("Atlas");
|
|
await expect(titleInput).toHaveValue("Research lead");
|
|
await expect(descriptionInput).toHaveValue("Builds durable, source-backed research briefs.");
|
|
await captureScreenshot(page, testInfo, "29-reloaded-bot-profile");
|
|
|
|
// Wrap in the narrow dialog without pushing Delete below the Playwright viewport.
|
|
const longUnbrokenName = "A".repeat(48);
|
|
expect(longUnbrokenName.length).toBe(48);
|
|
await nameInput.fill(longUnbrokenName);
|
|
await page.getByRole("button", { name: "Save", exact: true }).click();
|
|
const longNameBot = botList.getByRole("button", {
|
|
name: new RegExp(`^${longUnbrokenName}`),
|
|
});
|
|
await expect(longNameBot).toBeVisible();
|
|
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
|
|
|
|
await longNameBot.click({ button: "right" });
|
|
await page.getByRole("menuitem", { name: "Delete" }).click();
|
|
const deleteDialog = page.getByRole("alertdialog", {
|
|
name: `Delete ${longUnbrokenName}?`,
|
|
});
|
|
await expect(deleteDialog).toBeVisible();
|
|
await captureScreenshot(page, testInfo, "30-delete-bot-confirmation");
|
|
await deleteDialog.getByRole("button", { name: "Delete", exact: true }).click();
|
|
|
|
await expect(botList.getByText(longUnbrokenName, { exact: true })).toHaveCount(0);
|
|
await expect(botList.getByRole("button", { name: /^Chief/ })).toBeVisible();
|
|
await page.waitForURL((url) => url.pathname !== deletedBotPath);
|
|
|
|
await page.goto(deletedBotPath);
|
|
await page.waitForURL((url) => url.pathname !== deletedBotPath);
|
|
await expect(botList.getByText(longUnbrokenName, { exact: true })).toHaveCount(0);
|
|
await expect(botList.getByRole("button", { name: /^Chief/ })).toBeVisible();
|
|
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
|
|
await captureScreenshot(page, testInfo, "31-deleted-bot-fallback");
|
|
});
|
|
|
|
test("a bot export restores as a new bot", async ({ page }, testInfo) => {
|
|
const stamp = Date.now();
|
|
await signup(page, `bot-import-${stamp}@rakazo.test`, "password12", "Bot Import");
|
|
await completeOnboarding(page);
|
|
await page.goto("/app");
|
|
await page.waitForURL(/\/app\/[^/]+$/);
|
|
|
|
await openNewBot(page);
|
|
const form = page.getByTestId("create-bot-form");
|
|
await expect(form.getByRole("button", { name: "Import bot" })).toBeVisible();
|
|
for (const colorScheme of ["light", "dark"] as const) {
|
|
await page.emulateMedia({ colorScheme });
|
|
await expect(page.locator("html")).toHaveAttribute("data-theme", colorScheme);
|
|
await captureScreenshot(page, testInfo, `27d-create-bot-import-${colorScheme}`);
|
|
}
|
|
|
|
const fileInput = form.getByTestId("import-bot-file");
|
|
await fileInput.setInputFiles({
|
|
name: "broken.json",
|
|
mimeType: "application/json",
|
|
buffer: Buffer.from("{"),
|
|
});
|
|
await expect(page.getByTestId("create-bot-error")).toHaveText("Could not import this file");
|
|
|
|
await fileInput.setInputFiles({
|
|
name: "archivist-export.json",
|
|
mimeType: "application/json",
|
|
buffer: Buffer.from(
|
|
JSON.stringify({
|
|
version: 1,
|
|
exportedAt: new Date().toISOString(),
|
|
bot: {
|
|
name: "Archivist",
|
|
title: "Keeps the records",
|
|
description: "Files and finds documents.",
|
|
instructions: "Name every file by date.",
|
|
},
|
|
memory: [{ path: "MEMORY.md", content: "# Archive\n\nPrefers ISO dates." }],
|
|
routines: [
|
|
{
|
|
name: "Weekly sweep",
|
|
prompt: "Tidy the archive",
|
|
crons: ["0 8 * * 1"],
|
|
timezone: "UTC",
|
|
},
|
|
],
|
|
files: [{ path: "notes/index.md", content: "imported" }],
|
|
history: [],
|
|
}),
|
|
),
|
|
});
|
|
await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "closed");
|
|
const botList = page.locator("aside").first();
|
|
await expect(botList.getByRole("button", { name: /^Archivist/ })).toBeVisible();
|
|
await page.locator("main").getByRole("button", { name: "Archivist", exact: true }).click();
|
|
await expect(page.locator("label:has-text('Title') input")).toHaveValue("Keeps the records");
|
|
for (const colorScheme of ["light", "dark"] as const) {
|
|
await page.emulateMedia({ colorScheme });
|
|
await expect(page.locator("html")).toHaveAttribute("data-theme", colorScheme);
|
|
await captureScreenshot(page, testInfo, `27e-imported-bot-${colorScheme}`);
|
|
}
|
|
});
|