2026-09-07 23:02:55 +08:00
|
|
|
import { expect, test, type Page } from "@playwright/test";
|
|
|
|
|
|
|
|
|
|
const headers = { "X-WQ-Request": "1" };
|
|
|
|
|
async function login(page: Page) {
|
|
|
|
|
await page.goto("/");
|
|
|
|
|
await page.getByLabel("密码", { exact: true }).fill("browser-test-password");
|
|
|
|
|
await page.getByRole("button", { name: "进入工作空间" }).click();
|
|
|
|
|
await expect(
|
|
|
|
|
page.getByRole("button", { name: "打开研究助手" }),
|
|
|
|
|
).toBeVisible();
|
|
|
|
|
}
|
|
|
|
|
async function configure(page: Page) {
|
|
|
|
|
const body = { base_url: "https://model.test/v1", model: "test-model" };
|
|
|
|
|
await page.request.put("/api/v1/ai/settings", {
|
|
|
|
|
headers,
|
|
|
|
|
data: { ...body, api_key: "synthetic-model-key" },
|
|
|
|
|
});
|
|
|
|
|
const result = await page.request.post("/api/v1/ai/settings/test", {
|
|
|
|
|
headers,
|
|
|
|
|
});
|
|
|
|
|
expect((await result.json()).ready).toBe(true);
|
|
|
|
|
await page.request.put("/api/v1/ai/settings", {
|
|
|
|
|
headers,
|
|
|
|
|
data: { ...body, enabled: true },
|
|
|
|
|
});
|
|
|
|
|
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" },
|
|
|
|
|
});
|
|
|
|
|
await expect
|
|
|
|
|
.poll(
|
|
|
|
|
async () =>
|
|
|
|
|
(await (await page.request.get("/api/v1/alphas")).json()).total,
|
|
|
|
|
)
|
|
|
|
|
.toBe(620);
|
|
|
|
|
await page.reload();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test("model settings, text stream, contextual query, confirmation and preserved draft", async ({
|
|
|
|
|
page,
|
|
|
|
|
}) => {
|
|
|
|
|
const errors: string[] = [];
|
|
|
|
|
page.on("pageerror", (error) => errors.push(error.message));
|
|
|
|
|
await login(page);
|
|
|
|
|
await page.getByRole("button", { name: "个人信息", exact: true }).click();
|
|
|
|
|
await page
|
|
|
|
|
.getByRole("textbox", { name: "模型 Base URL" })
|
|
|
|
|
.fill("https://model.test/v1");
|
|
|
|
|
await page
|
|
|
|
|
.getByRole("textbox", { name: "模型标识", exact: true })
|
|
|
|
|
.fill("test-model");
|
|
|
|
|
await page.getByLabel("模型 API Key").fill("synthetic-model-key");
|
|
|
|
|
await page.getByRole("button", { name: "保存模型配置" }).click();
|
|
|
|
|
await expect(page.getByLabel("模型 API Key")).toHaveValue("");
|
|
|
|
|
await page.getByRole("button", { name: "测试连接" }).click();
|
|
|
|
|
await expect(page.getByText("工具调用与结果回传成功")).toBeVisible();
|
|
|
|
|
await page
|
|
|
|
|
.getByText("启用研究助手(能力测试通过后可开启)", { exact: true })
|
|
|
|
|
.click();
|
|
|
|
|
await page.getByRole("button", { name: "保存模型配置" }).click();
|
|
|
|
|
await configure(page);
|
|
|
|
|
await page.getByRole("button", { name: "Alpha 管理", exact: true }).click();
|
|
|
|
|
await page.getByRole("textbox", { name: "搜索 Alpha" }).fill("TEST0001");
|
|
|
|
|
await page.getByRole("button", { name: "查询", exact: true }).click();
|
|
|
|
|
await page.locator(".alpha-link").click();
|
|
|
|
|
await page.getByRole("tab", { name: "研究记录" }).click();
|
|
|
|
|
await page
|
|
|
|
|
.getByRole("textbox", { name: "研究备注" })
|
|
|
|
|
.fill("尚未保存的人工研究草稿");
|
|
|
|
|
// A global launcher must remain reachable when the detail mask is open.
|
|
|
|
|
await page.getByRole("button", { name: "打开研究助手" }).click();
|
|
|
|
|
const chat = page.getByRole("complementary", { name: "AI 研究助手" });
|
|
|
|
|
await chat.getByRole("button", { name: "新会话", exact: true }).click();
|
|
|
|
|
await expect(chat.locator(".ai-context")).toContainText("TEST0001");
|
|
|
|
|
const input = chat.getByRole("textbox", { name: "发送给研究助手" });
|
|
|
|
|
await input.fill("修改当前 Alpha 的备注");
|
|
|
|
|
await chat.getByRole("button", { name: "发送", exact: true }).click();
|
|
|
|
|
await expect(chat.getByRole("button", { name: "确认执行" })).toBeEnabled();
|
|
|
|
|
const before = await (
|
|
|
|
|
await page.request.get("/api/v1/alphas/TEST0001")
|
|
|
|
|
).json();
|
|
|
|
|
expect(before.research.note).not.toBe("AI 测试研究记录");
|
|
|
|
|
await page.screenshot({
|
|
|
|
|
path: "../output/playwright/ai-approval.png",
|
|
|
|
|
fullPage: true,
|
|
|
|
|
});
|
|
|
|
|
await chat.getByRole("button", { name: "确认执行" }).click();
|
|
|
|
|
await expect
|
|
|
|
|
.poll(
|
|
|
|
|
async () =>
|
|
|
|
|
(await (await page.request.get("/api/v1/alphas/TEST0001")).json())
|
|
|
|
|
.research.note,
|
|
|
|
|
)
|
|
|
|
|
.toBe("AI 测试研究记录");
|
|
|
|
|
await expect(chat.locator(".ai-run-state")).toContainText("已完成");
|
|
|
|
|
await expect(page.getByRole("textbox", { name: "研究备注" })).toHaveValue(
|
|
|
|
|
"尚未保存的人工研究草稿",
|
|
|
|
|
);
|
|
|
|
|
await expect(page.getByText(/研究记录已被其他操作更新/)).toBeVisible();
|
|
|
|
|
await input.fill("筛选换手率不超过 15% 的 Alpha");
|
|
|
|
|
await chat.getByRole("button", { name: "发送", exact: true }).click();
|
|
|
|
|
await expect(chat.getByRole("button", { name: "应用到列表" })).toBeVisible();
|
|
|
|
|
const conversation = (
|
|
|
|
|
await (await page.request.get("/api/v1/ai/conversations")).json()
|
|
|
|
|
)[0];
|
|
|
|
|
const history = await (
|
|
|
|
|
await page.request.get(`/api/v1/ai/conversations/${conversation.id}`)
|
|
|
|
|
).json();
|
|
|
|
|
expect(JSON.stringify(history)).not.toContain("尚未保存的人工研究草稿");
|
|
|
|
|
expect(history.runs.at(-1).tools[0].result.filters.turnover_max).toBe(0.15);
|
|
|
|
|
await page.reload();
|
|
|
|
|
await page.getByRole("button", { name: "打开研究助手" }).click();
|
|
|
|
|
await expect(
|
|
|
|
|
chat.getByText("筛选换手率不超过 15% 的 Alpha", { exact: true }),
|
|
|
|
|
).toBeVisible();
|
|
|
|
|
await chat.getByRole("button", { name: "应用到列表" }).click();
|
|
|
|
|
await expect(page.getByRole("textbox", { name: "搜索 Alpha" })).toHaveValue(
|
|
|
|
|
"",
|
|
|
|
|
);
|
|
|
|
|
await page.screenshot({
|
|
|
|
|
path: "../output/playwright/ai-query.png",
|
|
|
|
|
fullPage: true,
|
|
|
|
|
});
|
|
|
|
|
expect(errors).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("collapse, navigation, reload, narrow viewport and cancellation preserve execution", async ({
|
|
|
|
|
page,
|
|
|
|
|
}) => {
|
|
|
|
|
await login(page);
|
|
|
|
|
await configure(page);
|
|
|
|
|
await page.getByRole("button", { name: "打开研究助手" }).click();
|
|
|
|
|
const chat = page.getByRole("complementary", { name: "AI 研究助手" });
|
|
|
|
|
await chat.getByRole("button", { name: "新会话", exact: true }).click();
|
|
|
|
|
await chat.getByRole("textbox", { name: "发送给研究助手" }).fill("SLOW");
|
|
|
|
|
await chat.getByRole("button", { name: "发送", exact: true }).click();
|
|
|
|
|
await expect(chat.getByText("正在查询", { exact: true })).toBeVisible();
|
|
|
|
|
await chat.getByRole("button", { name: "收起研究助手" }).click();
|
|
|
|
|
await page.getByRole("button", { name: "个人信息", exact: true }).click();
|
|
|
|
|
await page.reload();
|
|
|
|
|
await page.getByRole("button", { name: "打开研究助手" }).click();
|
|
|
|
|
await expect(
|
|
|
|
|
chat.getByText("正在查询,查询完成。", { exact: true }),
|
|
|
|
|
).toBeVisible({ timeout: 10000 });
|
|
|
|
|
await page.setViewportSize({ width: 850, height: 900 });
|
|
|
|
|
await chat
|
|
|
|
|
.getByRole("textbox", { name: "发送给研究助手" })
|
|
|
|
|
.fill("SLOW again");
|
|
|
|
|
await chat.getByRole("button", { name: "发送", exact: true }).click();
|
|
|
|
|
await expect(chat.getByRole("button", { name: "停止生成" })).toBeVisible();
|
|
|
|
|
await chat.getByRole("button", { name: "停止生成" }).click();
|
|
|
|
|
await expect(chat.locator(".ai-run-state")).toContainText("已停止");
|
|
|
|
|
await chat.getByRole("textbox", { name: "发送给研究助手" }).focus();
|
|
|
|
|
await page.keyboard.press("Escape");
|
|
|
|
|
await expect(chat).not.toBeVisible();
|
|
|
|
|
await page.getByRole("button", { name: "退出登录" }).click();
|
|
|
|
|
await expect(
|
|
|
|
|
page.getByRole("heading", { name: "登录研究工作空间" }),
|
|
|
|
|
).toBeVisible();
|
|
|
|
|
expect((await page.request.get("/api/v1/ai/conversations")).status()).toBe(
|
|
|
|
|
401,
|
|
|
|
|
);
|
|
|
|
|
});
|
2026-09-07 23:31:23 +08:00
|
|
|
|
|
|
|
|
test("Lark workspace keeps pagination, account details and chat usable at every width", async ({
|
|
|
|
|
page,
|
|
|
|
|
}) => {
|
|
|
|
|
await login(page);
|
|
|
|
|
await configure(page);
|
|
|
|
|
await page.getByRole("button", { name: "Alpha 管理", exact: true }).click();
|
|
|
|
|
await expect(page.locator(".alpha-link")).toHaveCount(25);
|
|
|
|
|
await expect(page.locator(".sidebar")).toHaveCSS(
|
|
|
|
|
"background-color",
|
|
|
|
|
"rgb(249, 249, 249)",
|
|
|
|
|
);
|
|
|
|
|
await expect(page.locator(".nav-item.active")).toHaveCSS(
|
|
|
|
|
"background-color",
|
|
|
|
|
"rgba(31, 35, 41, 0.05)",
|
|
|
|
|
);
|
|
|
|
|
const chat = page.getByRole("complementary", { name: "AI 研究助手" });
|
|
|
|
|
for (const width of [1920, 1440, 1280, 850, 390]) {
|
|
|
|
|
await page.setViewportSize({ width, height: 900 });
|
|
|
|
|
const footer = page.locator('footer[aria-label="Alpha 分页"]');
|
|
|
|
|
await expect(footer).toBeVisible();
|
|
|
|
|
const before = (await footer.boundingBox())!;
|
|
|
|
|
expect(before.y + before.height).toBeLessThanOrEqual(900);
|
|
|
|
|
expect(before.y + before.height).toBeGreaterThan(864);
|
|
|
|
|
await page
|
|
|
|
|
.locator(".semi-table-body")
|
|
|
|
|
.evaluate((el) => el.scrollTo({ top: 1000 }));
|
|
|
|
|
expect((await footer.boundingBox())!.y).toBeCloseTo(before.y, 0);
|
|
|
|
|
await page.getByRole("button", { name: "打开研究助手" }).click();
|
|
|
|
|
await expect(chat).toBeVisible();
|
|
|
|
|
await expect(chat).toHaveCSS("background-color", "rgb(255, 255, 255)");
|
|
|
|
|
const box = (await chat.boundingBox())!;
|
|
|
|
|
expect(box.x + box.width).toBeCloseTo(width, 0);
|
|
|
|
|
if (width >= 1440) {
|
|
|
|
|
expect(
|
|
|
|
|
(await page.locator(".main-shell").boundingBox())!.width,
|
|
|
|
|
).toBeGreaterThan(600);
|
|
|
|
|
expect(
|
|
|
|
|
(await footer.boundingBox())!.x + (await footer.boundingBox())!.width,
|
|
|
|
|
).toBeLessThan(box.x);
|
|
|
|
|
} else {
|
|
|
|
|
await expect(page.locator(".main-shell")).toHaveAttribute("inert", "");
|
|
|
|
|
await expect(page.locator(".ai-mask")).toBeVisible();
|
|
|
|
|
}
|
|
|
|
|
expect(
|
|
|
|
|
await page.evaluate(
|
|
|
|
|
() => document.documentElement.scrollWidth <= window.innerWidth,
|
|
|
|
|
),
|
|
|
|
|
).toBe(true);
|
|
|
|
|
await page.screenshot({
|
|
|
|
|
path: `../output/playwright/lark-chat-${width}.png`,
|
|
|
|
|
});
|
|
|
|
|
await chat.getByRole("button", { name: "收起研究助手" }).click();
|
|
|
|
|
}
|
|
|
|
|
await page.setViewportSize({ width: 1440, height: 1000 });
|
|
|
|
|
await page.getByRole("button", { name: "个人信息", exact: true }).click();
|
|
|
|
|
await expect(page.getByRole("button", { name: "连接设置" })).toHaveAttribute(
|
|
|
|
|
"aria-expanded",
|
|
|
|
|
"false",
|
|
|
|
|
);
|
|
|
|
|
await expect(page.locator(".permission-list li")).toHaveCount(10);
|
|
|
|
|
await expect(page.getByRole("row", { name: /回测/ })).toContainText(
|
|
|
|
|
"10,000(本地设定)",
|
|
|
|
|
);
|
|
|
|
|
await page
|
|
|
|
|
.getByRole("textbox", { name: "模型标识", exact: true })
|
|
|
|
|
.fill("保留模型设置草稿");
|
|
|
|
|
await page.getByRole("button", { name: "打开研究助手" }).click();
|
|
|
|
|
const resize = chat.getByRole("separator", { name: "调整助手宽度" });
|
|
|
|
|
await resize.focus();
|
|
|
|
|
for (let i = 0; i < 11; i++) await resize.press("ArrowLeft");
|
|
|
|
|
expect((await chat.boundingBox())!.width).toBe(640);
|
|
|
|
|
await chat.getByRole("button", { name: "新会话", exact: true }).click();
|
|
|
|
|
const input = chat.getByRole("textbox", { name: "发送给研究助手" });
|
|
|
|
|
await input.fill("保留聊天草稿");
|
|
|
|
|
await page.getByRole("button", { name: "Alpha 管理", exact: true }).click();
|
|
|
|
|
await page.getByRole("button", { name: "个人信息", exact: true }).click();
|
|
|
|
|
await expect(input).toHaveValue("保留聊天草稿");
|
|
|
|
|
await expect(
|
|
|
|
|
page.getByRole("textbox", { name: "模型标识", exact: true }),
|
|
|
|
|
).toHaveValue("保留模型设置草稿");
|
|
|
|
|
await chat.getByRole("button", { name: "收起研究助手" }).click();
|
|
|
|
|
await page.screenshot({
|
|
|
|
|
path: "../output/playwright/lark-account.png",
|
|
|
|
|
fullPage: true,
|
|
|
|
|
});
|
|
|
|
|
});
|