import { test, expect, type Page } from "@playwright/test"; const scope = { instrument_type: "EQUITY", region: "USA", universe: "TOP3000", delay: 1, }; const query = new URLSearchParams( Object.entries(scope).map(([k, v]) => [k, String(v)]), ).toString(); const headers = { "X-WQ-Request": "1" }; async function setup(page: Page) { await page.goto("/#datasets"); await page.getByLabel("密码", { exact: true }).fill("browser-test-password"); await page.getByRole("button", { name: "进入工作空间" }).click(); await expect( page.getByRole("button", { name: "同步目录", exact: true }), ).toBeVisible(); await page.request.put("/api/v1/account/credentials", { headers, data: { email: "test@example.com", password: "synthetic-only" }, }); const connect = await ( await page.request.post("/api/v1/account/connect", { headers }) ).json(); await expect .poll( async () => ( await ( await page.request.get(`/api/v1/sync-jobs/${connect.id}`) ).json() ).status, ) .toBe("completed"); await page.getByRole("button", { name: "同步目录", exact: true }).click(); await expect .poll( async () => ( await ( await page.request.get(`/api/v1/catalog/datasets?${query}`) ).json() ).total, ) .toBe(3); await page.keyboard.press("Escape"); await expect( page.getByRole("button", { name: "TEST 财务报表", exact: true }), ).toBeVisible(); } async function openFields(page: Page) { await page .getByRole("row") .filter({ hasText: "TEST 财务报表" }) .getByRole("button", { name: "查看字段", exact: true }) .click(); const sync = page .getByRole("dialog", { name: "数据字段", exact: true }) .getByRole("button", { name: "同步全部字段", exact: true }); if (await sync.isVisible()) { await sync.click(); await expect .poll( async () => ( await ( await page.request.get( `/api/v1/catalog/datasets/TEST_FIN/fields?${query}`, ) ).json() ).complete_count, ) .toBe(123); await page.keyboard.press("Escape"); } } test("目录筛选、双层详情、完整输入、排除、备注与刷新", async ({ page }) => { const errors: string[] = []; page.on("pageerror", (e) => errors.push(e.message)); await page.setViewportSize({ width: 1440, height: 1000 }); await setup(page); await page.getByLabel("分类", { exact: true }).click(); await page.getByRole("option", { name: /基本面/ }).click(); await page.getByLabel("子分类", { exact: true }).click(); await page.getByRole("option", { name: /财务报表/ }).click(); await openFields(page); const fields = page.getByRole("dialog", { name: "数据字段", exact: true }); await expect(fields).toBeVisible(); await expect(fields).toContainText("全部 123 个字段"); expect((await fields.boundingBox())!.width).toBeCloseTo(1080, 0); await page.getByLabel("搜索字段").fill("字段 12"); await expect(fields.getByRole("button", { name: /TEST 字段/ })).toHaveCount( 3, ); await page .getByRole("button", { name: "TEST 字段 122", exact: true }) .click(); const detail = page.getByRole("dialog", { name: "字段详情", exact: true }); await expect(detail).toContainText("FUTURE_TYPE"); expect((await detail.boundingBox())!.width).toBeCloseTo(432, 0); await page.getByLabel("研究备注", { exact: true }).fill("保留字段研究假设"); await page.getByRole("button", { name: "保存备注", exact: true }).click(); await expect(page.getByText("研究备注已保存")).toBeVisible(); await page.keyboard.press("Escape"); await expect(detail).not.toBeVisible(); await expect(page.getByLabel("搜索字段")).toHaveValue("字段 12"); await expect( page.getByRole("button", { name: "TEST 字段 122", exact: true }), ).toBeFocused(); await page .getByRole("button", { name: "用于 Alpha 模板", exact: true }) .click(); await page.getByRole("button", { name: "保存输入草稿", exact: true }).click(); await expect( page.getByRole("heading", { name: "输入草稿已保存" }), ).toBeVisible(); expect( ( await (await page.request.get(`/api/v1/catalog/inputs?${query}`)).json() )[0].field_ids, ).toHaveLength(123); await page.keyboard.press("Escape"); await page .getByRole("checkbox", { name: "选择TEST 字段 122", exact: true }) .press("Space"); await page.getByRole("button", { name: "覆盖率排序" }).click(); await expect(fields).toContainText("122 / 123 个字段"); await page .getByRole("button", { name: "用于 Alpha 模板", exact: true }) .click(); await page.getByRole("button", { name: "保存输入草稿", exact: true }).click(); await expect( page.getByRole("heading", { name: "输入草稿已保存" }), ).toBeVisible(); const subset = ( await (await page.request.get(`/api/v1/catalog/inputs?${query}`)).json() )[0]; expect(subset.field_ids).toHaveLength(122); expect(subset.field_ids).not.toContain("TEST_FIN_122"); await page.keyboard.press("Escape"); await page.getByRole("button", { name: "恢复全选" }).click(); await page .getByRole("checkbox", { name: "选择本数据集全部字段" }) .press("Space"); await expect( page.getByRole("button", { name: "用于 Alpha 模板", exact: true }), ).toBeDisabled(); await page.getByRole("button", { name: "恢复全选" }).click(); await page .getByRole("button", { name: "TEST 字段 122", exact: true }) .click(); await expect(page.getByLabel("研究备注", { exact: true })).toHaveValue( "保留字段研究假设", ); await page.screenshot({ path: "../output/playwright/dataset-desktop.png" }); await page.keyboard.press("Escape"); await page.keyboard.press("Escape"); await expect(page.getByLabel("分类", { exact: true })).toContainText( "基本面", ); await page.reload(); await expect( page.getByRole("button", { name: "已保存输入 (2)" }), ).toBeVisible(); expect(errors).toEqual([]); }); test("窄屏抽屉、键盘隔离和研究范围切换", async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await setup(page); await openFields(page); const fields = page.getByRole("dialog", { name: "数据字段", exact: true }); await expect(fields).toBeVisible(); expect((await fields.boundingBox())!.width).toBeCloseTo(390, 0); await page .getByRole("button", { name: "TEST 字段 000", exact: true }) .click(); await expect( page.getByRole("dialog", { name: "字段详情", exact: true }), ).toBeVisible(); await page.keyboard.press("Escape"); await expect(fields).toBeVisible(); expect( await page.evaluate( () => document.documentElement.scrollWidth <= innerWidth, ), ).toBe(true); await page.screenshot({ path: "../output/playwright/dataset-mobile.png" }); await page.keyboard.press("Escape"); await page.getByLabel("Region", { exact: true }).click(); await page.getByRole("option", { name: /CHN/ }).click(); await expect(page.getByLabel("Universe", { exact: true })).toContainText( "TOP2000", ); await expect( page.getByRole("button", { name: "用于 Alpha 模板", exact: true }), ).toBeDisabled(); }); test("非首页排除、聊天暂存详情、遮罩逐层关闭和多尺寸", async ({ page }) => { const errors: string[] = []; page.on("pageerror", (e) => errors.push(e.message)); await page.setViewportSize({ width: 1280, height: 900 }); await setup(page); await openFields(page); const fields = page.getByRole("dialog", { name: "数据字段", exact: true }); await fields.getByRole("button", { name: "Next", exact: true }).click(); await expect( page.getByRole("button", { name: "TEST 字段 025", exact: true }), ).toBeVisible(); await page .getByRole("checkbox", { name: "选择TEST 字段 025", exact: true }) .press("Space"); await page.getByLabel("搜索字段").fill("字段 12"); await expect(fields).toContainText("122 / 123 个字段"); await page.getByLabel("字段类型", { exact: true }).click(); await page.getByRole("option", { name: /VECTOR/ }).click(); await expect( page.getByRole("button", { name: "TEST 字段 120", exact: true }), ).toBeVisible(); await page .getByRole("button", { name: "TEST 字段 120", exact: true }) .click(); await page.getByLabel("研究备注", { exact: true }).fill("未保存草稿需要恢复"); await page .getByRole("dialog", { name: "字段详情", exact: true }) .getByRole("button", { name: "AI 助手", exact: true }) .click(); await expect(fields).not.toBeVisible(); await page.keyboard.press("Escape"); await expect(page.getByLabel("研究备注", { exact: true })).toHaveValue( "未保存草稿需要恢复", ); for (const width of [850, 390, 1920]) { await page.setViewportSize({ width, height: 900 }); await expect( page.getByRole("button", { name: "关闭详情", exact: true }), ).toBeVisible(); expect( await page.evaluate( () => document.documentElement.scrollWidth <= innerWidth, ), ).toBe(true); } await page.setViewportSize({ width: 1440, height: 900 }); await page.mouse.click(10, 400); await expect( page.getByRole("dialog", { name: "字段详情", exact: true }), ).not.toBeVisible(); await expect(fields).toBeVisible(); await expect(fields).toContainText("122 / 123 个字段"); await page.mouse.click(10, 400); await expect(fields).not.toBeVisible(); expect(errors).toEqual([]); }); test("平台选项失败可重试,地区联动限制 Delay 和 Universe", async ({ page }) => { await setup(page); await page.getByLabel("Delay", { exact: true }).click(); await page.getByRole("option", { name: /Delay 0/ }).click(); await page.getByLabel("Region", { exact: true }).click(); await page.getByRole("option", { name: /IND/ }).click(); await expect(page.getByLabel("Universe", { exact: true })).toContainText( "TOP500", ); await expect(page.getByLabel("Delay", { exact: true })).toContainText( "Delay 1", ); await page.getByLabel("Delay", { exact: true }).click(); await expect(page.getByRole("option", { name: /Delay 0/ })).toHaveCount(0); await page.keyboard.press("Escape"); await page.route("**/api/v1/catalog/scopes", (route) => route.fulfill({ status: 502, contentType: "application/json", body: JSON.stringify({ detail: "测试平台选项不可用" }), }), ); await page.getByRole("button", { name: "Alpha 管理", exact: true }).click(); await page.getByRole("button", { name: "数据目录", exact: true }).click(); await expect(page.getByText("测试平台选项不可用")).toBeVisible(); await expect( page.getByRole("button", { name: "同步目录", exact: true }), ).toBeDisabled(); await page.unroute("**/api/v1/catalog/scopes"); await page.getByRole("button", { name: "重试获取选项" }).click(); await expect( page.getByRole("button", { name: "同步目录", exact: true }), ).toBeEnabled(); });