R100/A1-A6 建立产品设计规则基线
这个提交包含在:
@@ -0,0 +1,274 @@
|
||||
import { expect, test, type Page, type Route, type TestInfo } from "@playwright/test";
|
||||
|
||||
function envelope(data: unknown) {
|
||||
return { requestId: "mock-r100-design", data };
|
||||
}
|
||||
|
||||
async function fulfillJson(route: Route, data: unknown, status = 200) {
|
||||
await route.fulfill({
|
||||
status,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(envelope(data)),
|
||||
});
|
||||
}
|
||||
|
||||
const me = {
|
||||
id: "employee-r100",
|
||||
employeeNo: "E100",
|
||||
name: "Employee",
|
||||
phone: "13800000000",
|
||||
email: "[email protected]",
|
||||
departmentId: "dept-r100",
|
||||
directManagerId: null,
|
||||
status: "active",
|
||||
notificationPushEnabled: true,
|
||||
roleCodes: ["manager"],
|
||||
profileCompleteness: {
|
||||
hasEmergencyContact: true,
|
||||
hasBankAccount: true,
|
||||
hasAvatar: false,
|
||||
hasIdNumber: false,
|
||||
score: 50,
|
||||
},
|
||||
wecomBinding: null,
|
||||
};
|
||||
|
||||
function navigationItem(key: string, href: string, label: string, icon: string) {
|
||||
return { key, href, label, icon, placement: "main", permissionCodes: [], featureFlagKey: null };
|
||||
}
|
||||
|
||||
async function mockDesignApis(page: Page) {
|
||||
await page.route((url) => url.pathname.startsWith("/api/"), async (route) => {
|
||||
const url = new URL(route.request().url());
|
||||
const path = url.pathname;
|
||||
|
||||
if (path === "/api/auth/sso/providers") {
|
||||
await fulfillJson(route, []);
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === "/api/auth/wecom/config") {
|
||||
await fulfillJson(route, {
|
||||
enabled: true,
|
||||
entryEnabled: true,
|
||||
oauthEnabled: true,
|
||||
corpId: "ww-r100",
|
||||
agentId: "1000002",
|
||||
loginUrl: "/api/auth/wecom/login?returnTo=%2Fmobile",
|
||||
bindEnabled: true,
|
||||
defaultReturnTo: "/mobile",
|
||||
status: "ready",
|
||||
statusReasons: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === "/api/auth/login" || path === "/api/auth/refresh") {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
headers: { "set-cookie": "has_session=1; Path=/; SameSite=Lax" },
|
||||
body: JSON.stringify(envelope({ accessToken: "mock-r100-token", expiresIn: 900, mustChangePassword: false })),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === "/api/me") {
|
||||
await fulfillJson(route, me);
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === "/api/system/navigation") {
|
||||
await fulfillJson(route, {
|
||||
workspace: {
|
||||
id: "workspace-r100",
|
||||
persisted: true,
|
||||
profile: "small_team_internal",
|
||||
orgMode: "single_company",
|
||||
primaryEntryMode: "password",
|
||||
notificationStrategy: "in_app",
|
||||
devicePriority: "mobile",
|
||||
moduleVisibility: {},
|
||||
createdAt: null,
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
},
|
||||
sections: {
|
||||
main: [
|
||||
navigationItem("portal", "/portal", "工作台", "home_app_logo"),
|
||||
navigationItem("expense", "/reimbursements", "报销", "request_quote"),
|
||||
navigationItem("leave", "/leave-requests", "请假", "event_available"),
|
||||
navigationItem("invoice", "/invoice-pool", "票据", "receipt_long"),
|
||||
],
|
||||
admin: [],
|
||||
backup: [],
|
||||
},
|
||||
recommendedRolePresets: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === "/api/portal/home") {
|
||||
await fulfillJson(route, {
|
||||
workspace: { id: "workspace-r100", persisted: true, profile: "small_team_internal" },
|
||||
greeting: { name: me.name },
|
||||
cards: [
|
||||
{ key: "pending_tasks", title: "待处理", value: 2, href: "/workflow/tasks", severity: "warning" },
|
||||
{ key: "my_applications", title: "我的申请", value: 1, href: "/reimbursements", severity: "normal" },
|
||||
],
|
||||
pendingTasks: { count: 2, href: "/workflow/tasks" },
|
||||
myApplications: [
|
||||
{ domain: "reimbursement", count: 1, href: "/reimbursements" },
|
||||
{ domain: "leave", count: 0, href: "/leave-requests" },
|
||||
],
|
||||
riskSummary: null,
|
||||
announcements: [{ id: "ann-r100", title: "试运行提醒", summary: "今天处理待办。", href: null }],
|
||||
quickLinks: [{ key: "capture", title: "上传发票", href: "/mobile/capture" }],
|
||||
todayPresence: { date: "2026-05-06", status: "office", label: "在岗", note: null, href: "/team" },
|
||||
teamPresence: null,
|
||||
weeklyReport: { weekStart: "2026-05-04", status: "draft", needsHelp: false, href: "/weekly-reports" },
|
||||
weeklySummary: null,
|
||||
todoSummary: { openCount: 2, highPriorityCount: 0, href: "/todos" },
|
||||
projectSummary: null,
|
||||
peopleLifecycle: null,
|
||||
adoptionSummary: null,
|
||||
recentActivities: [],
|
||||
teamStatusBoard: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === "/api/attendance/me/today") {
|
||||
await fulfillJson(route, {
|
||||
workDate: "2026-05-06",
|
||||
needClockIn: true,
|
||||
exemptionReason: null,
|
||||
item: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.startsWith("/api/employees/") && path.endsWith("/profile")) {
|
||||
await fulfillJson(route, {
|
||||
employee: {
|
||||
...me,
|
||||
avatarUrl: null,
|
||||
department: { id: "dept-r100", name: "研发部" },
|
||||
manager: null,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === "/api/workflow/tasks") {
|
||||
await fulfillJson(route, {
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === "/api/notifications") {
|
||||
await fulfillJson(route, {
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 5,
|
||||
unreadCount: 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await fulfillJson(route, {});
|
||||
});
|
||||
}
|
||||
|
||||
async function login(page: Page) {
|
||||
await page.goto("/login");
|
||||
await page.getByTestId("login-identifier").fill("[email protected]");
|
||||
await page.getByTestId("login-password").fill("EmployeePassword1234");
|
||||
await page.getByTestId("login-submit").click();
|
||||
await expect(page).toHaveURL(/\/portal/);
|
||||
}
|
||||
|
||||
async function collectDesignDiagnostics(page: Page) {
|
||||
return page.evaluate(() => {
|
||||
function isVisible(element: Element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return rect.width > 0 && rect.height > 0;
|
||||
}
|
||||
|
||||
const bodyText = document.body.innerText;
|
||||
const badCopy = ["先处理这几件事", "清爽"].filter((phrase) => bodyText.includes(phrase));
|
||||
const routeLikeText = bodyText
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => /^\/[a-z0-9-]+/.test(line))
|
||||
.slice(0, 10);
|
||||
const visibleTables = Array.from(document.querySelectorAll("table")).filter(isVisible).length;
|
||||
const darkControls = Array.from(document.querySelectorAll("a, button, [role='button'], [role='tab']"))
|
||||
.filter(isVisible)
|
||||
.map((element) => {
|
||||
const styles = window.getComputedStyle(element);
|
||||
return {
|
||||
text: (element.textContent ?? "").trim().replace(/\s+/g, " ").slice(0, 40),
|
||||
backgroundColor: styles.backgroundColor,
|
||||
color: styles.color,
|
||||
};
|
||||
})
|
||||
.filter((item) => {
|
||||
const darkBackgrounds = new Set(["rgb(0, 0, 0)", "rgb(24, 24, 27)", "rgb(15, 23, 42)"]);
|
||||
return darkBackgrounds.has(item.backgroundColor) && item.color === "rgb(255, 255, 255)";
|
||||
});
|
||||
|
||||
return {
|
||||
url: window.location.pathname,
|
||||
badCopy,
|
||||
routeLikeText,
|
||||
visibleTables,
|
||||
darkControls,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function attachDiagnostics(page: Page, testInfo: TestInfo, name: string) {
|
||||
const diagnostics = await collectDesignDiagnostics(page);
|
||||
await testInfo.attach(`${name}.json`, {
|
||||
body: JSON.stringify(diagnostics, null, 2),
|
||||
contentType: "application/json",
|
||||
});
|
||||
await testInfo.attach(`${name}.png`, {
|
||||
body: await page.screenshot({ fullPage: true }),
|
||||
contentType: "image/png",
|
||||
});
|
||||
}
|
||||
|
||||
test.describe("R100 design baseline diagnostics", () => {
|
||||
test("手机首页和我的页可生成视觉诊断", async ({ page }, testInfo) => {
|
||||
await mockDesignApis(page);
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await login(page);
|
||||
|
||||
await page.goto("/mobile");
|
||||
await expect(page.getByRole("heading", { name: "今天" })).toBeVisible();
|
||||
await expect(page.getByTestId("mobile-today-focus")).toBeVisible();
|
||||
await attachDiagnostics(page, testInfo, "r100-mobile-home");
|
||||
|
||||
await page.goto("/mobile/me");
|
||||
await expect(page.getByRole("heading", { name: "我的" })).toBeVisible();
|
||||
await expect(page.getByTestId("mobile-more-links")).toBeVisible();
|
||||
await attachDiagnostics(page, testInfo, "r100-mobile-me");
|
||||
});
|
||||
|
||||
test("PC 工作台可生成视觉诊断", async ({ page }, testInfo) => {
|
||||
await mockDesignApis(page);
|
||||
await page.setViewportSize({ width: 1440, height: 960 });
|
||||
await login(page);
|
||||
|
||||
await expect(page.getByTestId("topbar-profile-trigger")).toBeVisible();
|
||||
await expect(page.locator("aside")).toBeVisible();
|
||||
await attachDiagnostics(page, testInfo, "r100-pc-portal");
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户