fix: 统一报销付款状态与进度(09-04-reimbursement-state-gap-finance-view)
这个提交包含在:
@@ -126,8 +126,9 @@ test("PC 审批历史只进入右栏且移动历史保持正文末尾", () => {
|
||||
expect(desktopShell).not.toContain("slots.timeline");
|
||||
expect(desktopShell.indexOf("slots.actionPanel")).toBeLessThan(desktopShell.indexOf("slots.asideContext"));
|
||||
expect(mobileShell.indexOf("slots.afterEvidence")).toBeLessThan(mobileShell.indexOf("slots.timeline"));
|
||||
expect(approvalAdapter).toContain("asideContext: timeline");
|
||||
expect(approvalAdapter.match(/^\s+timeline,$/gm)).toHaveLength(1);
|
||||
expect(approvalAdapter).toContain("asideContext: <>{processingProgress}{timeline}</>");
|
||||
expect(approvalAdapter).toContain("timeline: <>{mobileDecisionContext}{processingProgress}{timeline}</>");
|
||||
expect(approvalAdapter).toContain('className="lg:static"');
|
||||
});
|
||||
|
||||
test("申请人进度与审批历史保持有意分工", () => {
|
||||
@@ -140,7 +141,7 @@ test("申请人进度与审批历史保持有意分工", () => {
|
||||
expect(desktopDetail.match(/targetLinkLabel="查看所在付款批次"/g)).toHaveLength(1);
|
||||
expect(mobileDetail.match(/targetLinkLabel="查看所在付款批次"/g)).toHaveLength(1);
|
||||
expect(approvalAdapter).toContain('data-testid="reimbursement-workflow-history"');
|
||||
expect(approvalAdapter).not.toContain("<ProcessingProgress");
|
||||
expect(approvalAdapter.match(/<ProcessingProgress /g)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("待组批工作台保持单一候选源且报销详情使用可刷新深链", () => {
|
||||
@@ -154,7 +155,7 @@ test("待组批工作台保持单一候选源且报销详情使用可刷新深
|
||||
expect(paymentPage).toContain("candidatesQuery.data?.total");
|
||||
expect(reimbursementDetail).toContain('hasPermission(permissions, "payment.batch.manage")');
|
||||
expect(reimbursementDetail).toContain("/payment-batches?view=pending&q=");
|
||||
expect(reimbursementDetail).toContain("等待财务组建付款批次");
|
||||
expect(reimbursementDetail).toContain("paymentHandoff?.canCreateBatch && canManagePaymentBatch");
|
||||
});
|
||||
|
||||
test("票据申报事实与系统金额证据状态分属正文和处理上下文", () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createRequire } from "node:module";
|
||||
|
||||
import { test, expect } from "./helpers/fixtures";
|
||||
import { login } from "./helpers/auth";
|
||||
import type { ProcessingProgress } from "../../src/types/processingProgress";
|
||||
|
||||
test.use({ serviceWorkers: "block" });
|
||||
const loadServerModule = createRequire(import.meta.url);
|
||||
@@ -103,7 +104,7 @@ function reimbursementListItem() {
|
||||
};
|
||||
}
|
||||
|
||||
function reimbursementDetail(status: "draft" | "reviewing" | "paid" = "draft", isTravel = false) {
|
||||
function reimbursementDetail(status: "draft" | "reviewing" | "approved" | "paid" = "draft", isTravel = false) {
|
||||
const workflowTaskId = "workflow-task-action-confirm";
|
||||
return {
|
||||
...reimbursementListItem(),
|
||||
@@ -309,7 +310,8 @@ async function mockSystemActionApis(
|
||||
paymentOrderFile?: Buffer;
|
||||
paymentOrderError?: boolean;
|
||||
paymentEntityType?: "reimbursement" | "travel_order";
|
||||
reimbursementStatus?: "draft" | "reviewing" | "paid";
|
||||
reimbursementStatus?: "draft" | "reviewing" | "approved" | "paid";
|
||||
reimbursementProgress?: () => ProcessingProgress;
|
||||
travelReimbursement?: boolean;
|
||||
meOverride?: Partial<typeof me>;
|
||||
} = {},
|
||||
@@ -419,7 +421,10 @@ async function mockSystemActionApis(
|
||||
}
|
||||
|
||||
if (path === `/api/reimbursements/${reimbursementId}` && request.method() === "GET") {
|
||||
await fulfillJson(route, reimbursementDetail(currentReimbursementStatus, options.travelReimbursement));
|
||||
await fulfillJson(route, {
|
||||
...reimbursementDetail(currentReimbursementStatus, options.travelReimbursement),
|
||||
...(options.reimbursementProgress ? { processingProgress: options.reimbursementProgress() } : {}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -646,6 +651,100 @@ async function mockSystemActionApis(
|
||||
return calls;
|
||||
}
|
||||
|
||||
test.describe("报销付款交接一致性", () => {
|
||||
test.skip(process.env.E2E_MODE === "live", "仅合成数据,不访问公网");
|
||||
|
||||
function progress(status: SyntheticPaymentBatchStatus | "none" | "unknown", canRead = true, exported = 1): ProcessingProgress {
|
||||
if (status === "unknown") return { stages: [] };
|
||||
const { buildPaymentProcessingSemantics } = loadServerModule("../../../server/src/services/paymentProcessingProgress");
|
||||
const semantics = buildPaymentProcessingSemantics({ status, requiresBankReceipt: true, paymentOrderExportCount: exported });
|
||||
const stages = Object.values(semantics) as ProcessingProgress["stages"];
|
||||
for (const stage of stages) Object.assign(stage, { actorName: null, targetHref: null });
|
||||
if (status === "none") {
|
||||
Object.assign(stages[0], { state: "current", result: "待组批", detail: "待财务组建付款批次" });
|
||||
} else if (canRead) {
|
||||
stages[status === "draft" ? 1 : 2].targetHref = `/payment-batches?id=${paymentBatchId}`;
|
||||
}
|
||||
return { stages };
|
||||
}
|
||||
|
||||
for (const mobile of [false, true]) {
|
||||
test(`${mobile ? "手机" : "桌面"}已复核制单后返回报销不再组批且进度跟随操作`, async ({ page }) => {
|
||||
await page.setViewportSize(mobile ? { width: 390, height: 844 } : { width: 1440, height: 960 });
|
||||
await mockSystemActionApis(page, { reimbursementStatus: "approved", reimbursementProgress: () => progress("reviewed") });
|
||||
await login(page, user);
|
||||
await page.goto(`${mobile ? "/mobile" : ""}/reimbursements/${reimbursementId}`);
|
||||
const actions = page.getByTestId(mobile ? "mobile-reimbursement-payment-actions" : "reimbursement-action-panel");
|
||||
await expect(actions).toContainText("已生成银行制单,等待付款完成并上传回单");
|
||||
await expect(page.getByText("去组建付款批次", { exact: true })).toHaveCount(0);
|
||||
const link = page.getByRole("link", { name: "查看所在付款批次", exact: true });
|
||||
await expect(link).toHaveCount(1);
|
||||
await link.click();
|
||||
await expect(page.getByTestId("payment-batch-processing-panel")).toBeVisible();
|
||||
await page.goBack();
|
||||
await expect(actions).toContainText("已生成银行制单");
|
||||
const timeline = page.getByTestId(mobile ? "mobile-reimbursement-processing-progress" : "reimbursement-workflow-progress");
|
||||
await expect(timeline).toHaveCount(1);
|
||||
const panel = actions.locator("..");
|
||||
expect(await panel.evaluate((element) => getComputedStyle(element).position)).not.toBe("sticky");
|
||||
const actionBounds = await actions.boundingBox();
|
||||
const progressBounds = await timeline.boundingBox();
|
||||
expect(progressBounds!.y).toBeGreaterThan(actionBounds!.y + actionBounds!.height);
|
||||
if (!mobile) expect(Math.abs(progressBounds!.x - actionBounds!.x)).toBeLessThan(35);
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 2)).toBe(true);
|
||||
});
|
||||
}
|
||||
|
||||
for (const [status, expected] of [
|
||||
["none", "待财务组建付款批次"], ["draft", "待非制单人复核付款"],
|
||||
["reviewed", "已生成银行制单"], ["failed", "付款登记失败"],
|
||||
["cancelled", "付款安排已取消"], ["unknown", "付款进度暂不可用"],
|
||||
] as const) {
|
||||
test(`${status}阶段不因无批次读取权限而误判待组批`, async ({ page }) => {
|
||||
await mockSystemActionApis(page, {
|
||||
reimbursementStatus: "approved", reimbursementProgress: () => progress(status, false),
|
||||
meOverride: { roleCodes: ["employee"], permissions: ["expense.view_own"] },
|
||||
});
|
||||
await login(page, user);
|
||||
await page.goto(`/reimbursements/${reimbursementId}`);
|
||||
await expect(page.getByTestId("reimbursement-action-panel")).toContainText(expected);
|
||||
await expect(page.getByRole("link", { name: /付款批次/ })).toHaveCount(0);
|
||||
});
|
||||
}
|
||||
|
||||
test("重新聚焦后不保留旧的组批动作", async ({ page }) => {
|
||||
let current: SyntheticPaymentBatchStatus | "none" = "none";
|
||||
await mockSystemActionApis(page, { reimbursementStatus: "approved", reimbursementProgress: () => progress(current) });
|
||||
await login(page, user);
|
||||
await page.goto(`/reimbursements/${reimbursementId}`);
|
||||
await expect(page.getByTestId("reimbursement-open-pending-payment-batches")).toBeVisible();
|
||||
current = "reviewed";
|
||||
await page.evaluate(() => {
|
||||
Object.defineProperty(document, "visibilityState", { configurable: true, value: "hidden" });
|
||||
document.dispatchEvent(new Event("visibilitychange", { bubbles: true }));
|
||||
Object.defineProperty(document, "visibilityState", { configurable: true, value: "visible" });
|
||||
document.dispatchEvent(new Event("visibilitychange", { bubbles: true }));
|
||||
});
|
||||
await expect(page.getByTestId("reimbursement-action-panel")).toContainText("已生成银行制单");
|
||||
await expect(page.getByTestId("reimbursement-open-pending-payment-batches")).toHaveCount(0);
|
||||
await page.reload();
|
||||
await expect(page.getByRole("link", { name: "查看所在付款批次", exact: true })).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("已付款只保留结果入口,进度未知不让财务猜测组批", async ({ page }) => {
|
||||
await mockSystemActionApis(page, { reimbursementStatus: "paid", reimbursementProgress: () => progress("executed") });
|
||||
await login(page, user);
|
||||
await page.goto(`/reimbursements/${reimbursementId}`);
|
||||
await expect(page.getByTestId("reimbursement-action-panel")).toContainText("付款已登记完成");
|
||||
await expect(page.getByRole("link", { name: "查看所在付款批次", exact: true })).toHaveCount(1);
|
||||
await expect(page.getByTestId("reimbursement-open-pending-payment-batches")).toHaveCount(0);
|
||||
await page.route(`**/api/reimbursements/${reimbursementId}`, (route) => fulfillJson(route, { ...reimbursementDetail("approved"), processingProgress: { stages: [] } }));
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("reimbursement-action-panel")).toContainText("付款进度暂不可用");
|
||||
await expect(page.getByTestId("reimbursement-open-pending-payment-batches")).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("R7E 全站高风险操作二次确认", () => {
|
||||
test("报销删除和敏感导出必须确认后才调用接口", async ({ page }) => {
|
||||
const calls = await mockSystemActionApis(page);
|
||||
|
||||
在新工单中引用
屏蔽一个用户