R84 R86 收口用户开通界面与自动化回归
这个提交包含在:
+1
-3
@@ -88,16 +88,14 @@ export async function fetchUsers(params: {
|
||||
}
|
||||
|
||||
export async function createUser(payload: {
|
||||
employeeNo: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
password: string;
|
||||
departmentId?: string;
|
||||
directManagerId?: string | null;
|
||||
roles: string[];
|
||||
}) {
|
||||
return apiRequestData<UserSummary>("/api/users", {
|
||||
return apiRequestData<UserSummary & { oneTimePassword: string }>("/api/users", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
@@ -110,8 +110,8 @@ export function LoginPage() {
|
||||
<h2 className="mt-2 text-xl font-semibold tracking-tight text-slate-900">使用账号密码登录</h2>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-600">
|
||||
{primary
|
||||
? "适合桌面浏览器直接登录;若企业微信入口暂未开放,也可正常使用。"
|
||||
: "企业微信免登失败、未绑定或超时时,可改用账号密码继续进入。"}
|
||||
? "适合桌面浏览器直接登录;若是首次开通,也可使用管理员转交的一次性口令进入。"
|
||||
: "企业微信免登失败、未绑定或超时时,可改用账号密码或一次性口令继续进入。"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -135,7 +135,7 @@ export function LoginPage() {
|
||||
data-testid="login-password"
|
||||
id="login-password"
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
placeholder="请输入密码"
|
||||
placeholder="请输入密码或一次性口令"
|
||||
type="password"
|
||||
value={password}
|
||||
/>
|
||||
|
||||
@@ -112,7 +112,7 @@ export function ProfilePage() {
|
||||
<dd className="mt-1 font-medium text-zinc-900">{me?.name ?? "-"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-zinc-500">员工号</dt>
|
||||
<dt className="text-zinc-500">员工号(系统生成)</dt>
|
||||
<dd className="mt-1 font-medium text-zinc-900">{me?.employeeNo ?? "-"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -49,7 +49,12 @@ export function UserManagementPage() {
|
||||
const [q, setQ] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
const [error, setError] = useState("");
|
||||
const [resetResult, setResetResult] = useState<{ userId: string; password: string } | null>(null);
|
||||
const [credentialResult, setCredentialResult] = useState<{
|
||||
mode: "create" | "reset";
|
||||
userId: string;
|
||||
employeeNo?: string;
|
||||
password: string;
|
||||
} | null>(null);
|
||||
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [importOpen, setImportOpen] = useState(false);
|
||||
@@ -74,10 +79,16 @@ export function UserManagementPage() {
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (payload: CreateUserFormData) => createUser(payload),
|
||||
onSuccess: async () => {
|
||||
onSuccess: async (data) => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["users"] });
|
||||
setCreateOpen(false);
|
||||
setError("");
|
||||
setCredentialResult({
|
||||
mode: "create",
|
||||
userId: data.id,
|
||||
employeeNo: data.employeeNo,
|
||||
password: data.oneTimePassword,
|
||||
});
|
||||
toast.success("用户已创建");
|
||||
},
|
||||
onError: (err) => {
|
||||
@@ -109,7 +120,7 @@ export function UserManagementPage() {
|
||||
const resetMutation = useMutation({
|
||||
mutationFn: async (item: UserSummary) => resetUserPassword(item.id),
|
||||
onSuccess: async (data) => {
|
||||
setResetResult({ userId: data.id, password: data.oneTimePassword });
|
||||
setCredentialResult({ mode: "reset", userId: data.id, password: data.oneTimePassword });
|
||||
await queryClient.invalidateQueries({ queryKey: ["users"] });
|
||||
setError("");
|
||||
toast.success("密码已重置");
|
||||
@@ -142,7 +153,7 @@ export function UserManagementPage() {
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title="系统用户管理"
|
||||
subtitle="创建/启用/禁用账号,重置密码并分配角色(仅试运行最小运营)。"
|
||||
subtitle="创建/启用/禁用账号,系统自动生成工号并展示一次性口令,管理员只负责最小开通与角色分配。"
|
||||
actions={
|
||||
<div className="flex gap-2">
|
||||
<UserImportDialog
|
||||
@@ -184,9 +195,21 @@ export function UserManagementPage() {
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{resetResult ? (
|
||||
{credentialResult ? (
|
||||
<p className="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800" data-testid="users-reset-result">
|
||||
已重置密码(user={resetResult.userId}),一次性口令:<span className="font-mono">{resetResult.password}</span>
|
||||
{credentialResult.mode === "create"
|
||||
? (
|
||||
<>
|
||||
已创建用户(user={credentialResult.userId}
|
||||
{credentialResult.employeeNo ? `,工号=${credentialResult.employeeNo}` : ""}
|
||||
),一次性口令:<span className="font-mono">{credentialResult.password}</span>
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
已重置密码(user={credentialResult.userId}),一次性口令:<span className="font-mono">{credentialResult.password}</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -12,11 +12,9 @@ import { Input } from "../../components/ui/input";
|
||||
import { Label } from "../../components/ui/label";
|
||||
|
||||
export interface CreateUserFormData {
|
||||
employeeNo: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
password: string;
|
||||
departmentId: string | undefined;
|
||||
directManagerId: string | null;
|
||||
roles: string[];
|
||||
@@ -48,16 +46,13 @@ export function CreateUserDialog({
|
||||
managers,
|
||||
roleOptions,
|
||||
}: CreateUserDialogProps) {
|
||||
const [employeeNo, setEmployeeNo] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
const [phone, setPhone] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [departmentId, setDepartmentId] = useState("");
|
||||
const [directManagerId, setDirectManagerId] = useState("");
|
||||
const [createRoles, setCreateRoles] = useState<Set<string>>(() => new Set(["employee"]));
|
||||
|
||||
const trimmedEmployeeNo = employeeNo.trim();
|
||||
const trimmedName = name.trim();
|
||||
const trimmedPhone = phone.trim();
|
||||
const trimmedEmail = email.trim();
|
||||
@@ -65,19 +60,15 @@ export function CreateUserDialog({
|
||||
|
||||
const canSubmit =
|
||||
!isPending &&
|
||||
trimmedEmployeeNo !== "" &&
|
||||
trimmedName !== "" &&
|
||||
trimmedPhone !== "" &&
|
||||
trimmedEmail !== "" &&
|
||||
password !== "" &&
|
||||
rolesArray.length >= 1;
|
||||
|
||||
function resetForm() {
|
||||
setEmployeeNo("");
|
||||
setName("");
|
||||
setPhone("");
|
||||
setEmail("");
|
||||
setPassword("");
|
||||
setDepartmentId("");
|
||||
setDirectManagerId("");
|
||||
setCreateRoles(new Set(["employee"]));
|
||||
@@ -92,11 +83,9 @@ export function CreateUserDialog({
|
||||
|
||||
function handleSubmit() {
|
||||
onSubmit({
|
||||
employeeNo: trimmedEmployeeNo,
|
||||
name: trimmedName,
|
||||
phone: trimmedPhone,
|
||||
email: trimmedEmail,
|
||||
password,
|
||||
departmentId: departmentId || undefined,
|
||||
directManagerId: directManagerId ? directManagerId : null,
|
||||
roles: rolesArray,
|
||||
@@ -113,20 +102,10 @@ export function CreateUserDialog({
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>新建用户</DialogTitle>
|
||||
<DialogDescription>请填写基础信息与初始密码(满足密码策略)。</DialogDescription>
|
||||
<DialogDescription>请填写基础信息。工号与一次性口令将在创建成功后由系统生成并展示一次。</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="employeeNo">工号</Label>
|
||||
<Input
|
||||
id="employeeNo"
|
||||
data-testid="users-create-employeeNo"
|
||||
value={employeeNo}
|
||||
onChange={(e) => setEmployeeNo(e.target.value)}
|
||||
placeholder="E1001"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">姓名</Label>
|
||||
<Input
|
||||
@@ -157,17 +136,6 @@ export function CreateUserDialog({
|
||||
placeholder="[email protected]"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password">初始密码</Label>
|
||||
<Input
|
||||
id="password"
|
||||
data-testid="users-create-password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="至少12位,含大小写与数字"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="departmentId">部门(可选)</Label>
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
function envelope(data: unknown) {
|
||||
return {
|
||||
requestId: "mock-request",
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
function mockCommonAuth(page: import("@playwright/test").Page, options: {
|
||||
mustChangePassword?: boolean;
|
||||
meName?: string;
|
||||
meEmployeeNo?: string;
|
||||
}) {
|
||||
const mustChangePassword = options.mustChangePassword === true;
|
||||
const meName = options.meName ?? "Admin";
|
||||
const meEmployeeNo = options.meEmployeeNo ?? "HYD000001";
|
||||
|
||||
return Promise.all([
|
||||
page.route("**/api/auth/sso/providers", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(envelope([])),
|
||||
});
|
||||
}),
|
||||
page.route("**/api/auth/wecom/config", async (route) => {
|
||||
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: ["disabled"],
|
||||
}),
|
||||
),
|
||||
});
|
||||
}),
|
||||
page.route("**/api/auth/login", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(
|
||||
envelope({
|
||||
accessToken: "test-access-token",
|
||||
expiresIn: 900,
|
||||
mustChangePassword,
|
||||
wecomBindingStatus: null,
|
||||
}),
|
||||
),
|
||||
});
|
||||
}),
|
||||
page.route("**/api/me", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(
|
||||
envelope({
|
||||
id: "employee-admin-1",
|
||||
employeeNo: meEmployeeNo,
|
||||
name: meName,
|
||||
phone: "13800000000",
|
||||
email: "[email protected]",
|
||||
departmentId: "dept-1",
|
||||
directManagerId: null,
|
||||
status: "active",
|
||||
notificationPushEnabled: true,
|
||||
roleCodes: ["admin"],
|
||||
permissions: ["system.users"],
|
||||
profileCompleteness: {
|
||||
hasEmergencyContact: true,
|
||||
hasBankAccount: true,
|
||||
hasAvatar: false,
|
||||
hasIdNumber: false,
|
||||
score: 50,
|
||||
},
|
||||
wecomBinding: null,
|
||||
}),
|
||||
),
|
||||
});
|
||||
}),
|
||||
page.route("**/api/system/navigation", async (route) => {
|
||||
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: {
|
||||
main: [{ key: "portal", href: "/portal", label: "Portal", icon: "home_app_logo", placement: "main", permissionCodes: [], featureFlagKey: null }],
|
||||
admin: [{ key: "users", href: "/system/users", label: "系统用户", icon: "manage_accounts", placement: "admin", permissionCodes: ["system.users"], featureFlagKey: null }],
|
||||
backup: [],
|
||||
},
|
||||
recommendedRolePresets: [],
|
||||
}),
|
||||
),
|
||||
});
|
||||
}),
|
||||
page.route((url) => url.pathname === "/api/notifications", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(
|
||||
envelope({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
unreadOnly: true,
|
||||
unreadCount: 0,
|
||||
items: [],
|
||||
}),
|
||||
),
|
||||
});
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
test("E2E 用户开通:新建用户时由系统生成工号和一次性口令", async ({ page }) => {
|
||||
let createPayload: Record<string, unknown> | null = null;
|
||||
|
||||
await mockCommonAuth(page, {});
|
||||
await page.route("**/api/users?**", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(
|
||||
envelope({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
items: [],
|
||||
}),
|
||||
),
|
||||
});
|
||||
});
|
||||
await page.route("**/api/departments?includeArchived=0", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(
|
||||
envelope({
|
||||
includeArchived: false,
|
||||
items: [{ id: "dept-1", name: "行政部", parentId: null, archivedAt: null, createdAt: null, updatedAt: null }],
|
||||
tree: [],
|
||||
}),
|
||||
),
|
||||
});
|
||||
});
|
||||
await page.route("**/api/employees?page=1&pageSize=200&status=active", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(
|
||||
envelope({
|
||||
page: 1,
|
||||
pageSize: 200,
|
||||
total: 0,
|
||||
items: [],
|
||||
}),
|
||||
),
|
||||
});
|
||||
});
|
||||
await page.route("**/api/users", async (route) => {
|
||||
if (route.request().method() !== "POST") {
|
||||
await route.fallback();
|
||||
return;
|
||||
}
|
||||
createPayload = route.request().postDataJSON() as Record<string, unknown>;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(
|
||||
envelope({
|
||||
id: "user-1",
|
||||
employeeNo: "HYD000123",
|
||||
name: "张三",
|
||||
phone: "13800000001",
|
||||
email: "[email protected]",
|
||||
status: "active",
|
||||
departmentId: "dept-1",
|
||||
title: "员工",
|
||||
roleCodes: ["employee"],
|
||||
createdAt: "2026-05-04T12:00:00.000Z",
|
||||
updatedAt: "2026-05-04T12:00:00.000Z",
|
||||
oneTimePassword: "ResetA1abc123def456",
|
||||
}),
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/login?redirect=%2Fsystem%2Fusers");
|
||||
await page.getByTestId("login-identifier").fill("[email protected]");
|
||||
await page.getByTestId("login-password").fill("AdminPassword1234");
|
||||
await page.getByTestId("login-submit").click();
|
||||
await expect(page).toHaveURL(/\/system\/users/);
|
||||
|
||||
await page.getByTestId("users-create-open").click();
|
||||
await expect(page.getByTestId("users-create-employeeNo")).toHaveCount(0);
|
||||
await expect(page.getByTestId("users-create-password")).toHaveCount(0);
|
||||
await page.getByTestId("users-create-name").fill("张三");
|
||||
await page.getByTestId("users-create-phone").fill("13800000001");
|
||||
await page.getByTestId("users-create-email").fill("[email protected]");
|
||||
await page.getByTestId("users-create-submit").click();
|
||||
|
||||
expect(createPayload).toEqual({
|
||||
name: "张三",
|
||||
phone: "13800000001",
|
||||
email: "[email protected]",
|
||||
departmentId: undefined,
|
||||
directManagerId: null,
|
||||
roles: ["employee"],
|
||||
});
|
||||
await expect(page.getByTestId("users-reset-result")).toContainText("工号=HYD000123");
|
||||
await expect(page.getByTestId("users-reset-result")).toContainText("ResetA1abc123def456");
|
||||
});
|
||||
|
||||
test("E2E 首次登录:一次性口令登录后强制改密", async ({ page }) => {
|
||||
let changePasswordPayload: Record<string, unknown> | null = null;
|
||||
|
||||
await mockCommonAuth(page, { mustChangePassword: true, meName: "新员工", meEmployeeNo: "HYD000123" });
|
||||
await page.route("**/api/auth/change-password", async (route) => {
|
||||
changePasswordPayload = route.request().postDataJSON() as Record<string, unknown>;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(
|
||||
envelope({
|
||||
accessToken: "post-change-token",
|
||||
expiresIn: 900,
|
||||
mustChangePassword: false,
|
||||
}),
|
||||
),
|
||||
});
|
||||
});
|
||||
await page.route("**/api/portal/home", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(
|
||||
envelope({
|
||||
workspace: { id: "workspace-1", persisted: true, profile: "small_team_internal" },
|
||||
greeting: { name: "新员工" },
|
||||
cards: [],
|
||||
pendingTasks: { count: 0, href: "/workflow/tasks" },
|
||||
myApplications: [],
|
||||
riskSummary: null,
|
||||
announcements: [],
|
||||
quickLinks: [],
|
||||
todayPresence: null,
|
||||
teamPresence: null,
|
||||
weeklyReport: null,
|
||||
weeklySummary: null,
|
||||
todoSummary: { openCount: 0, highPriorityCount: 0, href: "/todos" },
|
||||
projectSummary: { myCount: 0, overdueMilestonesCount: 0, highPriorityTaskCount: 0, openRiskCount: 0, followUpDueTodayCount: 0, closurePendingProjectCount: 0, href: "/projects" },
|
||||
peopleLifecycle: null,
|
||||
adoptionSummary: null,
|
||||
recentActivities: [],
|
||||
teamStatusBoard: [],
|
||||
}),
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/login");
|
||||
await page.getByTestId("login-identifier").fill("[email protected]");
|
||||
await page.getByTestId("login-password").fill("ResetA1abc123def456");
|
||||
await page.getByTestId("login-submit").click();
|
||||
await expect(page).toHaveURL(/\/change-password/);
|
||||
|
||||
await page.getByTestId("change-password-old").fill("ResetA1abc123def456");
|
||||
await page.getByTestId("change-password-new").fill("NewPassword1234");
|
||||
await page.getByLabel("确认新密码").fill("NewPassword1234");
|
||||
await page.getByTestId("change-password-submit").click();
|
||||
|
||||
expect(changePasswordPayload).toEqual({
|
||||
oldPassword: "ResetA1abc123def456",
|
||||
newPassword: "NewPassword1234",
|
||||
});
|
||||
await expect(page).toHaveURL(/\/portal/);
|
||||
});
|
||||
@@ -0,0 +1,171 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
function envelope(data: unknown) {
|
||||
return {
|
||||
requestId: "mock-request",
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
test("E2E 企微首次绑定:未绑定回退到密码登录后可继续进入目标页", 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/sso/providers", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(envelope([])),
|
||||
});
|
||||
});
|
||||
await page.route("**/api/auth/wecom/config", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(
|
||||
envelope({
|
||||
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: [],
|
||||
}),
|
||||
),
|
||||
});
|
||||
});
|
||||
await page.route("**/api/auth/login", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(
|
||||
envelope({
|
||||
accessToken: "wecom-fallback-token",
|
||||
expiresIn: 900,
|
||||
mustChangePassword: false,
|
||||
wecomBindingStatus: "bound",
|
||||
}),
|
||||
),
|
||||
});
|
||||
});
|
||||
await page.route("**/api/me", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(
|
||||
envelope({
|
||||
id: "employee-1",
|
||||
employeeNo: "HYD000123",
|
||||
name: "张三",
|
||||
phone: "13800000001",
|
||||
email: "[email protected]",
|
||||
departmentId: "dept-1",
|
||||
directManagerId: null,
|
||||
status: "active",
|
||||
notificationPushEnabled: true,
|
||||
roleCodes: ["employee"],
|
||||
permissions: [],
|
||||
profileCompleteness: {
|
||||
hasEmergencyContact: true,
|
||||
hasBankAccount: false,
|
||||
hasAvatar: false,
|
||||
hasIdNumber: false,
|
||||
score: 25,
|
||||
},
|
||||
wecomBinding: {
|
||||
externalUserId: "wecom-user-1",
|
||||
createdAt: "2026-05-04T12:00:00.000Z",
|
||||
},
|
||||
}),
|
||||
),
|
||||
});
|
||||
});
|
||||
await page.route("**/api/system/navigation", async (route) => {
|
||||
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: {
|
||||
main: [{ key: "portal", href: "/portal", label: "Portal", icon: "home_app_logo", placement: "main", permissionCodes: [], featureFlagKey: null }],
|
||||
admin: [],
|
||||
backup: [],
|
||||
},
|
||||
recommendedRolePresets: [],
|
||||
}),
|
||||
),
|
||||
});
|
||||
});
|
||||
await page.route((url) => url.pathname === "/api/notifications", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(
|
||||
envelope({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
unreadOnly: true,
|
||||
unreadCount: 0,
|
||||
items: [],
|
||||
}),
|
||||
),
|
||||
});
|
||||
});
|
||||
await page.route("**/api/portal/home", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(
|
||||
envelope({
|
||||
workspace: { id: "workspace-1", persisted: true, profile: "small_team_internal" },
|
||||
greeting: { name: "张三" },
|
||||
cards: [],
|
||||
pendingTasks: { count: 0, href: "/workflow/tasks" },
|
||||
myApplications: [],
|
||||
riskSummary: null,
|
||||
announcements: [],
|
||||
quickLinks: [],
|
||||
todayPresence: null,
|
||||
teamPresence: null,
|
||||
weeklyReport: null,
|
||||
weeklySummary: null,
|
||||
todoSummary: { openCount: 0, highPriorityCount: 0, href: "/todos" },
|
||||
projectSummary: { myCount: 0, overdueMilestonesCount: 0, highPriorityTaskCount: 0, openRiskCount: 0, followUpDueTodayCount: 0, closurePendingProjectCount: 0, href: "/projects" },
|
||||
peopleLifecycle: null,
|
||||
adoptionSummary: null,
|
||||
recentActivities: [],
|
||||
teamStatusBoard: [],
|
||||
}),
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/login?redirect=%2Fportal&wecom=unbound");
|
||||
await expect(page.getByTestId("login-wecom-notice")).toContainText("当前企业微信账号尚未绑定");
|
||||
await page.getByTestId("login-identifier").fill("[email protected]");
|
||||
await page.getByTestId("login-password").fill("ResetA1abc123def456");
|
||||
await page.getByTestId("login-submit").click();
|
||||
await expect(page).toHaveURL(/\/portal/);
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户