evOo-local/apps/web/e2e/helpers.ts
Ingo Wagner cce793151e fix(ui): remove Integrations button from sidebar, add E2E tests
- Remove 'Integrationen' button from sidebar (Shell.tsx)
- Fix auth heading text: 'Create your Rakazo' → 'Create your EvOo'
- Fix completeOnboarding helper: handle setup progress, dismiss overlays
- Fix vite proxy CORS: use proxyRes handler instead of headers option
- Add local provider fallback in modelSetup
- Add 4 new E2E tests (all passing):
  - sidebar-integrations-removed: Integrations button not in sidebar
  - sidebar-nav: expected nav items present, no Integrations
  - message-send: composer interactive, message appears
  - onboarding-complete: full onboarding flow works
- Add playwright config: PLAYWRIGHT_NO_SERVER support
2026-09-22 15:46:05 +00:00

219 lines
8.4 KiB
TypeScript

import { expect, type Page, type TestInfo } from "@playwright/test";
export function isRealSandboxProvider(provider = process.env.SANDBOX_PROVIDER) {
return provider === "e2b" || provider === "daytona" || provider === "box";
}
export function realSandboxTimeout(real: number, emulated: number) {
if (process.env.SANDBOX_PROVIDER === "box") return Math.max(real, 300_000);
return isRealSandboxProvider() ? real : emulated;
}
export function activeBotId(page: Page) {
const id = new URL(page.url()).pathname.split("/").filter(Boolean).at(-1);
if (!id || id === "app") throw new Error(`missing bot id in ${page.url()}`);
return id;
}
export async function rpc<T>(page: Page, procedure: string, body: unknown): Promise<T> {
const response = await page.request.post(`/rpc/${procedure}`, { data: { json: body } });
const parsed = (await response.json()) as { json?: T; error?: { message?: string } };
if (!response.ok() || parsed.error) {
throw new Error(`${procedure} ${response.status()}: ${parsed.error?.message ?? "failed"}`);
}
return parsed.json as T;
}
export async function completeOnboarding(page: Page, testInfo?: TestInfo) {
await page.waitForURL(/\/(onboarding|app)/, { timeout: 20_000 });
// Setup progress / "Don't stress" page — wait for it to settle
const setupProgress = page.locator('progress[aria-label="Setup progress"]');
await setupProgress.waitFor({ state: "hidden", timeout: 30_000 }).catch(() => {});
// Emoji step: "Design your Oo" — skip by clicking Continue
// Wait for the design page to be fully interactive
await page.getByText(/Design your Oo/i).first().waitFor({ timeout: 10000 });
await page.getByRole("button", { name: "Continue" }).click();
// Wait for next page or URL change
await page.waitForTimeout(1000);
// Preferences step: just skip
const prefHeading = page.getByRole("heading", { name: /Tell us about yourself/i });
if (await prefHeading.isVisible({ timeout: 5000 }).catch(() => false)) {
await page.getByRole("button", { name: /Continue/i }).first().click();
}
// Language step: skip (defaults to English)
const langHeading = page.getByRole("heading", { name: /Choose your language/i });
if (await langHeading.isVisible({ timeout: 5000 }).catch(() => false)) {
const skipBtn = page.getByRole("button", { name: /Skip/i });
if (await skipBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
await skipBtn.click();
} else {
await page.getByRole("button", { name: /Continue/i }).first().click();
}
}
// Voice step: skip when shown
const voiceHeading = page.getByRole("heading", { name: /Pick a voice/i });
if (await voiceHeading.isVisible({ timeout: 5000 }).catch(() => false)) {
await page.getByRole("button", { name: /^Skip$/i }).click();
}
// Optional Server integrations step (needsSetup). Skip when shown, then the
// first bot is created automatically — land in Chief's chat with no form.
const integrations = page.getByRole("heading", { name: "Server integrations", exact: true });
const chief = page.getByText("Chief").first();
const openingChat = page.getByText("Opening chat…");
// Try each target sequentially (or() can fail when none match)
let landed = false;
if (await integrations.isVisible({ timeout: 5000 }).catch(() => false)) {
if (testInfo) await captureScreenshot(page, testInfo, "02-connect-apps");
await page.getByRole("button", { name: "Skip", exact: true }).click();
landed = true;
}
if (!landed && await chief.isVisible({ timeout: 5000 }).catch(() => false)) {
landed = true;
}
if (!landed && await openingChat.isVisible({ timeout: 5000 }).catch(() => false)) {
landed = true;
}
if (landed) {
await page.waitForURL(/\/app(\/[^/]+)?/, { timeout: 20_000 }).catch(() => {});
}
// Final: ensure we're in the chat with Chief
await page.waitForURL(/\/app(\/[^/]+)?/, { timeout: 20_000 }).catch(() => {});
await expect(chief).toBeVisible({ timeout: 15_000 });
// Dismiss any onboarding overlay (e.g. "What do you want me on first?")
// Try multiple strategies: Escape key, close button, dismiss button
const overlay = page.locator('[role="dialog"], [role="presentation"], [data-overlay]');
const overlayVisible = await overlay.count({ timeout: 3000 }).catch(() => 0);
if (overlayVisible > 0) {
await page.keyboard.press('Escape');
await page.waitForTimeout(500);
}
// Also try clicking X
const closeBtn = page.getByRole("button", { name: "X" }).first();
if (await closeBtn.isVisible({ timeout: 1000 }).catch(() => false)) {
await closeBtn.click();
await page.waitForTimeout(500);
}
if (testInfo) {
await captureScreenshot(page, testInfo, "03-create-first-bot");
await captureScreenshot(page, testInfo, "06-onboarding-complete");
}
}
export async function signup(
page: Page,
email: string,
password: string,
name: string,
testInfo?: TestInfo,
) {
await page.goto("/sign-up");
await expect(page.getByRole("heading", { name: "Create your EvOo" })).toBeVisible();
if (testInfo) await captureScreenshot(page, testInfo, "01-sign-up");
await page.getByPlaceholder("Your name").fill(name);
await page.getByPlaceholder("Your email address").fill(email);
await page.getByPlaceholder("Password").fill(password);
await page.getByRole("button", { name: "Create account" }).click();
}
export async function captureScreenshot(page: Page, testInfo: TestInfo, name: string) {
const screenshotPath = testInfo.outputPath(`${name}.png`);
await page.screenshot({
animations: "disabled",
caret: "hide",
fullPage: true,
path: screenshotPath,
});
await testInfo.attach(name, { contentType: "image/png", path: screenshotPath });
}
export async function openNewBot(page: Page) {
await page.getByTestId("create-menu-trigger").click();
await page.getByTestId("create-new-bot").click();
await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "create");
await expect(page.getByTestId("create-bot-form")).toBeVisible();
}
export async function openNewGroup(page: Page) {
await page.getByTestId("create-menu-trigger").click();
await page.getByTestId("create-new-group").click();
}
export async function openNewSpace(page: Page) {
await page.getByTestId("create-menu-trigger").click();
await page.getByTestId("create-new-space").click();
}
/** Open the create form from the + picker, submit, and wait for the new chat. */
export async function createBotFromPicker(
page: Page,
options: {
name?: string;
title?: string;
description?: string;
computerMode?: "team" | "dedicated";
} = {},
) {
const name = options.name ?? "New Bot";
await openNewBot(page);
const form = page.getByTestId("create-bot-form");
await form.locator("label:has-text('Name') input").fill(name);
if (options.title != null) {
await form.locator("label:has-text('Title') input").fill(options.title);
}
if (options.description != null) {
await form.locator("label:has-text('Description') textarea").fill(options.description);
}
if (options.computerMode === "dedicated") {
await form.getByTestId("create-bot-private").click();
} else if (options.computerMode === "team") {
await form.getByTestId("create-bot-team").click();
}
await form.getByRole("button", { name: "Create", exact: true }).click();
await page.waitForURL(/\/app\/[^/]+$/);
await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "closed");
}
/** Open the user Settings overlay, optionally switching to a sidebar section. */
export async function openUserSettings(
page: Page,
section?: "general" | "models" | "memory" | "voice" | "usage" | "computer" | "updates",
) {
await page.getByTestId("user-menu-trigger").click();
await page.getByRole("button", { name: "Settings", exact: true }).click();
const settings = page.getByTestId("user-settings");
await expect(settings).toBeVisible();
if (section && section !== "general") {
await settings.getByTestId(`settings-nav-${section}`).click();
}
return settings;
}
/** Create a named bot via RPC for test setup (skips the + picker). */
export async function createNamedBot(
page: Page,
name: string,
options: { computerMode?: "team" | "dedicated" } = {},
) {
const bot = await rpc<{ id: string; name: string }>(page, "bots/create", {
name,
title: "",
description: "",
notifyOnFinish: true,
computerMode: options.computerMode ?? "team",
});
await page.goto(`/app/${bot.id}`);
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
return bot.id;
}