实现企微原生材料预览(07-27-yang-jun-reimbursement-readiness)
这个提交包含在:
@@ -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("绑定整单");
|
||||
|
||||
在新工单中引用
屏蔽一个用户