231 行
9.9 KiB
TypeScript
231 行
9.9 KiB
TypeScript
import { mkdirSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { expect, test, type Page, type TestInfo } from "@playwright/test";
|
|
|
|
import { EMPLOYEE, FINANCE, login, type E2EUser } from "./helpers/auth";
|
|
|
|
const ADMIN: E2EUser = {
|
|
identifier: "[email protected]",
|
|
password: "AdminPassword1234",
|
|
name: "王顺利",
|
|
employeeNo: "HYD000006",
|
|
};
|
|
const SCREENSHOT_DIR = path.resolve(process.cwd(), "../output/playwright/portal-task-first-redesign");
|
|
|
|
async function attachScreenshot(page: Page, testInfo: TestInfo, name: string) {
|
|
mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
|
const body = await page.screenshot({ path: path.join(SCREENSHOT_DIR, name), fullPage: true });
|
|
await testInfo.attach(name, {
|
|
body,
|
|
contentType: "image/png",
|
|
});
|
|
}
|
|
|
|
function envelope<T>(data: T) {
|
|
return { requestId: "visual-portal", data };
|
|
}
|
|
|
|
function portalPayload(mode: "employee" | "admin" | "finance") {
|
|
const displayName = mode === "employee" ? "陈工程" : mode === "finance" ? "赵财务" : "王顺利";
|
|
const base = {
|
|
workspace: { id: "workspace-visual", persisted: true, profile: "small_team_internal" },
|
|
greeting: { name: displayName },
|
|
myApplications: [{ domain: "reimbursement", count: 1, href: "/reimbursements" }],
|
|
riskSummary: null,
|
|
announcements: [],
|
|
quickLinks: [],
|
|
todayPresence: null,
|
|
weeklyReport: null,
|
|
recentActivities: [],
|
|
teamStatusBoard: [],
|
|
};
|
|
|
|
if (mode === "finance") {
|
|
return {
|
|
...base,
|
|
cards: [],
|
|
pendingTasks: { count: 1, href: "/workflow/tasks" },
|
|
teamPresence: null,
|
|
weeklySummary: null,
|
|
todoSummary: { openCount: 0, highPriorityCount: 0, href: "/todos" },
|
|
projectSummary: null,
|
|
peopleLifecycle: null,
|
|
roleCapabilities: {
|
|
canSubmitApplications: true,
|
|
canApproveTasks: true,
|
|
canUseFinance: true,
|
|
canManageSystem: false,
|
|
canViewTeam: true,
|
|
isAdminOnly: false,
|
|
},
|
|
financeSummary: { pendingBatchCount: 2, failedItemCount: 1, riskCount: 1, paymentBatchEnabled: true, href: "/ap-operations" },
|
|
adoptionSummary: null,
|
|
};
|
|
}
|
|
|
|
if (mode === "admin") {
|
|
return {
|
|
...base,
|
|
cards: [],
|
|
pendingTasks: { count: 0, href: "/workflow/tasks" },
|
|
teamPresence: { date: "2026-05-29", href: "/team", office: 6, remote: 1, out: 0, travel: 0, leave: 0, unspecified: 1, total: 8 },
|
|
weeklySummary: { weekStart: "2026-05-25", submittedCount: 6, missingCount: 2, needsHelpCount: 0, href: "/weekly-reports" },
|
|
todoSummary: { openCount: 0, highPriorityCount: 0, href: "/todos" },
|
|
projectSummary: { myCount: 3, overdueMilestonesCount: 1, highPriorityTaskCount: 0, openRiskCount: 0, followUpDueTodayCount: 0, closurePendingProjectCount: 0, href: "/projects" },
|
|
peopleLifecycle: { onboardingCount: 0, offboardingCount: 0, assetReturnPendingCount: 0, href: "/people" },
|
|
roleCapabilities: {
|
|
canSubmitApplications: false,
|
|
canApproveTasks: false,
|
|
canUseFinance: false,
|
|
canManageSystem: true,
|
|
canViewTeam: true,
|
|
isAdminOnly: true,
|
|
},
|
|
financeSummary: null,
|
|
adoptionSummary: { activeEmployeeCount: 8, login7dCount: 8, weeklySubmittedCount: 6, weeklyMissingCount: 2, presenceUpdatedCount: 7, presenceMissingCount: 1, announcementCount: 0, login7dRate: 100, weeklySubmissionRate: 75, presenceCoverageRate: 87.5, hints: ["本周仍有 2 人未提交周报"] },
|
|
};
|
|
}
|
|
|
|
return {
|
|
...base,
|
|
cards: [],
|
|
pendingTasks: { count: 0, href: "/workflow/tasks" },
|
|
teamPresence: null,
|
|
weeklySummary: null,
|
|
todoSummary: { openCount: 2, highPriorityCount: 1, href: "/todos" },
|
|
projectSummary: { myCount: 2, overdueMilestonesCount: 0, highPriorityTaskCount: 1, openRiskCount: 0, followUpDueTodayCount: 0, closurePendingProjectCount: 0, href: "/projects" },
|
|
peopleLifecycle: null,
|
|
roleCapabilities: {
|
|
canSubmitApplications: true,
|
|
canApproveTasks: false,
|
|
canUseFinance: false,
|
|
canManageSystem: false,
|
|
canViewTeam: false,
|
|
isAdminOnly: false,
|
|
},
|
|
financeSummary: null,
|
|
adoptionSummary: null,
|
|
weeklyReport: { weekStart: "2026-05-25", status: "draft", needsHelp: false, href: "/weekly-reports" },
|
|
};
|
|
}
|
|
|
|
async function mockVisualPortal(page: Page, mode: "employee" | "admin" | "finance") {
|
|
await page.addInitScript(() => {
|
|
window.localStorage.removeItem("hyb_oa_show_welcome");
|
|
window.localStorage.removeItem("hyb_oa_onboarding_active");
|
|
window.localStorage.removeItem("hyb_oa_getting_started_dismissed");
|
|
});
|
|
|
|
await page.route((url) => url.pathname.startsWith("/api/"), async (route) => {
|
|
const pathName = new URL(route.request().url()).pathname;
|
|
|
|
if (pathName === "/api/auth/capabilities") {
|
|
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope({ emailDelivery: { ready: true, statusReasons: [] }, emailLoginCode: { enabled: true }, passwordReset: { enabled: true } })) });
|
|
return;
|
|
}
|
|
if (pathName === "/api/auth/sso/providers") {
|
|
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope([])) });
|
|
return;
|
|
}
|
|
if (pathName === "/api/auth/wecom/config") {
|
|
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope({ enabled: false, entryEnabled: false, oauthEnabled: false, corpId: "", agentId: "", loginUrl: null, bindEnabled: false, defaultReturnTo: "/portal", status: "disabled", statusReasons: [] })) });
|
|
return;
|
|
}
|
|
if (pathName === "/api/auth/login") {
|
|
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope({ accessToken: "visual-token", expiresIn: 900 })) });
|
|
return;
|
|
}
|
|
if (pathName === "/api/me") {
|
|
const displayName = mode === "employee" ? "陈工程" : mode === "finance" ? "赵财务" : "王顺利";
|
|
const employeeNo = mode === "employee" ? "HYD000003" : mode === "finance" ? "HYD000004" : "HYD000006";
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(envelope({
|
|
id: `visual-${mode}`,
|
|
employeeNo,
|
|
name: displayName,
|
|
email: `${mode}@hyinda.com`,
|
|
roleCodes: mode === "employee" ? ["employee"] : mode === "finance" ? ["finance"] : ["admin"],
|
|
notificationPushEnabled: true,
|
|
profileCompleteness: { hasEmergencyContact: true, hasBankAccount: true, hasAvatar: false, hasIdNumber: false, score: 50 },
|
|
wecomBinding: null,
|
|
})),
|
|
});
|
|
return;
|
|
}
|
|
if (pathName === "/api/system/navigation") {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(envelope({
|
|
workspace: { id: "workspace-visual", persisted: true, profile: "small_team_internal" },
|
|
sections: {
|
|
main: [
|
|
{ key: "portal", href: "/portal", label: "工作台", icon: "home_app_logo", placement: "main", permissionCodes: [], featureFlagKey: null },
|
|
{ key: "expense", href: "/reimbursements", label: "报销", icon: "request_quote", placement: "main", permissionCodes: [], featureFlagKey: null },
|
|
],
|
|
admin: [],
|
|
backup: [],
|
|
},
|
|
recommendedRolePresets: [],
|
|
})),
|
|
});
|
|
return;
|
|
}
|
|
if (pathName === "/api/notifications") {
|
|
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope({ page: 1, pageSize: 1, total: 0, unreadOnly: true, unreadCount: 0, items: [] })) });
|
|
return;
|
|
}
|
|
if (pathName === "/api/portal/home") {
|
|
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope(portalPayload(mode))) });
|
|
return;
|
|
}
|
|
|
|
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope({})) });
|
|
});
|
|
}
|
|
|
|
test.describe("visual portal", () => {
|
|
test("员工 Portal 视觉基线", async ({ page }, testInfo) => {
|
|
await mockVisualPortal(page, "employee");
|
|
await page.setViewportSize({ width: 1440, height: 1200 });
|
|
await login(page, EMPLOYEE);
|
|
|
|
await expect(page.getByTestId("portal-work-summary")).toBeVisible();
|
|
await expect(page.getByRole("heading", { name: "今日待处理" })).toBeVisible();
|
|
await expect(page.getByText("当前焦点")).not.toBeVisible();
|
|
await expect(page.getByText("下一步", { exact: true })).not.toBeVisible();
|
|
await expect(page.locator("summary").filter({ hasText: "团队与通知" })).not.toBeVisible();
|
|
|
|
await attachScreenshot(page, testInfo, "portal-employee.png");
|
|
});
|
|
|
|
test("管理员 Portal 视觉基线", async ({ page }, testInfo) => {
|
|
await mockVisualPortal(page, "admin");
|
|
await page.setViewportSize({ width: 1440, height: 1200 });
|
|
await login(page, ADMIN);
|
|
|
|
await expect(page.locator('[aria-label="portal-card-pending_approvals"]')).toHaveCount(0);
|
|
await expect(page.getByRole("heading", { name: "今日待处理" })).toBeVisible();
|
|
await expect(page.getByText("下一步", { exact: true })).not.toBeVisible();
|
|
await expect(page.locator("summary").filter({ hasText: "团队与通知" })).not.toBeVisible();
|
|
await expect(page.locator("summary").filter({ hasText: "角色重点" })).not.toBeVisible();
|
|
await expect(page.getByText("试点观测摘要")).not.toBeVisible();
|
|
|
|
await attachScreenshot(page, testInfo, "portal-admin.png");
|
|
});
|
|
|
|
test("财务 Portal 显示财务待处理入口", async ({ page }, testInfo) => {
|
|
await mockVisualPortal(page, "finance");
|
|
await page.setViewportSize({ width: 1440, height: 1200 });
|
|
await login(page, FINANCE);
|
|
|
|
await expect(page.getByTestId("portal-work-summary")).toBeVisible();
|
|
await expect(page.getByRole("heading", { name: "今日待处理" })).toBeVisible();
|
|
await expect(page.locator("summary").filter({ hasText: "角色重点" })).not.toBeVisible();
|
|
|
|
await attachScreenshot(page, testInfo, "portal-finance.png");
|
|
});
|
|
});
|