文件
hyb-oa/client/tests/e2e/leave-team-calendar.spec.ts

329 行
11 KiB
TypeScript

import { expect, test, type Page } from "@playwright/test";
import { EMPLOYEE, MANAGER, login, type E2EUser } from "./helpers/auth";
const LEAVE_CALENDAR_HR: E2EUser = {
identifier: "[email protected]",
password: "HrPassword1234",
name: "孙人事",
};
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;
departmentId: string | null;
}> = [];
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");
const departmentId = url.searchParams.get("departmentId");
if (scope === "all" && dateFrom && dateTo) {
observedCalendarQueries.push({ dateFrom, dateTo, pageSize, departmentId });
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 请假团队日历:HR 可见全员周/月视图并显示截断提示", async ({ page }) => {
const observedCalendarQueries = await mockLeaveRequests(page);
await login(page, LEAVE_CALENDAR_HR);
await page.goto("/leave-requests");
await expect(page.locator("h1", { hasText: "请假记录" })).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);
expect(observedCalendarQueries.every((query) => query.departmentId == null)).toBe(true);
});
test("E2E 请假团队日历:经理只拉本部门数据且导航会触发新查询", async ({ page }) => {
const observedCalendarQueries = await mockLeaveRequests(page);
await login(page, MANAGER);
await page.goto("/leave-requests");
await expect(page.locator("h1", { hasText: "请假记录" })).toBeVisible();
await expect(page.getByTestId("leave-view-calendar")).toBeVisible();
const firstResponse = 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 firstResponse;
await expect(page.getByTestId("leave-team-calendar-grid")).toBeVisible();
await expect(page.getByTestId("leave-team-calendar-entry").filter({ hasText: "王小明" }).first()).toBeVisible();
await expect.poll(() => observedCalendarQueries.length).toBeGreaterThanOrEqual(1);
expect(observedCalendarQueries.every((query) => Boolean(query.departmentId))).toBe(true);
const firstQuery = observedCalendarQueries.at(-1);
if (!firstQuery) {
throw new Error("经理团队日历未记录到首个查询");
}
const previousResponse = 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-prev").click();
await previousResponse;
const previousQuery = observedCalendarQueries.at(-1);
if (!previousQuery) {
throw new Error("经理团队日历未记录到上一窗口查询");
}
expect(previousQuery.dateFrom).not.toBe(firstQuery.dateFrom);
const todayResponse = 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-today").click();
await todayResponse;
const todayQuery = observedCalendarQueries.at(-1);
if (!todayQuery) {
throw new Error("经理团队日历未记录到今天查询");
}
expect(todayQuery.dateFrom).toBe(firstQuery.dateFrom);
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.poll(() => page.getByTestId("leave-team-calendar-cell").count()).toBeGreaterThan(7);
const nextMonthResponse = 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-next").click();
await nextMonthResponse;
const nextMonthQuery = observedCalendarQueries.at(-1);
if (!nextMonthQuery) {
throw new Error("经理团队日历未记录到下一窗口查询");
}
expect(nextMonthQuery.dateFrom).not.toBe(todayQuery.dateFrom);
});
test("E2E 请假团队日历:员工视角不显示团队日历 Tab", async ({ page }) => {
await mockLeaveRequests(page);
await login(page, EMPLOYEE);
await page.goto("/leave-requests");
await expect(page.locator("h1", { hasText: "请假记录" })).toBeVisible();
await expect(page.getByRole("heading", { name: "日常请假入口" })).toBeVisible();
await expect(page.getByRole("heading", { name: "OA 应急请假草稿" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "创建应急草稿" })).toHaveCount(0);
await expect(page.getByTestId("leave-view-calendar")).toHaveCount(0);
await expect(page.getByTestId("leave-page-view-tabs")).toHaveCount(0);
});