Files
fa-agent/tests/e2e/workspace.spec.ts
2026-09-26 01:12:10 +08:00

274 lines
12 KiB
TypeScript

import { test, expect } from "@playwright/test";
import type { Page } from "@playwright/test";
import fs from "node:fs";
const password =
process.env.E2E_PASSWORD ||
fs.readFileSync("var/local-access.txt", "utf8").match(/密码:(.+)/)![1];
const csv =
"期间,科室,学科,医生等级,医生,产品一级分类,实际收入,成本,毛利贡献,客户编码\n2025-08,测试儿保,普儿,高级,医生A,医疗服务,10000,4000,6000,C1\n2025-08,测试儿保,普儿,高级,医生B,医疗服务,10000,4000,6000,C2\n2026-08,测试儿保,普儿,高级,医生A,医疗服务,5000,4000,3000,C1\n2026-08,测试儿保,普儿,高级,医生B,医疗服务,5000,4000,3000,C2\n";
async function login(page: Page) {
await page.goto("/");
await page.getByLabel("用户名", { exact: true }).fill("admin");
await page.getByLabel("密码", { exact: true }).fill(password);
await page.getByRole("button", { name: "登录", exact: true }).click();
await expect(page.getByRole("button", { name: "导入新表格" })).toBeVisible();
}
test("桌面:真实导入、异常、下钻、证据、设置、Copilot、导出", async ({
page,
}) => {
await page.setViewportSize({ width: 1512, height: 982 });
const errors: string[] = [];
page.on("pageerror", (e) => errors.push(e.message));
await login(page);
const uploaded = page.waitForResponse(
(r) => r.url().endsWith("/api/datasets") && r.request().method() === "POST",
);
await page.getByLabel("上传财务表格").setInputFiles({
name: "e2e-finance.csv",
mimeType: "text/csv",
buffer: Buffer.from(csv.replaceAll("C2", `C2-${Date.now()}`)),
});
await uploaded;
const dialog = page.getByRole("dialog");
await expect(dialog.getByRole("heading", { name: "确认导入字段" })).toBeVisible();
await dialog.getByRole("button", { name: "确认并导入" }).click();
await expect(
page.getByRole("heading", { name: "集团经营总览", exact: true }),
).toBeVisible({ timeout: 60000 });
await expect(page.locator(".kpi").first()).toContainText("10,000");
await page.getByRole("button", { name: "分析设置", exact: true }).click();
await page.getByLabel("最小样本数").fill("1");
await page.getByRole("button", { name: "保存设置", exact: true }).click();
await expect(page.locator(".kpi").first()).toContainText("10,000");
await page.screenshot({
path: "test-results/desktop-overview.png",
fullPage: true,
});
await page
.getByRole("button", { name: "异常追踪", exact: false })
.first()
.click();
await expect(
page.getByRole("heading", { name: "1 个分组需要关注" }),
).toBeVisible();
await page.getByLabel("下钻测试儿保", { exact: true }).click();
await expect(
page.getByRole("heading", { name: "测试儿保 · 异常调查" }),
).toBeVisible();
await page.getByLabel("下钻普儿", { exact: true }).click();
await expect(
page.getByRole("heading", { name: "普儿 · 异常调查" }),
).toBeVisible();
await page.getByLabel("下钻高级", { exact: true }).click();
await expect(
page.getByRole("heading", { name: "高级 · 异常调查" }),
).toBeVisible();
await page.getByLabel("调查状态", { exact: true }).selectOption("reviewing");
await page.getByLabel("调查备注", { exact: true }).fill("核对退款与产品口径");
await page.getByRole("button", { name: "保存调查记录", exact: true }).click();
await expect(
page.getByRole("button", { name: "已保存", exact: true }),
).toBeVisible();
await page
.getByRole("navigation", { name: "分析路径" })
.getByRole("button", { name: "普儿", exact: true })
.click();
await page.getByLabel("下钻高级", { exact: true }).click();
await expect(page.getByLabel("调查备注", { exact: true })).toHaveValue(
"核对退款与产品口径",
);
await expect(page.getByLabel("调查状态", { exact: true })).toHaveValue(
"reviewing",
);
await page.getByLabel("查看医生A证据", { exact: true }).click();
const evidenceDialog = page.getByRole("dialog");
await expect(evidenceDialog).toContainText("1 条");
await expect(evidenceDialog).toContainText("2026-08");
await expect(evidenceDialog).toContainText("CSV");
await evidenceDialog.getByRole("button", { name: "基期", exact: true }).click();
await expect(evidenceDialog).toContainText("2025-08");
await evidenceDialog.getByLabel("关闭", { exact: true }).click();
const evidenceOffsets: number[] = [];
await page.route("**/api/datasets/*/evidence", async (route) => {
const request = route.request().postDataJSON() as { offset?: number };
const offset = request.offset || 0;
evidenceOffsets.push(offset);
const count = offset === 0 ? 50 : 1;
await route.fulfill({
json: {
total: 51,
sheet: "CSV",
rows: Array.from({ length: count }, (_, index) => ({
source_row: offset + index + 1,
period: offset === 0 ? "2026-08" : "2026-09",
dimensions: {},
revenue: "1",
cost: "1",
gross: "0",
})),
},
});
});
await page.getByLabel("查看医生A证据", { exact: true }).click();
await expect(evidenceDialog).toContainText("第 1-50 条,共 51 条");
await evidenceDialog.getByRole("button", { name: "下一页", exact: true }).click();
await expect(evidenceDialog).toContainText("第 51-51 条,共 51 条");
await expect(evidenceDialog).toContainText("2026-09");
expect(evidenceOffsets).toEqual([0, 50]);
await evidenceDialog.getByLabel("关闭", { exact: true }).click();
await page
.getByRole("navigation", { name: "分析路径" })
.getByRole("button", { name: "集团总览", exact: true })
.click();
await expect(
page.getByRole("heading", { name: "集团经营总览", exact: true }),
).toBeVisible();
await page.getByRole("button", { name: "环比", exact: true }).click();
await expect(
page.getByRole("status").filter({ hasText: "缺少期间" }),
).toContainText("2026-07");
await page.getByRole("button", { name: "同比", exact: true }).click();
await expect(page.locator(".kpi").first()).toContainText("10,000");
await page.getByRole("button", { name: "分析设置", exact: true }).click();
await page.getByLabel("异常阈值").fill("0.8");
await page.getByRole("button", { name: "取消", exact: true }).click();
await page.getByRole("button", { name: "分析设置", exact: true }).click();
await expect(page.getByLabel("异常阈值")).toHaveValue("0.15");
await page.getByRole("button", { name: "取消", exact: true }).click();
await page.getByRole("button", { name: "Copilot", exact: true }).click();
await page.getByLabel("描述要分析的问题").fill("查看异常");
await page.locator(".copilot").getByRole("button", { name: "发送" }).click();
await expect(page.locator(".message")).toContainText("按当前规则识别出");
await expect(
page.getByRole("heading", { name: "异常追踪", exact: true }),
).toBeVisible();
await page.locator(".copilot").getByLabel("关闭", { exact: true }).click();
const downloaded = page.waitForEvent("download");
await page.getByLabel("导出当前范围").click();
const file = await downloaded;
expect(file.suggestedFilename()).toBe("traceiq-evidence.csv");
await page.getByRole("button", { name: "经营总览", exact: true }).click();
await expect(page.locator("canvas").first()).toBeVisible();
const pixels = await page
.locator("canvas")
.first()
.evaluate((canvas: HTMLCanvasElement) => {
const ctx = canvas.getContext("2d")!;
const p = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
let n = 0;
for (let i = 3; i < p.length; i += 4) if (p[i] > 0) n++;
return n;
});
expect(pixels).toBeGreaterThan(1000);
let designCalls = 0;
await page.route("**/formula-design", async (route) => {
designCalls++;
await route.fulfill({
json: designCalls === 1
? { complete: false, questions: ["缺失值和零分母如何处理?"], column: null }
: {
complete: true,
questions: [],
column: {
name: "收入成本差",
meaning: "实际收入减成本",
scope: "所选分组与期间",
missing: "结果不可用(#N/A);零分母不可用",
expression: {
operator: "subtract",
left: { metric: "revenue" },
right: { metric: "cost" },
},
},
},
});
});
await page.getByRole("button", { name: "生成公式工作簿" }).click();
const formulaDialog = page.getByRole("dialog", { name: "生成公式工作簿" });
await formulaDialog.getByRole("checkbox").check();
await formulaDialog.getByLabel("列计算描述").fill("新增收入成本差,缺失或零分母显示不可用");
await formulaDialog.getByRole("button", { name: "发送描述" }).click();
await expect(formulaDialog).toContainText("缺失值和零分母如何处理?");
await formulaDialog.getByLabel("列计算描述").fill("缺失项和零分母都按不可用处理");
await formulaDialog.getByRole("button", { name: "发送描述" }).click();
await formulaDialog.getByRole("button", { name: "确认并生成公式工作簿" }).click();
await expect(formulaDialog.getByRole("status")).toContainText("生成完成", { timeout: 30000 });
const formulaDownload = page.waitForEvent("download");
await formulaDialog.getByRole("button", { name: "下载公式工作簿" }).click();
expect((await formulaDownload).suggestedFilename()).toBe("traceiq-formula-analysis.xlsx");
await formulaDialog.getByLabel("关闭").click();
});
test("手机:登录、总览、设置、Copilot 布局无横向溢出", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await login(page);
await expect(page.locator(".kpi").first()).toBeVisible();
expect(
await page.evaluate(
() => document.documentElement.scrollWidth <= innerWidth,
),
).toBe(true);
await page.screenshot({
path: "test-results/mobile-overview.png",
fullPage: true,
});
await page.getByLabel("展开导航").click();
await page.getByRole("button", { name: "分析设置", exact: true }).click();
await expect(page.getByRole("dialog")).toBeVisible();
await page.screenshot({
path: "test-results/mobile-settings.png",
fullPage: true,
});
await page.getByRole("button", { name: "取消", exact: true }).click();
await page.getByRole("button", { name: "经营总览", exact: true }).click();
await page.getByRole("button", { name: "Copilot", exact: true }).click();
await expect(page.getByLabel("描述要分析的问题")).toBeVisible();
expect(
await page
.locator(".copilot")
.evaluate((el) => el.getBoundingClientRect().right <= innerWidth),
).toBe(true);
await page.locator(".copilot").getByLabel("关闭", { exact: true }).click();
});
test("原附件:全量导入结果在桌面及手机正确显示", async ({ page }) => {
test.skip(!process.env.REFERENCE_DATASET_NAME, "仅本机显式启用原附件验收");
const errors: string[] = [];
page.on("pageerror", (error) => errors.push(error.message));
await page.setViewportSize({ width: 1512, height: 982 });
await login(page);
await page
.getByLabel("选择数据集")
.selectOption({ label: process.env.REFERENCE_DATASET_NAME! });
await expect(page.locator(".kpi").nth(0)).toContainText("152,419,115.35");
await expect(page.locator(".kpi").nth(1)).toContainText("17,868,580.85");
await expect(page.locator(".kpi").nth(3)).toContainText("不可用");
await expect(page.locator(".kpi").nth(4)).toContainText("38,361");
await page.screenshot({
path: "test-results/reference-desktop.png",
fullPage: true,
});
await page.getByRole("button", { name: "环比", exact: true }).click();
await expect(
page.getByRole("status").filter({ hasText: "缺少期间" }),
).toContainText("2026-07");
await page.getByRole("button", { name: "同比", exact: true }).click();
await expect(page.locator(".kpi").first()).toContainText("-32.4%");
await page.setViewportSize({ width: 390, height: 844 });
await expect
.poll(() =>
page.evaluate(
() =>
document.documentElement.scrollWidth <= innerWidth &&
[...document.querySelectorAll(".chart canvas")].every(
(canvas) => canvas.getBoundingClientRect().right <= innerWidth,
),
),
)
.toBe(true);
await page.screenshot({
path: "test-results/reference-mobile.png",
fullPage: true,
});
expect(errors).toEqual([]);
});