R36-1/R36-2/R36-3/R36-5 收口请假编辑与团队日历

这个提交包含在:
2026-03-25 15:32:23 +08:00
父节点 18af4aedc4
当前提交 d0b9d9eaca
共修改 9 个文件,包含 874 行新增和 69 行删除
+42
查看文件
@@ -69,6 +69,48 @@ async function withdrawLeave(api: Awaited<ReturnType<typeof createAuthenticatedA
return apiPostData<{ leaveRequestId: string; leaveStatus: string }>(api, `/api/leave-requests/${leaveRequestId}/withdraw`, {});
}
test("E2E 请假编辑:草稿保存后保留原单并可继续提交", async ({ page }) => {
const employeeClient = await createAuthenticatedApiClient(EMPLOYEE);
const baseOffset = 80 + Number(String(Date.now()).slice(-2));
const startDate = formatDate(baseOffset);
const updatedEndDate = formatDate(baseOffset + 1);
try {
await login(page, EMPLOYEE);
await (await openNavLink(page, "请假")).click();
await expect(page.getByRole("heading", { name: "请假申请" })).toBeVisible();
const leaveId = await createLeaveDraft(page, startDate, startDate, `Playwright 编辑请假 ${Date.now()}`);
await page.goto("/leave-requests");
const draftRow = page.getByRole("row").filter({ hasText: startDate }).first();
await expect(draftRow).toContainText("草稿", { timeout: 30_000 });
const updateResponsePromise = page.waitForResponse((response) => {
return response.request().method() === "PATCH" && new URL(response.url()).pathname === `/api/leave-requests/${leaveId}` && response.ok();
});
await draftRow.getByRole("button", { name: "编辑" }).click();
await expect(page.getByText("编辑请假单")).toBeVisible();
await page.locator("#leave-end-date").fill(updatedEndDate);
await page.locator("#leave-reason").fill(`更新后的请假说明 ${Date.now()}`);
await page.getByRole("button", { name: "保存修改" }).click();
const updatePayload = (await (await updateResponsePromise).json()) as { data?: { id?: string } };
expect(updatePayload.data?.id).toBe(leaveId);
await page.goto("/leave-requests");
const updatedRow = page.getByRole("row").filter({ hasText: updatedEndDate }).first();
await expect(updatedRow).toContainText("草稿", { timeout: 30_000 });
await submitLeave(employeeClient.api, leaveId);
await page.goto("/leave-requests");
await expect(updatedRow).toContainText("审批中", { timeout: 30_000 });
} finally {
await employeeClient.dispose();
}
});
test("E2E 请假主链路:草稿 → 提交 → 撤回 + 提交 → 审批", async ({ page, workflowDetail }) => {
const employeeClient = await createAuthenticatedApiClient(EMPLOYEE);
const baseOffset = 30 + Number(String(Date.now()).slice(-2));
+88 -6
查看文件
@@ -1,8 +1,8 @@
import { expect, test, type Page } from "@playwright/test";
import { EMPLOYEE, login, type E2EUser } from "./helpers/auth";
import { EMPLOYEE, MANAGER, login, type E2EUser } from "./helpers/auth";
const LEAVE_CALENDAR_MANAGER: E2EUser = {
const LEAVE_CALENDAR_HR: E2EUser = {
identifier: "[email protected]",
password: "HrPassword1234",
name: "HR",
@@ -149,7 +149,12 @@ function buildCalendarItems(dateFrom: string, dateTo: string) {
}
async function mockLeaveRequests(page: Page) {
const observedCalendarQueries: Array<{ dateFrom: string; dateTo: string; pageSize: string }> = [];
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());
@@ -157,9 +162,10 @@ async function mockLeaveRequests(page: Page) {
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 });
observedCalendarQueries.push({ dateFrom, dateTo, pageSize, departmentId });
await route.fulfill({
status: 200,
contentType: "application/json",
@@ -194,10 +200,10 @@ async function mockLeaveRequests(page: Page) {
return observedCalendarQueries;
}
test("E2E 请假团队日历:具备团队查看权限的用户可见周/月视图并显示截断提示", async ({ page }) => {
test("E2E 请假团队日历:HR 可见全员周/月视图并显示截断提示", async ({ page }) => {
const observedCalendarQueries = await mockLeaveRequests(page);
await login(page, LEAVE_CALENDAR_MANAGER);
await login(page, LEAVE_CALENDAR_HR);
await page.goto("/leave-requests");
await expect(page.getByRole("heading", { name: "请假申请" })).toBeVisible();
@@ -229,6 +235,82 @@ test("E2E 请假团队日历:具备团队查看权限的用户可见周/月视
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 }) => {
+12
查看文件
@@ -223,6 +223,18 @@ test("Portal 管理视角重排正确,备用模块默认折叠", async ({ page
await expect(page.getByRole("link", { name: "采购申请" })).toBeVisible();
});
test("Portal 域管理角色也进入经理视图", async ({ page }) => {
await mockNavigation(page, "manager");
await mockPortalHome(page, "manager");
await mockMe(page, ["administration"], "Administration");
await login(page, EMPLOYEE);
await expect(page.getByText("今日管理重点")).toBeVisible();
await expect(page.locator('[aria-label="portal-card-pending_approvals"]')).toBeVisible();
await expect(page.locator('[aria-label="portal-card-team_presence"]')).toBeVisible();
});
test("企微 WebView 下顶栏显示入口提示", async ({ page }) => {
await page.addInitScript(() => {
Object.defineProperty(window.navigator, "userAgent", {