import { test, expect, type Page } from "@playwright/test"; import { choosePreparation } from "./preparation-helpers"; const headers = { "X-WQ-Request": "1" }; const scope = { instrument_type: "EQUITY", region: "USA", universe: "TOP3000", delay: 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 job = await ( await page.request.post("/api/v1/account/connect", { headers }) ).json(); await expect .poll( async () => (await (await page.request.get(`/api/v1/sync-jobs/${job.id}`)).json()) .status, ) .toBe("completed"); await page.reload(); } async function sync(page: Page, dataset_id: string | null) { const job = await ( await page.request.post("/api/v1/catalog/sync-jobs", { headers, data: { scope, dataset_id }, }) ).json(); await expect .poll( async () => (await (await page.request.get(`/api/v1/sync-jobs/${job.id}`)).json()) .status, ) .toBe("completed"); } test("数据集完整同步、使用、集合编辑与研究选择", async ({ page }) => { const errors: string[] = []; page.on("pageerror", (e) => errors.push(e.message)); await setup(page); await sync(page, null); await page.reload(); const row = page.getByRole("row").filter({ hasText: "TEST 财务报表" }); await expect( row.getByRole("button", { name: "使用", exact: true }), ).toBeDisabled(); await row.getByRole("button", { name: "同步", exact: true }).click(); await expect .poll( async () => ( await ( await page.request.get( "/api/v1/catalog/datasets/TEST_FIN?region=USA&universe=TOP3000&delay=1", ) ).json() ).complete_count, ) .toBe(123); await page.keyboard.press("Escape"); await page.reload(); await row.getByRole("button", { name: "查看", exact: true }).click(); const detail = page.getByRole("dialog", { name: "TEST 财务报表", exact: true, }); await expect(detail).toContainText("TEST_FIN_001"); await detail.getByLabel("研究备注").fill("验证备注"); await detail.getByRole("button", { name: "保存备注", exact: true }).click(); await page.keyboard.press("Escape"); await row.getByRole("button", { name: "使用", exact: true }).click(); await page.getByRole("link", { name: "打开数据准备" }).click(); const prep = page.getByRole("dialog", { name: "数据准备详情", exact: true }); await expect(prep).toContainText("123 字段 / 1 数据集"); await prep.getByLabel("集合名称", { exact: true }).fill("浏览器准备集合"); await prep.getByRole("button", { name: "保存名称和备注" }).click(); await expect(prep).toContainText("v2"); await prep .getByRole("row") .filter({ hasText: "TEST_FIN_000" }) .locator("span.semi-checkbox") .click(); await prep.getByRole("button", { name: "移除所选字段" }).click(); await expect(prep).toContainText("122 字段"); await page.keyboard.press("Escape"); const preparationRow = page .getByRole("row") .filter({ hasText: "浏览器准备集合" }); await expect(preparationRow).toBeVisible(); expect( await preparationRow.evaluate((row) => row.getBoundingClientRect().height), ).toBe(40); await page.screenshot({ path: "test-results/preparation-table-layout.png" }); await page .getByRole("navigation", { name: "主导航" }) .getByRole("button", { name: "特征工程", exact: true }) .click(); await page.getByRole("button", { name: "新建特征方案", exact: true }).click(); await choosePreparation(page, "浏览器准备集合", "选择特征数据准备"); await expect( page.locator("p").filter({ hasText: /浏览器准备集合 ·.*122 字段/ }), ).toBeVisible(); expect(errors).toEqual([]); }); test("在线字段跨页多选直接准备,本地只展示完整同步字段", async ({ page }) => { await setup(page); await page .getByRole("navigation", { name: "主导航" }) .getByRole("button", { name: "字段目录", exact: true }) .click(); const queries: string[] = []; page.on("request", (request) => { if (request.url().includes("/catalog/worldquant/fields")) queries.push(request.url()); }); await expect( page.getByRole("tab", { name: "本地同步", exact: true }), ).toHaveAttribute("aria-selected", "true"); await page.getByRole("tab", { name: "worldquant接口", exact: true }).click(); const online = page.getByRole("tabpanel", { name: "worldquant接口" }); await expect( online.getByRole("button", { name: "查询", exact: true }), ).toBeDisabled(); await expect( online.getByText("请输入关键词、数据集或其他筛选条件,然后点击查询。"), ).toBeVisible(); expect(queries).toEqual([]); await online.getByLabel("字段数据集").fill("TEST_FIN"); expect(queries).toEqual([]); await online.getByRole("button", { name: "查询", exact: true }).click(); await expect(online.getByText("TEST_FIN_001", { exact: true })).toBeVisible(); await online .getByRole("row") .filter({ hasText: "TEST_FIN_001" }) .locator("span.semi-checkbox") .click(); await online.locator(".semi-page-item").filter({ hasText: /^2$/ }).click(); await online .getByRole("row") .filter({ hasText: "TEST_FIN_025" }) .locator("span.semi-checkbox") .click(); await expect(online.getByText(/已选 2 个字段/)).toBeVisible(); await online .getByRole("button", { name: "加入数据准备", exact: true }) .click(); const add = page.getByRole("dialog", { name: "加入数据准备", exact: true }); await add.getByLabel("新集合名称").fill("在线选取集合"); await add.getByRole("button", { name: "新建集合并加入" }).click(); await expect(add).not.toBeVisible(); const collections = await ( await page.request.get("/api/v1/data-preparations?q=在线选取集合") ).json(); expect(collections.items[0].field_count).toBe(2); await page.getByRole("tab", { name: "本地同步", exact: true }).click(); const local = page.getByRole("tabpanel", { name: "本地同步" }); // Other browser tests share this isolated server, so verify the unsynced dataset explicitly. await local.getByLabel("字段数据集").fill("TEST_NEWS"); await local.getByRole("button", { name: "查询", exact: true }).click(); await expect(local.getByText("共 0 个字段", { exact: true })).toBeVisible(); await page.screenshot({ path: "test-results/preparations-fields.png", fullPage: true, }); }); test("目录表格保持紧凑行高、截断和固定分页", async ({ page }) => { await setup(page); await sync(page, null); await sync(page, "TEST_FIN"); const longName = "超长数据集名称与描述".repeat(20); await page.route("**/api/v1/catalog/datasets?*", async (route) => { const response = await route.fetch(); const body = await response.json(); body.items = body.items.map((item: Record) => ({ ...item, name: longName, })); await route.fulfill({ response, json: body }); }); await page.reload(); const table = page.locator(".workspace-table:visible").first(); await expect( table.getByRole("button", { name: longName }).first(), ).toBeVisible(); const metrics = await table .locator("tbody .workspace-cell") .first() .evaluate((el) => ({ rowHeight: el.closest("tr")!.getBoundingClientRect().height, overflow: getComputedStyle(el).overflow, cellWidth: el.getBoundingClientRect().width, buttonWidth: el.querySelector("button")!.getBoundingClientRect().width, })); expect(metrics.rowHeight).toBe(40); expect(metrics.overflow).toBe("hidden"); expect(metrics.buttonWidth).toBeLessThanOrEqual(metrics.cellWidth); await expect( table.locator(".semi-tag").filter({ hasText: "已同步字段" }), ).toBeVisible(); await page.screenshot({ path: "test-results/catalog-table-layout.png" }); await page .getByRole("navigation", { name: "主导航" }) .getByRole("button", { name: "字段目录", exact: true }) .click(); const local = page.getByRole("tabpanel", { name: "本地同步" }); await expect(local.getByText("TEST_FIN_001", { exact: true })).toBeVisible(); const checkLayout = async () => { const box = await local.locator(".workspace-table-footer").boundingBox(); expect(box!.y + box!.height).toBeGreaterThan( page.viewportSize()!.height - 40, ); expect(box!.y + box!.height).toBeLessThanOrEqual( page.viewportSize()!.height, ); const rows = await local .locator("tbody tr.semi-table-row") .evaluateAll((rows) => rows.map((row) => row.getBoundingClientRect().height), ); expect(rows.length).toBe(25); expect(rows.every((height) => height === 40)).toBe(true); }; await checkLayout(); await page.screenshot({ path: "test-results/field-table-layout.png" }); await page.setViewportSize({ width: 900, height: 760 }); await checkLayout(); await page.screenshot({ path: "test-results/field-table-layout-narrow.png" }); await page.getByRole("tab", { name: "worldquant接口", exact: true }).click(); const online = page.getByRole("tabpanel", { name: "worldquant接口" }); await online.getByLabel("字段数据集").fill("TEST_FIN"); await online.getByRole("button", { name: "查询", exact: true }).click(); await expect(online.getByText("TEST_FIN_001", { exact: true })).toBeVisible(); let queries = 0; page.on("request", (req) => { if (req.url().includes("/catalog/worldquant/fields")) queries++; }); await online.getByRole("button", { name: "重置", exact: true }).click(); await expect( online.getByRole("button", { name: "查询", exact: true }), ).toBeDisabled(); await expect(online.getByText("TEST_FIN_001", { exact: true })).toHaveCount( 0, ); expect(queries).toBe(0); });