补齐报销待组批财务入口 [09-04-reimbursement-state-gap-finance-view][R1]

这个提交包含在:
2026-09-05 09:14:13 +08:00
父节点 8e0af8235e
当前提交 5fe9f78bc9
共修改 9 个文件,包含 243 行新增和 67 行删除
-2
查看文件
@@ -110,8 +110,6 @@ test("E2E 主链路:登录→报销→待办审批→票据OCR→支付→登
const reimbursement = await apiGetData<{ formNumber: string | null }>(page.request, `/api/reimbursements/${reimbursementId}`);
const reimbursementDisplayCode = reimbursement.formNumber ?? reimbursementId;
await page.goto("/payment-batches");
await expect(page.getByTestId("payment-batch-create-panel")).toHaveCount(0);
await page.getByRole("button", { name: "新建批次" }).click();
const createCard = page.getByTestId("payment-batch-create-panel");
await expect(createCard).toBeVisible();
const createBatchResponsePromise = page.waitForResponse((response) => {
@@ -45,6 +45,8 @@ type MockScenario = {
businessDate: string | null;
summary: string | null;
}>;
paymentCandidateTotal?: number;
onCandidateQuery?: (query: URLSearchParams) => void;
contracts?: SyntheticContract[];
onCreatePaymentBatch?: (payload: unknown) => void;
};
@@ -156,7 +158,8 @@ function expectNoPageErrors(errors: string[]) {
async function mockSyntheticApis(page: Page, actor: SyntheticActor, scenario: MockScenario = {}) {
await page.route((url) => url.pathname.startsWith("/api/"), async (route) => {
const request = route.request();
const pathName = new URL(request.url()).pathname;
const requestUrl = new URL(request.url());
const pathName = requestUrl.pathname;
if (pathName === "/api/auth/capabilities") {
await fulfillJson(route, {
@@ -265,7 +268,13 @@ async function mockSyntheticApis(page: Page, actor: SyntheticActor, scenario: Mo
}
if (pathName === "/api/payment-batches/candidates") {
const items = scenario.paymentCandidates ?? [];
await fulfillJson(route, { items, page: 1, pageSize: 50, total: items.length });
scenario.onCandidateQuery?.(requestUrl.searchParams);
await fulfillJson(route, {
items,
page: Number(requestUrl.searchParams.get("page") ?? "1"),
pageSize: Number(requestUrl.searchParams.get("pageSize") ?? "20"),
total: scenario.paymentCandidateTotal ?? items.length,
});
return;
}
if (pathName === "/api/payment-batches" && request.method() === "POST") {
@@ -342,13 +351,18 @@ test("只有付款复核职责的经理可看应付账款但看不到导出", as
test("只有付款复核职责的经理看不到新建批次", async ({ page }) => {
test.skip(process.env.E2E_MODE === "live", "只使用本地合成角色,不访问公网");
const pageErrors = capturePageErrors(page);
await mockSyntheticApis(page, REVIEWER_ACTOR);
let candidateQueryCount = 0;
await mockSyntheticApis(page, REVIEWER_ACTOR, {
onCandidateQuery: () => { candidateQueryCount += 1; },
});
await login(page, REVIEWER_ACTOR);
await page.goto("/payment-batches");
await expect(page).toHaveURL(/\/payment-batches$/);
await page.goto("/payment-batches?view=pending");
await expect(page).toHaveURL(/\/payment-batches\?view=batches$/);
await expect(page.getByRole("heading", { level: 1, name: "付款", exact: true })).toBeVisible();
await expect(page.getByRole("button", { name: "新建批次", exact: true })).toHaveCount(0);
await expect(page.getByRole("button", { name: /待组批/ })).toHaveCount(0);
await expect(page.getByRole("button", { name: /付款批次/ })).toBeVisible();
expect(candidateQueryCount).toBe(0);
expectNoPageErrors(pageErrors);
});
@@ -394,8 +408,11 @@ test("财务从业务候选选择报销并使用所选实体创建付款批次",
test.skip(process.env.E2E_MODE === "live", "只使用本地合成角色,不访问公网");
const pageErrors = capturePageErrors(page);
let createPayload: unknown = null;
const candidateQueries: string[] = [];
await mockSyntheticApis(page, FINANCE_ACTOR, {
paymentCandidates: [PAYMENT_CANDIDATE],
paymentCandidateTotal: 21,
onCandidateQuery: (query) => candidateQueries.push(query.toString()),
onCreatePaymentBatch: (payload) => {
createPayload = payload;
},
@@ -403,9 +420,15 @@ test("财务从业务候选选择报销并使用所选实体创建付款批次",
await login(page, FINANCE_ACTOR);
await page.goto("/payment-batches");
await expect(page).toHaveURL(/\/payment-batches$/);
await expect(page).toHaveURL(/\/payment-batches\?view=pending$/);
await expect(page.getByRole("heading", { level: 1, name: "付款", exact: true })).toBeVisible();
await page.getByRole("button", { name: "新建批次", exact: true }).click();
await expect(page.getByRole("button", { name: "待组批 21", exact: true })).toHaveAttribute("aria-current", "page");
await expect(page.getByTestId("payment-batch-candidates")).toHaveCount(1);
await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth)).toBeLessThanOrEqual(2);
await page.getByRole("button", { name: "下一页", exact: true }).click();
await expect(page).toHaveURL(/view=pending&page=2/);
await expect.poll(() => candidateQueries.some((query) => query.includes("page=2") && query.includes("pageSize=20"))).toBe(true);
await page.getByRole("button", { name: "上一页", exact: true }).click();
const candidateRow = page.getByTestId("payment-batch-candidate-row");
await expect(candidateRow).toContainText(PAYMENT_CANDIDATE.businessNo);
@@ -413,13 +436,17 @@ test("财务从业务候选选择报销并使用所选实体创建付款批次",
await expect(candidateRow).toContainText("¥1,280.50");
await expect(page.getByText(PAYMENT_CANDIDATE_ENTITY_ID, { exact: true })).toHaveCount(0);
await expect(page.locator('input[placeholder*="UUID"], input[placeholder*="uuid"], input[placeholder*="entityId"], input[placeholder*="内部 ID"], input[placeholder*="内部ID"]')).toHaveCount(0);
await page.getByTestId("payment-batch-candidate-search").fill(PAYMENT_CANDIDATE.businessNo);
await expect(page).toHaveURL(new RegExp(`view=pending&q=${PAYMENT_CANDIDATE.businessNo}`));
await page.setViewportSize({ width: 375, height: 812 });
await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth)).toBeLessThanOrEqual(2);
await candidateRow.getByRole("checkbox").check();
await page.getByTestId("payment-batch-default-channel").selectOption("cash");
await expect(page.getByText("按现金提交负责人复核", { exact: false })).toBeVisible();
await page.getByRole("button", { name: "创建并提交复核", exact: true }).click();
await expect(page).toHaveURL(new RegExp(`/payment-batches\\?id=${CREATED_PAYMENT_BATCH_ID}$`));
await expect(page).toHaveURL(new RegExp(`/payment-batches\\?view=batches&id=${CREATED_PAYMENT_BATCH_ID}$`));
await expect(page.getByRole("heading", { level: 1, name: "付款", exact: true })).toBeVisible();
await expect(page.getByText("已提交付款复核,等待负责人处理", { exact: true })).toBeVisible();
expect(createPayload).toEqual({
@@ -143,6 +143,20 @@ test("申请人进度与审批历史保持有意分工", () => {
expect(approvalAdapter).not.toContain("<ProcessingProgress");
});
test("待组批工作台保持单一候选源且报销详情使用可刷新深链", () => {
const paymentPage = source("src/pages/PaymentBatchesPage.tsx");
const reimbursementDetail = source("src/pages/ReimbursementDetailPage.tsx");
expect(paymentPage.match(/data-testid="payment-batch-candidates"/g)).toHaveLength(1);
expect(paymentPage.match(/fetchPaymentBatchCandidates\(/g)).toHaveLength(1);
expect(paymentPage).toContain('next.set("view", "pending")');
expect(paymentPage).toContain('next.set("page", String(page))');
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("等待财务组建付款批次");
});
test("票据申报事实与系统金额证据状态分属正文和处理上下文", () => {
const baseDetail = {
items: [{
@@ -102,8 +102,6 @@ test("E2E 报销付款批次主链路:提交 -> 审批 -> 创建批次 -> 复
await page.goto("/payment-batches");
await expect(page).toHaveURL(/\/payment-batches/);
await expect(page.getByTestId("payment-batch-create-panel")).toHaveCount(0);
await page.getByRole("button", { name: "新建批次" }).click();
const createCard = page.getByTestId("payment-batch-create-panel");
await expect(createCard).toBeVisible();
const createBatchResponsePromise = page.waitForResponse((response) => {