/** * E2E test fixtures: spins up a real server on a random port with * mocked /api/chat and /api/embed-chat endpoints (no LLM calls). */ import { test as base, type Page } from "@playwright/test"; import { mkdtempSync, rmSync } from "fs"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; import type { AddressInfo } from "net"; import { createApp, resetSaveTimers } from "../src/create-app.js"; import { setDataDir } from "../src/utils.js"; const __filename = fileURLToPath(import.meta.url); const BACKEND_DIR = join(dirname(__filename), ".."); const STREAM_HEADERS = { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no", "X-Vercel-AI-UI-Message-Stream": "v1", }; function buildFakeStream(text: string): string { const textId = `txt-${Date.now()}`; const events = [ { type: "start" }, { type: "text-start", id: textId }, { type: "text-delta", id: textId, delta: text }, { type: "text-end", id: textId }, { type: "finish-step" }, { type: "finish" }, ]; const lines = events.map((e) => `data: ${JSON.stringify(e)}\n`).join("\n"); return lines + "\ndata: [DONE]\n\n"; } interface TestFixtures { serverUrl: string; appPage: Page; } export const test = base.extend({ serverUrl: [ async ({}, use) => { // Keep tmpDir under backend/ so relative staticDir path (../../frontend/dist) resolves const tmpDir = mkdtempSync(join(BACKEND_DIR, ".e2e-data-")); setDataDir(tmpDir); const { app, httpServer, hocuspocus } = createApp(); await new Promise((resolve) => httpServer.listen(0, resolve)); const port = (httpServer.address() as AddressInfo).port; const url = `http://localhost:${port}`; await use(url); resetSaveTimers(); try { await hocuspocus.destroy(); } catch {} try { httpServer.close(); } catch {} setDataDir(undefined); try { rmSync(tmpDir, { recursive: true, force: true }); } catch {} }, { scope: "test" }, ], appPage: async ({ page, serverUrl }, use) => { // Intercept chat API calls at the browser level await page.route("**/api/chat", async (route) => { await route.fulfill({ status: 200, headers: STREAM_HEADERS, body: buildFakeStream("Hello! I'm the AI assistant."), }); }); await page.route("**/api/embed-chat", async (route) => { await route.fulfill({ status: 200, headers: STREAM_HEADERS, body: buildFakeStream("Chart created successfully."), }); }); await page.goto(serverUrl); // Wait for editor to be ready (toolbar visible) await page.waitForSelector("[aria-label='Undo']", { timeout: 15_000 }); await use(page); }, }); export { expect } from "@playwright/test";