import { expect, type Page, type TestInfo } from "@playwright/test"; const rawPrimaryThreadId = process.env.FRONTEND_E2E_THREAD_ID?.trim(); function envThread(name: string) { return process.env[name]?.trim() ?? undefined; } export const PRIMARY_THREAD_ID = rawPrimaryThreadId ?? undefined; export const THREAD_FOR_WELCOME = PRIMARY_THREAD_ID; export const THREAD_WITH_HISTORY = PRIMARY_THREAD_ID; export const THREAD_WITH_MARKDOWN = envThread("FRONTEND_E2E_MARKDOWN_THREAD_ID"); export const THREAD_WITH_TODOS = envThread("FRONTEND_E2E_TODOS_THREAD_ID"); export const THREAD_WITH_ARTIFACTS = envThread("FRONTEND_E2E_ARTIFACTS_THREAD_ID"); export const THREAD_WITH_IMAGE_ARTIFACT = envThread( "FRONTEND_E2E_IMAGE_ARTIFACT_THREAD_ID", ); export const THREAD_WITH_HTML_ARTIFACT = envThread( "FRONTEND_E2E_HTML_ARTIFACT_THREAD_ID", ); export function skipIfMissingThread( testInfo: TestInfo, threadId: string | undefined, label: string, ) { testInfo.skip(!threadId, `未配置 ${label}。`); } export function buildChatUrl({ pathThreadId, xclawUsed, threadId, }: { pathThreadId?: string; xclawUsed: boolean; threadId?: string; }) { const resolvedThreadId = threadId ?? pathThreadId; if (!pathThreadId && !resolvedThreadId) { throw new Error("threadId is required for /workspace/chats/new routes."); } const query = new URLSearchParams(); query.set("xclaw_used", String(xclawUsed)); if (resolvedThreadId) { query.set("thread_id", resolvedThreadId); } const basePath = pathThreadId ? `/workspace/chats/${pathThreadId}` : "/workspace/chats/new"; return `${basePath}?${query.toString()}`; } export function invalidNewChatUrl() { const query = new URLSearchParams(); query.set("xclaw_used", "false"); return `/workspace/chats/new?${query.toString()}`; } export function newChatEntry(threadId: string) { return buildChatUrl({ xclawUsed: false, threadId, }); } export function reuseThreadWelcomeEntry(threadId: string) { return buildChatUrl({ xclawUsed: false, threadId, }); } export function reuseThreadChatEntry(threadId: string) { return buildChatUrl({ pathThreadId: threadId, xclawUsed: true, threadId, }); } export async function openChat( page: Page, url: string, options?: { expectInput?: boolean }, ) { await page.goto(url); if (options?.expectInput ?? true) { await expect(page.locator("textarea[name='message']")).toBeVisible(); } } export async function sendMessage(page: Page, text: string) { const textarea = page.locator("textarea[name='message']"); const submit = page.locator("button[aria-label='Submit']"); await textarea.click(); await textarea.fill(text); await submit.click(); } export async function waitForMessageListReady( page: Page, options?: { requireMessages?: boolean; minMessages?: number }, ) { const { requireMessages = false, minMessages = 1 } = options ?? {}; await expect(page.getByRole("main").first()).toBeVisible(); if (requireMessages) { await expect .poll( async () => await page.locator(".is-user, .is-assistant").count(), { timeout: 30_000 }, ) .toBeGreaterThan(minMessages - 1); } } export async function waitForAnyMessages(page: Page, timeoutMs = 15_000) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const count = await page.locator(".is-user, .is-assistant").count(); if (count > 0) { return count; } await page.waitForTimeout(300); } return 0; }