R74-A2 A3 A4 压榨试运行自动化空间
这个提交包含在:
@@ -0,0 +1,231 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { ADMIN, EMPLOYEE, apiPostData, createAuthenticatedApiClient, login, logout } from "../helpers/auth";
|
||||
import { openNavLink } from "../helpers/navigation";
|
||||
import { TEST_IDS } from "../helpers/testIds";
|
||||
|
||||
type StepStatus = "PASS" | "FAIL";
|
||||
|
||||
type TrialStep = {
|
||||
title: string;
|
||||
status: StepStatus;
|
||||
detail: string;
|
||||
screenshot: string;
|
||||
};
|
||||
|
||||
const rootDir = path.resolve(process.cwd(), "..");
|
||||
const reportPath = path.join(rootDir, "artifacts/r70/auto-trialrun-deep-report.md");
|
||||
const issueCandidatesPath = path.join(rootDir, "artifacts/r70/auto-trialrun-deep-issues.csv");
|
||||
const screenshotDir = path.join(rootDir, "deploy/trialrun/screenshots/r70-auto-trialrun-deep");
|
||||
|
||||
function formatWeekStart(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);
|
||||
}
|
||||
|
||||
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} 个失败步骤,需继续人工确认。`
|
||||
: "深层自动辅助试运行未发现阻断步骤;仍需人工判断整体体验是否顺手。",
|
||||
"",
|
||||
"## 步骤结果",
|
||||
"",
|
||||
"| Step | Status | Screenshot | Detail |",
|
||||
"| --- | --- | --- | --- |",
|
||||
...steps.map((item) => `| ${item.title} | ${item.status} | ${item.screenshot || "N/A"} | ${item.detail.replaceAll("|", "\\|")} |`),
|
||||
];
|
||||
|
||||
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-DEEP-${String(index + 1).padStart(3, "0")}`,
|
||||
"employee",
|
||||
"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.skip(process.env.PLAYWRIGHT_MANUAL !== "1", "仅在显式手工 Playwright 模式下执行");
|
||||
test.skip(process.env.R70_TRIALRUN_DEEP_ASSIST !== "1", "仅在显式 deep assist 模式下执行");
|
||||
test.setTimeout(240_000);
|
||||
|
||||
mkdirSync(screenshotDir, { recursive: true });
|
||||
const steps: TrialStep[] = [];
|
||||
let stepIndex = 0;
|
||||
|
||||
const suffix = Date.now();
|
||||
const uniqueToken = `Playwright 深层联动 ${suffix}`;
|
||||
const weekStart = formatWeekStart(280 + (suffix % 70));
|
||||
const announcementTitle = `Playwright 深层公告 ${suffix}`;
|
||||
const presenceNote = `深层辅助复核 ${suffix}`;
|
||||
const adminClient = await createAuthenticatedApiClient(ADMIN);
|
||||
|
||||
async function runStep(title: string, action: () => Promise<string | void>) {
|
||||
stepIndex += 1;
|
||||
const screenshot = path.join(screenshotDir, `${String(stepIndex).padStart(2, "0")}-${safeName(title)}.png`);
|
||||
const relativeScreenshot = path.relative(rootDir, screenshot);
|
||||
try {
|
||||
const detail = (await action()) ?? "ok";
|
||||
await page.screenshot({ path: screenshot, fullPage: true });
|
||||
steps.push({ 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({ title, status: "FAIL", detail, screenshot: relativeScreenshot });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await apiPostData<{ item: { id: string } }>(adminClient.api, "/api/announcements", {
|
||||
title: announcementTitle,
|
||||
summary: "用于深层自动辅助试运行验证真实公告、Portal 与搜索链路。",
|
||||
enabled: true,
|
||||
priority: 1,
|
||||
});
|
||||
|
||||
await login(page, EMPLOYEE);
|
||||
|
||||
await runStep("周报保存-生成待办-提交-完成闭环", async () => {
|
||||
await (await openNavLink(page, "周报")).click();
|
||||
await expect(page.getByRole("heading", { name: "周报", exact: true })).toBeVisible();
|
||||
|
||||
const weeklyLoadResponse = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url());
|
||||
return response.request().method() === "GET" && url.pathname === "/api/weekly-reports/me" && url.searchParams.get("weekStart") === weekStart && response.ok();
|
||||
}, { timeout: 30_000 });
|
||||
await page.getByLabel("周报归档周").fill(weekStart);
|
||||
await weeklyLoadResponse;
|
||||
await page.getByLabel("本周完成").fill(`${uniqueToken} 已完成`);
|
||||
await page.getByLabel("当前阻塞").fill(`${uniqueToken} 阻塞`);
|
||||
await page.getByLabel("下周计划").fill(`${uniqueToken} 下周计划`);
|
||||
|
||||
const saveResponse = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url());
|
||||
return response.request().method() === "PUT" && url.pathname === "/api/weekly-reports/me" && response.ok();
|
||||
}, { timeout: 30_000 });
|
||||
await page.getByTestId(TEST_IDS.weeklyReport.save).click();
|
||||
await saveResponse;
|
||||
await expect(page.getByText("周报已保存")).toBeVisible();
|
||||
|
||||
const createTodoResponse = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url());
|
||||
return response.request().method() === "POST" && url.pathname === "/api/todos" && response.ok();
|
||||
}, { timeout: 30_000 });
|
||||
await page.getByTestId(TEST_IDS.weeklyReport.createTodo).click();
|
||||
await createTodoResponse;
|
||||
await expect(page.getByText("已从周报生成待办")).toBeVisible();
|
||||
|
||||
const submitResponse = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url());
|
||||
return response.request().method() === "POST" && url.pathname === "/api/weekly-reports/me/submit" && response.ok();
|
||||
}, { timeout: 30_000 });
|
||||
await page.getByTestId(TEST_IDS.weeklyReport.submit).click();
|
||||
await submitResponse;
|
||||
await expect(page.getByText("周报已提交")).toBeVisible();
|
||||
|
||||
await (await openNavLink(page, "待办")).click();
|
||||
const todoRow = page.getByTestId(TEST_IDS.todo.row).filter({ hasText: uniqueToken }).first();
|
||||
await expect(todoRow).toBeVisible({ timeout: 30_000 });
|
||||
await todoRow.getByTestId(TEST_IDS.todo.complete).click();
|
||||
await expect(todoRow).toContainText("done", { timeout: 30_000 });
|
||||
return `weekStart=${weekStart}; token=${uniqueToken}`;
|
||||
});
|
||||
|
||||
await runStep("公告-团队状态-Portal 汇总联动", async () => {
|
||||
await page.goto("/announcements");
|
||||
await expect(page.getByRole("heading", { name: "公告" })).toBeVisible();
|
||||
await expect(page.getByText(announcementTitle)).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
await page.goto("/team");
|
||||
await expect(page.getByRole("heading", { name: "团队状态" })).toBeVisible();
|
||||
await page.locator("select.select-std").first().selectOption("remote");
|
||||
await page.getByPlaceholder("可选备注,如下午外出办事").fill(presenceNote);
|
||||
await page.getByRole("button", { name: "保存状态" }).click();
|
||||
await expect(page.getByText("今日状态已更新")).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
await page.goto("/portal");
|
||||
await page.getByRole("tab", { name: "公告" }).click();
|
||||
await expect(page.getByText(announcementTitle)).toBeVisible({ timeout: 30_000 });
|
||||
await expect(page.locator('[aria-label="portal-card-today_presence"]')).toBeVisible();
|
||||
return announcementTitle;
|
||||
});
|
||||
|
||||
await runStep("真实全局搜索命中公告并跳转", async () => {
|
||||
await page.goto("/portal");
|
||||
await page.getByTestId("global-search-trigger").click();
|
||||
await expect(page.getByRole("heading", { name: "全局搜索" })).toBeVisible();
|
||||
|
||||
const searchResponse = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url());
|
||||
return response.request().method() === "GET" && url.pathname === "/api/search" && response.ok();
|
||||
}, { timeout: 30_000 });
|
||||
await page.getByPlaceholder("搜索人员、项目、合同、公告、资产、请假、报销、周报、待办").fill(String(suffix));
|
||||
await searchResponse;
|
||||
|
||||
const resultButton = page.getByRole("button", { name: new RegExp(announcementTitle) }).first();
|
||||
await expect(resultButton).toBeVisible({ timeout: 30_000 });
|
||||
await resultButton.click();
|
||||
await expect(page).toHaveURL(/\/announcements$/);
|
||||
await expect(page.getByRole("heading", { name: "公告" })).toBeVisible();
|
||||
await expect(page.getByText(announcementTitle)).toBeVisible({ timeout: 30_000 });
|
||||
return announcementTitle;
|
||||
});
|
||||
} finally {
|
||||
writeReports(steps);
|
||||
await adminClient.dispose();
|
||||
await logout(page, EMPLOYEE.name).catch(() => undefined);
|
||||
}
|
||||
|
||||
const failures = steps.filter((item) => item.status === "FAIL");
|
||||
expect(failures, JSON.stringify(failures, null, 2)).toHaveLength(0);
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户