174 lines
5.7 KiB
TypeScript
174 lines
5.7 KiB
TypeScript
|
|
import { expect, test } from "@playwright/test";
|
||
|
|
|
||
|
|
// UI contract fixtures: no external model call or simulation is started by these tests.
|
||
|
|
const fixed = {
|
||
|
|
id: "semi-fixed-input",
|
||
|
|
dataset_id: "TEST_FIN",
|
||
|
|
field_ids: ["TEST_FIN_001"],
|
||
|
|
field_types: { TEST_FIN_001: "MATRIX" },
|
||
|
|
collection_version: "fixed-v1",
|
||
|
|
scope: {
|
||
|
|
instrument_type: "EQUITY",
|
||
|
|
region: "USA",
|
||
|
|
universe: "TOP3000",
|
||
|
|
delay: 1,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
for (const mode of ["templates", "variants", "features"] as const) {
|
||
|
|
test(`${mode}: AI generation keeps its hypothesis, fixed inputs and failure draft`, async ({
|
||
|
|
page,
|
||
|
|
}) => {
|
||
|
|
const errors: string[] = [];
|
||
|
|
page.on("pageerror", (error) => errors.push(error.message));
|
||
|
|
await page.route("**/api/v1/research/inputs", (route) =>
|
||
|
|
route.fulfill({ json: { items: [fixed] } }),
|
||
|
|
);
|
||
|
|
await page.route(
|
||
|
|
"**/api/v1/research/assets/semi-generated/versions",
|
||
|
|
(route) => route.fulfill({ json: [{ version: 1 }] }),
|
||
|
|
);
|
||
|
|
await page.goto(`/#${mode}`);
|
||
|
|
await page
|
||
|
|
.getByLabel("密码", { exact: true })
|
||
|
|
.fill("browser-test-password");
|
||
|
|
await page.getByRole("button", { name: "进入工作空间" }).click();
|
||
|
|
const feature = mode === "features";
|
||
|
|
await page
|
||
|
|
.getByRole("button", {
|
||
|
|
name: feature ? "新建特征方案" : "新建",
|
||
|
|
exact: true,
|
||
|
|
})
|
||
|
|
.click();
|
||
|
|
const sheet = page.locator(".research-detail:visible");
|
||
|
|
const input = sheet.getByRole("textbox", {
|
||
|
|
name: feature ? "特征经济假设" : "本次研究假设",
|
||
|
|
exact: true,
|
||
|
|
});
|
||
|
|
const generate = sheet.getByRole("button", {
|
||
|
|
name: feature ? "AI 生成新方案" : "AI 生成模板",
|
||
|
|
exact: true,
|
||
|
|
});
|
||
|
|
await expect(generate).toBeDisabled();
|
||
|
|
await sheet
|
||
|
|
.getByRole("combobox", {
|
||
|
|
name: feature ? "特征固定输入" : "固定研究输入",
|
||
|
|
exact: true,
|
||
|
|
})
|
||
|
|
.click();
|
||
|
|
await page
|
||
|
|
.getByRole("option")
|
||
|
|
.filter({ hasText: fixed.id.slice(0, 8) })
|
||
|
|
.click();
|
||
|
|
await input.click();
|
||
|
|
if (mode === "variants")
|
||
|
|
await sheet.getByLabel("种子 Alpha", { exact: true }).fill("TEST0001");
|
||
|
|
const hypothesis = "检验 <收益> 与规模\n第二轮保留原始假设";
|
||
|
|
await input.fill(hypothesis);
|
||
|
|
await expect(
|
||
|
|
sheet.locator(".research-ai-reference").filter({ hasText: fixed.id }),
|
||
|
|
).toBeVisible();
|
||
|
|
let release!: () => void;
|
||
|
|
const pending = new Promise<void>((resolve) => {
|
||
|
|
release = resolve;
|
||
|
|
});
|
||
|
|
let requestBody: Record<string, unknown> | undefined;
|
||
|
|
await page.route("**/api/v1/research/generate", async (route) => {
|
||
|
|
requestBody = route.request().postDataJSON();
|
||
|
|
await pending;
|
||
|
|
const template = {
|
||
|
|
name: "AI 生成验收",
|
||
|
|
description: "",
|
||
|
|
expression: "rank({field})",
|
||
|
|
category: "template",
|
||
|
|
variables: {
|
||
|
|
field: {
|
||
|
|
kind: "field",
|
||
|
|
field_type: "MATRIX",
|
||
|
|
values: ["TEST_FIN_001"],
|
||
|
|
},
|
||
|
|
},
|
||
|
|
};
|
||
|
|
await route.fulfill({
|
||
|
|
json: {
|
||
|
|
id: "semi-generated",
|
||
|
|
kind: feature ? "feature" : "template",
|
||
|
|
name: "AI 生成验收",
|
||
|
|
version: 1,
|
||
|
|
content: feature
|
||
|
|
? {
|
||
|
|
name: "AI 生成验收",
|
||
|
|
hypothesis,
|
||
|
|
input_ids: [fixed.id],
|
||
|
|
steps: [
|
||
|
|
{
|
||
|
|
name: "排名",
|
||
|
|
rationale: "降低尺度差异",
|
||
|
|
expression: "rank(TEST_FIN_001)",
|
||
|
|
},
|
||
|
|
],
|
||
|
|
template,
|
||
|
|
}
|
||
|
|
: template,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
});
|
||
|
|
await input.press("Shift+Enter");
|
||
|
|
await expect
|
||
|
|
.poll(() => requestBody)
|
||
|
|
.toMatchObject({
|
||
|
|
hypothesis,
|
||
|
|
input_ids: [fixed.id],
|
||
|
|
method: feature
|
||
|
|
? "feature"
|
||
|
|
: mode === "variants"
|
||
|
|
? "structure"
|
||
|
|
: "template",
|
||
|
|
});
|
||
|
|
if (mode === "variants")
|
||
|
|
expect(requestBody?.parent_alpha_ids).toEqual(["TEST0001"]);
|
||
|
|
await expect(generate).toBeDisabled();
|
||
|
|
await expect(input).toContainText("第二轮保留原始假设");
|
||
|
|
release();
|
||
|
|
await expect(
|
||
|
|
sheet.getByRole("textbox", { name: "模板名称", exact: true }),
|
||
|
|
).toHaveValue("AI 生成验收");
|
||
|
|
await expect(generate).toBeEnabled();
|
||
|
|
await expect(input).toContainText("检验 <收益> 与规模");
|
||
|
|
await page.screenshot({ path: `../output/playwright/semi-ai-${mode}.png` });
|
||
|
|
await page.setViewportSize({ width: 390, height: 900 });
|
||
|
|
await expect(generate).toBeInViewport();
|
||
|
|
expect(await sheet.evaluate((el) => el.scrollWidth <= el.clientWidth)).toBe(
|
||
|
|
true,
|
||
|
|
);
|
||
|
|
await page.screenshot({
|
||
|
|
path: `../output/playwright/semi-ai-${mode}-390.png`,
|
||
|
|
});
|
||
|
|
if (mode === "templates") {
|
||
|
|
await page.evaluate(() =>
|
||
|
|
document.body.setAttribute("theme-mode", "dark"),
|
||
|
|
);
|
||
|
|
await page.screenshot({
|
||
|
|
path: "../output/playwright/semi-ai-templates-dark.png",
|
||
|
|
});
|
||
|
|
await page.evaluate(() =>
|
||
|
|
document.body.setAttribute("theme-mode", "light"),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
await page.route("**/api/v1/research/generate", (route) =>
|
||
|
|
route.fulfill({ status: 503, json: { detail: "模拟模型暂时不可用" } }),
|
||
|
|
);
|
||
|
|
await generate.click();
|
||
|
|
await expect(
|
||
|
|
sheet.getByText("模拟模型暂时不可用", { exact: true }),
|
||
|
|
).toBeVisible();
|
||
|
|
await expect(input).toContainText("第二轮保留原始假设");
|
||
|
|
await expect(generate).toBeEnabled();
|
||
|
|
await input.fill("a".repeat(10001));
|
||
|
|
await expect(generate).toBeDisabled();
|
||
|
|
await expect(
|
||
|
|
sheet.getByRole("alert").filter({ hasText: "10,000" }),
|
||
|
|
).toBeVisible();
|
||
|
|
expect(errors).toEqual([]);
|
||
|
|
});
|
||
|
|
}
|