文件
hyb-oa/client/tests/e2e/manual/r70-trialrun-assist.spec.ts

271 行
11 KiB
TypeScript

import { expect, test, type Page } from "@playwright/test";
import { mkdirSync, writeFileSync } from "node:fs";
import path from "node:path";
import { ADMIN, EMPLOYEE, MANAGER, apiPostData, createAuthenticatedApiClient, login, logout } from "../helpers/auth";
import { openNavLink } from "../helpers/navigation";
import { TEST_IDS } from "../helpers/testIds";
type StepStatus = "PASS" | "FAIL";
type TrialStep = {
role: "employee" | "manager" | "admin";
title: string;
status: StepStatus;
detail: string;
screenshot: string;
};
const rootDir = path.resolve(process.cwd(), "..");
const reportPath = path.join(rootDir, "artifacts/r70/auto-trialrun-report.md");
const issueCandidatesPath = path.join(rootDir, "artifacts/r70/auto-issue-candidates.csv");
const screenshotDir = path.join(rootDir, "deploy/trialrun/screenshots/r70-auto-trialrun");
function formatDate(offsetDays: number) {
const value = new Date();
value.setUTCDate(value.getUTCDate() + offsetDays);
return value.toISOString().slice(0, 10);
}
function csvCell(value: string) {
return `"${value.replaceAll('"', '""')}"`;
}
function safeName(value: string) {
return value.replace(/[^a-z0-9\u4e00-\u9fa5]+/gi, "-").replace(/^-|-$/g, "").slice(0, 80);
}
async function selectFirstEnabledOption(page: Page, selector: string) {
const locator = page.locator(selector);
await expect
.poll(async () => {
const optionValues = await locator.locator("option").evaluateAll((options) =>
options
.map((option) => ({ value: (option as HTMLOptionElement).value, disabled: (option as HTMLOptionElement).disabled }))
.filter((option) => option.value && !option.disabled)
.map((option) => option.value),
);
return optionValues[0] ?? "";
})
.not.toBe("");
const [firstValue] = await locator.locator("option").evaluateAll((options) =>
options
.map((option) => ({ value: (option as HTMLOptionElement).value, disabled: (option as HTMLOptionElement).disabled }))
.filter((option) => option.value && !option.disabled)
.map((option) => option.value),
);
await locator.selectOption(firstValue);
}
async function createAndSubmitLeaveForManager(page: Page) {
const employeeClient = await createAuthenticatedApiClient(EMPLOYEE);
const startDate = formatDate(260 + (Date.now() % 30));
try {
const createResponsePromise = page.waitForResponse(
(response) => response.request().method() === "POST" && new URL(response.url()).pathname === "/api/leave-requests" && response.ok(),
);
await selectFirstEnabledOption(page, "#leave-type");
await page.locator("#leave-start-date").fill(startDate);
await page.locator("#leave-end-date").fill(startDate);
await page.locator("#leave-start-session").selectOption("am");
await page.locator("#leave-end-session").selectOption("pm");
await page.locator("#leave-reason").fill(`R70 自动辅助试运行 ${Date.now()}`);
await page.getByRole("button", { name: "创建草稿" }).click();
const createPayload = (await (await createResponsePromise).json()) as { data?: { id?: string } };
const leaveRequestId = createPayload.data?.id;
if (!leaveRequestId) {
throw new Error(`创建请假草稿后未返回 id: ${JSON.stringify(createPayload)}`);
}
const submitResult = await apiPostData<{ leaveRequestId: string; currentTaskId: string }>(
employeeClient.api,
`/api/leave-requests/${leaveRequestId}/submit`,
{},
);
return { startDate, leaveRequestId, currentTaskId: submitResult.currentTaskId };
} finally {
await employeeClient.dispose();
}
}
function writeReports(steps: TrialStep[]) {
mkdirSync(path.dirname(reportPath), { recursive: true });
const failures = steps.filter((item) => item.status === "FAIL");
const now = new Date().toISOString();
const lines = [
"# R70 自动辅助试运行报告",
"",
`生成时间:${now}`,
"",
"## 结论",
"",
failures.length
? `自动辅助试运行发现 ${failures.length} 个失败步骤,需人工确认后写入 R70 issue-log。`
: "自动辅助试运行未发现阻断步骤。仍需人工判断流程是否顺手、文案是否清晰。",
"",
"## 步骤结果",
"",
"| Role | Step | Status | Screenshot | Detail |",
"| --- | --- | --- | --- | --- |",
...steps.map((item) =>
`| ${item.role} | ${item.title} | ${item.status} | ${item.screenshot || "N/A"} | ${item.detail.replaceAll("|", "\\|")} |`,
),
"",
"## 说明",
"",
"- 截图输出在 `deploy/trialrun/screenshots/r70-auto-trialrun/`,该目录被 `.gitignore` 忽略。",
"- 本报告只证明自动路径可执行,不替代真实用户对体验、文案和流程合理性的判断。",
];
writeFileSync(reportPath, `${lines.join("\n")}\n`, "utf-8");
const issueHeader = [
"ID",
"Role",
"Severity",
"Module",
"Title",
"Steps",
"Expected",
"Actual",
"Screenshot",
"Owner",
"Disposition",
"Status",
"Notes",
];
const issueRows = failures.map((item, index) =>
[
`R70-AUTO-${String(index + 1).padStart(3, "0")}`,
item.role,
"P1",
item.title,
`自动辅助试运行失败:${item.title}`,
item.title,
"页面可打开且关键元素可见",
item.detail,
item.screenshot || "N/A",
"TBD",
"needs review",
"OPEN",
"由 R70 自动辅助试运行生成,需人工复核严重级别。",
].map(csvCell).join(","),
);
writeFileSync(issueCandidatesPath, `${issueHeader.map(csvCell).join(",")}\n${issueRows.join("\n")}${issueRows.length ? "\n" : ""}`, "utf-8");
}
test("R70 自动辅助试运行:员工、经理、管理员路径巡检", async ({ page }) => {
test.setTimeout(240_000);
mkdirSync(screenshotDir, { recursive: true });
const steps: TrialStep[] = [];
let stepIndex = 0;
let managerTaskId = "";
async function runStep(role: TrialStep["role"], title: string, action: () => Promise<string | void>) {
stepIndex += 1;
const screenshot = path.join(screenshotDir, `${String(stepIndex).padStart(2, "0")}-${role}-${safeName(title)}.png`);
const relativeScreenshot = path.relative(rootDir, screenshot);
try {
const detail = (await action()) ?? "ok";
await page.screenshot({ path: screenshot, fullPage: true });
steps.push({ role, title, status: "PASS", detail, screenshot: relativeScreenshot });
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
await page.screenshot({ path: screenshot, fullPage: true }).catch(() => undefined);
steps.push({ role, title, status: "FAIL", detail, screenshot: relativeScreenshot });
}
}
try {
await runStep("employee", "Portal 首页", async () => {
await login(page, EMPLOYEE);
await expect(page.getByRole("heading", { name: /今日待办聚合|今日工作台/ }).first()).toBeVisible();
});
await runStep("employee", "请假提交并生成经理待办", async () => {
await (await openNavLink(page, "请假记录")).click();
await expect(page.getByRole("heading", { name: "请假申请" })).toBeVisible();
const leave = await createAndSubmitLeaveForManager(page);
await page.goto("/leave-requests");
managerTaskId = leave.currentTaskId;
await expect(page.getByRole("row").filter({ hasText: leave.startDate }).first()).toContainText("审批中", { timeout: 30_000 });
return `leaveRequestId=${leave.leaveRequestId}; taskId=${leave.currentTaskId}`;
});
await runStep("employee", "报销入口", async () => {
await (await openNavLink(page, "我的报销")).click();
await expect(page.getByTestId("reimbursements-create")).toBeVisible();
});
await runStep("employee", "周报与待办入口", async () => {
await (await openNavLink(page, "周报")).click();
await expect(page.getByRole("heading", { name: "周报", exact: true })).toBeVisible();
await page.goto("/todos");
await expect(page.getByRole("heading", { name: "待办", exact: true })).toBeVisible();
});
await runStep("employee", "公告与全局搜索", async () => {
await (await openNavLink(page, "公告")).click();
await expect(page.getByRole("heading", { name: "公告" })).toBeVisible();
await page.getByTestId("global-search-trigger").click();
await expect(page.getByRole("heading", { name: "全局搜索" })).toBeVisible();
});
await runStep("employee", "移动首页", async () => {
try {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/mobile");
await expect(page.getByRole("heading", { name: "移动首页" })).toBeVisible();
} finally {
await page.setViewportSize({ width: 1280, height: 720 });
}
});
await runStep("manager", "待我审批与快捷审批入口", async () => {
await logout(page, EMPLOYEE.name);
await login(page, MANAGER);
await (await openNavLink(page, "待我审批")).click();
await expect(page.getByTestId(TEST_IDS.workflow.tasksTable)).toBeVisible();
if (managerTaskId) {
await expect(page.getByTestId(TEST_IDS.workflow.taskRow).filter({ hasText: managerTaskId }).first()).toBeVisible();
}
});
await runStep("manager", "团队状态与项目工作台", async () => {
await (await openNavLink(page, "团队")).click();
await expect(page.getByRole("heading", { name: "团队状态" })).toBeVisible();
await (await openNavLink(page, "项目")).click();
await expect(page.getByRole("heading", { name: "项目", exact: true })).toBeVisible();
});
await runStep("admin", "管理入口与人员组织", async () => {
await logout(page, MANAGER.name);
await login(page, ADMIN);
await (await openNavLink(page, /^管理$/)).click();
await expect(page.getByRole("heading", { name: "管理", exact: true })).toBeVisible();
await page.getByRole("link", { name: /人员/ }).first().click();
await expect(page.getByRole("heading", { name: "人员管理" })).toBeVisible();
await page.goto("/organization");
await expect(page.getByRole("heading", { name: "组织架构" })).toBeVisible();
});
await runStep("admin", "管理、功能管理、审计日志", async () => {
await (await openNavLink(page, /^管理$/)).click();
await expect(page.getByRole("heading", { name: "管理", exact: true })).toBeVisible();
await page.getByRole("link", { name: /功能管理/ }).first().click();
await expect(page.getByRole("heading", { name: "功能管理" })).toBeVisible();
await page.goto("/audit-logs");
await expect(page.getByRole("heading", { name: "审计工作台" })).toBeVisible();
});
} finally {
writeReports(steps);
}
const failures = steps.filter((item) => item.status === "FAIL");
expect(failures, JSON.stringify(failures, null, 2)).toHaveLength(0);
});