2026-09-07 14:54:20 +08:00
|
|
|
import { expect, test } from "@playwright/test";
|
|
|
|
|
import { readFile } from "node:fs/promises";
|
|
|
|
|
|
2026-09-08 10:40:49 +08:00
|
|
|
test("account → scoped sync → research → resync → PnL → filtered export → logout", async ({
|
2026-09-07 14:54:20 +08:00
|
|
|
page,
|
|
|
|
|
}) => {
|
|
|
|
|
const failures: string[] = [];
|
|
|
|
|
page.on("pageerror", (error) => failures.push(error.message));
|
|
|
|
|
await page.goto("/");
|
|
|
|
|
await expect(
|
|
|
|
|
page.getByRole("heading", { name: "登录研究工作空间" }),
|
|
|
|
|
).toBeVisible();
|
|
|
|
|
await page.getByLabel("密码", { exact: true }).fill("incorrect");
|
|
|
|
|
await page.getByRole("button", { name: "进入工作空间" }).click();
|
|
|
|
|
await expect(page.getByText("用户名或密码不正确")).toBeVisible();
|
|
|
|
|
await page.getByLabel("密码", { exact: true }).fill("browser-test-password");
|
|
|
|
|
await page.getByRole("button", { name: "进入工作空间" }).click();
|
2026-09-08 21:29:21 +08:00
|
|
|
await page.getByRole("button", { name: "用户菜单", exact: true }).click();
|
|
|
|
|
await page.getByRole("button", { name: "查看个人信息", exact: true }).click();
|
2026-09-07 23:31:23 +08:00
|
|
|
const connectionSettings = page.getByRole("button", {
|
|
|
|
|
name: "连接设置",
|
|
|
|
|
exact: true,
|
|
|
|
|
});
|
|
|
|
|
if ((await connectionSettings.getAttribute("aria-expanded")) === "false")
|
|
|
|
|
await connectionSettings.click();
|
2026-09-07 14:54:20 +08:00
|
|
|
await page
|
|
|
|
|
.getByRole("textbox", { name: "WorldQuant 邮箱" })
|
|
|
|
|
.fill("test@example.com");
|
|
|
|
|
await page
|
|
|
|
|
.getByLabel("WorldQuant 密码", { exact: true })
|
|
|
|
|
.fill("synthetic-password");
|
|
|
|
|
await page.getByRole("button", { name: "保存凭据" }).click();
|
|
|
|
|
await expect(page.getByText("已保存", { exact: true })).toBeVisible();
|
|
|
|
|
await page
|
|
|
|
|
.getByRole("button", { name: "连接 WorldQuant", exact: true })
|
|
|
|
|
.click();
|
|
|
|
|
await expect
|
|
|
|
|
.poll(
|
|
|
|
|
async () =>
|
|
|
|
|
(await (await page.request.get("/api/v1/account")).json())
|
|
|
|
|
.connection_status,
|
|
|
|
|
)
|
|
|
|
|
.toBe("connected");
|
|
|
|
|
await page.keyboard.press("Escape");
|
|
|
|
|
await expect(page.locator(".job-panel")).not.toBeVisible();
|
2026-09-07 23:31:23 +08:00
|
|
|
await expect(page.locator(".permission-list li")).toHaveCount(10);
|
|
|
|
|
await expect(page.locator(".session-summary")).toContainText("4 小时");
|
|
|
|
|
const simulation = page.getByRole("row", { name: /回测/ });
|
|
|
|
|
await expect(simulation).toContainText("10,000(本地设定)");
|
|
|
|
|
await expect(simulation).toContainText("9,997");
|
2026-09-07 14:54:20 +08:00
|
|
|
await page.getByRole("button", { name: "Alpha 管理", exact: true }).click();
|
2026-09-08 10:40:49 +08:00
|
|
|
await expect(
|
|
|
|
|
page.getByRole("tab", { name: "待提交", exact: true }),
|
|
|
|
|
).toHaveAttribute("aria-selected", "true");
|
|
|
|
|
await expect(
|
|
|
|
|
page.getByRole("button", { name: "全量同步已提交" }),
|
|
|
|
|
).toHaveCount(0);
|
|
|
|
|
await page.getByRole("button", { name: "按天同步", exact: true }).click();
|
|
|
|
|
await page.getByLabel("同步开始日期").fill("2025-01-01");
|
|
|
|
|
await page.getByLabel("同步结束日期").fill("2025-01-01");
|
|
|
|
|
await page.getByRole("button", { name: "开始同步", exact: true }).click();
|
2026-09-08 21:52:46 +08:00
|
|
|
await expect(page.locator(".job-panel")).toBeVisible();
|
2026-09-08 10:40:49 +08:00
|
|
|
await expect
|
|
|
|
|
.poll(
|
|
|
|
|
async () =>
|
|
|
|
|
(
|
|
|
|
|
await (
|
|
|
|
|
await page.request.get("/api/v1/alphas?submission=UNSUBMITTED")
|
|
|
|
|
).json()
|
|
|
|
|
).total,
|
|
|
|
|
)
|
|
|
|
|
.toBe(413);
|
|
|
|
|
await page.keyboard.press("Escape");
|
|
|
|
|
await page.getByRole("tab", { name: "已提交", exact: true }).click();
|
|
|
|
|
await page.getByRole("button", { name: "全量同步已提交" }).click();
|
2026-09-08 21:52:46 +08:00
|
|
|
await expect(page.locator(".job-panel")).toBeVisible();
|
2026-09-07 14:54:20 +08:00
|
|
|
await expect
|
|
|
|
|
.poll(
|
|
|
|
|
async () =>
|
|
|
|
|
(await (await page.request.get("/api/v1/alphas")).json()).total,
|
|
|
|
|
{ timeout: 20000 },
|
|
|
|
|
)
|
|
|
|
|
.toBe(620);
|
|
|
|
|
await page.keyboard.press("Escape");
|
|
|
|
|
await expect(page.locator(".job-panel")).not.toBeVisible();
|
2026-09-08 10:40:49 +08:00
|
|
|
await page.getByRole("tab", { name: "待提交", exact: true }).click();
|
2026-09-08 21:29:21 +08:00
|
|
|
await expect(page.locator(".table-toolbar .count-pill")).toHaveText("413");
|
2026-09-07 14:54:20 +08:00
|
|
|
await page.screenshot({
|
|
|
|
|
path: "../output/playwright/alpha-library.png",
|
|
|
|
|
fullPage: true,
|
|
|
|
|
});
|
|
|
|
|
await page.getByRole("textbox", { name: "搜索 Alpha" }).fill("TEST0007");
|
|
|
|
|
await page.getByRole("button", { name: "查询", exact: true }).click();
|
|
|
|
|
await expect(page.locator(".alpha-link")).toHaveCount(1);
|
|
|
|
|
await page.locator(".alpha-link").click();
|
|
|
|
|
await expect(
|
|
|
|
|
page.getByRole("heading", { name: "Selection", exact: true }),
|
|
|
|
|
).toBeVisible();
|
|
|
|
|
await expect(
|
|
|
|
|
page.getByRole("heading", { name: "Combo", exact: true }),
|
|
|
|
|
).toBeVisible();
|
|
|
|
|
await page.getByRole("tab", { name: "研究记录" }).click();
|
|
|
|
|
await page
|
|
|
|
|
.getByRole("textbox", { name: "研究备注" })
|
|
|
|
|
.fill("需要检验跨区域稳定性;保留这条研究假设。");
|
|
|
|
|
await page
|
|
|
|
|
.getByRole("textbox", { name: "研究标签" })
|
|
|
|
|
.fill("组合研究, 低换手");
|
|
|
|
|
await page.getByText("收藏此 Alpha", { exact: true }).click();
|
|
|
|
|
await page.getByRole("button", { name: "保存研究记录" }).click();
|
|
|
|
|
await expect(page.getByText("研究记录已保存")).toBeVisible();
|
|
|
|
|
await page.getByRole("tab", { name: "PnL 曲线" }).click();
|
|
|
|
|
await page.getByRole("button", { name: "获取 PnL", exact: true }).click();
|
|
|
|
|
await expect
|
|
|
|
|
.poll(
|
|
|
|
|
async () =>
|
|
|
|
|
(await (await page.request.get("/api/v1/alphas/TEST0007/pnl")).json())
|
|
|
|
|
.cached,
|
|
|
|
|
)
|
|
|
|
|
.toBe(true);
|
|
|
|
|
await page.keyboard.press("Escape");
|
|
|
|
|
await expect(page.locator(".job-panel")).not.toBeVisible();
|
|
|
|
|
await expect(page.locator(".pnl-chart")).toBeVisible();
|
|
|
|
|
await page.screenshot({
|
|
|
|
|
path: "../output/playwright/alpha-detail.png",
|
|
|
|
|
fullPage: true,
|
|
|
|
|
});
|
|
|
|
|
await page.keyboard.press("Escape");
|
|
|
|
|
// Use the browser's authenticated API context to await the same durable task contract.
|
|
|
|
|
const job = await (
|
|
|
|
|
await page.request.post("/api/v1/sync-jobs", {
|
|
|
|
|
headers: { "X-WQ-Request": "1" },
|
|
|
|
|
data: { kind: "alpha_refresh", alpha_ids: ["TEST0007"] },
|
|
|
|
|
})
|
|
|
|
|
).json();
|
|
|
|
|
await expect
|
|
|
|
|
.poll(
|
|
|
|
|
async () =>
|
|
|
|
|
(await (await page.request.get(`/api/v1/sync-jobs/${job.id}`)).json())
|
|
|
|
|
.status,
|
|
|
|
|
)
|
|
|
|
|
.toBe("completed");
|
|
|
|
|
await page.reload();
|
|
|
|
|
await page.getByRole("textbox", { name: "搜索 Alpha" }).fill("TEST0007");
|
|
|
|
|
await page.getByRole("button", { name: "查询", exact: true }).click();
|
|
|
|
|
await expect(page.locator(".alpha-link")).toHaveCount(1);
|
|
|
|
|
await page.locator(".alpha-link").click();
|
|
|
|
|
await page.getByRole("tab", { name: "研究记录" }).click();
|
|
|
|
|
await expect(page.getByRole("textbox", { name: "研究备注" })).toHaveValue(
|
|
|
|
|
"需要检验跨区域稳定性;保留这条研究假设。",
|
|
|
|
|
);
|
|
|
|
|
await expect(
|
|
|
|
|
page.getByRole("checkbox", { name: "收藏此 Alpha" }),
|
|
|
|
|
).toBeChecked();
|
|
|
|
|
await page.keyboard.press("Escape");
|
|
|
|
|
await page.getByRole("button", { name: "重置", exact: true }).click();
|
|
|
|
|
await expect(page.locator(".alpha-link")).toHaveCount(25);
|
|
|
|
|
const downloading = page.waitForEvent("download");
|
|
|
|
|
await page.getByRole("button", { name: /导出/ }).click();
|
|
|
|
|
const download = await downloading;
|
|
|
|
|
const contents = await readFile((await download.path())!, "utf-8");
|
|
|
|
|
expect(contents).toContain("TEST0619");
|
2026-09-08 10:40:49 +08:00
|
|
|
expect(contents).toContain("TEST0001");
|
|
|
|
|
expect(contents).not.toContain("TEST0000");
|
|
|
|
|
expect((contents.match(/TEST\d{4}/g) ?? []).length).toBe(413);
|
2026-09-08 21:29:21 +08:00
|
|
|
await page.getByRole("button", { name: "用户菜单" }).click();
|
2026-09-07 14:54:20 +08:00
|
|
|
await page.getByRole("button", { name: "退出登录" }).click();
|
|
|
|
|
await expect(
|
|
|
|
|
page.getByRole("heading", { name: "登录研究工作空间" }),
|
|
|
|
|
).toBeVisible();
|
|
|
|
|
expect((await page.request.get("/api/v1/alphas")).status()).toBe(401);
|
|
|
|
|
expect(failures).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("batch tags, column visibility, server pagination and saved preferences", async ({
|
|
|
|
|
page,
|
|
|
|
|
}) => {
|
2026-09-09 18:20:08 +08:00
|
|
|
await page.goto("/#alphas");
|
2026-09-07 14:54:20 +08:00
|
|
|
await page.getByLabel("密码", { exact: true }).fill("browser-test-password");
|
|
|
|
|
await page.getByRole("button", { name: "进入工作空间" }).click();
|
|
|
|
|
await expect(
|
2026-09-08 21:29:21 +08:00
|
|
|
page.getByRole("tab", { name: "待提交", exact: true }),
|
2026-09-07 14:54:20 +08:00
|
|
|
).toBeVisible();
|
|
|
|
|
// Each test can run alone against the isolated browser acceptance server.
|
|
|
|
|
if ((await (await page.request.get("/api/v1/alphas")).json()).total !== 620) {
|
|
|
|
|
const headers = { "X-WQ-Request": "1" };
|
|
|
|
|
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" },
|
|
|
|
|
});
|
2026-09-08 10:40:49 +08:00
|
|
|
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",
|
|
|
|
|
},
|
|
|
|
|
});
|
2026-09-07 14:54:20 +08:00
|
|
|
await expect
|
|
|
|
|
.poll(
|
|
|
|
|
async () =>
|
|
|
|
|
(await (await page.request.get("/api/v1/alphas")).json()).total,
|
|
|
|
|
{ timeout: 20000 },
|
|
|
|
|
)
|
|
|
|
|
.toBe(620);
|
|
|
|
|
}
|
|
|
|
|
await page.getByRole("textbox", { name: "搜索 Alpha" }).fill("TEST006");
|
|
|
|
|
await page.getByRole("button", { name: "查询", exact: true }).click();
|
2026-09-08 10:40:49 +08:00
|
|
|
await expect(page.locator(".alpha-link")).toHaveCount(6);
|
2026-09-07 14:54:20 +08:00
|
|
|
const selectAll = page.getByRole("checkbox", { name: "Select all rows" });
|
|
|
|
|
await selectAll.focus();
|
|
|
|
|
await selectAll.press("Space");
|
|
|
|
|
await expect(selectAll).toBeChecked();
|
|
|
|
|
await page.getByRole("button", { name: "批量编辑", exact: true }).click();
|
|
|
|
|
await page.getByRole("textbox", { name: "批量添加标签" }).fill("批量验收");
|
|
|
|
|
await page
|
|
|
|
|
.getByRole("dialog")
|
|
|
|
|
.getByRole("button", { name: "confirm", exact: true })
|
|
|
|
|
.click();
|
|
|
|
|
await expect
|
|
|
|
|
.poll(
|
|
|
|
|
async () =>
|
|
|
|
|
(
|
|
|
|
|
await (
|
|
|
|
|
await page.request.get(
|
|
|
|
|
"/api/v1/alphas?tag=" + encodeURIComponent("批量验收"),
|
|
|
|
|
)
|
|
|
|
|
).json()
|
|
|
|
|
).total,
|
|
|
|
|
)
|
2026-09-08 10:40:49 +08:00
|
|
|
.toBe(6);
|
2026-09-07 14:54:20 +08:00
|
|
|
await page.getByRole("button", { name: "显示列设置" }).click();
|
|
|
|
|
await page
|
|
|
|
|
.locator(".columns-picker")
|
2026-09-09 19:02:37 +08:00
|
|
|
.getByText("自定义标签", { exact: true })
|
2026-09-07 14:54:20 +08:00
|
|
|
.click();
|
|
|
|
|
await expect(
|
2026-09-09 19:02:37 +08:00
|
|
|
page.getByRole("columnheader", { name: "自定义标签", exact: true }),
|
|
|
|
|
).toHaveCount(0);
|
|
|
|
|
await page
|
|
|
|
|
.locator(".columns-picker")
|
|
|
|
|
.getByText("自定义标签", { exact: true })
|
|
|
|
|
.click();
|
|
|
|
|
await expect(
|
|
|
|
|
page.getByRole("columnheader", { name: "自定义标签", exact: true }),
|
2026-09-07 14:54:20 +08:00
|
|
|
).toBeVisible();
|
|
|
|
|
await page.getByRole("button", { name: "显示列设置" }).click();
|
|
|
|
|
await page.getByRole("button", { name: "重置", exact: true }).click();
|
|
|
|
|
await expect(page.locator(".alpha-link")).toHaveCount(25);
|
|
|
|
|
const firstId = await page.locator(".alpha-link").first().innerText();
|
|
|
|
|
await page.locator(".semi-page-item").filter({ hasText: /^2$/ }).click();
|
|
|
|
|
await expect(page.locator(".alpha-link").first()).not.toHaveText(firstId);
|
2026-09-08 21:29:21 +08:00
|
|
|
await page.getByRole("button", { name: "用户菜单", exact: true }).click();
|
|
|
|
|
await page.getByRole("button", { name: "查看个人信息", exact: true }).click();
|
|
|
|
|
await expect(page.locator(".account-page .preferences-form")).toHaveCount(0);
|
|
|
|
|
await page.getByRole("button", { name: "配置", exact: true }).click();
|
2026-09-07 14:54:20 +08:00
|
|
|
await page.getByRole("textbox", { name: "显示名称" }).fill("Alpha 研究室");
|
|
|
|
|
await page.getByRole("button", { name: "保存偏好", exact: true }).click();
|
|
|
|
|
await expect(page.locator(".account-name")).toHaveText("Alpha 研究室");
|
|
|
|
|
await page.reload();
|
2026-09-08 21:29:21 +08:00
|
|
|
await page.getByRole("button", { name: "配置", exact: true }).click();
|
2026-09-07 14:54:20 +08:00
|
|
|
await expect(page.getByRole("textbox", { name: "显示名称" })).toHaveValue(
|
|
|
|
|
"Alpha 研究室",
|
|
|
|
|
);
|
|
|
|
|
await page.screenshot({
|
|
|
|
|
path: "../output/playwright/account.png",
|
|
|
|
|
fullPage: true,
|
|
|
|
|
});
|
|
|
|
|
});
|