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

这个提交包含在:
2026-03-25 15:32:23 +08:00
父节点 18af4aedc4
当前提交 d0b9d9eaca
共修改 9 个文件,包含 874 行新增和 69 行删除
+33 -1
查看文件
@@ -90,6 +90,7 @@ export type LeaveListQuery = {
pageSize?: number;
dateFrom?: string;
dateTo?: string;
departmentId?: string;
};
function toQueryString(query: Record<string, string | number | undefined>) {
@@ -194,7 +195,38 @@ export async function createLeaveRequest(payload: {
export async function submitLeaveRequest(leaveRequestId: string) {
return apiRequestData<Record<string, unknown>>(`/api/leave-requests/${leaveRequestId}/submit`, {
method: "POST",
body: JSON.stringify({}),
body: JSON.stringify({}),
});
}
export async function patchLeaveRequest(
leaveRequestId: string,
payload: {
leaveTypeId: string;
startDate: string;
endDate: string;
startSession: LeaveSession;
endSession: LeaveSession;
reason?: string;
file?: File | null;
},
) {
const formData = new FormData();
formData.append("leaveTypeId", payload.leaveTypeId);
formData.append("startDate", payload.startDate);
formData.append("endDate", payload.endDate);
formData.append("startSession", payload.startSession);
formData.append("endSession", payload.endSession);
if (payload.reason?.trim()) {
formData.append("reason", payload.reason.trim());
}
if (payload.file) {
formData.append("file", payload.file);
}
return apiRequestData<LeaveRequestItem>(`/api/leave-requests/${leaveRequestId}`, {
method: "PATCH",
body: formData,
});
}
+14 -4
查看文件
@@ -54,6 +54,7 @@ const ROLE_PERMISSIONS: Record<string, readonly string[]> = {
"expense.transfer",
"expense.view_all",
"leave.approve",
"leave.view_all",
"procurement.approve",
"procurement.view_all",
"travel.approve",
@@ -61,12 +62,9 @@ const ROLE_PERMISSIONS: Record<string, readonly string[]> = {
"asset.view_all",
"report.dashboard.view",
"project.view_all",
"project.manage",
"budget.view",
"project.view_all",
"project.manage",
"budget.manage",
"project.view_all",
"project.manage",
],
ceo: [
...EMPLOYEE_BASE_PERMISSIONS,
@@ -184,3 +182,15 @@ export function hasAnyPermission(permissionCodes: string[], candidates: string[]
return candidates.some((candidate) => hasPermission(permissionCodes, candidate));
}
export function isManagerRole(permissionCodes: string[]) {
if (hasPermission(permissionCodes, "*") || hasPermission(permissionCodes, "system.users")) {
return true;
}
return permissionCodes.some(
(permission) =>
permission.endsWith(".approve") ||
(permission.endsWith(".manage") && !permission.endsWith(".view_all")),
);
}
+210 -53
查看文件
@@ -7,6 +7,7 @@ import {
fetchLeaveBalances,
fetchLeaveRequests,
fetchLeaveTypes,
patchLeaveRequest,
submitLeaveRequest,
withdrawLeaveRequest,
type LeaveRequestItem,
@@ -109,6 +110,14 @@ function buildCalendarWindow(anchorDate: Date, mode: LeaveCalendarMode) {
};
}
function shiftCalendarAnchorDate(anchorDate: Date, mode: LeaveCalendarMode, direction: -1 | 1) {
if (mode === "week") {
return addUtcDays(anchorDate, direction * 7);
}
return new Date(Date.UTC(anchorDate.getUTCFullYear(), anchorDate.getUTCMonth() + direction, 1));
}
function leaveRequestIntersectsDate(item: LeaveRequestItem, dateKey: string) {
return item.startDate <= dateKey && item.endDate >= dateKey;
}
@@ -187,14 +196,18 @@ export function LeaveRequestPage() {
const queryClient = useQueryClient();
const { me, permissions } = useAuth();
const canViewAll = hasPermission(permissions, "leave.view_all") || hasPermission(permissions, "leave.manage");
const hasLeaveViewAll = hasPermission(permissions, "leave.view_all");
const hasLeaveManage = hasPermission(permissions, "leave.manage");
const canViewAll = hasLeaveViewAll || hasLeaveManage;
const canViewTeamCalendar = hasLeaveViewAll;
const useDepartmentCalendarScope = hasLeaveViewAll && !hasLeaveManage && Boolean(me?.departmentId);
const [statusFilter, setStatusFilter] = useState<LeaveRequestStatus | "">("");
const [scope, setScope] = useState<"own" | "all">("own");
const [page, setPage] = useState(1);
const [pageView, setPageView] = useState<LeavePageView>("records");
const [calendarMode, setCalendarMode] = useState<LeaveCalendarMode>("week");
const [calendarAnchorDate] = useState(() => new Date());
const [calendarAnchorDate, setCalendarAnchorDate] = useState(() => new Date());
const [leaveTypeId, setLeaveTypeId] = useState("");
const [startDate, setStartDate] = useState("");
@@ -203,6 +216,8 @@ export function LeaveRequestPage() {
const [endSession, setEndSession] = useState<LeaveSession>("pm");
const [reason, setReason] = useState("");
const [attachmentFile, setAttachmentFile] = useState<File | null>(null);
const [attachmentInputKey, setAttachmentInputKey] = useState(0);
const [editingLeaveRequest, setEditingLeaveRequest] = useState<LeaveRequestItem | null>(null);
const [formError, setFormError] = useState("");
const [withdrawTarget, setWithdrawTarget] = useState<{ id: string; label: string } | null>(null);
@@ -239,8 +254,16 @@ export function LeaveRequestPage() {
);
const calendarRequestsQuery = useQuery({
queryKey: ["leave", "calendar", calendarMode, calendarWindow.queryFrom, calendarWindow.queryTo],
enabled: canViewAll && pageView === "calendar",
queryKey: [
"leave",
"calendar",
calendarMode,
calendarAnchorDate.toISOString(),
calendarWindow.queryFrom,
calendarWindow.queryTo,
useDepartmentCalendarScope ? me?.departmentId ?? "no-department" : "all",
],
enabled: canViewTeamCalendar && pageView === "calendar",
queryFn: () =>
fetchLeaveRequests({
scope: "all",
@@ -248,6 +271,7 @@ export function LeaveRequestPage() {
pageSize: CALENDAR_PAGE_SIZE,
dateFrom: calendarWindow.queryFrom,
dateTo: calendarWindow.queryTo,
departmentId: useDepartmentCalendarScope ? me?.departmentId ?? undefined : undefined,
}),
});
@@ -280,40 +304,100 @@ export function LeaveRequestPage() {
);
}, [calendarRequestsQuery.data?.items, calendarWindow.dates]);
function resetForm() {
setEditingLeaveRequest(null);
setLeaveTypeId("");
setStartDate("");
setEndDate("");
setStartSession("am");
setEndSession("pm");
setReason("");
setAttachmentFile(null);
setAttachmentInputKey((current) => current + 1);
setFormError("");
}
function startEditing(item: LeaveRequestItem) {
setEditingLeaveRequest(item);
setLeaveTypeId(item.leaveType?.id ?? "");
setStartDate(item.startDate);
setEndDate(item.endDate);
setStartSession(item.startSession);
setEndSession(item.endSession);
setReason(item.reason ?? "");
setAttachmentFile(null);
setAttachmentInputKey((current) => current + 1);
setFormError("");
}
function buildFormPayload({ requireAttachment }: { requireAttachment: boolean }) {
if (!leaveTypeId) {
throw new Error("请选择假期类型");
}
if (!startDate || !endDate) {
throw new Error("请选择开始和结束日期");
}
if (!selectedLeaveType) {
throw new Error("假期类型不存在或已失效");
}
if (requestedDaysPreview == null || requestedDaysPreview <= 0) {
throw new Error("请检查开始/结束日期与半天时段");
}
if (
requireAttachment &&
selectedLeaveType.requiresAttachment &&
!attachmentFile &&
!editingLeaveRequest?.attachment
) {
throw new Error("当前假种必须上传附件");
}
if (
attachmentFile &&
selectedLeaveType.attachmentMimeTypes.length > 0 &&
!selectedLeaveType.attachmentMimeTypes.includes(attachmentFile.type)
) {
throw new Error("附件类型不符合当前假种要求");
}
return {
leaveTypeId,
startDate,
endDate,
startSession,
endSession,
reason,
file: attachmentFile,
};
}
const createMutation = useMutation({
mutationFn: async () => createLeaveRequest(buildFormPayload({ requireAttachment: true })),
onSuccess: async () => {
await Promise.all([
queryClient.invalidateQueries({ queryKey: ["leave", "requests"] }),
queryClient.invalidateQueries({ queryKey: ["leave", "calendar"] }),
queryClient.invalidateQueries({ queryKey: ["leave", "balances"] }),
]);
resetForm();
toast.success("请假草稿已创建");
},
onError: (error) => {
const message = resolveError(error);
setFormError(message);
toast.error(message);
},
});
const updateMutation = useMutation({
mutationFn: async () => {
if (!leaveTypeId) {
throw new Error("请选择假期类型");
}
if (!startDate || !endDate) {
throw new Error("请选择开始和结束日期");
}
if (!selectedLeaveType) {
throw new Error("假期类型不存在或已失效");
}
if (requestedDaysPreview == null || requestedDaysPreview <= 0) {
throw new Error("请检查开始/结束日期与半天时段");
}
if (selectedLeaveType.requiresAttachment && !attachmentFile) {
throw new Error("当前假种必须上传附件");
}
if (
attachmentFile &&
selectedLeaveType.attachmentMimeTypes.length > 0 &&
!selectedLeaveType.attachmentMimeTypes.includes(attachmentFile.type)
) {
throw new Error("附件类型不符合当前假种要求");
if (!editingLeaveRequest) {
throw new Error("未选择可编辑的请假单");
}
return createLeaveRequest({
leaveTypeId,
startDate,
endDate,
startSession,
endSession,
reason,
file: attachmentFile,
});
return patchLeaveRequest(
editingLeaveRequest.id,
buildFormPayload({ requireAttachment: false }),
);
},
onSuccess: async () => {
await Promise.all([
@@ -321,15 +405,8 @@ export function LeaveRequestPage() {
queryClient.invalidateQueries({ queryKey: ["leave", "calendar"] }),
queryClient.invalidateQueries({ queryKey: ["leave", "balances"] }),
]);
setLeaveTypeId("");
setStartDate("");
setEndDate("");
setStartSession("am");
setEndSession("pm");
setReason("");
setAttachmentFile(null);
setFormError("");
toast.success("请假草稿已创建");
resetForm();
toast.success("请假单已保存,可继续提交");
},
onError: (error) => {
const message = resolveError(error);
@@ -371,6 +448,13 @@ export function LeaveRequestPage() {
const totalPages = Math.max(1, Math.ceil((requestsQuery.data?.total ?? 0) / PAGE_SIZE));
const calendarIsTruncated = (calendarRequestsQuery.data?.total ?? 0) > (calendarRequestsQuery.data?.items.length ?? 0);
const isSavingForm = createMutation.isPending || updateMutation.isPending;
const formTitle = editingLeaveRequest ? "编辑请假单" : "新建请假草稿";
const formDescription = editingLeaveRequest
? "修改原单内容并保留同一请假单 ID,保存后仍可继续提交。"
: "先填写时间段与假种,再补充附件和说明。";
const currentAttachmentLabel =
attachmentFile?.name ?? (editingLeaveRequest?.attachment?.originalName ?? null);
const leaveRecordsPanel = (
<SectionPanel title="请假记录" description="支持按状态和范围收敛列表,草稿与驳回单可再次提交。">
@@ -439,6 +523,7 @@ export function LeaveRequestPage() {
{(requestsQuery.data?.items ?? []).length ? (
requestsQuery.data?.items.map((item) => {
const isOwner = item.ownerId === me?.id;
const canEdit = isOwner && ["draft", "rejected"].includes(item.status);
const canSubmit = isOwner && ["draft", "rejected"].includes(item.status);
const canWithdraw = isOwner && item.status === "reviewing";
@@ -460,6 +545,15 @@ export function LeaveRequestPage() {
<TableCell>{item.updatedAt ? new Date(item.updatedAt).toLocaleString() : "-"}</TableCell>
<TableCell>
<div className="flex flex-wrap gap-2">
<Button
size="sm"
variant="secondary"
type="button"
disabled={!canEdit || isSavingForm}
onClick={() => startEditing(item)}
>
编辑
</Button>
<Button
size="sm"
type="button"
@@ -516,15 +610,49 @@ export function LeaveRequestPage() {
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div>
<div className="text-sm font-semibold text-zinc-900">{calendarWindow.title}</div>
<div className="text-xs text-zinc-500">固定拉取当前可见窗口内的请假记录;若超出 100 条会提示截断。</div>
<div className="text-xs text-zinc-500">
{useDepartmentCalendarScope ? "当前仅展示本部门窗口内请假记录;若超出 100 条会提示截断。" : "固定拉取当前可见窗口内的请假记录;若超出 100 条会提示截断。"}
</div>
</div>
<Tabs value={calendarMode} onValueChange={(value) => setCalendarMode(value as LeaveCalendarMode)} data-testid="leave-calendar-mode-tabs">
<TabsList className="flex w-full flex-wrap justify-start gap-1 md:w-auto">
<div className="flex flex-col gap-2 md:items-end">
<div className="flex flex-wrap gap-2">
<Button
size="sm"
type="button"
variant="secondary"
data-testid="leave-calendar-prev"
onClick={() => setCalendarAnchorDate((current) => shiftCalendarAnchorDate(current, calendarMode, -1))}
>
上一个
</Button>
<Button
size="sm"
type="button"
variant="secondary"
data-testid="leave-calendar-today"
onClick={() => setCalendarAnchorDate(new Date())}
>
今天
</Button>
<Button
size="sm"
type="button"
variant="secondary"
data-testid="leave-calendar-next"
onClick={() => setCalendarAnchorDate((current) => shiftCalendarAnchorDate(current, calendarMode, 1))}
>
下一个
</Button>
</div>
<Tabs value={calendarMode} onValueChange={(value) => setCalendarMode(value as LeaveCalendarMode)} data-testid="leave-calendar-mode-tabs">
<TabsList className="flex w-full flex-wrap justify-start gap-1 md:w-auto">
<TabsTrigger value="week" data-testid="leave-calendar-mode-week">周视图</TabsTrigger>
<TabsTrigger value="month" data-testid="leave-calendar-mode-month">月视图</TabsTrigger>
</TabsList>
</Tabs>
</TabsList>
</Tabs>
</div>
</div>
{calendarIsTruncated ? (
@@ -612,7 +740,7 @@ export function LeaveRequestPage() {
</section>
<div className="page-shell-grid">
<SectionPanel title="新建请假草稿" description="先填写时间段与假种,再补充附件和说明。">
<SectionPanel title={formTitle} description={formDescription}>
<div className="space-y-4">
{formError ? (
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
@@ -677,6 +805,7 @@ export function LeaveRequestPage() {
<div className="space-y-2">
<Label htmlFor="leave-attachment">附件</Label>
<Input
key={attachmentInputKey}
id="leave-attachment"
type="file"
accept={selectedLeaveType?.attachmentMimeTypes?.join(",") || undefined}
@@ -690,6 +819,11 @@ export function LeaveRequestPage() {
? `当前假种要求上传附件(允许:${selectedLeaveType.attachmentMimeTypes.join(", ") || "默认文档/图片"})`
: "附件为可选项"}
</p>
{editingLeaveRequest ? (
<p className="text-xs text-zinc-500">
当前附件:{currentAttachmentLabel ?? "未上传"}。未重新选择文件时保留原附件。
</p>
) : null}
</div>
</div>
@@ -708,9 +842,32 @@ export function LeaveRequestPage() {
<div>
预计请假天数:<span className="font-semibold text-zinc-900">{requestedDaysPreview ?? "-"}</span>
</div>
<Button type="button" disabled={createMutation.isPending} onClick={() => createMutation.mutate()}>
{createMutation.isPending ? "创建中..." : "创建草稿"}
</Button>
<div className="flex flex-wrap gap-2">
{editingLeaveRequest ? (
<Button type="button" variant="secondary" onClick={resetForm} disabled={isSavingForm}>
取消编辑
</Button>
) : null}
<Button
type="button"
disabled={isSavingForm}
onClick={() => {
if (editingLeaveRequest) {
updateMutation.mutate();
return;
}
createMutation.mutate();
}}
>
{editingLeaveRequest
? updateMutation.isPending
? "保存中..."
: "保存修改"
: createMutation.isPending
? "创建中..."
: "创建草稿"}
</Button>
</div>
</div>
</div>
</SectionPanel>
@@ -774,7 +931,7 @@ export function LeaveRequestPage() {
</SectionPanel>
</div>
{canViewAll ? (
{canViewTeamCalendar ? (
<Tabs data-testid="leave-page-view-tabs" value={pageView} onValueChange={(value) => setPageView(value as LeavePageView)}>
<TabsList className="flex w-full flex-wrap justify-start gap-1">
<TabsTrigger data-testid="leave-view-records" value="records">请假记录</TabsTrigger>
+2 -2
查看文件
@@ -4,7 +4,7 @@ import { fetchPortalHome, type PortalHomePayload } from "../api/portal";
import { mapApiErrorMessage } from "../api/errorMessages";
import { isApiError } from "../api/types";
import { useAuth } from "../auth/AuthContext";
import { hasAnyPermission } from "../auth/permissions";
import { hasAnyPermission, isManagerRole } from "../auth/permissions";
import { EmptyStateCard } from "../components/common/EmptyStateCard";
import { MetricCard } from "../components/patterns/MetricCard";
import { PageScaffold } from "../components/patterns/PageScaffold";
@@ -201,7 +201,7 @@ export function PortalHomePage() {
const { permissions } = useAuth();
const portalQuery = useQuery({ queryKey: ["portal-home"], queryFn: () => fetchPortalHome() });
const canSetupWorkspace = hasAnyPermission(permissions, ["system.settings"]);
const isManagerView = hasAnyPermission(permissions, ["system.users", "report.dashboard.view", "*"]);
const isManagerView = isManagerRole(permissions);
if (portalQuery.isLoading) {
return <div className="card-std p-6 text-sm text-zinc-500">加载 Portal 中...</div>;
+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", {