V1-V6 收口移动视口与 PDF 发票上传

这个提交包含在:
2026-05-10 23:30:13 +08:00
父节点 3c9cd3aa41
当前提交 491b42a232
共修改 47 个文件,包含 387 行新增和 244 行删除
+6 -6
查看文件
@@ -34,11 +34,11 @@ test("E2E 轻交互 OCR:报销详情上传后自动添加并提交", async ({
await expect(page.getByRole("dialog", { name: "添加发票" })).toBeVisible();
const marker = `MOCK_R73_SUCCESS_${Date.now()}`;
const invoiceFileName = `r73-success-${Date.now()}.png`;
const invoiceFileName = `r73-success-${Date.now()}.pdf`;
await page.getByTestId(TEST_IDS.reimbursement.invoiceUploadFile).setInputFiles({
name: invoiceFileName,
mimeType: "image/png",
buffer: Buffer.from(marker, "utf8"),
mimeType: "application/pdf",
buffer: Buffer.from(`%PDF-1.4\n${marker}\n%%EOF\n`, "utf8"),
});
await page.getByTestId(TEST_IDS.reimbursement.invoiceUploadSubmit).scrollIntoViewIfNeeded();
await page.getByTestId(TEST_IDS.reimbursement.invoiceUploadSubmit).click();
@@ -62,9 +62,9 @@ test("E2E 轻交互 OCR:票据中心缺关键字段后可在当前页修正并
await expect(page.getByTestId("invoice-pool-table")).toBeVisible();
await page.getByTestId("invoice-upload-file").setInputFiles({
name: `r73-missing-${Date.now()}.png`,
mimeType: "image/png",
buffer: Buffer.from("MOCK_OCR_MISSING:invoiceCode,sellerName", "utf8"),
name: `r73-missing-${Date.now()}.pdf`,
mimeType: "application/pdf",
buffer: Buffer.from("%PDF-1.4\nMOCK_OCR_MISSING:invoiceCode,sellerName\n%%EOF\n", "utf8"),
});
await page.getByTestId("invoice-upload-submit").click();
+3 -3
查看文件
@@ -30,15 +30,15 @@ test("E2E 主链路:登录→报销→待办审批→票据OCR→支付→登
// 2) employee: upload invoice and wait OCR
await (await openNavLink(page, "票据中心")).click();
const marker = `MOCK_E2E_${Date.now()}_${Math.random().toString(16).slice(2)}`;
const invoiceFileName = `e2e-${Date.now()}.png`;
const invoiceFileName = `e2e-${Date.now()}.pdf`;
const uploadResponsePromise = page.waitForResponse((response) => {
const url = new URL(response.url());
return response.request().method() === "POST" && url.pathname === "/api/invoice-pool/upload" && response.ok();
});
await page.getByTestId("invoice-upload-file").setInputFiles({
name: invoiceFileName,
mimeType: "image/png",
buffer: Buffer.from(marker, "utf8"),
mimeType: "application/pdf",
buffer: Buffer.from(`%PDF-1.4\n${marker}\n%%EOF\n`, "utf8"),
});
await page.getByTestId("invoice-upload-submit").click();
const uploadPayload = (await (await uploadResponsePromise).json()) as { data?: { invoicePoolId?: string | null } };
@@ -19,11 +19,12 @@ function resolveInvoiceFiles() {
const batchFiles = process.env.PLAYWRIGHT_OCR_LIVE_FILES?.split(/[\n,]/).map((item) => item.trim()).filter(Boolean) ?? [];
const batchDir = process.env.PLAYWRIGHT_OCR_LIVE_DIR?.trim() ?? "";
if (directFile) {
if (directFile && /\.pdf$/i.test(directFile)) {
files.add(path.resolve(directFile));
}
for (const file of batchFiles) {
if (!/\.pdf$/i.test(file)) continue;
files.add(path.resolve(file));
}
@@ -31,7 +32,7 @@ function resolveInvoiceFiles() {
for (const entry of readdirSync(batchDir).sort((left, right) => left.localeCompare(right, "zh-CN"))) {
const fullPath = path.join(batchDir, entry);
if (!statSync(fullPath).isFile()) continue;
if (!/\.(pdf|png|jpg|jpeg|webp)$/i.test(entry)) continue;
if (!/\.pdf$/i.test(entry)) continue;
files.add(path.resolve(fullPath));
}
}
@@ -76,7 +77,7 @@ test("手工 live 腾讯 OCR 批量辅助复核:真实票样逐张上传并输
test.skip(process.env.PLAYWRIGHT_OCR_LIVE !== "1", "仅在显式 live OCR 辅助复核模式下执行");
const invoiceFiles = resolveInvoiceFiles();
test.skip(!invoiceFiles.length, "需要提供 PLAYWRIGHT_OCR_LIVE_FILE / PLAYWRIGHT_OCR_LIVE_FILES / PLAYWRIGHT_OCR_LIVE_DIR");
test.skip(!invoiceFiles.length, "需要提供 PDF 格式的 PLAYWRIGHT_OCR_LIVE_FILE / PLAYWRIGHT_OCR_LIVE_FILES / PLAYWRIGHT_OCR_LIVE_DIR");
const results: BatchResult[] = [];
const liveOcrUser = resolveLiveOcrUser(EMPLOYEE);
@@ -1,4 +1,5 @@
import { existsSync } from "node:fs";
import path from "node:path";
import { test, expect } from "../helpers/fixtures";
import { EMPLOYEE, login } from "../helpers/auth";
@@ -9,7 +10,7 @@ test("手工 live 腾讯 OCR 辅助复核:真实 VAT 发票上传后进入可
test.skip(process.env.PLAYWRIGHT_OCR_LIVE !== "1", "仅在显式 live OCR 辅助复核模式下执行");
const invoiceFilePath = process.env.PLAYWRIGHT_OCR_LIVE_FILE?.trim() ?? "";
test.skip(!invoiceFilePath || !existsSync(invoiceFilePath), "需要提供存在的 PLAYWRIGHT_OCR_LIVE_FILE");
test.skip(!invoiceFilePath || !existsSync(invoiceFilePath) || path.extname(invoiceFilePath).toLowerCase() !== ".pdf", "需要提供存在的 PDF 格式 PLAYWRIGHT_OCR_LIVE_FILE");
const liveOcrUser = resolveLiveOcrUser(EMPLOYEE);
@@ -85,8 +85,10 @@ test.describe("R105 pretrial visual acceptance", () => {
await page.goto("/mobile/capture");
await expect(page.getByRole("heading", { name: "票据上传", exact: true })).toBeVisible();
const fileInput = page.getByTestId("mobile-capture-file");
const fileInput = page.getByTestId("mobile-capture-pdf-file");
await expect(fileInput).toHaveAttribute("multiple", "");
await expect(fileInput).toHaveAttribute("accept", "application/pdf");
await expect(fileInput).not.toHaveAttribute("capture", /.+/);
await expect(page.getByTestId("mobile-capture-upload")).toBeVisible();
await expectNoDesktopTableOnMobile(page);
await expectNoLegacyDesignCopy(page);
@@ -438,8 +438,8 @@ test.describe.serial("R108 企业微信移动端视觉专项", () => {
await employee390.page.context().setOffline(true);
await employee390.page.evaluate(() => window.dispatchEvent(new Event("offline")));
await expect(employee390.page.getByText("离线", { exact: true })).toBeVisible();
await employee390.page.getByTestId("mobile-capture-file").setInputFiles([
{ name: "r108-mobile-a.jpg", mimeType: "image/jpeg", buffer: Buffer.from("r108-mobile-a") },
await employee390.page.getByTestId("mobile-capture-pdf-file").setInputFiles([
{ name: "r108-mobile-a.pdf", mimeType: "application/pdf", buffer: Buffer.from("%PDF-1.4\n%%EOF\n") },
{ name: "r108-mobile-b.pdf", mimeType: "application/pdf", buffer: Buffer.from("%PDF-1.4\n%%EOF\n") },
]);
await expect(employee390.page.getByText("已选择 2 个")).toBeVisible();
@@ -161,9 +161,9 @@ test.describe.serial("R109 企业微信移动报销反馈专项", () => {
await employee.page.getByTestId("reimbursement-invoice-upload-file").setInputFiles([
{
name: `r109-mobile-${Date.now()}.jpg`,
mimeType: "image/jpeg",
buffer: Buffer.from(`r109-mobile-upload-${Date.now()}`),
name: `r109-mobile-${Date.now()}.pdf`,
mimeType: "application/pdf",
buffer: Buffer.from(`%PDF-1.4\nr109-mobile-upload-${Date.now()}\n%%EOF\n`),
},
]);
await employee.page.getByTestId("reimbursement-invoice-upload-submit").click();
@@ -145,7 +145,9 @@ test.describe.serial("R110 手机和 PC 路由分流", () => {
await mobile.page.goto("/invoice-pool");
await expect(mobile.page).toHaveURL(/\/mobile\/capture$/);
await expect(mobile.page.getByTestId("mobile-capture-file")).toBeAttached();
await expect(mobile.page.getByTestId("mobile-capture-pdf-file")).toBeAttached();
await expect(mobile.page.getByTestId("mobile-capture-pdf-file")).toHaveAttribute("accept", "application/pdf");
await expect(mobile.page.getByTestId("mobile-capture-pdf-file")).not.toHaveAttribute("capture", /.+/);
await expectNoPcShell(mobile.page);
await mobile.page.goto("/assets");
@@ -483,16 +483,29 @@ async function collectVisualMetrics(page: Page) {
document.querySelector("nav a[aria-current='page']");
const activeStyle = activeNav ? getComputedStyle(activeNav) : null;
const activeRgb = activeStyle ? parseRgb(activeStyle.backgroundColor) : null;
const stickyActions = document.querySelector("[data-testid='mobile-reimbursement-sticky-actions']");
const stickyRect = stickyActions?.getBoundingClientRect();
const addButton = document.querySelector("[data-testid='mobile-reimbursement-sticky-invoice-add']");
const submitButton = document.querySelector("[data-testid='reimbursement-submit']");
const addRect = addButton?.getBoundingClientRect();
const submitRect = submitButton?.getBoundingClientRect();
const bodyText = document.body?.innerText ?? "";
return {
width: window.innerWidth,
height: window.innerHeight,
scrollWidth: Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth ?? 0),
tableCount: Array.from(document.querySelectorAll("table")).filter((item) => item.getBoundingClientRect().height > 0).length,
navCount: document.querySelectorAll("[data-testid^='mobile-nav-']").length,
navHeight: navRect ? Math.round(navRect.height) : 0,
activeNavBackground: activeStyle?.backgroundColor ?? "",
activeNavAverage: activeRgb ? Math.round((activeRgb.r + activeRgb.g + activeRgb.b) / 3) : 255,
activeNavAlpha: activeRgb?.a ?? 0,
stickyActionsVisible: Boolean(stickyRect && stickyRect.height > 0 && stickyRect.bottom <= window.innerHeight + 2),
stickyBottomGap: stickyRect ? Math.round(window.innerHeight - stickyRect.bottom) : null,
stickyHeight: stickyRect ? Math.round(stickyRect.height) : 0,
stickyButtonsSameRow: Boolean(addRect && submitRect && Math.abs(addRect.top - submitRect.top) <= 2),
stickyButtonMinHeight: Math.min(addRect?.height ?? 0, submitRect?.height ?? 0),
activeElementTestId: document.activeElement?.getAttribute("data-testid") ?? "",
bodyText,
};
@@ -560,11 +573,12 @@ test.describe("R111 国内报销发票与附件双端体验", () => {
const beforeDialogMetrics = await collectVisualMetrics(mobile.page);
expect(beforeDialogMetrics.scrollWidth).toBeLessThanOrEqual(beforeDialogMetrics.width + 2);
expect(beforeDialogMetrics.tableCount).toBe(0);
expect(beforeDialogMetrics.navHeight).toBeLessThanOrEqual(68);
if (beforeDialogMetrics.activeNavAlpha > 0) {
expect(beforeDialogMetrics.activeNavAlpha).toBeGreaterThan(0.9);
expect(beforeDialogMetrics.activeNavAverage, beforeDialogMetrics.activeNavBackground).toBeLessThan(245);
}
expect(beforeDialogMetrics.navCount).toBe(0);
expect(beforeDialogMetrics.navHeight).toBe(0);
expect(beforeDialogMetrics.stickyActionsVisible).toBe(true);
expect(beforeDialogMetrics.stickyBottomGap).toBeLessThanOrEqual(2);
expect(beforeDialogMetrics.stickyButtonsSameRow).toBe(true);
expect(beforeDialogMetrics.stickyButtonMinHeight).toBeGreaterThanOrEqual(44);
await mobile.page.getByTestId("reimbursement-invoice-add").click();
await expect(mobile.page.getByTestId("reimbursement-invoice-dialog")).toBeVisible();
@@ -592,20 +606,12 @@ test.describe("R111 国内报销发票与附件双端体验", () => {
await expect(invoiceDialog.getByText("暂无可添加发票")).toBeVisible();
await invoiceDialog.getByRole("button", { name: "上传" }).click();
await expect(invoiceDialog.getByText("拍照或选图片")).toBeVisible();
await expect(invoiceDialog.getByText("选择 PDF/文件")).toBeVisible();
await expect(mobile.page.getByTestId("reimbursement-invoice-upload-file")).toHaveAttribute("accept", "image/*");
await expect(mobile.page.getByTestId("reimbursement-invoice-upload-file")).toHaveAttribute("capture", "environment");
await expect(mobile.page.getByTestId("reimbursement-invoice-upload-document")).toHaveAttribute(
"accept",
/application\/pdf/,
);
await expect(mobile.page.getByTestId("reimbursement-invoice-upload-document")).not.toHaveAttribute(
"capture",
/.+/,
);
await expect(invoiceDialog.getByText("选择 PDF 发票")).toBeVisible();
await expect(invoiceDialog.getByText("拍照或选图片")).toHaveCount(0);
await expect(mobile.page.getByTestId("reimbursement-invoice-upload-file")).toHaveAttribute("accept", "application/pdf");
await expect(mobile.page.getByTestId("reimbursement-invoice-upload-file")).not.toHaveAttribute("capture", /.+/);
await mobile.page.getByTestId("reimbursement-invoice-upload-document").setInputFiles({
await mobile.page.getByTestId("reimbursement-invoice-upload-file").setInputFiles({
name: "北京悦海堂发票.pdf",
mimeType: "application/pdf",
buffer: Buffer.from("%PDF r111 mobile invoice"),
@@ -476,12 +476,9 @@ test.describe("R124 企业微信移动入口矩阵", () => {
await mobile.page.goto("/mobile/capture");
await expect(mobile.page).toHaveURL(/\/mobile\/capture$/);
await expect(mobile.page.getByTestId("mobile-capture-camera-file")).toBeAttached();
await expect(mobile.page.getByTestId("mobile-capture-image-file")).toBeAttached();
await expect(mobile.page.getByTestId("mobile-capture-camera-file")).toHaveCount(0);
await expect(mobile.page.getByTestId("mobile-capture-image-file")).toHaveCount(0);
await expect(mobile.page.getByTestId("mobile-capture-pdf-file")).toBeAttached();
await expect(mobile.page.getByTestId("mobile-capture-camera-file")).toHaveAttribute("capture", "environment");
await expect(mobile.page.getByTestId("mobile-capture-camera-file")).toHaveAttribute("accept", "image/*");
await expect(mobile.page.getByTestId("mobile-capture-image-file")).not.toHaveAttribute("capture", /.+/);
await expect(mobile.page.getByTestId("mobile-capture-pdf-file")).toHaveAttribute("accept", "application/pdf");
await expect(mobile.page.getByTestId("mobile-capture-pdf-file")).not.toHaveAttribute("capture", /.+/);
await mobile.page.getByTestId("mobile-capture-pdf-file").setInputFiles({
@@ -4,8 +4,22 @@ import { expect, test, type Page, type Route } from "@playwright/test";
const ROOT_DIR = path.resolve(process.cwd(), "..");
const SCREENSHOT_DIR = path.join(ROOT_DIR, "output/playwright/r5-real-device-feedback-mobile-ux");
const WECOM_UA =
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 wxwork/4.1.20 MicroMessenger/8.0.1 Language/zh_CN";
const DEVICE_CASES = [
{
key: "iphone11-ios14",
label: "iPhone 11 iOS14",
viewport: { width: 414, height: 896 },
userAgent:
"Mozilla/5.0 (iPhone; CPU iPhone OS 14_8 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 wxwork/4.1.20 MicroMessenger/8.0.1 Language/zh_CN",
},
{
key: "redmi-k60",
label: "Redmi K60",
viewport: { width: 393, height: 873 },
userAgent:
"Mozilla/5.0 (Linux; Android 14; Redmi K60 Build/UKQ1.230804.001) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/124.0.0.0 Mobile Safari/537.36 wxwork/4.1.20 MicroMessenger/8.0.1 Language/zh_CN",
},
] as const;
const employee = {
id: "11111111-1111-4111-8111-111111111111",
@@ -200,61 +214,115 @@ async function login(page: Page) {
async function collectMobileMetrics(page: Page) {
return page.evaluate(() => {
const bottomNav = document.querySelector("[data-testid='mobile-nav-reimbursements']")?.closest("nav");
const bottomNavRect = bottomNav?.getBoundingClientRect();
const bottomNavStyle = bottomNav ? window.getComputedStyle(bottomNav) : null;
const navItems = ["home", "approvals", "reimbursements", "me"].map((key) => {
const element = document.querySelector(`[data-testid='mobile-nav-${key}']`);
const rect = element?.getBoundingClientRect();
return {
key,
visible: Boolean(rect && rect.width > 0 && rect.height > 0),
width: rect?.width ?? 0,
height: rect?.height ?? 0,
};
});
const stickyActions = document.querySelector("[data-testid='mobile-reimbursement-sticky-actions']");
const stickyRect = stickyActions?.getBoundingClientRect();
const stickyButtons = [
document.querySelector("[data-testid='mobile-reimbursement-sticky-invoice-add']"),
document.querySelector("[data-testid='reimbursement-submit']"),
].filter(Boolean).map((element) => element!.getBoundingClientRect());
return {
width: window.innerWidth,
height: window.innerHeight,
scrollWidth: Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth ?? 0),
tableCount: Array.from(document.querySelectorAll("table")).filter((item) => item.getBoundingClientRect().height > 0).length,
bottomNavVisible: Boolean(bottomNav && bottomNav.getBoundingClientRect().height > 0),
bottomNavVisible: Boolean(bottomNavRect && bottomNavRect.height > 0),
bottomNavFixed: bottomNavStyle?.position === "fixed",
bottomNavBottomGap: bottomNavRect ? window.innerHeight - bottomNavRect.bottom : null,
navItems,
stickyActionsVisible: Boolean(stickyRect && stickyRect.height > 0 && stickyRect.bottom <= window.innerHeight + 2),
stickyBottomGap: stickyRect ? window.innerHeight - stickyRect.bottom : null,
stickyButtonsSameRow:
stickyButtons.length === 2 ? Math.abs(stickyButtons[0].top - stickyButtons[1].top) <= 2 : stickyButtons.length <= 1,
stickyButtonMinHeight: stickyButtons.length ? Math.min(...stickyButtons.map((rect) => rect.height)) : 0,
stickyButtonMinWidth: stickyButtons.length ? Math.min(...stickyButtons.map((rect) => rect.width)) : 0,
text: document.body?.innerText ?? "",
};
});
}
test.describe("R5 真机反馈移动交互", () => {
test.use({
viewport: { width: 390, height: 844 },
isMobile: true,
hasTouch: true,
userAgent: WECOM_UA,
locale: "zh-CN",
serviceWorkers: "block",
});
for (const device of DEVICE_CASES) {
test.describe(device.label, () => {
test.use({
viewport: device.viewport,
isMobile: true,
hasTouch: true,
userAgent: device.userAgent,
locale: "zh-CN",
serviceWorkers: "block",
});
test("中文头像首字、报销底部 Tab 和详情深层页交互符合企业微信体验", async ({ page }) => {
await mockApis(page);
await login(page);
test("中文头像首字、底部 Tab 固定和详情深层页双按钮符合企业微信体验", async ({ page }) => {
await mockApis(page);
await login(page);
await page.goto("/mobile/me");
await expect(page.getByText("王", { exact: true })).toBeVisible();
await page.screenshot({ path: screenshotPath("mobile-me-avatar"), fullPage: true });
await page.goto("/mobile/me");
await expect(page.getByText("王", { exact: true })).toBeVisible();
await page.screenshot({ path: screenshotPath(`${device.key}-mobile-me-avatar`), fullPage: true });
await page.goto("/mobile/reimbursements");
await expect(page.getByTestId("mobile-nav-reimbursements")).toBeVisible();
await expect(page.getByTestId("mobile-reimbursements-list")).toBeVisible();
await page.screenshot({ path: screenshotPath("mobile-reimbursements-tab"), fullPage: true });
await page.goto("/mobile");
await expect(page.getByTestId("mobile-bottom-nav")).toBeVisible();
await expect(page.getByTestId("mobile-nav-home")).toBeVisible();
await page.screenshot({ path: screenshotPath(`${device.key}-mobile-home-bottom-nav`), fullPage: true });
const listMetrics = await collectMobileMetrics(page);
expect(listMetrics.bottomNavVisible).toBe(true);
expect(listMetrics.scrollWidth).toBeLessThanOrEqual(listMetrics.width + 2);
const homeMetrics = await collectMobileMetrics(page);
expect(homeMetrics.bottomNavVisible).toBe(true);
expect(homeMetrics.bottomNavFixed).toBe(true);
expect(homeMetrics.bottomNavBottomGap).not.toBeNull();
expect(homeMetrics.bottomNavBottomGap!).toBeLessThanOrEqual(2);
expect(homeMetrics.scrollWidth).toBeLessThanOrEqual(homeMetrics.width + 2);
for (const item of homeMetrics.navItems) {
expect(item.visible, `${device.label} ${item.key} nav item visible`).toBe(true);
expect(item.width, `${device.label} ${item.key} nav item touch width`).toBeGreaterThanOrEqual(44);
expect(item.height, `${device.label} ${item.key} nav item touch height`).toBeGreaterThanOrEqual(44);
}
await page.goto("/mobile/reimbursements/r5-reimbursement-1");
await expect(page.getByTestId("mobile-reimbursement-detail")).toBeVisible();
await expect(page.getByTestId("mobile-nav-reimbursements")).toHaveCount(0);
await expect(page.getByRole("link", { name: "返回" })).toBeVisible();
await expect(page.getByTestId("mobile-reimbursement-sticky-actions")).toBeVisible();
await expect(page.getByTestId("mobile-reimbursement-sticky-invoice-add")).toBeVisible();
await expect(page.getByTestId("reimbursement-submit")).toBeVisible();
await page.screenshot({ path: screenshotPath("mobile-reimbursement-detail-sticky"), fullPage: true });
await page.goto("/mobile/reimbursements");
await expect(page.getByTestId("mobile-nav-reimbursements")).toBeVisible();
await expect(page.getByTestId("mobile-reimbursements-list")).toBeVisible();
await page.screenshot({ path: screenshotPath(`${device.key}-mobile-reimbursements-tab`), fullPage: true });
const detailMetrics = await collectMobileMetrics(page);
expect(detailMetrics.bottomNavVisible).toBe(false);
expect(detailMetrics.stickyActionsVisible).toBe(true);
expect(detailMetrics.tableCount).toBe(0);
expect(detailMetrics.scrollWidth).toBeLessThanOrEqual(detailMetrics.width + 2);
expect(detailMetrics.text).not.toContain("发票池");
expect(detailMetrics.text).not.toContain("绑定整单");
});
const listMetrics = await collectMobileMetrics(page);
expect(listMetrics.bottomNavVisible).toBe(true);
expect(listMetrics.bottomNavFixed).toBe(true);
expect(listMetrics.bottomNavBottomGap).not.toBeNull();
expect(listMetrics.bottomNavBottomGap!).toBeLessThanOrEqual(2);
expect(listMetrics.scrollWidth).toBeLessThanOrEqual(listMetrics.width + 2);
await page.goto("/mobile/reimbursements/r5-reimbursement-1");
await expect(page.getByTestId("mobile-reimbursement-detail")).toBeVisible();
await expect(page.getByTestId("mobile-nav-reimbursements")).toHaveCount(0);
await expect(page.getByRole("link", { name: "返回" })).toBeVisible();
await expect(page.getByTestId("mobile-reimbursement-sticky-actions")).toBeVisible();
await expect(page.getByTestId("mobile-reimbursement-sticky-invoice-add")).toBeVisible();
await expect(page.getByTestId("reimbursement-submit")).toBeVisible();
await page.screenshot({ path: screenshotPath(`${device.key}-mobile-reimbursement-detail-sticky`), fullPage: true });
const detailMetrics = await collectMobileMetrics(page);
expect(detailMetrics.bottomNavVisible).toBe(false);
expect(detailMetrics.stickyActionsVisible).toBe(true);
expect(detailMetrics.stickyBottomGap).not.toBeNull();
expect(detailMetrics.stickyBottomGap!).toBeLessThanOrEqual(2);
expect(detailMetrics.stickyButtonsSameRow).toBe(true);
expect(detailMetrics.stickyButtonMinHeight).toBeGreaterThanOrEqual(44);
expect(detailMetrics.stickyButtonMinWidth).toBeGreaterThanOrEqual(120);
expect(detailMetrics.tableCount).toBe(0);
expect(detailMetrics.scrollWidth).toBeLessThanOrEqual(detailMetrics.width + 2);
expect(detailMetrics.text).not.toContain("发票池");
expect(detailMetrics.text).not.toContain("绑定整单");
});
});
}
});
@@ -183,9 +183,9 @@ async function createSubmittedReimbursement(api: AuthedApi, label: string): Prom
await api.post("/api/invoice-pool/upload", {
multipart: {
file: {
name: `notify-${label}.png`,
mimeType: "image/png",
buffer: Buffer.from(`PW_R75_NOTIFY_${label}_${Date.now()}_${Math.random().toString(16).slice(2)}`, "utf8"),
name: `notify-${label}.pdf`,
mimeType: "application/pdf",
buffer: Buffer.from(`%PDF-1.4\nPW_R75_NOTIFY_${label}_${Date.now()}_${Math.random().toString(16).slice(2)}\n%%EOF\n`, "utf8"),
},
ocr_type: "general",
},
@@ -342,9 +342,9 @@ test.describe("R92 手机端票据与报销可用性", () => {
await loginThenUseMobileViewport(page);
await page.goto("/mobile/capture");
await page.getByTestId("mobile-capture-file").setInputFiles([
{ name: "mobile-a.jpg", mimeType: "image/jpeg", buffer: Buffer.from("mobile-a") },
{ name: "mobile-b.jpg", mimeType: "image/jpeg", buffer: Buffer.from("mobile-b") },
await page.getByTestId("mobile-capture-pdf-file").setInputFiles([
{ name: "mobile-a.pdf", mimeType: "application/pdf", buffer: Buffer.from("%PDF-a") },
{ name: "mobile-b.pdf", mimeType: "application/pdf", buffer: Buffer.from("%PDF-b") },
]);
await expect(page.getByText("已选择 2 个")).toBeVisible();
await attachScreenshot(page, testInfo, "r97-mobile-capture-multi-upload.png");
@@ -88,9 +88,9 @@ test("E2E 报销支付批次主链路:提交 -> 审批 -> 创建批次 -> 复
return response.request().method() === "POST" && url.pathname === "/api/invoice-pool/upload" && response.ok();
});
await page.getByTestId("invoice-upload-file").setInputFiles({
name: `r78-${Date.now()}.png`,
mimeType: "image/png",
buffer: Buffer.from(marker, "utf8"),
name: `r78-${Date.now()}.pdf`,
mimeType: "application/pdf",
buffer: Buffer.from(`%PDF-1.4\n${marker}\n%%EOF\n`, "utf8"),
});
await page.getByTestId("invoice-upload-submit").click();
const uploadPayload = (await (await uploadResponsePromise).json()) as { data?: { invoicePoolId?: string | null } };
@@ -31,9 +31,9 @@ test("E2E 报销审批进度:提交后详情页展示当前步骤进度", asyn
return response.request().method() === "POST" && url.pathname === "/api/invoice-pool/upload" && response.ok();
});
await page.getByTestId("invoice-upload-file").setInputFiles({
name: `progress-${Date.now()}.png`,
mimeType: "image/png",
buffer: Buffer.from(marker, "utf8"),
name: `progress-${Date.now()}.pdf`,
mimeType: "application/pdf",
buffer: Buffer.from(`%PDF-1.4\n${marker}\n%%EOF\n`, "utf8"),
});
await page.getByTestId("invoice-upload-submit").click();
const uploadPayload = (await (await uploadResponsePromise).json()) as { data?: { invoicePoolId?: string | null } };
+3 -3
查看文件
@@ -52,15 +52,15 @@ test("E2E 差旅支付主链路:申请→审批→支付→绑票", async ({ p
await expect(orderCard).not.toContainText("标记已支付");
await page.goto("/invoice-pool");
const invoiceFileName = `travel-payment-${Date.now()}.png`;
const invoiceFileName = `travel-payment-${Date.now()}.pdf`;
const uploadResponsePromise = page.waitForResponse((response) => {
const url = new URL(response.url());
return response.request().method() === "POST" && url.pathname === "/api/invoice-pool/upload" && response.ok();
});
await page.getByTestId("invoice-upload-file").setInputFiles({
name: invoiceFileName,
mimeType: "image/png",
buffer: Buffer.from(`TRAVEL_PAYMENT_${Date.now()}`, "utf8"),
mimeType: "application/pdf",
buffer: Buffer.from(`%PDF-1.4\nTRAVEL_PAYMENT_${Date.now()}\n%%EOF\n`, "utf8"),
});
await page.getByTestId("invoice-upload-submit").click();
const uploadPayload = (await (await uploadResponsePromise).json()) as { data?: { invoicePoolId?: string | null } };