import { expect, test, type Page } from "@playwright/test"; import { initialSettings } from "../src/backtests/types"; import { changeScope, fieldOptions, settingsErrors, type SettingsRow, } from "../src/settings/model"; const headers = { "X-WQ-Request": "1" }; async function login(page: Page) { await page.goto("/#backtests"); await page.getByLabel("密码", { exact: true }).fill("browser-test-password"); await page.getByRole("button", { name: "进入工作空间" }).click(); await expect(page.getByRole("button", { name: "新建回测" })).toBeVisible(); 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"); } async function select(page: Page, name: string, option: string) { const control = page.getByRole("combobox", { name, exact: true }); await control.click(); const popup = page.locator( `[id="${await control.getAttribute("aria-controls")}"]`, ); await popup .getByRole("option") .filter({ hasText: new RegExp(`^${option}$`) }) .click(); await expect(control).toContainText(option); await expect(popup).not.toBeVisible(); } async function openEditor(page: Page) { await page.getByRole("button", { name: "新建回测", exact: true }).click(); await page.getByRole("button", { name: "同步合法设置", exact: true }).click(); await page.getByRole("button", { name: "选择组合", exact: true }).click(); await select(page, "Region", "USA"); await select(page, "Universe", "TOP3000"); await page.getByRole("button", { name: "应用组合", exact: true }).click(); } test("scope changes are transactional and explicit parameters reach the preview unchanged", async ({ page, }) => { const errors: string[] = []; page.on("pageerror", (error) => errors.push(error.message)); await login(page); await openEditor(page); const picker = page.getByRole("region", { name: "组合选择", exact: true }); await expect(picker).toContainText("EQUITY / USA / TOP3000 / Delay 1"); await page.getByRole("button", { name: "更改组合", exact: true }).click(); await select(page, "Region", "CHN"); await expect(picker).toContainText("TOP2000U"); await picker.getByRole("button", { name: "取消", exact: true }).click(); await expect(picker).toContainText("EQUITY / USA / TOP3000 / Delay 1"); await page.getByRole("button", { name: "更改组合", exact: true }).click(); await expect( page.getByRole("combobox", { name: "Region", exact: true }), ).toContainText("USA"); await select(page, "Region", "CHN"); await expect( page.getByRole("combobox", { name: "Region", exact: true }), ).toContainText("CHN"); await page.getByRole("button", { name: "应用组合", exact: true }).click(); await expect(picker).toContainText("EQUITY / CHN / TOP2000U / Delay 1"); await select(page, "Neutralization", "NONE"); await page.getByLabel("Decay", { exact: true }).fill("12"); await page.getByText("更多参数", { exact: true }).click(); await select(page, "NaN Handling", "ON"); await select(page, "Max Position", "ON"); await select(page, "Visualization", "是"); await expect(page.getByLabel("其他参数", { exact: true })).toHaveCount(0); await page.getByLabel("运行名称", { exact: true }).fill("组合参数一致性"); await page.getByLabel("回测候选").fill("rank(close)"); for (const width of [1440, 900, 390]) { await page.setViewportSize({ width, height: 1000 }); const bounds = await page .locator(".simulation-settings .settings-field:visible") .evaluateAll((nodes) => nodes.map((node) => { const r = node.getBoundingClientRect(); return { left: r.left, right: r.right, width: r.width }; }), ); expect( bounds.every((r) => r.left >= 0 && r.right <= width + 1 && r.width > 80), ).toBeTruthy(); await page.screenshot({ path: `../output/playwright/shared-settings-${width}.png`, }); } await page.setViewportSize({ width: 1440, height: 1000 }); const response = page.waitForResponse( (r) => r.url().endsWith("/backtests/previews") && r.request().method() === "POST", ); await page.getByRole("button", { name: "预览回测", exact: true }).click(); const preview = await (await response).json(); expect(preview.items[0].settings).toMatchObject({ region: "CHN", universe: "TOP2000U", neutralization: "NONE", decay: 12, nanHandling: "ON", maxPosition: "ON", visualization: true, }); expect(errors).toEqual([]); }); test("metadata failure retries and invalid imported settings stay visible without submission", async ({ page, }) => { await login(page); await page.getByRole("button", { name: "新建回测", exact: true }).click(); await page.route("**/catalog/setting-options/refresh", (route) => route.fulfill({ status: 502, json: { detail: "模拟选项同步失败" } }), ); 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("**/catalog/setting-options/refresh"); await page.getByRole("button", { name: "同步合法设置", exact: true }).click(); await expect(page.getByText(/模拟选项同步失败/)).toHaveCount(0); await page.getByText("每行一条表达式,共用参数", { exact: true }).click(); await page .getByRole("option") .filter({ hasText: "结构化候选 JSON,逐项完整参数" }) .click(); const text = JSON.stringify([ { client_item_id: "old", expression: "rank(close)", settings: { ...initialSettings, region: "USA", universe: "TOP3000", neutralization: "REMOVED_VALUE", }, }, ]); await page.getByLabel("回测候选").fill(text); let submits = 0; page.on("request", (request) => { if ( request.url().endsWith("/backtests/previews") && request.method() === "POST" ) submits++; }); await page.getByRole("button", { name: "预览回测", exact: true }).click(); await expect(page.getByText(/REMOVED_VALUE 不可用/)).toBeVisible(); await expect(page.getByLabel("回测候选")).toHaveValue(text); expect(submits).toBe(0); }); test("scope invalidation and platform restrictions preserve types and reject incompatible fixed inputs", () => { const scope = { instrument_type: "EQUITY", region: "USA", delay: 1, universe: "TOP3000", }; const options = [ { ...scope, universes: ["TOP3000"] }, { ...scope, region: "CHN", universes: ["TOP2000U", "TOP500"] }, ]; expect(changeScope(options, scope, "region", "CHN")).toEqual({ ...scope, region: "CHN", universe: undefined, }); const row: SettingsRow = { ...scope, neutralizations: ["NONE"], fields: { visualization: { choices: [false] }, decay: { minimum: -10, maximum: 20000 }, }, }; expect(fieldOptions(row, "decay")).toEqual({ minimum: 0, maximum: 10000 }); expect(fieldOptions(row, "visualization").choices).toEqual([false]); expect( fieldOptions( { ...row, fields: { decay: { choices: [-1, 0, 1.5, 10, 20000] } } }, "decay", ).choices, ).toEqual([0, 10]); expect( settingsErrors( { ...initialSettings, region: "USA", universe: "TOP3000", neutralization: "NONE", }, [row], [scope], ), ).toEqual([]); const errors = settingsErrors( { ...initialSettings, region: "USA", universe: "TOP3000", decay: 0.5, visualization: "false" as unknown as boolean, }, [row], [{ ...scope, region: "CHN" }], ); expect(errors.join(" ")).toContain("相同组合"); expect(errors.join(" ")).toContain("INDUSTRY 不可用"); expect(errors.join(" ")).toContain("整数"); expect(errors.join(" ")).toContain("Visualization"); }); test("new preparation edits its scope inline and cancels without changing the collection", async ({ page, }) => { await login(page); await page .getByRole("navigation", { name: "主导航" }) .getByRole("button", { name: "数据准备", exact: true }) .click(); await page.getByRole("button", { name: "新建集合", exact: true }).click(); const dialog = page.getByRole("dialog", { name: "新建数据准备集合", exact: true, }); const picker = dialog.getByRole("region", { name: "组合选择", exact: true }); await picker.getByRole("button", { name: "更改组合" }).click(); await select(page, "Region", "CHN"); await expect(dialog).toBeVisible(); await expect(page.getByRole("dialog")).toHaveCount(1); await picker.getByRole("button", { name: "取消", exact: true }).click(); await expect(picker).toContainText("EQUITY / USA / TOP3000 / Delay 1"); await picker.getByRole("button", { name: "更改组合" }).click(); await select(page, "Region", "CHN"); await page.screenshot({ path: "../output/playwright/preparation-scope-picker.png", }); await picker.getByRole("button", { name: "应用组合", exact: true }).click(); await expect(picker).toContainText("EQUITY / CHN / TOP2000U / Delay 1"); await dialog.getByLabel("集合名称", { exact: true }).fill("组合组件新建验收"); const response = page.waitForResponse( (r) => r.url().endsWith("/data-preparations") && r.request().method() === "POST", ); await dialog.getByRole("button", { name: "confirm", exact: true }).click(); expect((await (await response).json()).scope).toMatchObject({ region: "CHN", universe: "TOP2000U", delay: 1, }); });