实现报销材料连续预览(07-27-yang-jun-reimbursement-readiness)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-08-01 00:17:54 +08:00
父节点 b6485a278e
当前提交 8a9279efce
共修改 26 个文件,包含 1211 行新增和 178 行删除
@@ -73,10 +73,13 @@ function buildInvoice() {
return {
id: pendingInvoiceId,
status: "bound",
invoiceCode: "044002600111",
invoiceCode: null as string | null,
invoiceNumber: "12345678",
invoiceDate: "2026-05-08",
sellerName: "北京悦海堂餐饮管理有限公司",
sellerName: "北京悦海堂餐饮管理有限公司" as string | null,
buyerName: null as string | null,
documentCategory: "full_vat_invoice" as string | null,
documentSubtype: "VatElectronicInvoiceFull" as string | null,
totalAmount: "1683.00",
originalName: "北京悦海堂发票.pdf",
mimeType: "application/pdf",
@@ -90,7 +93,10 @@ function buildUnlinkedInvoice() {
...buildInvoice(),
id: unlinkedInvoiceId,
invoiceNumber: "12345679",
sellerName: "未关联材料供应方",
sellerName: null,
buyerName: "测试购买方",
documentCategory: "electronic_train_ticket",
documentSubtype: "ElectronicTrainTicketFull",
originalName: "未关联发票.pdf",
linkedItemId: null,
};
@@ -351,6 +357,30 @@ async function mockApis(page: Page, state: ReturnType<typeof buildReimbursementS
return;
}
if (pathName === `/api/reimbursements/${reimbursementId}/materials/preview-bundle` && request.method() === "POST") {
const pages = [
...state.invoices.map((_, index) => ({
url: `https://oa.hyinda.com/api/reimbursements/${reimbursementId}/materials/preview/77777777-7777-4777-8777-777777777777/${index}?previewToken=invoice-${index}`,
materialType: "invoice",
materialIndex: index + 1,
pageNumber: 1,
})),
...state.attachments.map((_, index) => ({
url: `https://oa.hyinda.com/api/reimbursements/${reimbursementId}/materials/preview/77777777-7777-4777-8777-777777777777/${state.invoices.length + index}?previewToken=attachment-${index}`,
materialType: "attachment",
materialIndex: index + 1,
pageNumber: 1,
})),
];
await fulfillJson(route, { expiresAt: "2026-07-31T10:02:00.000Z", pages });
return;
}
if (pathName.includes(`/api/reimbursements/${reimbursementId}/materials/preview/`) && request.method() === "GET") {
await route.fulfill({ status: 200, contentType: "image/png", body: Buffer.from("synthetic-preview-image") });
return;
}
if (
pathName === `/api/reimbursements/${reimbursementId}/attachments/${attachmentId}/preview-url`
&& request.method() === "POST"
@@ -611,17 +641,15 @@ test.describe("R111 国内报销发票与附件双端体验", () => {
await expect(page.getByText("北京悦海堂餐饮管理有限公司").first()).toBeVisible();
await expect(page.getByText("明细:业务招待 · 客户招待")).toBeVisible();
await expect(page.getByTestId("reimbursement-materials")).toContainText("发票 2 · 补充附件 0");
await expect(page.getByTestId("reimbursement-materials")).toContainText("数电发票 · 12345678");
await expect(page.getByTestId("reimbursement-materials")).toContainText("电子火车票 · 12345679");
await expect(page.getByTestId("reimbursement-materials")).not.toContainText("无代码");
await expect(page.getByTestId("reimbursement-materials")).not.toContainText("未识别销售方");
const itemRow = page.getByTestId("reimbursement-item-row");
await expect(itemRow.getByRole("button", { name: /查看关联发票/ })).toHaveCount(1);
await expect(itemRow).not.toContainText("未关联材料供应方");
await expect(itemRow.getByRole("button", { name: /查看关联发票/ })).toHaveCount(0);
await expect(itemRow).not.toContainText("电子火车票");
await expect(page.getByRole("button", { name: /查看发票原件/ })).toHaveCount(2);
await expect(page.getByRole("button", { name: "查看原件", exact: true })).toHaveCount(0);
await itemRow.getByRole("button", { name: /查看关联发票/ }).click();
await expect.poll(() => page.evaluate(() => (window as typeof window & { __r111OpenCalls: unknown[][] }).__r111OpenCalls.length)).toBe(1);
const invoiceOpenUrl = await page.evaluate(
() => String((window as typeof window & { __r111OpenCalls: unknown[][] }).__r111OpenCalls[0]?.[0] ?? ""),
);
expect(invoiceOpenUrl).toContain(`/api/invoice-pool/${pendingInvoiceId}/file?token=`);
await page.locator("#reimbursement-attachment-upload").setInputFiles({
name: "付款截图.png",
@@ -631,8 +659,13 @@ test.describe("R111 国内报销发票与附件双端体验", () => {
await page.getByRole("button", { name: "上传附件" }).click();
await expect(page.getByText("付款截图.png")).toBeVisible();
await expect(page.getByTestId("reimbursement-materials")).toContainText("发票 2 · 补充附件 1");
await page.getByTestId("reimbursement-attachments").getByRole("button", { name: /查看补充附件/ }).click();
await expect.poll(() => page.evaluate(() => (window as typeof window & { __r111OpenCalls: unknown[][] }).__r111OpenCalls.length)).toBe(2);
await page.getByTestId("reimbursement-materials-open-all").click();
await page.waitForURL(new RegExp(`/reimbursements/${reimbursementId}/materials$`));
await expect(page.getByTestId("reimbursement-material-viewer")).toContainText("共 3 页");
await expect(page.getByRole("img", { name: /预览/ })).toHaveCount(3);
await page.getByRole("button", { name: "返回报销单" }).click();
await page.waitForURL(new RegExp(`/reimbursements/${reimbursementId}$`));
expect(await page.evaluate(() => (window as typeof window & { __r111OpenCalls: unknown[][] }).__r111OpenCalls.length)).toBe(0);
const bodyText = await page.locator("body").innerText();
expect(bodyText).not.toContain("绑定整单");
@@ -733,16 +766,14 @@ test.describe("R111 国内报销发票与附件双端体验", () => {
await mobile.page.keyboard.press("Escape");
await expect(mobile.page.getByText("北京悦海堂餐饮管理有限公司").first()).toBeVisible();
await expect(mobile.page.getByTestId("mobile-reimbursement-materials")).toContainText("发票 2 · 补充附件 0");
await expect(mobile.page.getByTestId("mobile-reimbursement-materials")).toContainText("数电发票 · 12345678");
await expect(mobile.page.getByTestId("mobile-reimbursement-materials")).toContainText("电子火车票 · 12345679");
await expect(mobile.page.getByTestId("mobile-reimbursement-materials")).not.toContainText("无代码");
await expect(mobile.page.getByTestId("mobile-reimbursement-materials")).not.toContainText("未识别销售方");
const mobileItem = mobile.page.locator("article.mobile-list-card").filter({ hasText: "客户招待" }).first();
await expect(mobileItem.getByRole("button", { name: /查看关联发票/ })).toHaveCount(1);
await expect(mobileItem).not.toContainText("未关联材料供应方");
await expect(mobileItem.getByRole("button", { name: /查看关联发票/ })).toHaveCount(0);
await expect(mobileItem).not.toContainText("电子火车票");
await expect(mobile.page.getByRole("button", { name: "查看原件", exact: true })).toHaveCount(0);
const linkedInvoiceTarget = await mobileItem.getByRole("button", { name: /查看关联发票/ }).boundingBox();
expect(linkedInvoiceTarget?.height ?? 0).toBeGreaterThanOrEqual(44);
await mobile.page.getByRole("button", { name: /查看发票原件:044002600111-12345678/ }).click();
await expect.poll(() => mobile.page.evaluate(() => (
window as typeof window & { __r111BridgeCalls: Array<{ name: string }> }
).__r111BridgeCalls.filter((call) => call.name === "previewFile").length)).toBe(1);
await mobile.page.getByRole("button", { name: "添加附件" }).click();
await mobile.page.getByTestId("mobile-reimbursement-materials").locator("input[type='file']").setInputFiles({
@@ -753,26 +784,25 @@ test.describe("R111 国内报销发票与附件双端体验", () => {
await mobile.page.getByTestId("mobile-reimbursement-materials").getByRole("button", { name: "上传附件" }).click();
await expect(mobile.page.getByText("付款截图.png")).toBeVisible();
await expect(mobile.page.getByTestId("mobile-reimbursement-materials")).toContainText("发票 2 · 补充附件 1");
await mobile.page.getByTestId("mobile-reimbursement-attachments").getByRole("button", { name: /查看补充附件/ }).click();
const openAllButton = mobile.page.getByTestId("mobile-reimbursement-materials-open-all");
const openAllTarget = await openAllButton.boundingBox();
expect(openAllTarget?.height ?? 0).toBeGreaterThanOrEqual(44);
await openAllButton.click();
await expect.poll(() => mobile.page.evaluate(() => (
window as typeof window & { __r111BridgeCalls: Array<{ name: string }> }
).__r111BridgeCalls.filter((call) => call.name === "previewFile").length)).toBe(2);
).__r111BridgeCalls.filter((call) => call.name === "imagePreview").length)).toBe(1);
const nativePreviewCalls = await mobile.page.evaluate(() => (
window as typeof window & { __r111BridgeCalls: Array<{ name: string; params: Record<string, unknown> }> }
).__r111BridgeCalls.filter((call) => call.name === "previewFile"));
).__r111BridgeCalls.filter((call) => call.name === "imagePreview"));
expect(nativePreviewCalls).toEqual([
expect.objectContaining({
params: expect.objectContaining({
url: expect.stringContaining(`/api/invoice-pool/${pendingInvoiceId}/file?token=`),
name: "北京悦海堂发票.pdf",
size: 4096,
}),
}),
expect.objectContaining({
params: expect.objectContaining({
url: expect.stringContaining(`/attachments/${attachmentId}/file?previewToken=`),
name: "付款截图.png",
size: 2048,
current: expect.stringContaining(`/materials/preview/`),
urls: expect.arrayContaining([
expect.stringContaining("previewToken=invoice-0"),
expect.stringContaining("previewToken=invoice-1"),
expect.stringContaining("previewToken=attachment-0"),
]),
}),
}),
]);