R123/A1-A6 收敛移动发票反馈并补部署检查
这个提交包含在:
@@ -11,6 +11,8 @@ const reimbursementId = "11111111-1111-4111-8111-111111111111";
|
||||
const ownerId = "employee-r111";
|
||||
const pendingInvoiceId = "22222222-2222-4222-8222-222222222222";
|
||||
const attachmentId = "33333333-3333-4333-8333-333333333333";
|
||||
const availableInvoiceId = "44444444-4444-4444-8444-444444444444";
|
||||
const failedInvoiceId = "55555555-5555-4555-8555-555555555555";
|
||||
|
||||
function envelope(data: unknown) {
|
||||
return { requestId: "mock-r111-invoice-reimbursement", data };
|
||||
@@ -92,10 +94,54 @@ function buildAttachment() {
|
||||
};
|
||||
}
|
||||
|
||||
function buildAvailableInvoice() {
|
||||
return {
|
||||
id: availableInvoiceId,
|
||||
invoiceCode: "044002600111",
|
||||
invoiceNumber: "87654321",
|
||||
invoiceDate: "2026-05-08",
|
||||
sellerName: "北京悦海堂餐饮管理有限公司",
|
||||
totalAmount: "1683.00",
|
||||
originalName: "已有发票.pdf",
|
||||
createdAt: "2026-05-08T08:00:00.000Z",
|
||||
};
|
||||
}
|
||||
|
||||
function buildFailedInvoice() {
|
||||
return {
|
||||
id: failedInvoiceId,
|
||||
status: "ocr_failed",
|
||||
ocrType: "vat",
|
||||
ocrProvider: "tencent",
|
||||
ocrAttempts: 1,
|
||||
ocrLastError: "识别服务未返回完整发票信息",
|
||||
ocrCompletedAt: "2026-05-08T08:05:00.000Z",
|
||||
invoiceCode: null,
|
||||
invoiceNumber: null,
|
||||
invoiceDate: null,
|
||||
amount: null,
|
||||
taxAmount: null,
|
||||
totalAmount: null,
|
||||
linkedReimbursementId: null,
|
||||
linkedItemId: null,
|
||||
linkedTravelOrderId: null,
|
||||
originalName: "识别失败发票.pdf",
|
||||
mimeType: "application/pdf",
|
||||
fileSize: 4096,
|
||||
verification: null,
|
||||
owner: { id: ownerId, employeeNo: "E111", name: "测试员工" },
|
||||
uploadedBy: { id: ownerId, employeeNo: "E111", name: "测试员工" },
|
||||
createdAt: "2026-05-08T08:01:00.000Z",
|
||||
updatedAt: "2026-05-08T08:05:00.000Z",
|
||||
};
|
||||
}
|
||||
|
||||
function buildReimbursementState() {
|
||||
return {
|
||||
invoices: [] as ReturnType<typeof buildInvoice>[],
|
||||
attachments: [] as ReturnType<typeof buildAttachment>[],
|
||||
availableInvoices: [buildAvailableInvoice()],
|
||||
failedInvoices: [buildFailedInvoice()],
|
||||
statusPollCount: 0,
|
||||
};
|
||||
}
|
||||
@@ -283,18 +329,18 @@ async function mockApis(page: Page, state: ReturnType<typeof buildReimbursementS
|
||||
|
||||
if (pathName === "/api/invoice-pool/bindable") {
|
||||
await fulfillJson(route, {
|
||||
items: [
|
||||
{
|
||||
id: "44444444-4444-4444-8444-444444444444",
|
||||
invoiceCode: "044002600111",
|
||||
invoiceNumber: "87654321",
|
||||
invoiceDate: "2026-05-08",
|
||||
sellerName: "北京悦海堂餐饮管理有限公司",
|
||||
totalAmount: "1683.00",
|
||||
originalName: "已有发票.pdf",
|
||||
createdAt: "2026-05-08T08:00:00.000Z",
|
||||
},
|
||||
],
|
||||
items: state.availableInvoices,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === "/api/invoice-pool" && url.searchParams.get("status") === "ocr_failed") {
|
||||
await fulfillJson(route, {
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: state.failedInvoices.length,
|
||||
ownerScope: "own",
|
||||
items: state.failedInvoices,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -314,26 +360,27 @@ async function mockApis(page: Page, state: ReturnType<typeof buildReimbursementS
|
||||
|
||||
if (pathName === "/api/invoice-pool/status") {
|
||||
state.statusPollCount += 1;
|
||||
const stillPending = state.statusPollCount === 1;
|
||||
await fulfillJson(route, {
|
||||
items: [
|
||||
{
|
||||
id: pendingInvoiceId,
|
||||
status: "available",
|
||||
reason: "available",
|
||||
message: "发票可添加",
|
||||
invoiceCode: "044002600111",
|
||||
invoiceNumber: "12345678",
|
||||
invoiceDate: "2026-05-08",
|
||||
sellerName: "北京悦海堂餐饮管理有限公司",
|
||||
totalAmount: "1683.00",
|
||||
status: stillPending ? "pending_ocr" : "available",
|
||||
reason: stillPending ? "pending_ocr" : "available",
|
||||
message: stillPending ? "发票正在识别" : "发票可添加",
|
||||
invoiceCode: stillPending ? null : "044002600111",
|
||||
invoiceNumber: stillPending ? null : "12345678",
|
||||
invoiceDate: stillPending ? null : "2026-05-08",
|
||||
sellerName: stillPending ? null : "北京悦海堂餐饮管理有限公司",
|
||||
totalAmount: stillPending ? null : "1683.00",
|
||||
originalName: "北京悦海堂发票.pdf",
|
||||
linkedReimbursementId: null,
|
||||
linkedItemId: null,
|
||||
linkedTravelOrderId: null,
|
||||
ocrAttempts: 1,
|
||||
ocrAttempts: stillPending ? 0 : 1,
|
||||
ocrLastError: null,
|
||||
ocrCompletedAt: "2026-05-08T08:05:00.000Z",
|
||||
updatedAt: "2026-05-08T08:05:00.000Z",
|
||||
ocrCompletedAt: stillPending ? null : "2026-05-08T08:05:00.000Z",
|
||||
updatedAt: stillPending ? "2026-05-08T08:04:00.000Z" : "2026-05-08T08:05:00.000Z",
|
||||
},
|
||||
],
|
||||
missingIds: [],
|
||||
@@ -367,6 +414,28 @@ async function mockApis(page: Page, state: ReturnType<typeof buildReimbursementS
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === `/api/invoice-pool/${availableInvoiceId}/archive`) {
|
||||
state.availableInvoices = [];
|
||||
await fulfillJson(route, {
|
||||
id: availableInvoiceId,
|
||||
status: "archived",
|
||||
archivedReason: "manual",
|
||||
archivedAt: "2026-05-08T08:20:00.000Z",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === `/api/invoice-pool/${failedInvoiceId}/archive`) {
|
||||
state.failedInvoices = [];
|
||||
await fulfillJson(route, {
|
||||
id: failedInvoiceId,
|
||||
status: "archived",
|
||||
archivedReason: "manual",
|
||||
archivedAt: "2026-05-08T08:21:00.000Z",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await fulfillJson(route, {});
|
||||
});
|
||||
}
|
||||
@@ -494,15 +563,39 @@ test.describe("R111 国内报销发票与附件双端体验", () => {
|
||||
const dialogMetrics = await collectVisualMetrics(mobile.page);
|
||||
expect(dialogMetrics.activeElementTestId).not.toBe("mobile-reimbursement-invoice-search");
|
||||
|
||||
const invoiceDialog = mobile.page.getByTestId("reimbursement-invoice-dialog");
|
||||
await invoiceDialog.getByRole("button", { name: "已有发票" }).click();
|
||||
const availableInvoiceCard = invoiceDialog.locator("article").filter({ hasText: "044002600111-87654321" });
|
||||
const failedInvoiceCard = invoiceDialog.locator("article").filter({ hasText: "识别失败发票.pdf" });
|
||||
await expect(availableInvoiceCard).toBeVisible();
|
||||
await expect(failedInvoiceCard.getByText("识别失败", { exact: true })).toBeVisible();
|
||||
await expect(failedInvoiceCard.getByText("识别失败,可删除后重新上传。")).toBeVisible();
|
||||
|
||||
await availableInvoiceCard.getByRole("button", { name: "删除" }).click();
|
||||
await expect(mobile.page.getByRole("heading", { name: "删除发票?" })).toBeVisible();
|
||||
await mobile.page.getByRole("dialog").last().getByRole("button", { name: "删除" }).click();
|
||||
await expect(availableInvoiceCard).toHaveCount(0);
|
||||
await expect(failedInvoiceCard).toBeVisible();
|
||||
|
||||
await failedInvoiceCard.getByRole("button", { name: "删除" }).click();
|
||||
await expect(mobile.page.getByRole("heading", { name: "删除发票?" })).toBeVisible();
|
||||
await mobile.page.getByRole("dialog").last().getByRole("button", { name: "删除" }).click();
|
||||
await expect(failedInvoiceCard).toHaveCount(0);
|
||||
await expect(invoiceDialog.getByText("暂无可添加发票")).toBeVisible();
|
||||
|
||||
await invoiceDialog.getByRole("button", { name: "上传" }).click();
|
||||
|
||||
await mobile.page.getByTestId("reimbursement-invoice-upload-file").setInputFiles({
|
||||
name: "北京悦海堂发票.pdf",
|
||||
mimeType: "application/pdf",
|
||||
buffer: Buffer.from("%PDF r111 mobile invoice"),
|
||||
});
|
||||
await mobile.page.getByTestId("reimbursement-invoice-upload-submit").click();
|
||||
await expect(mobile.page.getByTestId("reimbursement-invoice-pending-hint")).toHaveCount(1);
|
||||
await expect(mobile.page.getByTestId("reimbursement-invoice-dialog")).not.toContainText("识别完成后可在");
|
||||
await expect(
|
||||
mobile.page.getByTestId("reimbursement-invoice-dialog").getByText("已添加 1 张发票"),
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
await mobile.page.keyboard.press("Escape");
|
||||
await expect(mobile.page.getByText("北京悦海堂餐饮管理有限公司")).toBeVisible();
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户