feat: R27-2 R27-5 优化企微入口与 Portal 体验
这个提交包含在:
@@ -0,0 +1,238 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { EMPLOYEE, login } from "./helpers/auth";
|
||||
|
||||
const ADMIN = { identifier: "[email protected]", password: "AdminPassword1234", name: "Admin" };
|
||||
|
||||
function envelope(data: unknown) {
|
||||
return {
|
||||
requestId: "mock-request",
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function mockMe(page: import("@playwright/test").Page, roleCodes: string[], name: string) {
|
||||
return page.route("**/api/me", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(envelope({
|
||||
id: "employee-mock-1",
|
||||
employeeNo: "E001",
|
||||
name,
|
||||
phone: "13800000000",
|
||||
email: `${name.toLowerCase()}@hyinda.com`,
|
||||
departmentId: "dept-1",
|
||||
directManagerId: null,
|
||||
status: "active",
|
||||
roleCodes,
|
||||
wecomBinding: null,
|
||||
})),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function mockNavigation(page: import("@playwright/test").Page, mode: "employee" | "manager") {
|
||||
return page.route("**/api/system/navigation", async (route) => {
|
||||
const sections = mode === "manager"
|
||||
? {
|
||||
main: [
|
||||
{ key: "portal", href: "/portal", label: "Portal", icon: "home_app_logo", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "team", href: "/team", label: "团队", icon: "groups", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "leave", href: "/leave-requests", label: "请假", icon: "event_available", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "expense", href: "/reimbursements", label: "报销", icon: "request_quote", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "assets", href: "/assets", label: "资产", icon: "inventory_2", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "weekly", href: "/weekly-reports", label: "周报", icon: "article", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "todos", href: "/todos", label: "待办", icon: "check_circle", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "announcements", href: "/announcements", label: "公告", icon: "campaign", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "projects", href: "/projects", label: "项目", icon: "workspaces", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
],
|
||||
admin: [
|
||||
{ key: "workspace", href: "/system/workspace", label: "工作区设置", icon: "tune", placement: "admin", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "people", href: "/people", label: "人员管理", icon: "group", placement: "admin", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "workflow", href: "/system/workflow-definitions", label: "流程定义", icon: "schema", placement: "admin", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "settings", href: "/system/settings", label: "系统设置", icon: "settings", placement: "admin", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "feature-flags", href: "/system/feature-flags", label: "功能开关", icon: "toggle_on", placement: "admin", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "audit", href: "/audit-logs", label: "审计日志", icon: "manage_search", placement: "admin", permissionCodes: [], featureFlagKey: null },
|
||||
],
|
||||
backup: [
|
||||
{ key: "purchase", href: "/purchase-requests", label: "采购申请", icon: "shopping_cart", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "travel", href: "/travel-requests", label: "差旅申请", icon: "flight", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
],
|
||||
}
|
||||
: {
|
||||
main: [
|
||||
{ key: "portal", href: "/portal", label: "Portal", icon: "home_app_logo", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "team", href: "/team", label: "团队", icon: "groups", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "leave", href: "/leave-requests", label: "请假", icon: "event_available", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "expense", href: "/reimbursements", label: "报销", icon: "request_quote", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "assets", href: "/assets", label: "资产", icon: "inventory_2", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "weekly", href: "/weekly-reports", label: "周报", icon: "article", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "todos", href: "/todos", label: "待办", icon: "check_circle", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "announcements", href: "/announcements", label: "公告", icon: "campaign", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "projects", href: "/projects", label: "项目", icon: "workspaces", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
],
|
||||
admin: [],
|
||||
backup: [],
|
||||
};
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(envelope({
|
||||
workspace: {
|
||||
id: "workspace-1",
|
||||
persisted: true,
|
||||
profile: "small_team_internal",
|
||||
orgMode: "single_company",
|
||||
primaryEntryMode: "password",
|
||||
notificationStrategy: "in_app",
|
||||
devicePriority: "desktop",
|
||||
moduleVisibility: {},
|
||||
createdAt: null,
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
},
|
||||
sections,
|
||||
recommendedRolePresets: [],
|
||||
})),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function mockPortalHome(page: import("@playwright/test").Page, mode: "employee" | "manager") {
|
||||
return page.route("**/api/portal/home", async (route) => {
|
||||
const data = mode === "manager"
|
||||
? {
|
||||
workspace: { id: "workspace-1", persisted: true, profile: "small_team_internal" },
|
||||
greeting: { name: "Manager" },
|
||||
cards: [
|
||||
{ key: "pending_approvals", title: "审批待办", value: 3, href: "/workflow/tasks", severity: "warning" },
|
||||
{ key: "team_presence", title: "今日团队状态", value: "6/8 在岗", href: "/team", severity: "warning" },
|
||||
{ key: "weekly_missing", title: "未提交周报", value: 2, href: "/weekly-reports", severity: "warning" },
|
||||
{ key: "weekly_help", title: "需协助员工", value: 1, href: "/weekly-reports", severity: "high" },
|
||||
{ key: "people_lifecycle", title: "入/离职待办", value: 1, href: "/people", severity: "warning" },
|
||||
{ key: "asset_return_pending", title: "资产待归还", value: 1, href: "/people", severity: "warning" },
|
||||
{ key: "project_milestones", title: "逾期里程碑", value: 2, href: "/projects", severity: "warning" },
|
||||
{ key: "project_risks", title: "项目风险", value: 1, href: "/projects", severity: "high" },
|
||||
],
|
||||
pendingTasks: { count: 3, href: "/workflow/tasks" },
|
||||
myApplications: [
|
||||
{ domain: "reimbursement", count: 1, href: "/reimbursements" },
|
||||
{ domain: "leave", count: 0, href: "/leave-requests" },
|
||||
],
|
||||
riskSummary: { openCount: 1, href: "/ap-operations" },
|
||||
announcements: [{ id: "announcement-1", title: "试点公告", summary: "今天统一从 Portal 进入。", href: null }],
|
||||
quickLinks: [{ key: "projects", title: "项目", href: "/projects" }],
|
||||
todayPresence: null,
|
||||
teamPresence: { date: "2026-03-22", href: "/team", office: 6, remote: 1, out: 0, travel: 1, leave: 1, unspecified: 1, total: 8 },
|
||||
weeklyReport: null,
|
||||
weeklySummary: { weekStart: "2026-03-16", submittedCount: 6, missingCount: 2, needsHelpCount: 1, href: "/weekly-reports" },
|
||||
todoSummary: { openCount: 4, highPriorityCount: 1, href: "/todos" },
|
||||
projectSummary: { myCount: 5, overdueMilestonesCount: 2, highPriorityTaskCount: 2, openRiskCount: 1, followUpDueTodayCount: 2, closurePendingProjectCount: 1, href: "/projects" },
|
||||
peopleLifecycle: { onboardingCount: 1, offboardingCount: 0, assetReturnPendingCount: 1, href: "/people" },
|
||||
adoptionSummary: { activeEmployeeCount: 8, login7dCount: 8, weeklySubmittedCount: 6, weeklyMissingCount: 2, presenceUpdatedCount: 7, presenceMissingCount: 1, announcementCount: 1, login7dRate: 100, weeklySubmissionRate: 75, presenceCoverageRate: 87.5, hints: ["本周仍有 2 人未提交周报"] },
|
||||
}
|
||||
: {
|
||||
workspace: { id: "workspace-1", persisted: true, profile: "small_team_internal" },
|
||||
greeting: { name: "Employee" },
|
||||
cards: [
|
||||
{ key: "my_todos", title: "我的待办", value: 4, href: "/todos", severity: "warning" },
|
||||
{ key: "today_presence", title: "今日状态", value: "在岗", href: "/team", severity: "normal" },
|
||||
{ key: "my_weekly_report", title: "本周周报", value: "待填写", href: "/weekly-reports", severity: "warning" },
|
||||
{ key: "my_projects", title: "我的项目", value: 2, href: "/projects", severity: "warning" },
|
||||
{ key: "my_leave", title: "我的请假", value: 1, href: "/leave-requests", severity: "normal" },
|
||||
{ key: "my_reimbursements", title: "我的报销", value: 2, href: "/reimbursements", severity: "normal" },
|
||||
{ key: "my_assets", title: "我的资产", value: 1, href: "/assets", severity: "normal" },
|
||||
],
|
||||
pendingTasks: { count: 0, href: "/workflow/tasks" },
|
||||
myApplications: [
|
||||
{ domain: "reimbursement", count: 2, href: "/reimbursements" },
|
||||
{ domain: "leave", count: 1, href: "/leave-requests" },
|
||||
],
|
||||
riskSummary: null,
|
||||
announcements: [{ id: "announcement-1", title: "本周安排", summary: "本周统一回填周报。", href: null }],
|
||||
quickLinks: [{ key: "weekly", title: "周报", href: "/weekly-reports" }],
|
||||
todayPresence: { date: "2026-03-22", status: "office", label: "在岗", note: "今天在公司办公", href: "/team" },
|
||||
teamPresence: null,
|
||||
weeklyReport: { weekStart: "2026-03-16", status: "draft", needsHelp: false, href: "/weekly-reports" },
|
||||
weeklySummary: null,
|
||||
todoSummary: { openCount: 4, highPriorityCount: 1, href: "/todos" },
|
||||
projectSummary: { myCount: 2, overdueMilestonesCount: 1, highPriorityTaskCount: 1, openRiskCount: 0, followUpDueTodayCount: 1, closurePendingProjectCount: 0, href: "/projects" },
|
||||
peopleLifecycle: null,
|
||||
adoptionSummary: null,
|
||||
};
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(envelope(data)),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test("Portal 员工视角优先显示今日动作顺序", async ({ page }) => {
|
||||
await mockNavigation(page, "employee");
|
||||
await mockPortalHome(page, "employee");
|
||||
await mockMe(page, ["employee"], "Employee");
|
||||
|
||||
await login(page, EMPLOYEE);
|
||||
|
||||
await expect(page.locator('[aria-label^="portal-card-"]').first()).toBeVisible();
|
||||
|
||||
await expect(page.locator('[aria-label^="portal-card-"]').first()).toBeVisible();
|
||||
|
||||
const labels = await page.locator('[aria-label^="portal-card-"]').evaluateAll((nodes) =>
|
||||
nodes.map((node) => node.getAttribute("aria-label")),
|
||||
);
|
||||
expect(labels).toEqual([
|
||||
"portal-card-my_todos",
|
||||
"portal-card-today_presence",
|
||||
"portal-card-my_weekly_report",
|
||||
"portal-card-my_projects",
|
||||
]);
|
||||
|
||||
await expect(page.getByText("我的申请与资产")).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "公告" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("Portal 管理视角重排正确,备用模块默认折叠", async ({ page }) => {
|
||||
await mockNavigation(page, "manager");
|
||||
await mockPortalHome(page, "manager");
|
||||
await mockMe(page, ["admin"], "Admin");
|
||||
|
||||
await login(page, ADMIN);
|
||||
|
||||
const labels = await page.locator('[aria-label^="portal-card-"]').evaluateAll((nodes) =>
|
||||
nodes.map((node) => node.getAttribute("aria-label")),
|
||||
);
|
||||
expect(labels).toEqual([
|
||||
"portal-card-pending_approvals",
|
||||
"portal-card-team_presence",
|
||||
"portal-card-weekly_missing",
|
||||
"portal-card-weekly_help",
|
||||
]);
|
||||
|
||||
await expect(page.getByText("今日管理重点")).toBeVisible();
|
||||
await expect(page.getByText("试点观测摘要")).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "采购申请" })).not.toBeVisible();
|
||||
await page.locator("summary").filter({ hasText: "备用模块" }).click();
|
||||
await expect(page.getByRole("link", { name: "采购申请" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("企微 WebView 下顶栏显示入口提示", async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
Object.defineProperty(window.navigator, "userAgent", {
|
||||
configurable: true,
|
||||
value: "Mozilla/5.0 wxwork/4.1.20 MicroMessenger/7.0.1",
|
||||
});
|
||||
});
|
||||
await mockNavigation(page, "employee");
|
||||
await mockPortalHome(page, "employee");
|
||||
await mockMe(page, ["employee"], "Employee");
|
||||
|
||||
await login(page, EMPLOYEE);
|
||||
|
||||
await expect(page.getByTestId("topbar-wecom-entry-badge")).toBeVisible();
|
||||
});
|
||||
@@ -2,20 +2,29 @@ import { expect, test } from "@playwright/test";
|
||||
|
||||
import { EMPLOYEE, login } from "./helpers/auth";
|
||||
|
||||
test("E2E 企微入口:登录页展示入口并保留账号密码回退", async ({ page }) => {
|
||||
function mockWecomConfigPayload() {
|
||||
return {
|
||||
enabled: true,
|
||||
entryEnabled: true,
|
||||
oauthEnabled: true,
|
||||
corpId: "wx-test-corp",
|
||||
agentId: "1000001",
|
||||
loginUrl: "/api/auth/wecom/login?returnTo=%2Fportal",
|
||||
bindEnabled: true,
|
||||
defaultReturnTo: "/portal",
|
||||
status: "ready",
|
||||
statusReasons: [],
|
||||
};
|
||||
}
|
||||
|
||||
test("E2E 企微入口:普通浏览器下企微按钮为次级入口", async ({ page }) => {
|
||||
await page.route("**/api/auth/wecom/config", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
requestId: "mock-wecom-config",
|
||||
data: {
|
||||
enabled: true,
|
||||
corpId: "wx-test-corp",
|
||||
agentId: "1000001",
|
||||
oauthEnabled: true,
|
||||
loginUrl: "/api/auth/wecom/login?returnTo=%2Fportal",
|
||||
},
|
||||
data: mockWecomConfigPayload(),
|
||||
}),
|
||||
});
|
||||
});
|
||||
@@ -28,13 +37,46 @@ test("E2E 企微入口:登录页展示入口并保留账号密码回退", asyn
|
||||
});
|
||||
|
||||
await page.goto("/login");
|
||||
await expect(page.getByRole("button", { name: "使用企业微信进入工作台" })).toBeVisible();
|
||||
await expect(page.getByTestId("login-password-primary-panel")).toBeVisible();
|
||||
await expect(page.getByTestId("wecom-entry-secondary")).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "使用企业微信进入工作台" }).click();
|
||||
await page.getByTestId("wecom-entry-secondary").click();
|
||||
await expect(page).toHaveURL(/\/api\/auth\/wecom\/login\?returnTo=/);
|
||||
|
||||
await login(page, EMPLOYEE);
|
||||
await page.goto("/settings/profile");
|
||||
await expect(page.getByText("企业微信绑定")).toBeVisible();
|
||||
await expect(page.getByText(/接入状态:/)).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "企业微信绑定" })).toBeVisible();
|
||||
await expect(page.getByText("接入状态", { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test("E2E 企微入口:企业微信场景优先显示企微 CTA,并展示未绑定提示", async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
Object.defineProperty(window.navigator, "userAgent", {
|
||||
configurable: true,
|
||||
value: "Mozilla/5.0 wxwork/4.1.20 MicroMessenger/7.0.1",
|
||||
});
|
||||
});
|
||||
await page.route("**/api/auth/wecom/config", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
requestId: "mock-wecom-config",
|
||||
data: mockWecomConfigPayload(),
|
||||
}),
|
||||
});
|
||||
});
|
||||
await page.route("**/api/auth/wecom/login?**", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "text/plain",
|
||||
body: "mock wecom login entry",
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/login?redirect=%2Fportal&wecom=unbound");
|
||||
await expect(page.getByTestId("login-wecom-primary-panel")).toBeVisible();
|
||||
await expect(page.getByTestId("wecom-entry-primary")).toBeVisible();
|
||||
await expect(page.getByTestId("login-wecom-notice")).toContainText("当前企业微信账号尚未绑定");
|
||||
await expect(page.getByTestId("login-password-secondary-panel")).toBeVisible();
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户