PRELAUNCH 收口正式启用前公网预回归
这个提交包含在:
@@ -82,14 +82,15 @@ type LiveSummary = {
|
||||
error?: string;
|
||||
};
|
||||
|
||||
const issueId = "P2FLIVE";
|
||||
const issueId = process.env.QA_ISSUE_ID?.trim() || "P2FLIVE";
|
||||
const markerPrefix = process.env.P2_FIN_LIVE_MARKER_PREFIX?.trim() || issueId;
|
||||
const baseUrl = process.env.LIVE_SMOKE_BASE_URL?.trim() || "https://oa.hyinda.com";
|
||||
const reportDir = process.env.P2_FIN_LIVE_REPORT_DIR?.trim() ||
|
||||
path.resolve(process.cwd(), "..", "artifacts/qa-flow/p2-fin-live-local");
|
||||
const summaryPath = path.join(reportDir, "live-result.json");
|
||||
|
||||
function nowMarker() {
|
||||
return `P2FLIVE-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||
return `${markerPrefix}-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
function today() {
|
||||
@@ -210,7 +211,7 @@ async function firstEnabledExpenseTypeId(api: APIRequestContext) {
|
||||
|
||||
async function createTestReimbursement(api: APIRequestContext, marker: string) {
|
||||
const reimbursement = await apiPostData<ReimbursementDetail>(api, "/api/reimbursements", {
|
||||
description: `P2FINLIVE 公网付款测试 ${marker}`,
|
||||
description: `${markerPrefix} 公网付款测试 ${marker}`,
|
||||
reimbursementDate: today(),
|
||||
});
|
||||
const expenseTypeId = await firstEnabledExpenseTypeId(api);
|
||||
@@ -218,7 +219,7 @@ async function createTestReimbursement(api: APIRequestContext, marker: string) {
|
||||
expenseTypeId,
|
||||
expenseDate: today(),
|
||||
amount: "168.30",
|
||||
description: `P2FINLIVE 小额测试 ${marker}`,
|
||||
description: `${markerPrefix} 小额测试 ${marker}`,
|
||||
});
|
||||
return apiGetData<ReimbursementDetail>(api, `/api/reimbursements/${reimbursement.id}`);
|
||||
}
|
||||
@@ -298,7 +299,7 @@ async function createPaymentBatch(api: APIRequestContext, input: { reimbursement
|
||||
entityIds: [input.reimbursementId],
|
||||
defaultPaymentDate: today(),
|
||||
defaultPaymentChannel: "bank",
|
||||
voucherPrefix: `P2LIVE-${input.marker.slice(-6)}`,
|
||||
voucherPrefix: `${markerPrefix.slice(0, 12)}-${input.marker.slice(-6)}`,
|
||||
});
|
||||
return data.item;
|
||||
}
|
||||
@@ -402,14 +403,14 @@ test.describe("P2-FIN-LIVE 公网付款批次短窗口", () => {
|
||||
admin.api,
|
||||
"payment_batch_v1",
|
||||
"force_on",
|
||||
`P2FLIVE short window ${marker}`,
|
||||
`${issueId} short window ${marker}`,
|
||||
);
|
||||
summary.stages.flagOn = { paymentFlagOn: snapshotFlag(paymentFlagOn) };
|
||||
expect(paymentFlagOn.effectiveEnabled).toBe(true);
|
||||
const erpFlagAfterPaymentOn = await loadFlag(admin.api, "erp_sync");
|
||||
expect(erpFlagAfterPaymentOn.effectiveEnabled, "erp_sync must not be opened by P2FLIVE").toBe(false);
|
||||
expect(erpFlagAfterPaymentOn.effectiveEnabled, `erp_sync must not be opened by ${issueId}`).toBe(false);
|
||||
|
||||
const batchName = `P2FINLIVE 公网付款批次 ${marker}`;
|
||||
const batchName = `${markerPrefix} 公网付款批次 ${marker}`;
|
||||
const adminWildcard = isWildcardSuperAdmin(admin);
|
||||
const rejectedCreate: Record<string, string | number> = {
|
||||
employee: await expectPostRejected(employee.api, "/api/payment-batches", {
|
||||
@@ -443,7 +444,7 @@ test.describe("P2-FIN-LIVE 公网付款批次短窗口", () => {
|
||||
: await expectPostRejected(admin.api, `/api/payment-batches/${batch.id}/execute`, {}, "admin-only execute payment batch");
|
||||
|
||||
batch = (await apiPostData<{ item: PaymentBatch }>(financeMgr.api, `/api/payment-batches/${batch.id}/review`, {
|
||||
comment: `P2FLIVE 复核 ${marker}`,
|
||||
comment: `${issueId} 复核 ${marker}`,
|
||||
})).item;
|
||||
expect(batch.reviewRequirement.completedReviewCount).toBeGreaterThanOrEqual(1);
|
||||
|
||||
@@ -493,7 +494,7 @@ test.describe("P2-FIN-LIVE 公网付款批次短窗口", () => {
|
||||
} finally {
|
||||
if (admin) {
|
||||
try {
|
||||
const rolledBack = await forcePaymentBatchOff(admin.api, `P2FLIVE rollback ${marker}`);
|
||||
const rolledBack = await forcePaymentBatchOff(admin.api, `${issueId} rollback ${marker}`);
|
||||
summary.stages.rollback = { paymentFlagAfterRollback: snapshotFlag(rolledBack) };
|
||||
expect(rolledBack.effectiveEnabled).toBe(false);
|
||||
} catch (error) {
|
||||
|
||||
@@ -98,10 +98,25 @@ type TrialState = {
|
||||
};
|
||||
|
||||
const rootDir = path.resolve(process.cwd(), "..");
|
||||
const reportDir = path.join(rootDir, "artifacts/r7e/browser-business-trialrun");
|
||||
const trialrunIssueId = process.env.TRIALRUN_ISSUE_ID?.trim() || process.env.QA_ISSUE_ID?.trim() || "R7E";
|
||||
const trialrunMarkerPrefix = process.env.TRIALRUN_MARKER_PREFIX?.trim() || trialrunIssueId;
|
||||
const trialrunSafePrefix = safeName(trialrunMarkerPrefix).toUpperCase().slice(0, 12) || "R7E";
|
||||
const trialrunReportTitle = process.env.TRIALRUN_REPORT_TITLE?.trim() || `${trialrunMarkerPrefix} 浏览器业务先行试运行报告`;
|
||||
function resolveRootOutputPath(value: string | undefined, fallback: string) {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) return fallback;
|
||||
return path.isAbsolute(trimmed) ? trimmed : path.resolve(process.cwd(), trimmed);
|
||||
}
|
||||
const reportDir = resolveRootOutputPath(
|
||||
process.env.TRIALRUN_REPORT_DIR,
|
||||
path.join(rootDir, "artifacts/r7e/browser-business-trialrun"),
|
||||
);
|
||||
const reportPath = path.join(reportDir, "report.md");
|
||||
const issueCandidatesPath = path.join(reportDir, "issues.csv");
|
||||
const screenshotDir = path.join(rootDir, "output/playwright/r7e-browser-business-trialrun");
|
||||
const screenshotDir = resolveRootOutputPath(
|
||||
process.env.TRIALRUN_SCREENSHOT_DIR,
|
||||
path.join(rootDir, "output/playwright/r7e-browser-business-trialrun"),
|
||||
);
|
||||
const validFlows = new Set<FlowKey>([
|
||||
"preflight",
|
||||
"reimbursement-finance",
|
||||
@@ -114,13 +129,14 @@ const validFlows = new Set<FlowKey>([
|
||||
"all",
|
||||
]);
|
||||
const loginRateLimitRetryMs = Number.parseInt(process.env.R7E_LOGIN_RETRY_WAIT_MS ?? "65000", 10);
|
||||
const trialrunTestTimeoutMs = Number.parseInt(process.env.TRIALRUN_TEST_TIMEOUT_MS ?? "900000", 10);
|
||||
const defaultCloseoutReimbursementId = "40876ef3-0652-4972-aa14-1aad1a855ae0";
|
||||
const defaultCloseoutReimbursementCode = "BX-20260514-0016";
|
||||
const wecomUa =
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 wxwork/4.1.20 MicroMessenger/8.0.1 Language/zh_CN";
|
||||
|
||||
function nowStamp() {
|
||||
return String(Date.now());
|
||||
return `${trialrunMarkerPrefix}-${Date.now()}`;
|
||||
}
|
||||
|
||||
function formatDate(offsetDays: number) {
|
||||
@@ -282,13 +298,13 @@ async function createPaymentBatchViaApi(api: APIRequestContext, input: {
|
||||
entityIds: [input.reimbursementId],
|
||||
defaultPaymentDate: new Date().toISOString().slice(0, 10),
|
||||
defaultPaymentChannel: "bank",
|
||||
voucherPrefix: `R7E-PAY-${input.marker.slice(-8)}`,
|
||||
voucherPrefix: `${trialrunSafePrefix}-PAY-${input.marker.slice(-8)}`,
|
||||
});
|
||||
}
|
||||
|
||||
async function reviewPaymentBatchViaApi(api: APIRequestContext, batchId: string, marker: string) {
|
||||
return apiPostData<{ item: PaymentBatch }>(api, `/api/payment-batches/${batchId}/review`, {
|
||||
comment: `R7E 非人工付款复核 ${marker}`,
|
||||
comment: `${trialrunMarkerPrefix} 非人工付款复核 ${marker}`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -303,7 +319,7 @@ async function createReimbursementItem(api: APIRequestContext, reimbursementId:
|
||||
expenseTypeId,
|
||||
expenseDate: new Date().toISOString().slice(0, 10),
|
||||
amount: "168.30",
|
||||
description: `R7E 浏览器报销 ${marker}`,
|
||||
description: `${trialrunMarkerPrefix} 浏览器报销 ${marker}`,
|
||||
},
|
||||
});
|
||||
if (!response.ok()) {
|
||||
@@ -471,15 +487,17 @@ function writeReports(steps: TrialStep[]) {
|
||||
const flowIssueCandidatesPath = path.join(reportDir, `issues-${safeName(flowName)}.csv`);
|
||||
|
||||
const lines = [
|
||||
"# R7E 浏览器业务先行试运行报告",
|
||||
`# ${trialrunReportTitle}`,
|
||||
"",
|
||||
`生成时间:${generatedAt}`,
|
||||
`执行 flow:${flowName}`,
|
||||
`Issue:${trialrunIssueId}`,
|
||||
`测试数据标记:${trialrunMarkerPrefix}`,
|
||||
"",
|
||||
"## 结论",
|
||||
"",
|
||||
failures.length
|
||||
? `发现 ${failures.length} 个失败步骤,需分流到 R7E/R7E-16 或 QAOPS8。`
|
||||
? `发现 ${failures.length} 个失败步骤,需分流到 ${trialrunIssueId} 或后续 Issue。`
|
||||
: skips.length
|
||||
? `未发现失败步骤,但有 ${skips.length} 个 SKIP 步骤,不能据此关闭对应人工验收。`
|
||||
: "公网浏览器业务线自动辅助试运行未发现阻断步骤。",
|
||||
@@ -513,15 +531,15 @@ function writeReports(steps: TrialStep[]) {
|
||||
const issueHeader = ["ID", "Flow", "Severity", "Title", "Actual", "Screenshot", "Disposition", "Status", "Notes"];
|
||||
const issueRows = failures.map((item, index) =>
|
||||
[
|
||||
`R7E-BROWSER-${String(index + 1).padStart(3, "0")}`,
|
||||
`${trialrunSafePrefix}-BROWSER-${String(index + 1).padStart(3, "0")}`,
|
||||
item.flow,
|
||||
item.flow === "公共前置" ? "BLOCKER" : "HIGH",
|
||||
item.title,
|
||||
item.detail,
|
||||
item.screenshot || "N/A",
|
||||
item.flow === "移动" || item.flow === "企业微信UA" ? "QAOPS8/UX8/R7E" : "R7E/WF8/BIZ8",
|
||||
item.flow === "移动" || item.flow === "企业微信UA" ? `QAOPS/UX/${trialrunIssueId}` : `${trialrunIssueId}/WF/BIZ`,
|
||||
"OPEN",
|
||||
"由 R7E 浏览器业务先行试运行生成,需人工复核严重级别。",
|
||||
`由 ${trialrunIssueId} 浏览器业务先行试运行生成,需人工复核严重级别。`,
|
||||
].map(csvCell).join(","),
|
||||
);
|
||||
const issueContent = `${issueHeader.map(csvCell).join(",")}\n${issueRows.join("\n")}${issueRows.length ? "\n" : ""}`;
|
||||
@@ -529,11 +547,11 @@ function writeReports(steps: TrialStep[]) {
|
||||
writeFileSync(flowIssueCandidatesPath, issueContent, "utf-8");
|
||||
}
|
||||
|
||||
test("R7E 浏览器业务先行试运行:报销、审批、请假、采购、财务、移动浏览器", async ({ page, browser, reimbursementDetail, workflowDetail }) => {
|
||||
test(`${trialrunIssueId} 浏览器业务先行试运行:报销、审批、请假、采购、财务、移动浏览器`, async ({ page, browser, reimbursementDetail, workflowDetail }) => {
|
||||
test.skip(process.env.PLAYWRIGHT_MANUAL !== "1", "仅在显式手工 Playwright 模式下执行");
|
||||
test.skip(process.env.E2E_MODE !== "live", "仅在 live 模式下执行");
|
||||
test.skip(process.env.R7E_BROWSER_TRIALRUN !== "1", "仅在 R7E 浏览器业务试运行模式下执行");
|
||||
test.setTimeout(900_000);
|
||||
test.skip(process.env.R7E_BROWSER_TRIALRUN !== "1", `仅在 ${trialrunIssueId} 浏览器业务试运行模式下执行`);
|
||||
test.setTimeout(Number.isFinite(trialrunTestTimeoutMs) && trialrunTestTimeoutMs > 0 ? trialrunTestTimeoutMs : 900_000);
|
||||
|
||||
mkdirSync(screenshotDir, { recursive: true });
|
||||
const steps: TrialStep[] = [];
|
||||
@@ -695,7 +713,7 @@ test("R7E 浏览器业务先行试运行:报销、审批、请假、采购、
|
||||
if (existingBatch && ["draft", "reviewed", "failed"].includes(existingBatch.status)) {
|
||||
batch = existingBatch;
|
||||
} else {
|
||||
const batchName = `R7E 非人工付款批次 ${marker}`;
|
||||
const batchName = `${trialrunMarkerPrefix} 非人工付款批次 ${marker}`;
|
||||
const created = await createPaymentBatchViaApi(financeApi.api, { reimbursementId, name: batchName, marker });
|
||||
batch = created.item;
|
||||
}
|
||||
@@ -763,7 +781,7 @@ test("R7E 浏览器业务先行试运行:报销、审批、请假、采购、
|
||||
await (await openNavLink(page, "请假")).click();
|
||||
await expect(page.getByRole("heading", { name: "请假申请" })).toBeVisible();
|
||||
|
||||
const leaveId = await createLeaveDraft(page, startDate, `R7E 审批边界 ${marker}`);
|
||||
const leaveId = await createLeaveDraft(page, startDate, `${trialrunMarkerPrefix} 审批边界 ${marker}`);
|
||||
const submitResult = await apiPostData<{ leaveRequestId: string; currentTaskId: string }>(employeeApi.api, `/api/leave-requests/${leaveId}/submit`, {});
|
||||
const taskId = submitResult.currentTaskId;
|
||||
if (!taskId) {
|
||||
@@ -853,7 +871,7 @@ test("R7E 浏览器业务先行试运行:报销、审批、请假、采购、
|
||||
await page.goto(`/reimbursements/${reimbursementId}`);
|
||||
await reimbursementDetail.assertLoaded();
|
||||
|
||||
const uploadResult = await uploadLiveInvoicePdf(page, `r7e-${marker}.pdf`, readFileSync(pdfSample));
|
||||
const uploadResult = await uploadLiveInvoicePdf(page, `${trialrunSafePrefix.toLowerCase()}-${marker}.pdf`, readFileSync(pdfSample));
|
||||
if (!uploadResult.invoiceId) {
|
||||
return {
|
||||
status: "SKIP",
|
||||
@@ -937,7 +955,7 @@ test("R7E 浏览器业务先行试运行:报销、审批、请假、采购、
|
||||
await (await openNavLink(page, "请假")).click();
|
||||
await expect(page.getByRole("heading", { name: "请假申请" })).toBeVisible();
|
||||
|
||||
const leaveId = await createLeaveDraft(page, startDate, `R7E 浏览器请假 ${marker}`);
|
||||
const leaveId = await createLeaveDraft(page, startDate, `${trialrunMarkerPrefix} 浏览器请假 ${marker}`);
|
||||
state.leaveRequestId = leaveId;
|
||||
state.leaveDate = startDate;
|
||||
const submitResult = await apiPostData<{ leaveRequestId: string; currentTaskId: string }>(employeeApi.api, `/api/leave-requests/${leaveId}/submit`, {});
|
||||
@@ -996,7 +1014,7 @@ test("R7E 浏览器业务先行试运行:报销、审批、请假、采购、
|
||||
state.purchaseRequestId = purchaseRequestId;
|
||||
await page.goto(`/purchase-requests?id=${purchaseRequestId}`);
|
||||
await expect(page.getByTestId("purchase-title")).toBeVisible();
|
||||
await page.getByTestId("purchase-title").fill(`R7E 浏览器采购 ${marker}`);
|
||||
await page.getByTestId("purchase-title").fill(`${trialrunMarkerPrefix} 浏览器采购 ${marker}`);
|
||||
await selectFirstEnabledOption(page.getByTestId("purchase-legal-entity"));
|
||||
await selectFirstEnabledOption(page.getByTestId("purchase-cost-center"));
|
||||
await selectFirstEnabledOption(page.getByTestId("purchase-supplier"));
|
||||
@@ -1007,11 +1025,11 @@ test("R7E 浏览器业务先行试运行:报销、审批、请假、采购、
|
||||
await page.getByTestId("purchase-add-item").click();
|
||||
}
|
||||
await expect(itemDescriptionInputs.first()).toBeVisible({ timeout: 30_000 });
|
||||
await itemDescriptionInputs.first().fill(`R7E 采购明细 ${marker}`);
|
||||
await itemDescriptionInputs.first().fill(`${trialrunMarkerPrefix} 采购明细 ${marker}`);
|
||||
await page.locator('[data-testid^="purchase-item-quantity-"]:visible').first().fill("1");
|
||||
await page.locator('[data-testid^="purchase-item-unit-price-"]:visible').first().fill("188");
|
||||
|
||||
const purchaseAttachmentName = `r7e-purchase-attachment-${marker}.txt`;
|
||||
const purchaseAttachmentName = `${trialrunSafePrefix.toLowerCase()}-purchase-attachment-${marker}.txt`;
|
||||
const uploadAttachmentResponsePromise = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url());
|
||||
return response.request().method() === "POST" && url.pathname === `/api/purchase-requests/${purchaseRequestId}/attachments`;
|
||||
@@ -1019,7 +1037,7 @@ test("R7E 浏览器业务先行试运行:报销、审批、请假、采购、
|
||||
await page.getByTestId("purchase-attachment-file-input").setInputFiles({
|
||||
name: purchaseAttachmentName,
|
||||
mimeType: "text/plain",
|
||||
buffer: Buffer.from(`R7E purchase attachment ${marker}\n`, "utf-8"),
|
||||
buffer: Buffer.from(`${trialrunMarkerPrefix} purchase attachment ${marker}\n`, "utf-8"),
|
||||
});
|
||||
const uploadAttachmentResponse = await uploadAttachmentResponsePromise;
|
||||
if (!uploadAttachmentResponse.ok()) {
|
||||
@@ -1137,7 +1155,7 @@ test("R7E 浏览器业务先行试运行:报销、审批、请假、采购、
|
||||
};
|
||||
}
|
||||
|
||||
const batchName = `R7E 浏览器付款批次 ${marker}`;
|
||||
const batchName = `${trialrunMarkerPrefix} 浏览器付款批次 ${marker}`;
|
||||
state.paymentBatchName = batchName;
|
||||
await page.goto("/payment-batches");
|
||||
await expect(page.getByRole("heading", { name: "支付批次", exact: true })).toBeVisible();
|
||||
@@ -1161,7 +1179,7 @@ test("R7E 浏览器业务先行试运行:报销、审批、请假、采购、
|
||||
await createCard.getByPlaceholder("用逗号或换行分隔 reimbursement / travel_order ID").fill(state.reimbursementId);
|
||||
await createCard.locator('input[type="date"]').fill(new Date().toISOString().slice(0, 10));
|
||||
await createCard.locator("select").nth(1).selectOption("bank");
|
||||
await createCard.getByPlaceholder("例如 PBX").fill(`R7E-${marker.slice(-6)}`);
|
||||
await createCard.getByPlaceholder("例如 PBX").fill(`${trialrunSafePrefix}-${marker.slice(-6)}`);
|
||||
await createCard.getByRole("button", { name: "创建批次" }).click();
|
||||
const createBatchResponse = await createBatchResponsePromise;
|
||||
if (!createBatchResponse.ok()) {
|
||||
@@ -1182,7 +1200,7 @@ test("R7E 浏览器业务先行试运行:报销、审批、请假、采购、
|
||||
await expect(pendingRow).toBeVisible({ timeout: 30_000 });
|
||||
await pendingRow.getByRole("button", { name: "快捷复核" }).click();
|
||||
await expect(page.getByRole("dialog")).toContainText(batchName);
|
||||
await page.getByPlaceholder("请输入复核意见").fill(`R7E 浏览器复核 ${marker}`);
|
||||
await page.getByPlaceholder("请输入复核意见").fill(`${trialrunMarkerPrefix} 浏览器复核 ${marker}`);
|
||||
await page.getByRole("button", { name: "提交复核" }).click();
|
||||
await expect(pendingRow).toContainText(/复核 1\/1|复核 1\/2/, { timeout: 30_000 });
|
||||
await logoutLiveUser(page, { name: financeMgr.name }).catch(() => undefined);
|
||||
|
||||
在新工单中引用
屏蔽一个用户