2026-09-08 10:40:49 +08:00
|
|
|
import { expect, test } from "@playwright/test";
|
|
|
|
|
|
|
|
|
|
test("submission tabs, day selection, local correlation and reload", async ({
|
|
|
|
|
page,
|
|
|
|
|
}) => {
|
|
|
|
|
const errors: string[] = [];
|
|
|
|
|
page.on("pageerror", (error) => errors.push(error.message));
|
2026-09-10 14:09:39 +08:00
|
|
|
const failedChecks = [
|
|
|
|
|
"LOW_SHARPE",
|
|
|
|
|
"LOW_FITNESS",
|
|
|
|
|
"LOW_GLB_AMER_SHARPE",
|
|
|
|
|
"LOW_GLB_EMEA_SHARPE",
|
|
|
|
|
"LOW_GLB_APAC_SHARPE",
|
|
|
|
|
"LOW_2Y_SHARPE",
|
|
|
|
|
];
|
|
|
|
|
await page.route("**/api/v1/alphas?*", async (route) => {
|
|
|
|
|
const response = await route.fetch();
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
if (data.items?.length) data.items[0].failed_checks = failedChecks;
|
|
|
|
|
await route.fulfill({ response, json: data });
|
|
|
|
|
});
|
2026-09-09 18:20:08 +08:00
|
|
|
await page.goto("/#alphas");
|
2026-09-08 10:40:49 +08:00
|
|
|
await page.getByLabel("密码", { exact: true }).fill("browser-test-password");
|
|
|
|
|
await page.getByRole("button", { name: "进入工作空间" }).click();
|
|
|
|
|
await expect(
|
2026-09-09 08:34:34 +08:00
|
|
|
page.getByRole("tab", { name: "待提交", exact: true }),
|
2026-09-08 10:40:49 +08:00
|
|
|
).toBeVisible();
|
|
|
|
|
const headers = { "X-WQ-Request": "1" };
|
|
|
|
|
const credentials = await page.request.put("/api/v1/account/credentials", {
|
|
|
|
|
headers,
|
|
|
|
|
data: { email: "test@example.com", password: "synthetic-password" },
|
|
|
|
|
});
|
|
|
|
|
expect(credentials.ok()).toBe(true);
|
|
|
|
|
expect(
|
|
|
|
|
(await page.request.post("/api/v1/account/connect", { headers })).ok(),
|
|
|
|
|
).toBe(true);
|
|
|
|
|
await expect
|
|
|
|
|
.poll(
|
|
|
|
|
async () =>
|
|
|
|
|
(await (await page.request.get("/api/v1/account")).json())
|
|
|
|
|
.connection_status,
|
|
|
|
|
)
|
|
|
|
|
.toBe("connected");
|
|
|
|
|
await page.reload();
|
|
|
|
|
await page.getByRole("button", { name: "Alpha 管理", exact: true }).click();
|
|
|
|
|
await page.getByRole("tab", { name: "已提交", exact: true }).click();
|
|
|
|
|
await page.getByRole("button", { name: "按天同步", exact: true }).click();
|
|
|
|
|
await expect(page.getByRole("dialog")).toContainText("提交日期");
|
|
|
|
|
await page.getByRole("button", { name: "开始同步", exact: true }).click();
|
|
|
|
|
await expect(page.getByRole("alert")).toContainText("请选择有效的起止日期");
|
|
|
|
|
// Separate validation from submission; Semi ignores repeated OK clicks within 100 ms.
|
|
|
|
|
await page
|
|
|
|
|
.getByRole("dialog")
|
|
|
|
|
.getByRole("button", { name: "cancel", exact: true })
|
|
|
|
|
.click();
|
|
|
|
|
await page.getByRole("button", { name: "按天同步", exact: true }).click();
|
|
|
|
|
await page.getByLabel("同步开始日期").fill("2025-02-02");
|
|
|
|
|
await page.getByLabel("同步结束日期").fill("2025-02-02");
|
|
|
|
|
const request = page.waitForResponse(
|
|
|
|
|
(response) =>
|
|
|
|
|
response.url().endsWith("/api/v1/sync-jobs") &&
|
|
|
|
|
response.request().method() === "POST",
|
|
|
|
|
);
|
|
|
|
|
await page.getByRole("button", { name: "开始同步", exact: true }).click();
|
|
|
|
|
const job = await (await request).json();
|
|
|
|
|
expect(job.payload).toMatchObject({
|
|
|
|
|
submission: "SUBMITTED",
|
|
|
|
|
date_from: "2025-02-02",
|
|
|
|
|
date_to: "2025-02-02",
|
|
|
|
|
});
|
|
|
|
|
await expect
|
|
|
|
|
.poll(
|
|
|
|
|
async () =>
|
|
|
|
|
(await (await page.request.get(`/api/v1/sync-jobs/${job.id}`)).json())
|
|
|
|
|
.status,
|
|
|
|
|
)
|
|
|
|
|
.toBe("completed");
|
|
|
|
|
const complete = await (
|
|
|
|
|
await page.request.get(`/api/v1/sync-jobs/${job.id}`)
|
|
|
|
|
).json();
|
|
|
|
|
expect(complete.processed).toBe(103);
|
|
|
|
|
await expect(page.locator(".job-panel")).toContainText("2025-02-02");
|
|
|
|
|
await page.keyboard.press("Escape");
|
2026-09-10 14:55:23 +08:00
|
|
|
await page.getByRole("button", { name: "筛选 Alpha", exact: true }).click();
|
2026-09-08 10:40:49 +08:00
|
|
|
await page.getByRole("textbox", { name: "搜索 Alpha" }).fill("TEST0003");
|
|
|
|
|
await page.getByRole("button", { name: "查询", exact: true }).click();
|
|
|
|
|
await expect(page.locator(".alpha-link")).toHaveCount(1);
|
2026-09-10 14:09:39 +08:00
|
|
|
const failedCell = page.locator(".alpha-failed-checks");
|
|
|
|
|
await failedCell.scrollIntoViewIfNeeded();
|
|
|
|
|
expect((await failedCell.boundingBox())!.height).toBeLessThanOrEqual(24);
|
|
|
|
|
await failedCell.hover();
|
|
|
|
|
await expect(page.getByRole("tooltip")).toHaveText(failedChecks.join("\n"));
|
|
|
|
|
await page.screenshot({
|
|
|
|
|
path: "../output/playwright/alpha-failed-checks-tooltip.png",
|
|
|
|
|
animations: "disabled",
|
|
|
|
|
});
|
|
|
|
|
await page.getByRole("tab", { name: "待提交", exact: true }).hover();
|
|
|
|
|
|
2026-09-08 10:40:49 +08:00
|
|
|
await page.getByRole("tab", { name: "待提交", exact: true }).click();
|
2026-09-10 14:55:23 +08:00
|
|
|
await page.getByRole("button", { name: "筛选 Alpha", exact: true }).click();
|
2026-09-08 10:40:49 +08:00
|
|
|
await expect(page.getByRole("textbox", { name: "搜索 Alpha" })).toHaveValue(
|
|
|
|
|
"",
|
|
|
|
|
);
|
2026-09-10 14:55:23 +08:00
|
|
|
await page.getByRole("button", { name: "关闭筛选", exact: true }).click();
|
2026-09-08 10:40:49 +08:00
|
|
|
await page.getByRole("button", { name: "按天同步", exact: true }).click();
|
2026-09-10 14:55:23 +08:00
|
|
|
await expect(
|
|
|
|
|
page.getByRole("dialog", { name: "按天同步待提交 Alpha" }),
|
|
|
|
|
).toContainText("创建日期");
|
2026-09-08 10:40:49 +08:00
|
|
|
await page.getByLabel("同步开始日期").fill("2025-01-01");
|
|
|
|
|
await page.getByLabel("同步结束日期").fill("2025-01-01");
|
|
|
|
|
await page.getByRole("button", { name: "开始同步", exact: true }).click();
|
|
|
|
|
await expect
|
|
|
|
|
.poll(
|
|
|
|
|
async () =>
|
|
|
|
|
(
|
|
|
|
|
await (
|
|
|
|
|
await page.request.get("/api/v1/alphas?submission=UNSUBMITTED")
|
|
|
|
|
).json()
|
|
|
|
|
).total,
|
|
|
|
|
)
|
|
|
|
|
.toBe(413);
|
|
|
|
|
await page.keyboard.press("Escape");
|
2026-09-10 14:55:23 +08:00
|
|
|
await page.getByRole("button", { name: "筛选 Alpha", exact: true }).click();
|
2026-09-08 10:40:49 +08:00
|
|
|
await page.getByRole("textbox", { name: "搜索 Alpha" }).fill("TEST0004");
|
|
|
|
|
await page.getByRole("button", { name: "查询", exact: true }).click();
|
2026-09-10 11:45:05 +08:00
|
|
|
await page.route("**/api/v1/alphas/TEST0004", async (route) => {
|
|
|
|
|
const response = await route.fetch();
|
|
|
|
|
const detail = await response.json();
|
2026-09-10 13:31:42 +08:00
|
|
|
detail.is_metrics.glbAmer = {
|
2026-09-10 11:45:05 +08:00
|
|
|
yearly: { sharpe: 1.25, status: "PASS" },
|
|
|
|
|
samples: 120,
|
|
|
|
|
enabled: false,
|
|
|
|
|
};
|
2026-09-10 13:31:42 +08:00
|
|
|
detail.is_metrics.glbApac = {
|
|
|
|
|
pnl: 439696,
|
|
|
|
|
bookSize: 20000000,
|
|
|
|
|
longCount: 215,
|
|
|
|
|
turnover: 0.0242,
|
|
|
|
|
sharpe: 0.85,
|
|
|
|
|
};
|
|
|
|
|
detail.is_metrics.glbEmea = { pnl: 571980, fitness: 0.19, sharpe: 0.72 };
|
|
|
|
|
detail.is_metrics.investabilityConstrained = {
|
|
|
|
|
pnl: 3696382,
|
|
|
|
|
longCount: 890,
|
|
|
|
|
turnover: 0.0919,
|
|
|
|
|
};
|
|
|
|
|
detail.is_metrics.history = [
|
|
|
|
|
{ status: "PASS", sharpe: 1.1 },
|
|
|
|
|
{ status: "FAIL", sharpe: 0.2 },
|
|
|
|
|
];
|
|
|
|
|
detail.is_metrics.emptyObject = {};
|
|
|
|
|
detail.is_metrics.emptyArray = [];
|
|
|
|
|
detail.is_metrics.unavailable = null;
|
2026-09-10 11:45:05 +08:00
|
|
|
await route.fulfill({ response, json: detail });
|
|
|
|
|
});
|
2026-09-10 14:55:23 +08:00
|
|
|
await expect(page.locator(".alpha-link")).toHaveCount(1);
|
2026-09-08 10:40:49 +08:00
|
|
|
await page.locator(".alpha-link").click();
|
2026-09-10 11:45:05 +08:00
|
|
|
const sheet = page.locator(".alpha-detail");
|
|
|
|
|
await expect(sheet.getByRole("tab")).toHaveText([
|
|
|
|
|
"概览与表达式",
|
|
|
|
|
"指标",
|
|
|
|
|
"相关性检查",
|
|
|
|
|
"基线比较",
|
|
|
|
|
"变体关系",
|
|
|
|
|
"研究信息",
|
|
|
|
|
]);
|
|
|
|
|
const width = await sheet
|
|
|
|
|
.locator(".semi-sidesheet-inner")
|
|
|
|
|
.evaluate((element) => element.getBoundingClientRect().width);
|
|
|
|
|
expect(width).toBeCloseTo(1440 * 0.8, 0);
|
|
|
|
|
await expect(
|
|
|
|
|
sheet.getByRole("button", { name: "研究此 Alpha 的变体" }),
|
|
|
|
|
).not.toBeVisible();
|
|
|
|
|
await sheet.getByRole("tab", { name: "指标", exact: true }).click();
|
|
|
|
|
const metrics = sheet.locator(".detail-section").filter({
|
|
|
|
|
has: page.getByRole("heading", { name: "IS 指标", exact: true }),
|
|
|
|
|
});
|
|
|
|
|
await expect(
|
|
|
|
|
metrics
|
2026-09-10 13:31:42 +08:00
|
|
|
.locator(".detail-field-group")
|
|
|
|
|
.filter({ has: page.locator("dt", { hasText: "yearly" }) })
|
|
|
|
|
.first(),
|
|
|
|
|
).toContainText("1.25");
|
2026-09-10 11:45:05 +08:00
|
|
|
await expect(
|
2026-09-10 13:31:42 +08:00
|
|
|
metrics.locator(".semi-tag").filter({ hasText: "PASS" }).first(),
|
2026-09-10 11:45:05 +08:00
|
|
|
).toBeVisible();
|
|
|
|
|
await expect(
|
|
|
|
|
metrics.locator(".semi-tag").filter({ hasText: "否" }),
|
|
|
|
|
).toBeVisible();
|
2026-09-10 13:31:42 +08:00
|
|
|
await expect(metrics).toContainText("[]");
|
|
|
|
|
await expect(metrics).toContainText("{}");
|
|
|
|
|
await expect(metrics).toContainText("未提供");
|
|
|
|
|
for (const [viewportWidth, columns] of [
|
|
|
|
|
[1440, 3],
|
|
|
|
|
[900, 2],
|
|
|
|
|
[600, 1],
|
|
|
|
|
[390, 1],
|
|
|
|
|
]) {
|
|
|
|
|
await page.setViewportSize({ width: viewportWidth, height: 1000 });
|
|
|
|
|
const grid = metrics.locator(
|
|
|
|
|
":scope > .detail-field-container > .detail-field-grid",
|
|
|
|
|
);
|
|
|
|
|
await expect
|
|
|
|
|
.poll(() =>
|
|
|
|
|
grid.evaluate(
|
|
|
|
|
(element) =>
|
|
|
|
|
getComputedStyle(element).gridTemplateColumns.split(" ").length,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
.toBe(columns);
|
|
|
|
|
const layout = await metrics.evaluate((element) => {
|
|
|
|
|
const groups = Array.from(
|
|
|
|
|
element.querySelectorAll<HTMLElement>(".detail-field-group"),
|
|
|
|
|
);
|
|
|
|
|
return {
|
|
|
|
|
fullRows: groups.every((group) => {
|
|
|
|
|
const box = group.getBoundingClientRect();
|
|
|
|
|
const parent = group.parentElement!.getBoundingClientRect();
|
|
|
|
|
return (
|
|
|
|
|
Math.abs(box.x - parent.x) < 1 &&
|
|
|
|
|
Math.abs(box.width - parent.width) < 1
|
|
|
|
|
);
|
|
|
|
|
}),
|
|
|
|
|
leftAligned: Array.from(element.querySelectorAll("dt, dd")).every(
|
|
|
|
|
(cell) => getComputedStyle(cell).textAlign === "left",
|
|
|
|
|
),
|
|
|
|
|
noOverflow: Array.from(
|
|
|
|
|
element.querySelectorAll<HTMLElement>(
|
|
|
|
|
".detail-field-container, dt, dd",
|
|
|
|
|
),
|
|
|
|
|
).every((cell) => cell.scrollWidth <= cell.clientWidth + 1),
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
expect(layout).toEqual({
|
|
|
|
|
fullRows: true,
|
|
|
|
|
leftAligned: true,
|
|
|
|
|
noOverflow: true,
|
|
|
|
|
});
|
|
|
|
|
await page.screenshot({
|
|
|
|
|
path: `../output/playwright/alpha-grid-${viewportWidth}.png`,
|
|
|
|
|
animations: "disabled",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
await page.setViewportSize({ width: 1440, height: 1000 });
|
2026-09-10 11:45:05 +08:00
|
|
|
await sheet.getByRole("tab", { name: "变体关系", exact: true }).click();
|
|
|
|
|
await expect(
|
|
|
|
|
sheet.getByRole("button", { name: "研究此 Alpha 的变体" }),
|
|
|
|
|
).toBeVisible();
|
|
|
|
|
await page.getByRole("tab", { name: "相关性检查", exact: true }).click();
|
2026-09-08 10:40:49 +08:00
|
|
|
await page.getByRole("button", { name: "检测自相关", exact: true }).click();
|
|
|
|
|
await expect
|
|
|
|
|
.poll(
|
|
|
|
|
async () =>
|
|
|
|
|
(
|
|
|
|
|
await (
|
|
|
|
|
await page.request.get("/api/v1/alphas/TEST0004/self-correlation")
|
|
|
|
|
).json()
|
|
|
|
|
).result?.status,
|
|
|
|
|
{ timeout: 20000 },
|
|
|
|
|
)
|
|
|
|
|
.toBe("high");
|
|
|
|
|
await page.keyboard.press("Escape");
|
|
|
|
|
await expect(page.locator(".job-panel")).not.toBeVisible();
|
|
|
|
|
await expect(page.locator(".correlation-panel")).toContainText("相关性偏高");
|
|
|
|
|
await expect(page.locator(".correlation-panel")).toContainText("1.0000");
|
2026-09-10 11:45:05 +08:00
|
|
|
const header = await sheet.locator(".detail-title").boundingBox();
|
|
|
|
|
const tabs = await sheet
|
|
|
|
|
.getByRole("tab", { name: "相关性检查", exact: true })
|
|
|
|
|
.boundingBox();
|
|
|
|
|
const content = sheet.locator(".alpha-detail-tabs > .semi-tabs-content");
|
|
|
|
|
await content.evaluate((element) => {
|
|
|
|
|
element.scrollTop = element.scrollHeight;
|
|
|
|
|
});
|
|
|
|
|
expect(
|
|
|
|
|
await content.evaluate((element) => element.scrollTop),
|
|
|
|
|
).toBeGreaterThan(0);
|
|
|
|
|
expect((await sheet.locator(".detail-title").boundingBox())!.y).toBe(
|
|
|
|
|
header!.y,
|
|
|
|
|
);
|
|
|
|
|
expect(
|
|
|
|
|
(await sheet
|
|
|
|
|
.getByRole("tab", { name: "相关性检查", exact: true })
|
|
|
|
|
.boundingBox())!.y,
|
|
|
|
|
).toBe(tabs!.y);
|
|
|
|
|
await expect(
|
|
|
|
|
sheet.getByRole("textbox", { name: "regular Description", exact: true }),
|
|
|
|
|
).toBeVisible();
|
|
|
|
|
await page.screenshot({
|
|
|
|
|
path: "../output/playwright/alpha-detail-scrolled.png",
|
|
|
|
|
});
|
|
|
|
|
await content.evaluate((element) => {
|
|
|
|
|
element.scrollTop = 0;
|
|
|
|
|
});
|
2026-09-08 10:40:49 +08:00
|
|
|
await page.screenshot({
|
|
|
|
|
path: "../output/playwright/local-correlation.png",
|
|
|
|
|
animations: "disabled",
|
|
|
|
|
});
|
|
|
|
|
await page.keyboard.press("Escape");
|
|
|
|
|
await page.reload();
|
2026-09-10 14:55:23 +08:00
|
|
|
await page.getByRole("button", { name: "筛选 Alpha", exact: true }).click();
|
2026-09-08 10:40:49 +08:00
|
|
|
await page.getByRole("textbox", { name: "搜索 Alpha" }).fill("TEST0004");
|
|
|
|
|
await page.getByRole("button", { name: "查询", exact: true }).click();
|
2026-09-09 08:34:34 +08:00
|
|
|
await expect(page.locator(".alpha-table:visible")).toContainText(
|
|
|
|
|
"相关性偏高",
|
|
|
|
|
);
|
2026-09-08 10:40:49 +08:00
|
|
|
await page.getByRole("tab", { name: "已提交", exact: true }).click();
|
|
|
|
|
await expect(
|
|
|
|
|
page.getByRole("button", { name: "全量同步已提交" }),
|
|
|
|
|
).toBeVisible();
|
|
|
|
|
for (const width of [1440, 390]) {
|
|
|
|
|
await page.setViewportSize({ width, height: 900 });
|
|
|
|
|
await expect(
|
|
|
|
|
page.getByRole("tab", { name: "已提交", exact: true }),
|
|
|
|
|
).toBeVisible();
|
|
|
|
|
await expect(
|
|
|
|
|
page.getByRole("button", { name: "按天同步", exact: true }),
|
|
|
|
|
).toBeVisible();
|
|
|
|
|
expect(
|
|
|
|
|
await page.evaluate(
|
|
|
|
|
() => document.documentElement.scrollWidth <= window.innerWidth,
|
|
|
|
|
),
|
|
|
|
|
).toBe(true);
|
|
|
|
|
await page.screenshot({
|
|
|
|
|
path: `../output/playwright/alpha-tabs-${width}.png`,
|
|
|
|
|
animations: "disabled",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
expect(errors).toEqual([]);
|
|
|
|
|
});
|