207 行
9.4 KiB
TypeScript
207 行
9.4 KiB
TypeScript
import { test, expect } from "./helpers/fixtures";
|
|
import { EMPLOYEE, MANAGER, apiPostData, createAuthenticatedApiClient, login, logout } from "./helpers/auth";
|
|
import { openNavLink } from "./helpers/navigation";
|
|
import { TEST_IDS } from "./helpers/testIds";
|
|
import type { Locator, Page } from "@playwright/test";
|
|
|
|
function formatDate(offsetDays: number) {
|
|
const value = new Date();
|
|
value.setUTCDate(value.getUTCDate() + offsetDays);
|
|
return value.toISOString().slice(0, 10);
|
|
}
|
|
|
|
function uniqueFutureOffset(minOffsetDays: number) {
|
|
return minOffsetDays + (Math.floor(Date.now() / 1000) % 2000);
|
|
}
|
|
|
|
|
|
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<ReturnType<typeof createAuthenticatedApiClient>>["api"], leaveRequestId: string) {
|
|
return apiPostData<{ leaveRequestId: string; currentTaskId: string }>(api, `/api/leave-requests/${leaveRequestId}/submit`, {});
|
|
}
|
|
|
|
async function withdrawLeave(api: Awaited<ReturnType<typeof createAuthenticatedApiClient>>["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 = uniqueFutureOffset(420);
|
|
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 });
|
|
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 });
|
|
await expect(updatedRow).toContainText("当前节点", { timeout: 30_000 });
|
|
} finally {
|
|
await employeeClient.dispose();
|
|
}
|
|
});
|
|
|
|
test("E2E 请假主链路:草稿 → 提交 → 撤回 + 提交 → 审批", async ({ page, workflowDetail }) => {
|
|
const employeeClient = await createAuthenticatedApiClient(EMPLOYEE);
|
|
const baseOffset = uniqueFutureOffset(2600);
|
|
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();
|
|
await expect(page.locator(`a[href="/workflow/tasks/${secondSubmit.currentTaskId}"]`).first()).toHaveCount(1, { timeout: 30_000 });
|
|
await page.goto(`/workflow/tasks/${secondSubmit.currentTaskId}`);
|
|
await workflowDetail.assertLoaded();
|
|
await workflowDetail.approve();
|
|
await expect(page.getByRole("main")).toContainText("已完成", { timeout: 30_000 });
|
|
await expect(page.getByRole("main")).toContainText("业务状态:已通过", { timeout: 30_000 });
|
|
|
|
await logout(page, MANAGER.name);
|
|
|
|
await login(page, EMPLOYEE);
|
|
await page.goto("/leave-requests");
|
|
const approvedRow = page.getByRole("row").filter({ hasText: secondDate }).first();
|
|
await expect(approvedRow).toContainText("已通过", { timeout: 30_000 });
|
|
await expect(approvedRow).toContainText("已通过,系统会按请假结果更新额度和团队状态", { timeout: 30_000 });
|
|
await expect(page.getByRole("row").filter({ hasText: firstDate }).first()).toContainText("草稿", { timeout: 30_000 });
|
|
} finally {
|
|
await employeeClient.dispose();
|
|
}
|
|
});
|
|
|
|
test("E2E 请假退回说明:审批退回后员工可看到处理意见", async ({ page, workflowDetail }) => {
|
|
const employeeClient = await createAuthenticatedApiClient(EMPLOYEE);
|
|
const baseOffset = uniqueFutureOffset(5200);
|
|
const leaveDate = formatDate(baseOffset);
|
|
const returnComment = `请补充外出交接安排 ${Date.now()}`;
|
|
|
|
try {
|
|
await login(page, EMPLOYEE);
|
|
await (await openNavLink(page, "请假")).click();
|
|
await expect(page.getByRole("heading", { name: "请假申请" })).toBeVisible();
|
|
|
|
const leaveId = await createLeaveDraft(page, leaveDate, leaveDate, `Playwright 退回请假 ${Date.now()}`);
|
|
const submitResult = await submitLeave(employeeClient.api, leaveId);
|
|
|
|
await logout(page, EMPLOYEE.name);
|
|
|
|
await login(page, MANAGER);
|
|
await page.goto(`/workflow/tasks/${submitResult.currentTaskId}`);
|
|
await workflowDetail.assertLoaded();
|
|
await workflowDetail.returnTask(returnComment);
|
|
await expect(page.getByRole("main")).toContainText("已完成", { timeout: 30_000 });
|
|
await expect(page.getByRole("main")).toContainText("业务状态:已终止", { timeout: 30_000 });
|
|
|
|
await logout(page, MANAGER.name);
|
|
|
|
await login(page, EMPLOYEE);
|
|
await page.goto("/leave-requests");
|
|
const returnedRow = page.getByRole("row").filter({ hasText: leaveDate }).first();
|
|
await expect(returnedRow).toContainText("草稿", { timeout: 30_000 });
|
|
await expect(returnedRow).toContainText("审批已退回", { timeout: 30_000 });
|
|
await expect(returnedRow).toContainText(returnComment, { timeout: 30_000 });
|
|
} finally {
|
|
await employeeClient.dispose();
|
|
}
|
|
});
|