实现企微原生材料预览(07-27-yang-jun-reimbursement-readiness)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-07-31 18:57:34 +08:00
父节点 e123d50f86
当前提交 410d8cd71b
共修改 23 个文件,包含 1352 行新增和 69 行删除
@@ -78,6 +78,8 @@ function buildInvoice() {
sellerName: "北京悦海堂餐饮管理有限公司",
totalAmount: "1683.00",
originalName: "北京悦海堂发票.pdf",
mimeType: "application/pdf",
fileSize: 4096,
linkedItemId: "item-r111",
};
}
@@ -223,6 +225,16 @@ async function mockApis(page: Page, state: ReturnType<typeof buildReimbursementS
return;
}
if (pathName === "/api/auth/wecom/js-sdk-signature" && request.method() === "POST") {
await fulfillJson(route, {
corpId: "ww-test",
timestamp: 1785484800,
nonceStr: "synthetic-r111-nonce",
signature: "1111111111111111111111111111111111111111",
});
return;
}
if (pathName === "/api/auth/login" || pathName === "/api/auth/refresh") {
await route.fulfill({
status: 200,
@@ -327,6 +339,33 @@ async function mockApis(page: Page, state: ReturnType<typeof buildReimbursementS
return;
}
if (
pathName === `/api/reimbursements/${reimbursementId}/attachments/${attachmentId}/preview-url`
&& request.method() === "POST"
) {
await fulfillJson(route, {
signedUrl: `https://oa.hyinda.com/api/reimbursements/${reimbursementId}/attachments/${attachmentId}/file?previewToken=synthetic`,
expiresAt: "2026-07-31T10:02:00.000Z",
originalName: "付款截图.png",
mimeType: "image/png",
fileSize: 2048,
});
return;
}
if (
pathName === `/api/reimbursements/${reimbursementId}/attachments/${attachmentId}/file`
&& request.method() === "GET"
) {
await route.fulfill({
status: 200,
contentType: "image/png",
headers: { "content-disposition": 'inline; filename="attachment.png"' },
body: Buffer.from("synthetic-r111-attachment"),
});
return;
}
if (pathName === "/api/invoice-pool/bindable") {
await fulfillJson(route, {
items: state.availableInvoices,
@@ -405,6 +444,17 @@ async function mockApis(page: Page, state: ReturnType<typeof buildReimbursementS
return;
}
if (pathName === `/api/invoice-pool/${pendingInvoiceId}/signed-url` && request.method() === "POST") {
await fulfillJson(route, {
signedUrl: `https://oa.hyinda.com/api/invoice-pool/${pendingInvoiceId}/file?token=synthetic`,
expiresAt: "2026-07-31T10:02:00.000Z",
originalName: "北京悦海堂发票.pdf",
mimeType: "application/pdf",
fileSize: 4096,
});
return;
}
if (pathName === `/api/invoice-pool/${pendingInvoiceId}/unbind`) {
state.invoices = [];
await fulfillJson(route, {
@@ -515,6 +565,14 @@ async function collectVisualMetrics(page: Page) {
test.describe("R111 国内报销发票与附件双端体验", () => {
test("PC 报销详情上传发票后自动添加,并提供附件材料", async ({ page }) => {
const state = buildReimbursementState();
await page.addInitScript(() => {
const openCalls: unknown[][] = [];
Object.defineProperty(window, "__r111OpenCalls", { value: openCalls, configurable: true });
window.open = ((...args: unknown[]) => {
openCalls.push(args);
return null;
}) as typeof window.open;
});
await mockApis(page, state);
await login(page);
@@ -540,6 +598,12 @@ test.describe("R111 国内报销发票与附件双端体验", () => {
await page.keyboard.press("Escape");
await expect(page.getByText("北京悦海堂餐饮管理有限公司")).toBeVisible();
await expect(page.getByText("明细:业务招待 · 客户招待")).toBeVisible();
await page.getByRole("button", { name: "查看原件" }).first().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",
@@ -548,6 +612,8 @@ test.describe("R111 国内报销发票与附件双端体验", () => {
});
await page.getByRole("button", { name: "上传附件" }).click();
await expect(page.getByText("付款截图.png")).toBeVisible();
await page.getByTestId("reimbursement-attachments").getByRole("button", { name: "查看原件" }).click();
await expect.poll(() => page.evaluate(() => (window as typeof window & { __r111OpenCalls: unknown[][] }).__r111OpenCalls.length)).toBe(2);
const bodyText = await page.locator("body").innerText();
expect(bodyText).not.toContain("绑定整单");
@@ -564,6 +630,26 @@ test.describe("R111 国内报销发票与附件双端体验", () => {
test("企业微信手机端保持移动布局,发票和附件能力与 PC 一致", async ({ browser }) => {
const state = buildReimbursementState();
const mobile = await newWecomPage(browser);
await mobile.page.addInitScript(() => {
const bridgeCalls: Array<{ name: string; params: Record<string, unknown> }> = [];
Object.defineProperty(window, "__r111BridgeCalls", { value: bridgeCalls, configurable: true });
Object.defineProperty(window, "WeixinJSBridge", {
configurable: true,
value: {
invoke(name: string, params: Record<string, unknown>, callback: (result: Record<string, unknown>) => void) {
bridgeCalls.push({ name, params });
callback({ err_msg: `${name}:ok` });
},
on() {},
},
});
const openCalls: unknown[][] = [];
Object.defineProperty(window, "__r111OpenCalls", { value: openCalls, configurable: true });
window.open = ((...args: unknown[]) => {
openCalls.push(args);
return null;
}) as typeof window.open;
});
await mockApis(mobile.page, state);
try {
@@ -627,6 +713,10 @@ test.describe("R111 国内报销发票与附件双端体验", () => {
).toBeVisible({ timeout: 15_000 });
await mobile.page.keyboard.press("Escape");
await expect(mobile.page.getByText("北京悦海堂餐饮管理有限公司")).toBeVisible();
await mobile.page.getByRole("button", { name: "查看原件" }).first().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.getByTestId("mobile-reimbursement-attachments").getByRole("button", { name: "选择" }).click();
await mobile.page.locator("[data-testid='mobile-reimbursement-attachments'] input[type='file']").setInputFiles({
@@ -636,6 +726,32 @@ test.describe("R111 国内报销发票与附件双端体验", () => {
});
await mobile.page.getByTestId("mobile-reimbursement-attachments").getByRole("button", { name: "上传附件" }).click();
await expect(mobile.page.getByText("付款截图.png")).toBeVisible();
await mobile.page.getByTestId("mobile-reimbursement-attachments").getByRole("button", { name: "查看原件" }).click();
await expect.poll(() => mobile.page.evaluate(() => (
window as typeof window & { __r111BridgeCalls: Array<{ name: string }> }
).__r111BridgeCalls.filter((call) => call.name === "previewFile").length)).toBe(2);
const nativePreviewCalls = await mobile.page.evaluate(() => (
window as typeof window & { __r111BridgeCalls: Array<{ name: string; params: Record<string, unknown> }> }
).__r111BridgeCalls.filter((call) => call.name === "previewFile"));
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,
}),
}),
]);
expect(await mobile.page.evaluate(() => (
window as typeof window & { __r111OpenCalls: unknown[][] }
).__r111OpenCalls.length)).toBe(0);
const bodyText = await mobile.page.locator("body").innerText();
expect(bodyText).not.toContain("绑定整单");