R5-M1-M5 收口正式审批规则与移动反馈
这个提交包含在:
@@ -163,6 +163,18 @@ export function mapApiErrorMessage(
|
||||
return "不能将任务转审给自己。";
|
||||
}
|
||||
|
||||
if (details?.workflow === "self_approval_not_allowed") {
|
||||
return "自己的申请不能审批,请等待下一审批人处理。";
|
||||
}
|
||||
|
||||
if (details?.workflow === "business_approval_role_required") {
|
||||
return "系统管理员不自动获得业务审批权,请配置正式审批角色后处理。";
|
||||
}
|
||||
|
||||
if (details?.workflow === "no_actionable_approver") {
|
||||
return "当前审批规则未找到审批人,请联系管理员修正规则。";
|
||||
}
|
||||
|
||||
if (details?.toApproverId === "target_not_approver") {
|
||||
return "目标人员不具备审批权限。";
|
||||
}
|
||||
|
||||
@@ -16,12 +16,17 @@ function buildInitials(name: string) {
|
||||
return "员工";
|
||||
}
|
||||
|
||||
const cjkMatch = trimmed.match(/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u);
|
||||
if (cjkMatch?.[0]) {
|
||||
return cjkMatch[0];
|
||||
}
|
||||
|
||||
if (/^[A-Za-z0-9 ]+$/.test(trimmed)) {
|
||||
const segments = trimmed.split(/\s+/).filter(Boolean);
|
||||
return segments.slice(0, 2).map((item) => item[0]?.toUpperCase() ?? "").join("") || trimmed.slice(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
return trimmed.slice(0, 2);
|
||||
return Array.from(trimmed).slice(0, 2).join("");
|
||||
}
|
||||
|
||||
export function EmployeeAvatar({
|
||||
|
||||
@@ -20,7 +20,7 @@ export const MOBILE_LEAVE_PERMISSIONS = ["leave.view_own", "leave.view_all"] as
|
||||
export const MOBILE_REIMBURSEMENT_PERMISSIONS = ["expense.view_own", "expense.view_all"] as const;
|
||||
|
||||
export type MobileNavItem = {
|
||||
key: "home" | "approvals" | "capture" | "me";
|
||||
key: "home" | "approvals" | "reimbursements" | "me";
|
||||
to: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
@@ -30,6 +30,6 @@ export type MobileNavItem = {
|
||||
export const MOBILE_NAV_ITEMS: MobileNavItem[] = [
|
||||
{ key: "home", to: "/mobile", label: "首页", icon: "home" },
|
||||
{ key: "approvals", to: "/mobile/approvals", label: "审批", icon: "fact_check", anyOf: MOBILE_APPROVAL_PERMISSIONS },
|
||||
{ key: "capture", to: "/mobile/capture", label: "票据", icon: "receipt_long", anyOf: MOBILE_CAPTURE_PERMISSIONS },
|
||||
{ key: "reimbursements", to: "/mobile/reimbursements", label: "报销", icon: "request_quote", anyOf: MOBILE_REIMBURSEMENT_PERMISSIONS },
|
||||
{ key: "me", to: "/mobile/me", label: "我的", icon: "person" },
|
||||
];
|
||||
|
||||
@@ -11,6 +11,8 @@ import { resolveMobileEquivalentPath, shouldPreferMobileWorkplace } from "../lib
|
||||
export function AppShellLayout() {
|
||||
const location = useLocation();
|
||||
const isMobileRoute = location.pathname.startsWith("/mobile");
|
||||
const isMobileDeepRoute = /^\/mobile\/reimbursements\/[^/]+$/.test(location.pathname);
|
||||
const showMobileBottomNav = isMobileRoute && !isMobileDeepRoute;
|
||||
const mobileEquivalentPath = !isMobileRoute && shouldPreferMobileWorkplace()
|
||||
? resolveMobileEquivalentPath(location.pathname, location.search)
|
||||
: null;
|
||||
@@ -42,7 +44,7 @@ export function AppShellLayout() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isMobileRoute ? <MobileBottomNav /> : null}
|
||||
{showMobileBottomNav ? <MobileBottomNav /> : null}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -758,9 +758,12 @@ export function MobileReimbursementDetailPage() {
|
||||
}
|
||||
|
||||
const displayCode = resolveReimbursementDisplayCode(detail);
|
||||
const showSubmitAction = Boolean(canEdit && canSubmitExpense);
|
||||
const showStickyActions = Boolean(showSubmitAction || canOpenInvoiceDialog);
|
||||
const showSecondaryActions = Boolean(actionError || canDeleteDraft || !canEdit);
|
||||
|
||||
return (
|
||||
<PageScaffold className="space-y-4 pb-4" data-testid="mobile-reimbursement-detail">
|
||||
<PageScaffold className={cn("space-y-4", showStickyActions ? "pb-28" : "pb-6")} data-testid="mobile-reimbursement-detail">
|
||||
<header className="flex items-center gap-3" data-testid="mobile-reimbursement-detail">
|
||||
<Button asChild size="sm" variant="secondary">
|
||||
<Link to="/mobile/reimbursements">返回</Link>
|
||||
@@ -1058,39 +1061,60 @@ export function MobileReimbursementDetailPage() {
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="rounded-lg border border-zinc-200 bg-white p-4 shadow-sm" data-testid="mobile-reimbursement-actions">
|
||||
<h2 className="text-base font-semibold text-zinc-900">操作</h2>
|
||||
{actionError ? (
|
||||
<div className="mt-3 whitespace-pre-wrap rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{actionError}
|
||||
{showSecondaryActions ? (
|
||||
<section className="rounded-lg border border-zinc-200 bg-white p-4 shadow-sm" data-testid="mobile-reimbursement-actions">
|
||||
<h2 className="text-base font-semibold text-zinc-900">操作</h2>
|
||||
{actionError ? (
|
||||
<div className="mt-3 whitespace-pre-wrap rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{actionError}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mt-3 grid gap-2">
|
||||
{canDeleteDraft ? (
|
||||
<Button
|
||||
className="w-full"
|
||||
disabled={deleteReimbursementMutation.isPending}
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={() => setConfirmDeleteReimbursement(true)}
|
||||
>
|
||||
删除草稿
|
||||
</Button>
|
||||
) : null}
|
||||
{!canEdit ? <div className="text-sm text-zinc-500">当前状态不可编辑。</div> : null}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{showStickyActions ? (
|
||||
<div
|
||||
className="sticky bottom-0 z-20 -mx-4 border-t border-zinc-200 bg-white/95 px-4 pb-[max(0.75rem,env(safe-area-inset-bottom))] pt-3 shadow-[0_-10px_24px_rgba(15,23,42,0.10)] backdrop-blur"
|
||||
data-testid="mobile-reimbursement-sticky-actions"
|
||||
>
|
||||
<div className={cn("grid gap-2", canOpenInvoiceDialog && showSubmitAction ? "grid-cols-2" : "grid-cols-1")}>
|
||||
{canOpenInvoiceDialog ? (
|
||||
<Button
|
||||
data-testid="mobile-reimbursement-sticky-invoice-add"
|
||||
variant={showSubmitAction ? "secondary" : "default"}
|
||||
type="button"
|
||||
onClick={() => closeInvoiceDialog(true)}
|
||||
>
|
||||
添加发票
|
||||
</Button>
|
||||
) : null}
|
||||
{showSubmitAction ? (
|
||||
<Button
|
||||
data-testid={TEST_IDS.reimbursement.submit}
|
||||
disabled={submitMutation.isPending}
|
||||
type="button"
|
||||
onClick={() => void handleSubmit()}
|
||||
>
|
||||
{submitMutation.isPending ? "提交中..." : "提交审批"}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mt-3 grid gap-2">
|
||||
{canEdit && canSubmitExpense ? (
|
||||
<Button
|
||||
className="w-full"
|
||||
data-testid={TEST_IDS.reimbursement.submit}
|
||||
disabled={submitMutation.isPending}
|
||||
type="button"
|
||||
onClick={() => void handleSubmit()}
|
||||
>
|
||||
{submitMutation.isPending ? "提交中..." : "提交审批"}
|
||||
</Button>
|
||||
) : null}
|
||||
{canDeleteDraft ? (
|
||||
<Button
|
||||
className="w-full"
|
||||
disabled={deleteReimbursementMutation.isPending}
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={() => setConfirmDeleteReimbursement(true)}
|
||||
>
|
||||
删除草稿
|
||||
</Button>
|
||||
) : null}
|
||||
{!canEdit ? <div className="text-sm text-zinc-500">当前状态不可编辑。</div> : null}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<Dialog open={isInvoiceDialogOpen} onOpenChange={closeInvoiceDialog}>
|
||||
<DialogContent
|
||||
|
||||
@@ -60,7 +60,7 @@ test.describe("R104 public visual acceptance", () => {
|
||||
await page.goto("/mobile");
|
||||
await expect(page.getByTestId("mobile-nav-home")).toBeVisible();
|
||||
await expect(page.getByTestId("mobile-nav-approvals")).toBeVisible();
|
||||
await expect(page.getByTestId("mobile-nav-capture")).toBeVisible();
|
||||
await expect(page.getByTestId("mobile-nav-reimbursements")).toBeVisible();
|
||||
await expect(page.getByTestId("mobile-nav-me")).toBeVisible();
|
||||
await expectNoDesignRegressionCopy(page);
|
||||
await page.screenshot({ path: screenshotPath("mobile-home.png"), fullPage: true });
|
||||
|
||||
@@ -77,7 +77,7 @@ test.describe("R105 pretrial visual acceptance", () => {
|
||||
|
||||
await page.goto("/mobile");
|
||||
await expect(page.getByTestId("mobile-nav-home")).toBeVisible();
|
||||
await expect(page.getByTestId("mobile-nav-capture")).toBeVisible();
|
||||
await expect(page.getByTestId("mobile-nav-reimbursements")).toBeVisible();
|
||||
await expectNoDesktopTableOnMobile(page);
|
||||
await expectNoLegacyDesignCopy(page);
|
||||
await expectNotBlackSurface(page.getByTestId("mobile-nav-home"));
|
||||
|
||||
@@ -469,7 +469,12 @@ test.describe("R124 企业微信移动入口矩阵", () => {
|
||||
await expect(mobile.page.getByTestId("mobile-nav-home")).toBeVisible();
|
||||
await expectMobilePageContract(mobile.page);
|
||||
|
||||
await mobile.page.getByTestId("mobile-nav-capture").click();
|
||||
await mobile.page.getByTestId("mobile-nav-reimbursements").click();
|
||||
await expect(mobile.page).toHaveURL(/\/mobile\/reimbursements$/);
|
||||
await expect(mobile.page.getByTestId("mobile-reimbursements-list")).toBeVisible();
|
||||
await expectMobilePageContract(mobile.page);
|
||||
|
||||
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();
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
import { mkdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
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 employee = {
|
||||
id: "11111111-1111-4111-8111-111111111111",
|
||||
employeeNo: "HYD000006",
|
||||
name: "王顺利",
|
||||
phone: "13800000000",
|
||||
email: "[email protected]",
|
||||
departmentId: "dept-1",
|
||||
directManagerId: "manager-1",
|
||||
status: "active",
|
||||
notificationPushEnabled: true,
|
||||
roleCodes: ["employee"],
|
||||
profileCompleteness: {
|
||||
hasEmergencyContact: true,
|
||||
hasBankAccount: true,
|
||||
hasAvatar: false,
|
||||
hasIdNumber: false,
|
||||
score: 50,
|
||||
},
|
||||
wecomBinding: null,
|
||||
};
|
||||
|
||||
function envelope(data: unknown) {
|
||||
return { requestId: "mock-r5-mobile-ux", data };
|
||||
}
|
||||
|
||||
async function fulfillJson(route: Route, data: unknown, status = 200) {
|
||||
await route.fulfill({
|
||||
status,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(envelope(data)),
|
||||
});
|
||||
}
|
||||
|
||||
function screenshotPath(fileName: string) {
|
||||
mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
||||
return path.join(SCREENSHOT_DIR, `${fileName}.png`);
|
||||
}
|
||||
|
||||
async function mockApis(page: Page) {
|
||||
await page.route((url) => url.pathname.startsWith("/api/"), async (route) => {
|
||||
const request = route.request();
|
||||
const url = new URL(request.url());
|
||||
const pathName = url.pathname;
|
||||
|
||||
if (pathName === "/api/auth/login" || pathName === "/api/auth/refresh") {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
headers: { "set-cookie": "has_session=1; Path=/; SameSite=Lax" },
|
||||
body: JSON.stringify(envelope({ accessToken: "mock-r5-token", expiresIn: 900 })),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === "/api/me") {
|
||||
await fulfillJson(route, employee);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === `/api/employees/${employee.id}/profile`) {
|
||||
await fulfillJson(route, {
|
||||
employee: {
|
||||
...employee,
|
||||
title: "负责人",
|
||||
avatarUrl: null,
|
||||
department: { id: "dept-1", name: "管理部" },
|
||||
directManager: { id: "manager-1", name: "马建勇" },
|
||||
},
|
||||
lifecycle: { events: [], checklistTasks: [], blockers: { openAssetCount: 0, pendingWorkflowTaskCount: 0, canCompleteOffboarding: true } },
|
||||
assignedAssets: [],
|
||||
organization: { reportingChain: [], directReports: [] },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === "/api/reimbursements" && request.method() === "GET") {
|
||||
await fulfillJson(route, {
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
scope: "own",
|
||||
items: [
|
||||
{
|
||||
id: "r5-reimbursement-1",
|
||||
formNumber: null,
|
||||
status: "draft",
|
||||
reimbursementDate: "2026-05-10",
|
||||
totalAmount: "168.30",
|
||||
description: "R5 移动交互验收",
|
||||
payment: {},
|
||||
owner: { id: employee.id, employeeNo: employee.employeeNo, name: employee.name },
|
||||
submitter: { id: employee.id, employeeNo: employee.employeeNo, name: employee.name },
|
||||
project: null,
|
||||
pendingWorkflow: null,
|
||||
createdAt: "2026-05-10T08:00:00.000Z",
|
||||
updatedAt: "2026-05-10T08:00:00.000Z",
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === "/api/reimbursements/r5-reimbursement-1") {
|
||||
await fulfillJson(route, {
|
||||
id: "r5-reimbursement-1",
|
||||
formNumber: null,
|
||||
submitterId: employee.id,
|
||||
ownerId: employee.id,
|
||||
departmentId: "dept-1",
|
||||
department: { id: "dept-1", name: "管理部" },
|
||||
status: "draft",
|
||||
createdAt: "2026-05-10T08:00:00.000Z",
|
||||
submittedAt: null,
|
||||
reimbursementDate: "2026-05-10",
|
||||
totalAmount: "168.30",
|
||||
description: "R5 移动交互验收",
|
||||
projectId: null,
|
||||
linkedTravelRequestId: null,
|
||||
linkedTravelRequest: null,
|
||||
project: null,
|
||||
owner: { id: employee.id, employee_no: employee.employeeNo, name: employee.name },
|
||||
submitter: { id: employee.id, employee_no: employee.employeeNo, name: employee.name },
|
||||
invoices: [],
|
||||
attachments: [],
|
||||
budgetHint: null,
|
||||
pendingWorkflow: null,
|
||||
workflowProgress: null,
|
||||
approvalHistory: [],
|
||||
riskFlags: [],
|
||||
items: [
|
||||
{
|
||||
id: "item-1",
|
||||
expenseTypeId: "expense-type-1",
|
||||
expenseDate: "2026-05-10",
|
||||
amount: "168.30",
|
||||
description: "正式运行口径验证",
|
||||
expenseType: { id: "expense-type-1", code: "office", name: "办公费", category: "office", enabled: true },
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === "/api/expense-types") {
|
||||
await fulfillJson(route, {
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
items: [{ id: "expense-type-1", code: "office", name: "办公费", category: "office", enabled: true }],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === "/api/auth/sso/providers") {
|
||||
await fulfillJson(route, []);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === "/api/auth/wecom/config") {
|
||||
await fulfillJson(route, {
|
||||
enabled: true,
|
||||
entryEnabled: true,
|
||||
oauthEnabled: true,
|
||||
corpId: "wx-test-corp",
|
||||
agentId: "1000002",
|
||||
loginUrl: "/api/auth/wecom/login?returnTo=%2Fmobile",
|
||||
bindEnabled: true,
|
||||
defaultReturnTo: "/mobile",
|
||||
status: "ready",
|
||||
statusReasons: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await fulfillJson(route, {});
|
||||
});
|
||||
}
|
||||
|
||||
async function login(page: Page) {
|
||||
await page.goto("/login");
|
||||
const accountLogin = page.getByRole("button", { name: "使用账号密码登录" });
|
||||
if (await accountLogin.isVisible().catch(() => false)) {
|
||||
await accountLogin.click();
|
||||
}
|
||||
await page.getByTestId("login-identifier").fill(employee.email);
|
||||
await page.getByTestId("login-password").fill("Password1234");
|
||||
await page.getByTestId("login-submit").click();
|
||||
await page.waitForURL(/\/(portal|mobile)/, { timeout: 30_000 });
|
||||
}
|
||||
|
||||
async function collectMobileMetrics(page: Page) {
|
||||
return page.evaluate(() => {
|
||||
const bottomNav = document.querySelector("[data-testid='mobile-nav-reimbursements']")?.closest("nav");
|
||||
const stickyActions = document.querySelector("[data-testid='mobile-reimbursement-sticky-actions']");
|
||||
const stickyRect = stickyActions?.getBoundingClientRect();
|
||||
return {
|
||||
width: window.innerWidth,
|
||||
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),
|
||||
stickyActionsVisible: Boolean(stickyRect && stickyRect.height > 0 && stickyRect.bottom <= window.innerHeight + 2),
|
||||
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",
|
||||
});
|
||||
|
||||
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/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 });
|
||||
|
||||
const listMetrics = await collectMobileMetrics(page);
|
||||
expect(listMetrics.bottomNavVisible).toBe(true);
|
||||
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("mobile-reimbursement-detail-sticky"), 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("绑定整单");
|
||||
});
|
||||
});
|
||||
@@ -309,6 +309,16 @@ async function mockR89Apis(page: Page) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === "/api/reimbursements") {
|
||||
await fulfillJson(route, {
|
||||
page: 1,
|
||||
pageSize: Number(url.searchParams.get("pageSize") ?? 20),
|
||||
total: 0,
|
||||
items: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === `/api/employees/${mockMe.id}/profile`) {
|
||||
await fulfillJson(route, {
|
||||
employee: {
|
||||
@@ -385,7 +395,7 @@ test.describe("R89 企业微信移动入口", () => {
|
||||
test.describe("R89 移动底部导航与核心页面", () => {
|
||||
test.use(browserAgnosticDevice("iPhone 13"));
|
||||
|
||||
test("密码登录后可完成移动首页、审批、票据和我的页关键路径", async ({ page }, testInfo) => {
|
||||
test("密码登录后可完成移动首页、审批、报销和我的页关键路径", async ({ page }, testInfo) => {
|
||||
await mockR89Apis(page);
|
||||
|
||||
await loginByPassword(page);
|
||||
@@ -396,7 +406,7 @@ test.describe("R89 移动底部导航与核心页面", () => {
|
||||
|
||||
await expect(page.getByTestId("mobile-nav-home")).toBeVisible();
|
||||
await expect(page.getByTestId("mobile-nav-approvals")).toBeVisible();
|
||||
await expect(page.getByTestId("mobile-nav-capture")).toBeVisible();
|
||||
await expect(page.getByTestId("mobile-nav-reimbursements")).toBeVisible();
|
||||
await expect(page.getByTestId("mobile-nav-me")).toBeVisible();
|
||||
|
||||
await page.getByTestId("mobile-nav-approvals").click();
|
||||
@@ -408,7 +418,12 @@ test.describe("R89 移动底部导航与核心页面", () => {
|
||||
await expect(page.getByTestId("mobile-approvals-reject")).toBeVisible();
|
||||
await attachScreenshot(page, testInfo, "r97-mobile-approvals.png");
|
||||
|
||||
await page.getByTestId("mobile-nav-capture").click();
|
||||
await page.getByTestId("mobile-nav-reimbursements").click();
|
||||
await expect(page).toHaveURL(/\/mobile\/reimbursements/);
|
||||
await expect(page.getByTestId("mobile-reimbursements-list")).toBeVisible();
|
||||
await attachScreenshot(page, testInfo, "r97-mobile-reimbursements.png");
|
||||
|
||||
await page.goto("/mobile/capture");
|
||||
await expect(page).toHaveURL(/\/mobile\/capture/);
|
||||
await expect(page.getByRole("heading", { name: "票据上传" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: /选择 PDF/ })).toBeVisible();
|
||||
|
||||
在新工单中引用
屏蔽一个用户