import { expect, test, type Page } from "@playwright/test"; const headers = { "X-WQ-Request": "1" }; async function login(page: Page) { await page.goto("/"); await page.getByLabel("密码", { exact: true }).fill("browser-test-password"); await page.getByRole("button", { name: "进入工作空间" }).click(); await expect( page.getByRole("button", { name: "打开研究助手" }), ).toBeVisible(); } async function configure(page: Page) { const body = { base_url: "https://model.test/v1", model: "test-model" }; await page.request.put("/api/v1/ai/settings", { headers, data: { ...body, api_key: "synthetic-model-key" }, }); const result = await page.request.post("/api/v1/ai/settings/test", { headers, }); expect((await result.json()).ready).toBe(true); await page.request.put("/api/v1/ai/settings", { headers, data: { ...body, enabled: true }, }); await page.request.put("/api/v1/account/credentials", { headers, data: { email: "test@example.com", password: "synthetic-password" }, }); await page.request.post("/api/v1/account/connect", { headers }); await expect .poll( async () => (await (await page.request.get("/api/v1/account")).json()) .connection_status, ) .toBe("connected"); await page.request.post("/api/v1/sync-jobs", { headers, data: { kind: "full_sync" }, }); await page.request.post("/api/v1/sync-jobs", { headers, data: { kind: "daily_sync", submission: "UNSUBMITTED", date_from: "2025-01-01", date_to: "2025-01-01", }, }); await expect .poll( async () => (await (await page.request.get("/api/v1/alphas")).json()).total, ) .toBe(620); await page.reload(); } test("model settings, text stream, contextual query, confirmation and preserved draft", async ({ page, }) => { const errors: string[] = []; page.on("pageerror", (error) => errors.push(error.message)); await login(page); await page.getByRole("button", { name: "个人信息", exact: true }).click(); await page .getByRole("textbox", { name: "模型 Base URL" }) .fill("https://model.test/v1"); await page .getByRole("textbox", { name: "模型标识", exact: true }) .fill("test-model"); await page.getByLabel("模型 API Key").fill("synthetic-model-key"); await page.getByRole("button", { name: "保存模型配置" }).click(); await expect(page.getByLabel("模型 API Key")).toHaveValue(""); await page.getByRole("button", { name: "测试连接" }).click(); await expect(page.getByText("工具调用与结果回传成功")).toBeVisible(); await page .getByText("启用研究助手(能力测试通过后可开启)", { exact: true }) .click(); await page.getByRole("button", { name: "保存模型配置" }).click(); await configure(page); await page.getByRole("button", { name: "Alpha 管理", exact: true }).click(); await page.getByRole("textbox", { name: "搜索 Alpha" }).fill("TEST0001"); await page.getByRole("button", { name: "查询", exact: true }).click(); await page.locator(".alpha-link").click(); await page.getByRole("tab", { name: "研究记录" }).click(); await page .getByRole("textbox", { name: "研究备注" }) .fill("尚未保存的人工研究草稿"); // A global launcher must remain reachable when the detail mask is open. await page.getByRole("button", { name: "打开研究助手" }).click(); const chat = page.getByRole("complementary", { name: "AI 研究助手" }); await chat.getByRole("button", { name: "新会话", exact: true }).click(); await expect(chat.locator(".ai-context")).toContainText("TEST0001"); const input = chat.getByRole("textbox", { name: "发送给研究助手" }); await input.fill("修改当前 Alpha 的备注"); await chat.getByRole("button", { name: "发送", exact: true }).click(); await expect(chat.getByRole("button", { name: "确认执行" })).toBeEnabled(); const before = await ( await page.request.get("/api/v1/alphas/TEST0001") ).json(); expect(before.research.note).not.toBe("AI 测试研究记录"); await page.screenshot({ path: "../output/playwright/ai-approval.png", fullPage: true, }); await chat.getByRole("button", { name: "确认执行" }).click(); await expect .poll( async () => (await (await page.request.get("/api/v1/alphas/TEST0001")).json()) .research.note, ) .toBe("AI 测试研究记录"); await expect(chat.locator(".ai-run-state")).toContainText("已完成"); await expect(page.getByRole("textbox", { name: "研究备注" })).toHaveValue( "尚未保存的人工研究草稿", ); await expect(page.getByText(/研究记录已被其他操作更新/)).toBeVisible(); await input.fill("筛选换手率不超过 15% 的 Alpha"); await chat.getByRole("button", { name: "发送", exact: true }).click(); await expect(chat.getByRole("button", { name: "应用到列表" })).toBeVisible(); const conversation = ( await (await page.request.get("/api/v1/ai/conversations")).json() )[0]; const history = await ( await page.request.get(`/api/v1/ai/conversations/${conversation.id}`) ).json(); expect(JSON.stringify(history)).not.toContain("尚未保存的人工研究草稿"); expect(history.runs.at(-1).tools[0].result.filters.turnover_max).toBe(0.15); await page.reload(); await page.getByRole("button", { name: "打开研究助手" }).click(); await expect( chat.getByText("筛选换手率不超过 15% 的 Alpha", { exact: true }), ).toBeVisible(); await chat.getByRole("button", { name: "应用到列表" }).click(); await expect(page.getByRole("textbox", { name: "搜索 Alpha" })).toHaveValue( "", ); await page.screenshot({ path: "../output/playwright/ai-query.png", fullPage: true, }); expect(errors).toEqual([]); }); test("collapse, navigation, reload, narrow viewport and cancellation preserve execution", async ({ page, }) => { await login(page); await configure(page); await page.getByRole("button", { name: "打开研究助手" }).click(); const chat = page.getByRole("complementary", { name: "AI 研究助手" }); await chat.getByRole("button", { name: "新会话", exact: true }).click(); await chat.getByRole("textbox", { name: "发送给研究助手" }).fill("SLOW"); await chat.getByRole("button", { name: "发送", exact: true }).click(); await expect(chat.getByText("正在查询", { exact: true })).toBeVisible(); await chat.getByRole("button", { name: "收起研究助手" }).click(); await page.getByRole("button", { name: "个人信息", exact: true }).click(); await page.reload(); await page.getByRole("button", { name: "打开研究助手" }).click(); await expect( chat.getByText("正在查询,查询完成。", { exact: true }), ).toBeVisible({ timeout: 10000 }); await page.setViewportSize({ width: 850, height: 900 }); await chat .getByRole("textbox", { name: "发送给研究助手" }) .fill("SLOW again"); await chat.getByRole("button", { name: "发送", exact: true }).click(); await expect(chat.getByRole("button", { name: "停止生成" })).toBeVisible(); await chat.getByRole("button", { name: "停止生成" }).click(); await expect(chat.locator(".ai-run-state")).toContainText("已停止"); await chat.getByRole("textbox", { name: "发送给研究助手" }).focus(); await page.keyboard.press("Escape"); await expect(chat).not.toBeVisible(); await page.getByRole("button", { name: "退出登录" }).click(); await expect( page.getByRole("heading", { name: "登录研究工作空间" }), ).toBeVisible(); expect((await page.request.get("/api/v1/ai/conversations")).status()).toBe( 401, ); }); test("Lark workspace keeps pagination, account details and chat usable at every width", async ({ page, }) => { await login(page); await configure(page); await page.getByRole("button", { name: "Alpha 管理", exact: true }).click(); await expect(page.locator(".alpha-link")).toHaveCount(25); await expect(page.locator(".sidebar")).toHaveCSS( "background-color", "rgb(249, 249, 249)", ); await expect(page.locator(".nav-item.active")).toHaveCSS( "background-color", "rgba(31, 35, 41, 0.05)", ); const chat = page.getByRole("complementary", { name: "AI 研究助手" }); for (const width of [1920, 1440, 1280, 850, 390]) { await page.setViewportSize({ width, height: 900 }); const footer = page.locator('footer[aria-label="Alpha 分页"]'); await expect(footer).toBeVisible(); const before = (await footer.boundingBox())!; expect(before.y + before.height).toBeLessThanOrEqual(900); expect(before.y + before.height).toBeGreaterThan(864); await page .locator(".alpha-page-view:not([hidden]) .semi-table-body") .evaluate((el) => el.scrollTo({ top: 1000 })); expect((await footer.boundingBox())!.y).toBeCloseTo(before.y, 0); await page.getByRole("button", { name: "打开研究助手" }).click(); await expect(chat).toBeVisible(); await expect(chat).toHaveCSS("background-color", "rgb(255, 255, 255)"); const box = (await chat.boundingBox())!; expect(box.x + box.width).toBeCloseTo(width, 0); if (width >= 1440) { expect( (await page.locator(".main-shell").boundingBox())!.width, ).toBeGreaterThan(600); expect( (await footer.boundingBox())!.x + (await footer.boundingBox())!.width, ).toBeLessThan(box.x); } else { await expect(page.locator(".main-shell")).toHaveAttribute("inert", ""); await expect(page.locator(".ai-mask")).toBeVisible(); } expect( await page.evaluate( () => document.documentElement.scrollWidth <= window.innerWidth, ), ).toBe(true); await page.screenshot({ path: `../output/playwright/lark-chat-${width}.png`, }); await chat.getByRole("button", { name: "收起研究助手" }).click(); } await page.setViewportSize({ width: 1440, height: 1000 }); await page.getByRole("button", { name: "个人信息", exact: true }).click(); await expect(page.getByRole("button", { name: "连接设置" })).toHaveAttribute( "aria-expanded", "false", ); await expect(page.locator(".permission-list li")).toHaveCount(10); await expect(page.getByRole("row", { name: /回测/ })).toContainText( "10,000(本地设定)", ); await page .getByRole("textbox", { name: "模型标识", exact: true }) .fill("保留模型设置草稿"); await page.getByRole("button", { name: "打开研究助手" }).click(); const resize = chat.getByRole("separator", { name: "调整助手宽度" }); await resize.focus(); for (let i = 0; i < 11; i++) await resize.press("ArrowLeft"); expect((await chat.boundingBox())!.width).toBe(640); await chat.getByRole("button", { name: "新会话", exact: true }).click(); const input = chat.getByRole("textbox", { name: "发送给研究助手" }); await input.fill("保留聊天草稿"); await page.getByRole("button", { name: "Alpha 管理", exact: true }).click(); await page.getByRole("button", { name: "个人信息", exact: true }).click(); await expect(input).toHaveValue("保留聊天草稿"); await expect( page.getByRole("textbox", { name: "模型标识", exact: true }), ).toHaveValue("保留模型设置草稿"); await chat.getByRole("button", { name: "收起研究助手" }).click(); await page.screenshot({ path: "../output/playwright/lark-account.png", fullPage: true, }); }); test("Markdown streams incomplete code, finishes safely and restores history", async ({ page, }) => { const errors: string[] = []; page.on("pageerror", (error) => errors.push(error.message)); await login(page); await configure(page); await page.getByRole("button", { name: "打开研究助手" }).click(); const chat = page.getByRole("complementary", { name: "AI 研究助手" }); await chat.getByRole("button", { name: "新会话", exact: true }).click(); const prefix = "## 研究结论\n\n**正在生成**\n\n```python\nrank(close)"; const suffix = "\n```\n\n| 指标 | 值 |\n| --- | --- |\n| Sharpe | 1.2 |\n\n[文档](https://example.com)\n\n\n\n[危险](javascript:alert(1))"; await page.evaluate(() => { const original = window.fetch.bind(window); window.fetch = async (url, init) => { if (!String(url).match(/\/ai\/conversations\/[^/]+\/runs$/)) return original(url, init); return new Response( new ReadableStream({ start(controller) { Object.assign(window, { pushMarkdown: (events: unknown[], done = false) => { for (const event of events) controller.enqueue( new TextEncoder().encode( `data: ${JSON.stringify(event)}\n\n`, ), ); if (done) { controller.enqueue( new TextEncoder().encode("data: [DONE]\n\n"), ); controller.close(); } }, }); }, }), { headers: { "content-type": "text/event-stream", "x-vercel-ai-ui-message-stream": "v1", }, }, ); }; }); await chat .getByRole("textbox", { name: "发送给研究助手" }) .fill("**用户原文**"); await chat.getByRole("button", { name: "发送", exact: true }).click(); await page.waitForFunction( () => typeof (window as any).pushMarkdown === "function", ); await page.evaluate( (delta) => (window as any).pushMarkdown([ { type: "start", messageId: "markdown-answer" }, { type: "text-start", id: "part" }, { type: "text-delta", id: "part", delta }, ]), prefix, ); await expect(chat.getByRole("heading", { name: "研究结论" })).toBeVisible(); await expect(chat.locator(".ai-markdown pre")).toContainText("rank(close)"); await expect(chat.locator(".ai-messages")).toHaveAttribute( "aria-busy", "true", ); await expect(chat.locator(".ai-message.user .ai-text")).toHaveText( "**用户原文**", ); await page.route("**/api/v1/ai/conversations/*", async (route) => { if (route.request().method() !== "GET") return route.continue(); await route.fulfill({ json: { id: route.request().url().split("/").at(-1), title: "Markdown", runs: [], messages: [ { id: "markdown-answer", role: "assistant", parts: [{ type: "text", text: prefix + suffix }], }, ], }, }); }); await page.evaluate( (delta) => (window as any).pushMarkdown( [ { type: "text-delta", id: "part", delta }, { type: "text-end", id: "part" }, { type: "finish" }, ], true, ), suffix, ); await expect(chat.locator(".ai-messages")).toHaveAttribute( "aria-busy", "false", ); await expect(chat.getByRole("cell", { name: "1.2" })).toBeVisible(); await expect( chat.getByRole("link", { name: "Link: https://example.com", exact: true }), ).toHaveAttribute("href", "https://example.com"); await expect( chat.locator(".ai-markdown script, .ai-markdown a[href^='javascript:']"), ).toHaveCount(0); await page.setViewportSize({ width: 390, height: 900 }); expect( await chat .locator(".ai-messages") .evaluate((el) => el.scrollWidth <= el.clientWidth), ).toBe(true); await page.screenshot({ path: "../output/playwright/ai-markdown.png" }); await page.reload(); await page.getByRole("button", { name: "打开研究助手" }).click(); await expect(chat.getByRole("heading", { name: "研究结论" })).toBeVisible(); await expect(chat.getByRole("cell", { name: "1.2" })).toBeVisible(); expect(errors).toEqual([]); });