R35-2 审批通知补结构化摘要

这个提交包含在:
2026-03-25 11:38:00 +08:00
父节点 a962f4b074
当前提交 6fba13573e
共修改 12 个文件,包含 1243 行新增和 266 行删除
@@ -0,0 +1,243 @@
import { expect, test, type Page } from "@playwright/test";
import { EMPLOYEE, login, type E2EUser } from "./helpers/auth";
const LEAVE_CALENDAR_MANAGER: E2EUser = {
identifier: "[email protected]",
password: "HrPassword1234",
name: "HR",
};
function envelope<T>(data: T) {
return {
requestId: "mock-request",
data,
};
}
function startOfUtcDay(value: Date) {
return new Date(Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate()));
}
function addUtcDays(value: Date, days: number) {
const next = startOfUtcDay(value);
next.setUTCDate(next.getUTCDate() + days);
return next;
}
function toDateKey(value: Date) {
return startOfUtcDay(value).toISOString().slice(0, 10);
}
function buildCalendarItems(dateFrom: string, dateTo: string) {
const start = startOfUtcDay(new Date(`${dateFrom}T00:00:00.000Z`));
const end = startOfUtcDay(new Date(`${dateTo}T00:00:00.000Z`));
const maxOffset = Math.max(0, Math.round((end.getTime() - start.getTime()) / 86400000));
const second = addUtcDays(start, Math.min(1, maxOffset));
const third = addUtcDays(start, Math.min(2, maxOffset));
return [
{
id: "leave-calendar-1",
ownerId: "employee-manager-1",
departmentId: "department-1",
status: "approved",
startDate: toDateKey(start),
endDate: toDateKey(start),
startSession: "am",
endSession: "pm",
requestedDays: 1,
reason: "家庭事务",
attachment: null,
leaveType: {
id: "leave-type-annual",
code: "annual",
name: "年假",
requiresAttachment: false,
attachmentMimeTypes: [],
},
owner: {
id: "employee-manager-1",
employeeNo: "E1001",
name: "王小明",
},
createdAt: "2026-03-25T08:00:00.000Z",
updatedAt: "2026-03-25T08:00:00.000Z",
},
{
id: "leave-calendar-2",
ownerId: "employee-manager-2",
departmentId: "department-1",
status: "reviewing",
startDate: toDateKey(second),
endDate: toDateKey(third.getTime() > end.getTime() ? second : third),
startSession: "am",
endSession: "pm",
requestedDays: third.getTime() > end.getTime() ? 1 : 2,
reason: "出差调休",
attachment: null,
leaveType: {
id: "leave-type-sick",
code: "sick",
name: "病假",
requiresAttachment: false,
attachmentMimeTypes: [],
},
owner: {
id: "employee-manager-2",
employeeNo: "E1002",
name: "李四",
},
createdAt: "2026-03-25T08:00:00.000Z",
updatedAt: "2026-03-25T08:00:00.000Z",
},
{
id: "leave-calendar-3",
ownerId: "employee-manager-3",
departmentId: "department-1",
status: "approved",
startDate: toDateKey(end),
endDate: toDateKey(end),
startSession: "pm",
endSession: "pm",
requestedDays: 0.5,
reason: "下午就医",
attachment: null,
leaveType: {
id: "leave-type-personal",
code: "personal",
name: "事假",
requiresAttachment: false,
attachmentMimeTypes: [],
},
owner: {
id: "employee-manager-3",
employeeNo: "E1003",
name: "赵六",
},
createdAt: "2026-03-25T08:00:00.000Z",
updatedAt: "2026-03-25T08:00:00.000Z",
},
{
id: "leave-calendar-4",
ownerId: "employee-manager-4",
departmentId: "department-1",
status: "approved",
startDate: toDateKey(second),
endDate: toDateKey(second),
startSession: "am",
endSession: "am",
requestedDays: 0.5,
reason: "半天外出",
attachment: null,
leaveType: {
id: "leave-type-annual",
code: "annual",
name: "年假",
requiresAttachment: false,
attachmentMimeTypes: [],
},
owner: {
id: "employee-manager-4",
employeeNo: "E1004",
name: "孙七",
},
createdAt: "2026-03-25T08:00:00.000Z",
updatedAt: "2026-03-25T08:00:00.000Z",
},
];
}
async function mockLeaveRequests(page: Page) {
const observedCalendarQueries: Array<{ dateFrom: string; dateTo: string; pageSize: string }> = [];
await page.route("**/api/leave-requests?**", async (route) => {
const url = new URL(route.request().url());
const scope = url.searchParams.get("scope") ?? "own";
const pageSize = url.searchParams.get("pageSize") ?? "10";
const dateFrom = url.searchParams.get("dateFrom");
const dateTo = url.searchParams.get("dateTo");
if (scope === "all" && dateFrom && dateTo) {
observedCalendarQueries.push({ dateFrom, dateTo, pageSize });
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(
envelope({
page: 1,
pageSize: Number(pageSize),
total: 5,
scope: "all",
items: buildCalendarItems(dateFrom, dateTo),
}),
),
});
return;
}
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(
envelope({
page: 1,
pageSize: Number(pageSize),
total: 0,
scope,
items: [],
}),
),
});
});
return observedCalendarQueries;
}
test("E2E 请假团队日历:具备团队查看权限的用户可见周/月视图并显示截断提示", async ({ page }) => {
const observedCalendarQueries = await mockLeaveRequests(page);
await login(page, LEAVE_CALENDAR_MANAGER);
await page.goto("/leave-requests");
await expect(page.getByRole("heading", { name: "请假申请" })).toBeVisible();
await expect(page.getByTestId("leave-view-calendar")).toBeVisible();
const weekResponse = page.waitForResponse((response) => {
const url = new URL(response.url());
return response.ok() && url.pathname === "/api/leave-requests" && url.searchParams.get("scope") === "all";
});
await page.getByTestId("leave-view-calendar").click();
await weekResponse;
await expect(page.getByTestId("leave-calendar-mode-week")).toHaveAttribute("data-state", "active");
await expect(page.getByTestId("leave-team-calendar-grid")).toBeVisible();
await expect(page.getByTestId("leave-team-calendar-entry").filter({ hasText: "王小明" }).first()).toBeVisible();
await expect(page.getByTestId("leave-team-calendar-entry").filter({ hasText: "李四" }).first()).toBeVisible();
await expect(page.getByTestId("leave-team-calendar-truncation")).toContainText("仅显示前 4 条,共 5 条");
await expect(page.getByTestId("leave-team-calendar-cell")).toHaveCount(7);
const monthResponse = page.waitForResponse((response) => {
const url = new URL(response.url());
return response.ok() && url.pathname === "/api/leave-requests" && url.searchParams.get("scope") === "all";
});
await page.getByTestId("leave-calendar-mode-month").click();
await monthResponse;
await expect(page.getByTestId("leave-calendar-mode-month")).toHaveAttribute("data-state", "active");
await expect.poll(() => page.getByTestId("leave-team-calendar-cell").count()).toBeGreaterThan(7);
await expect.poll(() => observedCalendarQueries.length).toBeGreaterThanOrEqual(2);
expect(observedCalendarQueries.every((query) => Boolean(query.dateFrom && query.dateTo))).toBe(true);
expect(observedCalendarQueries.every((query) => query.pageSize === "100")).toBe(true);
});
test("E2E 请假团队日历:员工视角不显示团队日历 Tab", async ({ page }) => {
await mockLeaveRequests(page);
await login(page, EMPLOYEE);
await page.goto("/leave-requests");
await expect(page.getByRole("heading", { name: "请假申请" })).toBeVisible();
await expect(page.getByTestId("leave-view-calendar")).toHaveCount(0);
await expect(page.getByTestId("leave-page-view-tabs")).toHaveCount(0);
});