汇总已落盘的借款本金与还款、兼容历史编号的周期化编号、付款责任与回单核验、报销进度和排序、PWA/采购事务、错误原因与日期校验及ERP退役。 统一共享预算池锁序和锁后判断,恢复差旅原预算选择,补原因码固定夹具及反向完整性检查。 纳入配套迁移、测试、受管QA、分层验收合同、规约及任务记录;原始证据留在ignored artifacts。 关联任务:09-17-company-loan-principal-repayment、09-17-erp-exclusive-retirement、09-18-reimbursement-periodic-numbering、09-08-reimbursement-payment-closeout、09-18-local-concurrency-product-fixes、09-19-budget-pool-concurrency、09-05-error-explainability-payment-readiness。 预算补审由Claude定向验证2文件928项通过,原4446/28及r6结果仍仅对应各自历史输入;本轮未重跑测试。 完整新候选门禁、R1真实入口接入与12项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
325 行
9.4 KiB
TypeScript
325 行
9.4 KiB
TypeScript
import { test, expect } from "./helpers/fixtures";
|
|
import type { Page, Route } from "@playwright/test";
|
|
|
|
import { ADMIN, EMPLOYEE, login, logout, type E2EUser } from "./helpers/auth";
|
|
import { openNavLink } from "./helpers/navigation";
|
|
|
|
test.use({ serviceWorkers: "block" });
|
|
|
|
function envelope(data: unknown) {
|
|
return {
|
|
requestId: "mock-leave-boundary",
|
|
data,
|
|
};
|
|
}
|
|
|
|
async function fulfillJson(route: Route, data: unknown, status = 200, headers: Record<string, string> = {}) {
|
|
await route.fulfill({
|
|
status,
|
|
contentType: "application/json",
|
|
headers,
|
|
body: JSON.stringify(envelope(data)),
|
|
});
|
|
}
|
|
|
|
function buildMe(user: E2EUser) {
|
|
const isAdmin = user.identifier === ADMIN.identifier;
|
|
return {
|
|
id: isAdmin ? "employee-admin" : "employee-user",
|
|
employeeNo: isAdmin ? "A001" : "E001",
|
|
name: user.name,
|
|
phone: "13800000000",
|
|
email: user.identifier,
|
|
departmentId: "department-leave",
|
|
directManagerId: null,
|
|
status: "active",
|
|
notificationPushEnabled: true,
|
|
roleCodes: isAdmin ? ["admin"] : ["employee"],
|
|
profileCompleteness: {
|
|
hasEmergencyContact: true,
|
|
hasBankAccount: true,
|
|
hasAvatar: false,
|
|
hasIdNumber: false,
|
|
score: 50,
|
|
},
|
|
wecomBinding: null,
|
|
};
|
|
}
|
|
|
|
function leaveRequestFor(user: E2EUser) {
|
|
const me = buildMe(user);
|
|
return {
|
|
id: "leave-boundary-1",
|
|
ownerId: me.id,
|
|
departmentId: me.departmentId,
|
|
status: "draft",
|
|
startDate: "2026-05-31",
|
|
endDate: "2026-05-31",
|
|
startSession: "am",
|
|
endSession: "pm",
|
|
requestedDays: 1,
|
|
reason: "企业微信同步记录",
|
|
attachment: null,
|
|
leaveType: {
|
|
id: "leave-type-annual",
|
|
code: "annual",
|
|
name: "年假",
|
|
requiresAttachment: false,
|
|
attachmentMimeTypes: [],
|
|
},
|
|
owner: {
|
|
id: me.id,
|
|
employeeNo: me.employeeNo,
|
|
name: me.name,
|
|
},
|
|
pendingWorkflow: null,
|
|
approvalHistory: [],
|
|
createdAt: "2026-05-30T09:00:00.000Z",
|
|
updatedAt: "2026-05-30T09:30:00.000Z",
|
|
};
|
|
}
|
|
|
|
async function mockLeaveBoundaryApis(page: Page) {
|
|
let activeUser = EMPLOYEE;
|
|
|
|
await page.route((url) => url.pathname.startsWith("/api/"), async (route) => {
|
|
const request = route.request();
|
|
const url = new URL(request.url());
|
|
const path = url.pathname;
|
|
|
|
if (path === "/api/auth/capabilities") {
|
|
await fulfillJson(route, {
|
|
emailDelivery: { ready: true, statusReasons: [] },
|
|
emailLoginCode: { enabled: true },
|
|
passwordReset: { enabled: true },
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (path === "/api/auth/sso/providers") {
|
|
await fulfillJson(route, []);
|
|
return;
|
|
}
|
|
|
|
if (path === "/api/auth/wecom/config") {
|
|
await fulfillJson(route, {
|
|
enabled: true,
|
|
entryEnabled: true,
|
|
oauthEnabled: true,
|
|
corpId: "ww-leave-boundary",
|
|
agentId: "1000002",
|
|
loginUrl: "/api/auth/wecom/login?returnTo=%2Fportal",
|
|
bindEnabled: true,
|
|
defaultReturnTo: "/portal",
|
|
status: "ready",
|
|
statusReasons: [],
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (path === "/api/auth/login") {
|
|
const payload = request.postDataJSON() as { identifier?: string };
|
|
activeUser = payload.identifier === ADMIN.identifier ? ADMIN : EMPLOYEE;
|
|
await fulfillJson(
|
|
route,
|
|
{ accessToken: `mock-token-${activeUser.employeeNo}`, expiresIn: 900, mustChangePassword: false },
|
|
200,
|
|
{ "set-cookie": "has_session=1; Path=/; SameSite=Strict" },
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (path === "/api/auth/refresh") {
|
|
await fulfillJson(route, { accessToken: `mock-token-${activeUser.employeeNo}`, expiresIn: 900 });
|
|
return;
|
|
}
|
|
|
|
if (path === "/api/auth/logout") {
|
|
await fulfillJson(route, {}, 200, { "set-cookie": "has_session=; Path=/; Max-Age=0" });
|
|
return;
|
|
}
|
|
|
|
if (path === "/api/me") {
|
|
await fulfillJson(route, buildMe(activeUser));
|
|
return;
|
|
}
|
|
|
|
if (path === "/api/system/navigation") {
|
|
await fulfillJson(route, {
|
|
workspace: {
|
|
id: "workspace-leave-boundary",
|
|
persisted: true,
|
|
profile: "small_team_internal",
|
|
orgMode: "single_company",
|
|
primaryEntryMode: "wecom",
|
|
notificationStrategy: "in_app",
|
|
devicePriority: "desktop",
|
|
moduleVisibility: {},
|
|
createdAt: null,
|
|
updatedAt: null,
|
|
updatedBy: null,
|
|
},
|
|
sections: {
|
|
main: [
|
|
{ key: "portal", href: "/portal", label: "工作台", icon: "home_app_logo", placement: "main", permissionCodes: [], featureFlagKey: null },
|
|
{ key: "expense", href: "/reimbursements", label: "报销", icon: "request_quote", placement: "main", permissionCodes: [], featureFlagKey: null },
|
|
{ key: "leave", href: "/leave-requests", label: "请假记录", icon: "event_available", placement: "main", permissionCodes: [], featureFlagKey: null },
|
|
],
|
|
admin: [],
|
|
backup: [],
|
|
},
|
|
recommendedRolePresets: [],
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (path === "/api/portal/home") {
|
|
await fulfillJson(route, {
|
|
workspace: { id: "workspace-leave-boundary", persisted: true, profile: "small_team_internal" },
|
|
greeting: { name: activeUser.name },
|
|
cards: [],
|
|
pendingTasks: { count: 0, href: "/workflow/tasks" },
|
|
myApplications: [{ domain: "leave", count: 1, href: "/leave-requests" }],
|
|
riskSummary: null,
|
|
announcements: [],
|
|
quickLinks: [],
|
|
todayPresence: { date: "2026-05-30", status: "office", label: "在岗", note: null, href: "/team" },
|
|
teamPresence: null,
|
|
weeklyReport: null,
|
|
weeklySummary: null,
|
|
todoSummary: null,
|
|
projectSummary: null,
|
|
peopleLifecycle: null,
|
|
adoptionSummary: null,
|
|
recentActivities: [],
|
|
teamStatusBoard: [],
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (path === "/api/attendance/me/today") {
|
|
await fulfillJson(route, {
|
|
workDate: "2026-05-30",
|
|
needClockIn: false,
|
|
exemptionReason: "wecom_owned",
|
|
item: null,
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (path === "/api/notifications") {
|
|
await fulfillJson(route, {
|
|
page: 1,
|
|
pageSize: 10,
|
|
total: 0,
|
|
unreadOnly: false,
|
|
unreadCount: 0,
|
|
items: [],
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (path === "/api/leave-types") {
|
|
await fulfillJson(route, {
|
|
items: [
|
|
{
|
|
id: "leave-type-annual",
|
|
code: "annual",
|
|
name: "年假",
|
|
description: null,
|
|
enabled: true,
|
|
requiresAttachment: false,
|
|
attachmentMimeTypes: [],
|
|
sortOrder: 1,
|
|
policy: {
|
|
id: "leave-policy-annual",
|
|
annualQuotaDays: 15,
|
|
enabled: true,
|
|
description: null,
|
|
},
|
|
createdAt: "2026-05-30T00:00:00.000Z",
|
|
updatedAt: "2026-05-30T00:00:00.000Z",
|
|
},
|
|
],
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (path === "/api/leave-balances") {
|
|
await fulfillJson(route, {
|
|
employeeId: buildMe(activeUser).id,
|
|
items: [
|
|
{
|
|
leaveType: {
|
|
id: "leave-type-annual",
|
|
code: "annual",
|
|
name: "年假",
|
|
description: null,
|
|
enabled: true,
|
|
requiresAttachment: false,
|
|
attachmentMimeTypes: [],
|
|
sortOrder: 1,
|
|
policy: null,
|
|
createdAt: null,
|
|
updatedAt: null,
|
|
},
|
|
policy: {
|
|
id: "leave-policy-annual",
|
|
annualQuotaDays: 15,
|
|
enabled: true,
|
|
description: null,
|
|
},
|
|
granted: 15,
|
|
adjusted: 0,
|
|
reserved: 1,
|
|
used: 2,
|
|
available: 12,
|
|
periodStart: "2026-01-01",
|
|
periodEnd: "2026-12-31",
|
|
},
|
|
],
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (path === "/api/leave-requests") {
|
|
await fulfillJson(route, {
|
|
page: 1,
|
|
pageSize: 10,
|
|
total: 1,
|
|
scope: "own",
|
|
items: [leaveRequestFor(activeUser)],
|
|
});
|
|
return;
|
|
}
|
|
|
|
await fulfillJson(route, {});
|
|
});
|
|
}
|
|
|
|
test("E2E 请假边界:员工只看企微主办记录,不显示 OA 日常提交入口", async ({ page }) => {
|
|
await mockLeaveBoundaryApis(page);
|
|
await login(page, EMPLOYEE);
|
|
await (await openNavLink(page, "请假记录")).click();
|
|
|
|
await expect(page.locator("h1", { hasText: "请假记录" })).toBeVisible();
|
|
await expect(page.getByText("企业微信主办请假、补卡、外出和加班")).toBeVisible();
|
|
await expect(page.getByRole("heading", { name: "日常请假入口" })).toBeVisible();
|
|
await expect(page.locator("#leave-type")).toHaveCount(0);
|
|
await expect(page.getByRole("button", { name: "创建应急草稿" })).toHaveCount(0);
|
|
await expect(page.getByRole("button", { name: "提交应急审批" })).toHaveCount(0);
|
|
|
|
await logout(page, EMPLOYEE.name);
|
|
});
|
|
|
|
test("E2E 请假边界:管理员保留 OA 应急草稿入口", async ({ page }) => {
|
|
await mockLeaveBoundaryApis(page);
|
|
await login(page, ADMIN);
|
|
await page.goto("/leave-requests");
|
|
|
|
await expect(page.locator("h1", { hasText: "请假记录" })).toBeVisible();
|
|
await expect(page.getByRole("heading", { name: "OA 应急请假草稿" })).toBeVisible();
|
|
await expect(page.locator("#leave-type")).toBeVisible();
|
|
await expect(page.getByRole("button", { name: "创建应急草稿" })).toBeVisible();
|
|
});
|