实现制度签收轻量回执(05-28-oa-policy-acknowledgement-v1)
这个提交包含在:
@@ -0,0 +1,307 @@
|
||||
import { expect, test, type Page, type Route } from "@playwright/test";
|
||||
|
||||
import { ADMIN, EMPLOYEE, login } from "./helpers/auth";
|
||||
|
||||
type MockUserKey = "admin" | "employee";
|
||||
|
||||
type MockAnnouncement = {
|
||||
id: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
linkUrl: string | null;
|
||||
startsAt: string | null;
|
||||
endsAt: string | null;
|
||||
enabled: boolean;
|
||||
priority: number;
|
||||
category: "general" | "policy";
|
||||
requiresAcknowledgement: boolean;
|
||||
acknowledgementStatement: string | null;
|
||||
acknowledgementDueAt: string | null;
|
||||
createdAt: string | null;
|
||||
updatedAt: string | null;
|
||||
acknowledgedEmployeeIds: Set<string>;
|
||||
};
|
||||
|
||||
function envelope(data: unknown) {
|
||||
return { requestId: "mock-policy-ack", data };
|
||||
}
|
||||
|
||||
async function fulfillJson(route: Route, data: unknown, status = 200) {
|
||||
await route.fulfill({
|
||||
status,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(envelope(data)),
|
||||
});
|
||||
}
|
||||
|
||||
function buildMe(user: MockUserKey) {
|
||||
if (user === "admin") {
|
||||
return {
|
||||
id: "admin-policy-ack",
|
||||
employeeNo: "A001",
|
||||
name: "Admin",
|
||||
phone: "mock-phone-admin",
|
||||
email: "[email protected]",
|
||||
departmentId: "dept-admin",
|
||||
directManagerId: null,
|
||||
status: "active",
|
||||
notificationPushEnabled: true,
|
||||
roleCodes: ["admin"],
|
||||
profileCompleteness: { hasEmergencyContact: true, hasBankAccount: true, hasAvatar: false, hasIdNumber: false, score: 60 },
|
||||
wecomBinding: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: "employee-policy-ack",
|
||||
employeeNo: "E001",
|
||||
name: "Employee",
|
||||
phone: "mock-phone-employee",
|
||||
email: "[email protected]",
|
||||
departmentId: "dept-employee",
|
||||
directManagerId: null,
|
||||
status: "active",
|
||||
notificationPushEnabled: true,
|
||||
roleCodes: ["employee"],
|
||||
profileCompleteness: { hasEmergencyContact: true, hasBankAccount: true, hasAvatar: false, hasIdNumber: false, score: 60 },
|
||||
wecomBinding: null,
|
||||
};
|
||||
}
|
||||
|
||||
function mapAnnouncementForUser(item: MockAnnouncement, currentUser: MockUserKey) {
|
||||
const me = buildMe(currentUser);
|
||||
const acknowledged = item.acknowledgedEmployeeIds.has(me.id);
|
||||
const overdue = Boolean(item.acknowledgementDueAt && new Date(item.acknowledgementDueAt).getTime() < Date.now());
|
||||
return {
|
||||
...item,
|
||||
acknowledgedEmployeeIds: undefined,
|
||||
acknowledgement: {
|
||||
status: !item.requiresAcknowledgement ? "not_required" : acknowledged ? "acknowledged" : overdue ? "overdue" : "pending",
|
||||
acknowledgedAt: acknowledged ? "2026-05-28T03:00:00.000Z" : null,
|
||||
overdue: !acknowledged && overdue,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function mapAnnouncementForAdmin(item: MockAnnouncement) {
|
||||
const employees = [
|
||||
{ id: "employee-policy-ack", employeeNo: "E001", name: "Employee", status: "pending" },
|
||||
{ id: "employee-policy-ack-2", employeeNo: "E002", name: "员工二", status: "pending" },
|
||||
];
|
||||
const pendingEmployees = item.requiresAcknowledgement
|
||||
? employees.filter((employee) => !item.acknowledgedEmployeeIds.has(employee.id))
|
||||
: [];
|
||||
return {
|
||||
...item,
|
||||
acknowledgedEmployeeIds: undefined,
|
||||
acknowledgementSummary: {
|
||||
required: item.requiresAcknowledgement,
|
||||
totalActiveEmployees: item.requiresAcknowledgement ? employees.length : 0,
|
||||
acknowledgedCount: item.requiresAcknowledgement ? employees.length - pendingEmployees.length : 0,
|
||||
pendingCount: pendingEmployees.length,
|
||||
overdueCount: 0,
|
||||
},
|
||||
pendingEmployees,
|
||||
};
|
||||
}
|
||||
|
||||
async function mockPolicyAckApis(page: Page) {
|
||||
let currentUser: MockUserKey = "employee";
|
||||
const announcements: MockAnnouncement[] = [];
|
||||
|
||||
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: false, entryEnabled: false, oauthEnabled: false, status: "disabled", statusReasons: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === "/api/auth/login") {
|
||||
const payload = request.postDataJSON() as { identifier?: string };
|
||||
currentUser = payload.identifier === ADMIN.identifier ? "admin" : "employee";
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
headers: { "set-cookie": "has_session=1; Path=/; SameSite=Lax" },
|
||||
body: JSON.stringify(envelope({ accessToken: `mock-policy-${currentUser}`, expiresIn: 900, mustChangePassword: false })),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === "/api/auth/refresh") {
|
||||
await fulfillJson(route, { accessToken: `mock-policy-${currentUser}`, expiresIn: 900, mustChangePassword: false });
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === "/api/me") {
|
||||
await fulfillJson(route, buildMe(currentUser));
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === "/api/system/navigation") {
|
||||
await fulfillJson(route, {
|
||||
workspace: {
|
||||
id: "workspace-policy-ack",
|
||||
persisted: true,
|
||||
profile: "small_team_internal",
|
||||
orgMode: "single_company",
|
||||
primaryEntryMode: "password",
|
||||
notificationStrategy: "in_app",
|
||||
devicePriority: "desktop",
|
||||
moduleVisibility: {},
|
||||
createdAt: null,
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
},
|
||||
sections: {
|
||||
main: [{ key: "announcements", href: "/announcements", label: "公告", icon: "campaign", placement: "main", permissionCodes: [], featureFlagKey: null }],
|
||||
admin: currentUser === "admin"
|
||||
? [{ key: "announcementsAdmin", href: "/system/announcements", label: "公告管理", icon: "campaign", placement: "admin", permissionCodes: ["system.settings"], featureFlagKey: null }]
|
||||
: [],
|
||||
backup: [],
|
||||
},
|
||||
recommendedRolePresets: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === "/api/portal/home") {
|
||||
await fulfillJson(route, {
|
||||
workspace: { id: "workspace-policy-ack", persisted: true, profile: "small_team_internal" },
|
||||
greeting: { name: buildMe(currentUser).name },
|
||||
cards: [],
|
||||
pendingTasks: { count: 0, href: "/workflow/tasks" },
|
||||
myApplications: [],
|
||||
riskSummary: null,
|
||||
announcements: [],
|
||||
quickLinks: [],
|
||||
todayPresence: null,
|
||||
teamPresence: null,
|
||||
weeklyReport: null,
|
||||
weeklySummary: null,
|
||||
todoSummary: null,
|
||||
projectSummary: null,
|
||||
peopleLifecycle: null,
|
||||
adoptionSummary: null,
|
||||
recentActivities: [],
|
||||
teamStatusBoard: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === "/api/notifications") {
|
||||
await fulfillJson(route, { items: [], unreadCount: 0, page: 1, pageSize: 20, total: 0 });
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === "/api/announcements" && request.method() === "GET") {
|
||||
const manage = url.searchParams.get("manage") === "1";
|
||||
await fulfillJson(route, {
|
||||
items: announcements.map((item) => manage ? mapAnnouncementForAdmin(item) : mapAnnouncementForUser(item, currentUser)),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === "/api/announcements" && request.method() === "POST") {
|
||||
const payload = request.postDataJSON() as {
|
||||
title: string;
|
||||
summary: string;
|
||||
linkUrl?: string | null;
|
||||
priority?: number;
|
||||
category?: "general" | "policy";
|
||||
requiresAcknowledgement?: boolean;
|
||||
acknowledgementStatement?: string | null;
|
||||
acknowledgementDueAt?: string | null;
|
||||
};
|
||||
const created: MockAnnouncement = {
|
||||
id: `policy-ann-${announcements.length + 1}`,
|
||||
title: payload.title,
|
||||
summary: payload.summary,
|
||||
linkUrl: payload.linkUrl ?? null,
|
||||
startsAt: null,
|
||||
endsAt: null,
|
||||
enabled: true,
|
||||
priority: payload.priority ?? 100,
|
||||
category: payload.category ?? "general",
|
||||
requiresAcknowledgement: Boolean(payload.requiresAcknowledgement),
|
||||
acknowledgementStatement: payload.acknowledgementStatement ?? null,
|
||||
acknowledgementDueAt: payload.acknowledgementDueAt ?? null,
|
||||
createdAt: "2026-05-28T02:00:00.000Z",
|
||||
updatedAt: "2026-05-28T02:00:00.000Z",
|
||||
acknowledgedEmployeeIds: new Set(),
|
||||
};
|
||||
announcements.push(created);
|
||||
await fulfillJson(route, { item: mapAnnouncementForAdmin(created) }, 201);
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.endsWith("/acknowledge") && request.method() === "POST") {
|
||||
const segments = path.split("/");
|
||||
const id = segments[segments.length - 2];
|
||||
const announcement = announcements.find((item) => item.id === id);
|
||||
if (announcement) {
|
||||
announcement.acknowledgedEmployeeIds.add(buildMe(currentUser).id);
|
||||
}
|
||||
await fulfillJson(route, { item: { id: "ack-1", announcementId: id, status: "acknowledged", acknowledgedAt: "2026-05-28T03:00:00.000Z" } }, 201);
|
||||
return;
|
||||
}
|
||||
|
||||
await fulfillJson(route, {});
|
||||
});
|
||||
}
|
||||
|
||||
async function clearBrowserSession(page: Page) {
|
||||
await page.context().clearCookies();
|
||||
await page.evaluate(() => {
|
||||
window.localStorage.clear();
|
||||
window.sessionStorage.clear();
|
||||
});
|
||||
}
|
||||
|
||||
test("制度签收公告:管理员创建、员工签收、管理员看到统计变化", async ({ page }) => {
|
||||
await mockPolicyAckApis(page);
|
||||
const title = `自动化制度签收 ${Date.now()}`;
|
||||
|
||||
await login(page, ADMIN);
|
||||
await page.goto("/system/announcements");
|
||||
await page.getByRole("tab", { name: "制度签收" }).click();
|
||||
await page.getByPlaceholder("例如:费用报销制度签收").fill(title);
|
||||
await page.getByPlaceholder("写清楚适用范围、入口和处理要求。").fill("请全员阅读并确认制度签收流程。");
|
||||
await page.locator('input[type="date"]').fill("2026-06-03");
|
||||
await page.getByRole("button", { name: "创建制度签收公告" }).click();
|
||||
await expect(page.getByText(title)).toBeVisible();
|
||||
await expect(page.getByText("已签 0/2 · 待签 2")).toBeVisible();
|
||||
|
||||
await clearBrowserSession(page);
|
||||
await login(page, EMPLOYEE);
|
||||
await page.goto("/announcements");
|
||||
await expect(page.getByText(title)).toBeVisible();
|
||||
await expect(page.getByText("待签收")).toBeVisible();
|
||||
await page.getByLabel("我已阅读并理解该制度内容,确认按公司制度执行。").check();
|
||||
await page.getByRole("button", { name: "确认签收" }).click();
|
||||
await expect(page.getByText("已签收")).toBeVisible();
|
||||
await expect(page.getByText(/完成本人签收/)).toBeVisible();
|
||||
|
||||
await clearBrowserSession(page);
|
||||
await login(page, ADMIN);
|
||||
await page.goto("/system/announcements");
|
||||
await expect(page.getByText(title)).toBeVisible();
|
||||
await expect(page.getByText("已签 1/2 · 待签 1")).toBeVisible();
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户