130 lines
3.5 KiB
TypeScript
130 lines
3.5 KiB
TypeScript
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;
|
|
}
|