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