V1-V6 收口移动视口与 PDF 发票上传
这个提交包含在:
@@ -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();
|
||||
|
||||
|
||||
@@ -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 } };
|
||||
|
||||
@@ -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 } };
|
||||
|
||||
在新工单中引用
屏蔽一个用户