ATTV1 实现考勤统一入口与企微同步
这个提交包含在:
@@ -23,27 +23,6 @@ type FeatureFlagItem = {
|
||||
effectiveEnabled: boolean;
|
||||
};
|
||||
|
||||
type AttendanceTodayResponse = {
|
||||
workDate: string;
|
||||
needClockIn: boolean;
|
||||
exemptionReason: string | null;
|
||||
item: {
|
||||
clockInAt: string | null;
|
||||
clockOutAt: string | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
type AttendanceTeamItem = {
|
||||
employeeId: string;
|
||||
employeeName: string;
|
||||
employeeNo: string;
|
||||
departmentId: string | null;
|
||||
attendanceStatus: string;
|
||||
needClockIn: boolean;
|
||||
clockInAt: string | null;
|
||||
clockOutAt: string | null;
|
||||
};
|
||||
|
||||
type NotificationListResponse = {
|
||||
unreadCount: number;
|
||||
items: Array<{
|
||||
@@ -106,10 +85,6 @@ function formatDate(offsetDays: number) {
|
||||
return value.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function toDateTimeLocal(date: string, time: string) {
|
||||
return `${date}T${time}`;
|
||||
}
|
||||
|
||||
async function readApiData<T>(response: APIResponse) {
|
||||
const payload = (await response.json()) as { data?: T; error?: { code?: string; message?: string } };
|
||||
if (!response.ok()) {
|
||||
@@ -142,22 +117,6 @@ async function ensureFeatureFlags(api: Awaited<ReturnType<typeof createAuthentic
|
||||
}
|
||||
}
|
||||
|
||||
async function findBackfillTarget(api: Awaited<ReturnType<typeof createAuthenticatedApiClient>>["api"]) {
|
||||
const offsets = [0, -1, -2, -3, -4, -5, -6, -7, 1, 2, 3, 4, 5];
|
||||
for (const offset of offsets) {
|
||||
const date = formatDate(offset);
|
||||
const payload = await apiGetData<{ date: string; items: AttendanceTeamItem[] }>(
|
||||
api,
|
||||
`/api/attendance/team?date=${date}`,
|
||||
);
|
||||
const target = payload.items.find((item) => item.needClockIn && (!item.clockInAt || !item.clockOutAt));
|
||||
if (target) {
|
||||
return { date, item: target };
|
||||
}
|
||||
}
|
||||
throw new Error("未找到可用于团队补卡的考勤缺口样本");
|
||||
}
|
||||
|
||||
async function createSubmittedReimbursement(api: AuthedApi, label: string): Promise<SubmittedReimbursementSeed> {
|
||||
const reimbursementDate = formatDate(30 + Math.floor(Math.random() * 45));
|
||||
const description = `Playwright 通知补盲 ${label}`;
|
||||
@@ -247,86 +206,36 @@ async function fetchEmployeeByKeyword(
|
||||
test.describe("R75 自动化补盲", () => {
|
||||
test.describe.configure({ mode: "serial" });
|
||||
|
||||
test("E2E R75 考勤补盲:员工今日状态 + 管理员团队补卡", async ({ page }) => {
|
||||
test("E2E R75 考勤补盲:OA 自建打卡关闭 + 统一考勤入口", async ({ page }) => {
|
||||
test.setTimeout(240_000);
|
||||
|
||||
const adminClient = await createAuthenticatedApiClient(ADMIN);
|
||||
const employeeClient = await createAuthenticatedApiClient(EMPLOYEE);
|
||||
|
||||
try {
|
||||
await ensureFeatureFlags(adminClient.api, ["attendance_v1"]);
|
||||
const backfillTarget = await findBackfillTarget(adminClient.api);
|
||||
await ensureFeatureFlags(adminClient.api, ["attendance_unified_v1"]);
|
||||
|
||||
const clockInResponse = await employeeClient.api.post("/api/attendance/clock-in", { data: {} });
|
||||
expect(clockInResponse.status()).toBe(409);
|
||||
const clockInPayload = (await clockInResponse.json()) as { error?: { details?: { featureFlag?: string } } };
|
||||
expect(clockInPayload.error?.details?.featureFlag).toBe("attendance_v1_disabled");
|
||||
|
||||
await login(page, EMPLOYEE);
|
||||
await page.goto("/attendance");
|
||||
await expect(page.getByRole("heading", { name: "考勤" })).toBeVisible();
|
||||
|
||||
const today = await apiGetData<AttendanceTodayResponse>(employeeClient.api, "/api/attendance/me/today");
|
||||
if (today.needClockIn) {
|
||||
if (!today.item?.clockInAt) {
|
||||
await page.getByRole("button", { name: "点击打上班卡" }).click();
|
||||
await expect(page.getByText("上班打卡成功")).toBeVisible();
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const latest = await apiGetData<AttendanceTodayResponse>(employeeClient.api, "/api/attendance/me/today");
|
||||
return Boolean(latest.item?.clockInAt);
|
||||
})
|
||||
.toBe(true);
|
||||
|
||||
await page.getByRole("button", { name: /点击下班|重新打下班卡/ }).click();
|
||||
await expect(page.getByText("下班打卡成功")).toBeVisible();
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const latest = await apiGetData<AttendanceTodayResponse>(employeeClient.api, "/api/attendance/me/today");
|
||||
return Boolean(latest.item?.clockOutAt);
|
||||
})
|
||||
.toBe(true);
|
||||
} else {
|
||||
await page.getByRole("button", { name: today.item.clockOutAt ? "重新打下班卡" : "点击下班" }).click();
|
||||
await expect(page.getByText("下班打卡成功")).toBeVisible();
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const latest = await apiGetData<AttendanceTodayResponse>(employeeClient.api, "/api/attendance/me/today");
|
||||
return Boolean(latest.item?.clockOutAt);
|
||||
})
|
||||
.toBe(true);
|
||||
}
|
||||
} else {
|
||||
await expect(page.getByText(/免打卡:/)).toBeVisible();
|
||||
}
|
||||
|
||||
await expect(page.getByText("本月打卡记录")).toBeVisible();
|
||||
await expect(page.getByText(today.workDate).first()).toBeVisible();
|
||||
await expect(page.getByText("企业微信采集打卡")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: /点击打上班卡|点击下班|重新打下班卡/ })).toHaveCount(0);
|
||||
await expect(page.getByText("本月打卡记录")).toHaveCount(0);
|
||||
|
||||
await logout(page, EMPLOYEE.name);
|
||||
|
||||
await login(page, ADMIN);
|
||||
await page.goto("/attendance");
|
||||
await page.getByRole("tab", { name: "团队考勤" }).click();
|
||||
await expect(page.getByText("查看指定日期团队上下班打卡状态")).toBeVisible();
|
||||
|
||||
await page.locator('input[type="date"]').fill(backfillTarget.date);
|
||||
|
||||
const targetRow = page.getByRole("row").filter({ hasText: backfillTarget.item.employeeNo }).first();
|
||||
await expect(targetRow).toBeVisible();
|
||||
await targetRow.getByRole("button", { name: "补卡" }).click();
|
||||
|
||||
await page.getByLabel("上班时间").fill(toDateTimeLocal(backfillTarget.date, "09:00"));
|
||||
await page.getByLabel("下班时间").fill(toDateTimeLocal(backfillTarget.date, "18:00"));
|
||||
await page.getByLabel("备注").fill("Playwright R75 团队补卡");
|
||||
await page.getByRole("button", { name: "保存补卡" }).click();
|
||||
await expect(page.getByText("补卡已保存")).toBeVisible();
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const payload = await apiGetData<{ items: AttendanceTeamItem[] }>(
|
||||
adminClient.api,
|
||||
`/api/attendance/team?date=${backfillTarget.date}`,
|
||||
);
|
||||
const item = payload.items.find((entry) => entry.employeeId === backfillTarget.item.employeeId);
|
||||
return Boolean(item?.clockInAt && item?.clockOutAt);
|
||||
})
|
||||
.toBe(true);
|
||||
await expect(page.getByRole("heading", { name: "考勤" })).toBeVisible();
|
||||
await expect(page.getByText("OA 统一处理请假、补卡、外出、加班和月度解释")).toBeVisible();
|
||||
await expect(page.getByRole("tab", { name: "修正申请" })).toBeVisible();
|
||||
await expect(page.getByRole("tab", { name: "团队考勤" })).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: "保存补卡" })).toHaveCount(0);
|
||||
} finally {
|
||||
await Promise.all([employeeClient.dispose(), adminClient.dispose()]);
|
||||
}
|
||||
|
||||
@@ -239,7 +239,7 @@ test.describe("visual shell", () => {
|
||||
await expect(page.getByRole("heading", { name: "今天" })).toBeVisible();
|
||||
await expect(page.getByTestId("mobile-today-focus")).toBeVisible();
|
||||
await expect(page.getByTestId("mobile-action-capture")).toBeVisible();
|
||||
await expect(page.getByTestId("mobile-attendance-card")).toBeVisible();
|
||||
await expect(page.getByTestId("mobile-attendance-summary-card")).toBeVisible();
|
||||
|
||||
await attachScreenshot(page, testInfo, "mobile-today.png");
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户