import { expect, test, type Locator, type Page } from "@playwright/test"; import { EMPLOYEE, MANAGER, apiPostData, createAuthenticatedApiClient, login, logout } from "./helpers/auth"; import { openNavLink } from "./helpers/navigation"; import { TEST_IDS } from "./helpers/testIds"; function formatDate(offsetDays: number) { const value = new Date(); value.setUTCDate(value.getUTCDate() + offsetDays); return value.toISOString().slice(0, 10); } async function selectFirstEnabledOption(locator: Locator) { await expect .poll(async () => { const optionValues = await locator.locator("option").evaluateAll((options) => options .map((option) => ({ value: (option as HTMLOptionElement).value, disabled: (option as HTMLOptionElement).disabled, })) .filter((option) => option.value && !option.disabled) .map((option) => option.value), ); return optionValues[0] ?? ""; }) .not.toBe(""); const [firstValue] = await locator.locator("option").evaluateAll((options) => options .map((option) => ({ value: (option as HTMLOptionElement).value, disabled: (option as HTMLOptionElement).disabled, })) .filter((option) => option.value && !option.disabled) .map((option) => option.value), ); await locator.selectOption(firstValue); } async function createLeaveDraft(page: Page, startDate: string, endDate: string, reason: string) { const createResponsePromise = page.waitForResponse( (response) => response.request().method() === "POST" && new URL(response.url()).pathname === "/api/leave-requests" && response.ok(), ); await selectFirstEnabledOption(page.locator("#leave-type")); await page.locator("#leave-start-date").fill(startDate); await page.locator("#leave-end-date").fill(endDate); await page.locator("#leave-start-session").selectOption("am"); await page.locator("#leave-end-session").selectOption("pm"); await page.locator("#leave-reason").fill(reason); await page.getByRole("button", { name: "创建草稿" }).click(); const createPayload = (await (await createResponsePromise).json()) as { data?: { id?: string } }; const leaveRequestId = createPayload.data?.id; if (!leaveRequestId) { throw new Error(`创建请假草稿后未返回 id: ${JSON.stringify(createPayload)}`); } return leaveRequestId; } async function submitLeave(api: Awaited>["api"], leaveRequestId: string) { return apiPostData<{ leaveRequestId: string; currentTaskId: string }>(api, `/api/leave-requests/${leaveRequestId}/submit`, {}); } async function withdrawLeave(api: Awaited>["api"], leaveRequestId: string) { return apiPostData<{ leaveRequestId: string; leaveStatus: string }>(api, `/api/leave-requests/${leaveRequestId}/withdraw`, {}); } test("E2E 请假主链路:草稿 → 提交 → 撤回 + 提交 → 审批", async ({ page }) => { const employeeClient = await createAuthenticatedApiClient(EMPLOYEE); const baseOffset = 30 + Number(String(Date.now()).slice(-2)); const firstDate = formatDate(baseOffset); const secondDate = formatDate(baseOffset + 7); try { await login(page, EMPLOYEE); await (await openNavLink(page, "请假")).click(); await expect(page.getByRole("heading", { name: "请假申请" })).toBeVisible(); const firstLeaveId = await createLeaveDraft(page, firstDate, firstDate, `Playwright 撤回请假 ${Date.now()}`); await submitLeave(employeeClient.api, firstLeaveId); await page.goto("/leave-requests"); const firstRow = page.getByRole("row").filter({ hasText: firstDate }).first(); await expect(firstRow).toContainText("审批中", { timeout: 30_000 }); await withdrawLeave(employeeClient.api, firstLeaveId); await page.goto("/leave-requests"); await expect(firstRow).toContainText("草稿", { timeout: 30_000 }); const secondLeaveId = await createLeaveDraft(page, secondDate, secondDate, `Playwright 审批请假 ${Date.now()}`); const secondSubmit = await submitLeave(employeeClient.api, secondLeaveId); await page.goto("/leave-requests"); const secondRow = page.getByRole("row").filter({ hasText: secondDate }).first(); await expect(secondRow).toContainText("审批中", { timeout: 30_000 }); await logout(page, EMPLOYEE.name); await login(page, MANAGER); await (await openNavLink(page, "待我处理")).click(); await expect(page.getByTestId(TEST_IDS.workflow.tasksTable)).toBeVisible(); const taskRow = page.getByTestId(TEST_IDS.workflow.taskRow).filter({ hasText: secondSubmit.currentTaskId }).first(); await expect(taskRow).toBeVisible({ timeout: 30_000 }); await taskRow.getByTestId(TEST_IDS.workflow.detailLink).click(); await page.getByTestId(TEST_IDS.workflow.approve).click(); await expect(page.getByTestId(TEST_IDS.workflow.summary)).toContainText(/状态:(completed|已完成)/); await logout(page, MANAGER.name); await login(page, EMPLOYEE); await page.goto("/leave-requests"); await expect(page.getByRole("row").filter({ hasText: secondDate }).first()).toContainText("已通过", { timeout: 30_000 }); await expect(page.getByRole("row").filter({ hasText: firstDate }).first()).toContainText("草稿", { timeout: 30_000 }); } finally { await employeeClient.dispose(); } });