326 行
11 KiB
TypeScript
326 行
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: "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;
|
|
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.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);
|
|
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.getByRole("heading", { name: "请假申请" })).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.getByRole("heading", { name: "请假申请" })).toBeVisible();
|
|
await expect(page.getByTestId("leave-view-calendar")).toHaveCount(0);
|
|
await expect(page.getByTestId("leave-page-view-tabs")).toHaveCount(0);
|
|
});
|