import { TIMEOUTS } from '../../utils/timeouts'; import { Page, Locator } from '@playwright/test'; import { selectRadixOptionAsync } from '@d8d/e2e-test-utils'; import { UserType, DisabledStatus } from '@d8d/shared-types'; /** * API 基础 URL */ const API_BASE_URL = process.env.E2E_BASE_URL || 'http://localhost:8080'; /** * 用户状态常量 */ export const USER_STATUS = { ENABLED: 0, DISABLED: 1, } as const; /** * 用户状态类型 */ export type UserStatus = typeof USER_STATUS[keyof typeof USER_STATUS]; /** * 用户状态显示名称映射 */ export const USER_STATUS_LABELS: Record = { 0: '启用', 1: '禁用', } as const; /** * 用户数据接口(创建用户) */ export interface UserData { /** 用户名(必填) */ username: string; /** 密码(必填) */ password: string; /** 昵称(可选) */ nickname?: string | null; /** 邮箱(可选) */ email?: string | null; /** 手机号(可选) */ phone?: string | null; /** 真实姓名(可选) */ name?: string | null; /** 用户类型(默认:管理员) */ userType?: UserType; /** 关联公司ID(企业用户必填) */ companyId?: number | null; /** 关联残疾人ID(人才用户必填) */ personId?: number | null; /** 是否禁用(默认:启用) */ isDisabled?: DisabledStatus; } /** * 用户更新数据接口(编辑用户) */ export interface UserUpdateData { /** 用户名 */ username?: string; /** 昵称 */ nickname?: string | null; /** 邮箱 */ email?: string | null; /** 手机号 */ phone?: string | null; /** 真实姓名 */ name?: string | null; /** 新密码(可选) */ password?: string; /** 用户类型 */ userType?: UserType; /** 关联公司ID(企业用户必填) */ companyId?: number | null; /** 关联残疾人ID(人才用户必填) */ personId?: number | null; /** 是否禁用 */ isDisabled?: DisabledStatus; } /** * 网络响应数据接口 */ export interface NetworkResponse { /** 请求URL */ url: string; /** 请求方法 */ method: string; /** 响应状态码 */ status: number; /** 是否成功 */ ok: boolean; /** 响应头 */ responseHeaders: Record; /** 响应体 */ responseBody: unknown; } /** * 表单提交结果接口 */ export interface FormSubmitResult { /** 提交是否成功 */ success: boolean; /** 是否有错误 */ hasError: boolean; /** 是否有成功消息 */ hasSuccess: boolean; /** 错误消息 */ errorMessage?: string; /** 成功消息 */ successMessage?: string; /** 网络响应列表 */ responses?: NetworkResponse[]; } /** * 用户管理 Page Object * * 用于用户管理功能的 E2E 测试 * 页面路径: /admin/users * * 支持三种用户类型: * - ADMIN:管理员,无关联 * - EMPLOYER:企业用户,需关联公司 * - TALENT:人才用户,需关联残疾人 * * @example * ```typescript * const userPage = new UserManagementPage(page); * await userPage.goto(); * await userPage.createUser({ username: 'testuser', password: 'password123' }); * ``` */ export class UserManagementPage { readonly page: Page; // ===== API 端点常量 ===== /** RESTful API 基础端点 */ private static readonly API_USERS_BASE = `${API_BASE_URL}/api/v1/users`; /** 获取所有用户列表 API */ private static readonly API_GET_ALL_USERS = `${API_BASE_URL}/api/v1/users`; /** 删除用户 API(需要拼接 /${userId}) */ private static readonly API_DELETE_USER = `${API_BASE_URL}/api/v1/users`; // ===== 页面级选择器 ===== /** 页面标题 */ readonly pageTitle: Locator; /** 创建用户按钮 */ readonly createUserButton: Locator; /** 搜索输入框 */ readonly searchInput: Locator; /** 搜索按钮 */ readonly searchButton: Locator; /** 用户列表表格 */ readonly userTable: Locator; // ===== 对话框选择器 ===== /** 创建对话框标题 */ readonly createDialogTitle: Locator; /** 编辑对话框标题 */ readonly editDialogTitle: Locator; // ===== 表单字段选择器 ===== /** 用户名输入框 */ readonly usernameInput: Locator; /** 密码输入框 */ readonly passwordInput: Locator; /** 昵称输入框 */ readonly nicknameInput: Locator; /** 邮箱输入框 */ readonly emailInput: Locator; /** 手机号输入框 */ readonly phoneInput: Locator; /** 真实姓名输入框 */ readonly nameInput: Locator; /** 用户类型选择器 */ readonly userTypeSelector: Locator; /** 企业选择器(用于 EMPLOYER 类型) */ readonly companySelector: Locator; /** 残疾人选择器(用于 TALENT 类型) */ readonly disabledPersonSelector: Locator; // ===== 按钮选择器 ===== /** 创建提交按钮 */ readonly createSubmitButton: Locator; /** 更新提交按钮 */ readonly updateSubmitButton: Locator; /** 取消按钮 */ readonly cancelButton: Locator; // ===== 删除确认对话框选择器 ===== /** 确认删除按钮 */ readonly confirmDeleteButton: Locator; constructor(page: Page) { this.page = page; // 初始化页面级选择器 // 使用 heading role 精确定位页面标题(避免与侧边栏按钮冲突) this.pageTitle = page.getByRole('heading', { name: '用户管理' }); // 使用 data-testid 定位创建用户按钮 this.createUserButton = page.getByTestId('create-user-button'); // 搜索相关元素 this.searchInput = page.getByTestId('search-user-input'); this.searchButton = page.getByTestId('search-user-button'); // 用户列表表格 this.userTable = page.locator('table'); // 对话框标题选择器 this.createDialogTitle = page.getByTestId('create-user-dialog-title'); this.editDialogTitle = page.getByTestId('edit-user-dialog-title'); // 表单字段选择器 - 使用 label 定位 this.usernameInput = page.getByLabel('用户名'); this.passwordInput = page.getByLabel('密码'); this.nicknameInput = page.getByLabel('昵称'); this.emailInput = page.getByLabel('邮箱'); this.phoneInput = page.getByLabel('手机号'); this.nameInput = page.getByLabel('真实姓名'); // 用户类型选择器(创建表单) this.userTypeSelector = page.getByTestId('用户类型-trigger'); // 用户类型选择器(编辑表单) this.userTypeSelectorEdit = page.getByTestId('用户类型-edit-trigger'); // 企业选择器(用于 EMPLOYER 类型) this.companySelector = page.getByTestId('关联企业-trigger'); // 残疾人选择器(用于 TALENT 类型) this.disabledPersonSelector = page.getByTestId('disabled-person-selector'); this.disabledPersonSelectorEdit = page.getByTestId('关联残疾人-edit-trigger'); // 按钮选择器 this.createSubmitButton = page.getByTestId('create-user-submit-button'); this.updateSubmitButton = page.getByTestId('update-user-submit-button'); this.cancelButton = page.getByRole('button', { name: '取消' }); // 删除确认对话框按钮 this.confirmDeleteButton = page.getByTestId('confirm-delete-user-button'); } // ===== 导航和基础验证 ===== /** * 导航到用户管理页面 */ async goto(): Promise { await this.page.goto('/admin/users'); await this.page.waitForLoadState('domcontentloaded'); // 等待页面标题出现 await this.pageTitle.waitFor({ state: 'visible', timeout: TIMEOUTS.PAGE_LOAD }); // 等待表格数据加载 await this.userTable.waitFor({ state: 'visible', timeout: TIMEOUTS.PAGE_LOAD_LONG }); await this.expectToBeVisible(); } /** * 验证页面关键元素可见 */ async expectToBeVisible(): Promise { await this.pageTitle.waitFor({ state: 'visible', timeout: TIMEOUTS.PAGE_LOAD }); await this.createUserButton.waitFor({ state: 'visible', timeout: TIMEOUTS.TABLE_LOAD }); } // ===== 对话框操作 ===== /** * 打开创建用户对话框 */ async openCreateDialog(): Promise { await this.createUserButton.click(); // 等待对话框出现 await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: TIMEOUTS.DIALOG }); } /** * 打开编辑用户对话框 * @param username 用户名 */ async openEditDialog(username: string): Promise { // 找到用户行并点击编辑按钮 const userRow = this.userTable.locator('tbody tr').filter({ hasText: username }); // 使用 role + name 组合定位编辑按钮,更健壮 const editButton = userRow.getByRole('button', { name: '编辑用户' }); await editButton.click(); // 等待编辑对话框出现 await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: TIMEOUTS.DIALOG }); } /** * 打开删除确认对话框 * @param username 用户名 */ async openDeleteDialog(username: string): Promise { // 找到用户行并点击删除按钮 const userRow = this.userTable.locator('tbody tr').filter({ hasText: username }); // 使用 role + name 组合定位删除按钮,更健壮 const deleteButton = userRow.getByRole('button', { name: '删除用户' }); await deleteButton.click(); // 等待删除确认对话框出现 await this.page.waitForSelector('[role="alertdialog"]', { state: 'visible', timeout: TIMEOUTS.DIALOG }); } /** * 填写用户表单 * @param data 用户数据 * @param companyName 公司名称(当用户类型为 EMPLOYER 时必须提供) * @param personName 残疾人姓名(当用户类型为 TALENT 时必须提供) */ async fillUserForm(data: UserData, companyName?: string, personName?: string): Promise { // 等待表单出现 await this.page.waitForSelector('form', { state: 'visible', timeout: TIMEOUTS.DIALOG }); // 填写用户名(必填字段) if (data.username) { await this.usernameInput.fill(data.username); } // 填写密码(必填字段) if (data.password) { await this.passwordInput.fill(data.password); } // 填写昵称(可选字段) if (data.nickname !== undefined) { await this.nicknameInput.fill(data.nickname); } // 填写邮箱(可选字段) if (data.email !== undefined) { await this.emailInput.fill(data.email); } // 填写手机号(可选字段) if (data.phone !== undefined) { await this.phoneInput.fill(data.phone); } // 填写真实姓名(可选字段) if (data.name !== undefined) { await this.nameInput.fill(data.name); } // 选择用户类型(如果提供了且不是默认的 ADMIN) const userType = data.userType || UserType.ADMIN; if (userType !== UserType.ADMIN) { // 用户类型选择器使用标准 Radix UI Select 组件 // 通过 data-testid 定位并点击 // 等待用户类型选择器在 DOM 中存在 await this.page.waitForSelector('[data-testid="用户类型-trigger"]', { state: 'attached', timeout: TIMEOUTS.DIALOG }); // 滚动到用户类型选择器可见 await this.userTypeSelector.scrollIntoViewIfNeeded(); // 等待用户类型选择器可见 await this.userTypeSelector.waitFor({ state: 'visible', timeout: TIMEOUTS.DIALOG }); await this.userTypeSelector.click(); // 等待选项出现 await this.page.waitForSelector('[role="option"]', { state: 'visible', timeout: TIMEOUTS.DIALOG }); // 选择对应的用户类型 const userTypeLabel = userType === UserType.EMPLOYER ? '企业用户' : '人才用户'; await this.page.getByRole('option', { name: userTypeLabel }).click(); } // 填写企业选择器(当用户类型为 EMPLOYER 时) if (userType === UserType.EMPLOYER && data.companyId && companyName) { // 等待企业选择器可见(通过 data-testid 定位) await this.page.waitForSelector('[data-testid="关联企业-trigger"]', { state: 'visible', timeout: TIMEOUTS.DIALOG }); // 点击企业选择器触发器 await this.companySelector.click(); // 等待选项出现 await this.page.waitForSelector('[role="option"]', { state: 'visible', timeout: TIMEOUTS.DIALOG }); // 选择对应的公司 await this.page.getByRole('option', { name: companyName }).click(); } // 填写残疾人选择器(当用户类型为 TALENT 时) if (userType === UserType.TALENT && data.personId && personName) { // 使用 @d8d/e2e-test-utils 的 selectRadixOptionAsync 选择残疾人 await selectRadixOptionAsync(this.page, '关联残疾人', personName); } } /** * 填写编辑用户表单 * @param data 用户更新数据 * @param companyName 公司名称(当用户类型为 EMPLOYER 时必须提供) * @param personName 残疾人姓名(当用户类型为 TALENT 时必须提供) */ async fillEditUserForm(data: UserUpdateData, companyName?: string, personName?: string): Promise { // 等待表单出现 await this.page.waitForSelector('form', { state: 'visible', timeout: TIMEOUTS.DIALOG }); // 填写用户名 if (data.username !== undefined) { await this.usernameInput.fill(data.username); } // 填写新密码(可选) if (data.password !== undefined) { await this.passwordInput.fill(data.password); } // 填写昵称 if (data.nickname !== undefined) { await this.nicknameInput.fill(data.nickname); } // 填写邮箱 if (data.email !== undefined) { await this.emailInput.fill(data.email); } // 填写手机号 if (data.phone !== undefined) { await this.phoneInput.fill(data.phone); } // 填写真实姓名 if (data.name !== undefined) { await this.nameInput.fill(data.name); } // 选择用户类型(如果提供了且不是默认的 ADMIN) const userType = data.userType || UserType.ADMIN; if (userType !== UserType.ADMIN) { // 用户类型选择器使用标准 Radix UI Select 组件 // 通过 data-testid 定位并点击 // 等待用户类型选择器在 DOM 中存在 await this.page.waitForSelector('[data-testid="用户类型-trigger"]', { state: 'attached', timeout: TIMEOUTS.DIALOG }); // 滚动到用户类型选择器可见 await this.userTypeSelector.scrollIntoViewIfNeeded(); // 等待用户类型选择器可见 await this.userTypeSelector.waitFor({ state: 'visible', timeout: TIMEOUTS.DIALOG }); await this.userTypeSelector.click(); // 等待选项出现 await this.page.waitForSelector('[role="option"]', { state: 'visible', timeout: TIMEOUTS.DIALOG }); // 选择对应的用户类型 const userTypeLabel = userType === UserType.EMPLOYER ? '企业用户' : '人才用户'; await this.page.getByRole('option', { name: userTypeLabel }).click(); } // 填写企业选择器(当用户类型为 EMPLOYER 时) if (userType === UserType.EMPLOYER && data.companyId && companyName) { // 等待企业选择器可见(通过 data-testid 定位) await this.page.waitForSelector('[data-testid="关联企业-trigger"]', { state: 'visible', timeout: TIMEOUTS.DIALOG }); // 点击企业选择器触发器 await this.companySelector.click(); // 等待选项出现 await this.page.waitForSelector('[role="option"]', { state: 'visible', timeout: TIMEOUTS.DIALOG }); // 选择对应的公司 await this.page.getByRole('option', { name: companyName }).click(); } // 填写残疾人选择器(当用户类型为 TALENT 时) if (userType === UserType.TALENT && data.personId && personName) { // 使用 @d8d/e2e-test-utils 的 selectRadixOptionAsync 选择残疾人 await selectRadixOptionAsync(this.page, '关联残疾人', personName); } } /** * 提交表单 * @returns 表单提交结果 */ async submitForm(): Promise { // 收集网络响应 const responses: NetworkResponse[] = []; // 使用 waitForResponse 捕获特定 API 响应,避免并发测试中的监听器干扰 const createUserPromise = this.page.waitForResponse( response => response.url().includes('/api/v1/users') && response.request().method() === 'POST', { timeout: TIMEOUTS.TABLE_LOAD } ).catch(() => null); const updateUserPromise = this.page.waitForResponse( response => response.url().includes('/api/v1/users') && response.request().method() === 'PUT', { timeout: TIMEOUTS.TABLE_LOAD } ).catch(() => null); // 捕获列表刷新响应(表单提交后会刷新用户列表) const getAllUsersPromise = this.page.waitForResponse( response => response.url().includes('/api/v1/users') && response.request().method() === 'GET', { timeout: TIMEOUTS.TABLE_LOAD } ).catch(() => null); try { // 点击提交按钮(优先使用 data-testid 选择器) let submitButton = this.page.locator('[data-testid="create-user-submit-button"]'); if (await submitButton.count() === 0) { submitButton = this.page.getByRole('button', { name: /^(创建|更新)用户$/ }); } await submitButton.click(); // 等待 API 响应并收集 const [createResponse, updateResponse] = await Promise.all([ createUserPromise, updateUserPromise, ]); // getAllUsersPromise 单独处理,不需要等待其结果 void getAllUsersPromise; // 处理捕获到的响应(创建或更新) const mainResponse = createResponse || updateResponse; if (mainResponse) { const responseBody = await mainResponse.text().catch(() => ''); let jsonBody = null; try { jsonBody = JSON.parse(responseBody); } catch { // JSON 解析失败时,使用原始文本作为 response body } responses.push({ url: mainResponse.url(), method: mainResponse.request()?.method() ?? 'UNKNOWN', status: mainResponse.status(), ok: mainResponse.ok(), responseHeaders: await mainResponse.allHeaders().catch(() => ({})), responseBody: jsonBody || responseBody, }); } // 等待网络请求完成 try { await this.page.waitForLoadState('networkidle', { timeout: TIMEOUTS.DIALOG }); } catch { console.debug('submitForm: networkidle 超时,继续检查 Toast 消息'); } } catch (error) { console.debug('submitForm 异常:', error); } // 主动等待 Toast 消息显示(最多等待 5 秒) const errorToast = this.page.locator('[data-sonner-toast][data-type="error"]'); const successToast = this.page.locator('[data-sonner-toast][data-type="success"]'); // 等待任一 Toast 出现 await Promise.race([ errorToast.waitFor({ state: 'attached', timeout: TIMEOUTS.DIALOG }).catch(() => false), successToast.waitFor({ state: 'attached', timeout: TIMEOUTS.DIALOG }).catch(() => false), new Promise(resolve => setTimeout(() => resolve(false), 5000)) ]); // 再次检查 Toast 是否存在 let hasError = (await errorToast.count()) > 0; let hasSuccess = (await successToast.count()) > 0; // 如果标准选择器找不到,尝试更宽松的选择器 if (!hasError && !hasSuccess) { // 尝试通过文本内容查找 const allToasts = this.page.locator('[data-sonner-toast]'); const count = await allToasts.count(); for (let i = 0; i < count; i++) { const text = await allToasts.nth(i).textContent() || ''; if (text.includes('成功') || text.toLowerCase().includes('success')) { hasSuccess = true; break; } else if (text.includes('失败') || text.includes('错误') || text.toLowerCase().includes('error')) { hasError = true; break; } } } let errorMessage: string | null = null; let successMessage: string | null = null; if (hasError) { errorMessage = await errorToast.textContent(); } if (hasSuccess) { successMessage = await successToast.textContent(); } console.debug('submitForm 结果:', { hasError, hasSuccess, errorMessage, successMessage, responsesCount: responses.length }); return { success: hasSuccess || (!hasError && !hasSuccess && responses.some(r => r.ok)), hasError, hasSuccess, errorMessage: errorMessage ?? undefined, successMessage: successMessage ?? undefined, responses, }; } /** * 取消对话框 */ async cancelDialog(): Promise { await this.cancelButton.click(); await this.waitForDialogClosed(); } /** * 等待对话框关闭 */ async waitForDialogClosed(): Promise { // 首先检查对话框是否已经关闭 const dialog = this.page.locator('[role="dialog"]'); const count = await dialog.count(); if (count === 0) { console.debug('waitForDialogClosed: 对话框不存在,认为已关闭'); return; } // 等待对话框隐藏 await dialog.waitFor({ state: 'hidden', timeout: TIMEOUTS.DIALOG }) .catch(() => { console.debug('waitForDialogClosed: 对话框关闭超时,可能已经关闭'); }); // 额外等待以确保 DOM 更新完成 await this.page.waitForTimeout(TIMEOUTS.MEDIUM); } /** * 确认删除操作 */ async confirmDelete(): Promise { await this.confirmDeleteButton.click(); // 等待确认对话框关闭和网络请求完成 await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: TIMEOUTS.DIALOG }) .catch(() => { console.debug('confirmDelete: 确认对话框关闭超时,继续执行'); }); try { await this.page.waitForLoadState('domcontentloaded', { timeout: TIMEOUTS.DIALOG }); } catch { console.debug('confirmDelete: 等待 DOM 加载超时,继续执行'); } await this.page.waitForTimeout(TIMEOUTS.LONG); } /** * 取消删除操作 */ async cancelDelete(): Promise { const cancelButton = this.page.locator('[role="alertdialog"]').getByRole('button', { name: '取消' }); await cancelButton.click(); await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: TIMEOUTS.DIALOG }) .catch(() => { // 继续执行 }); } // ===== CRUD 操作方法 ===== /** * 创建用户(完整流程) * @param data 用户数据 * @param companyName 公司名称(当用户类型为 EMPLOYER 时必须提供) * @param personName 残疾人姓名(当用户类型为 TALENT 时必须提供) * @returns 表单提交结果 */ async createUser(data: UserData, companyName?: string, personName?: string): Promise { await this.openCreateDialog(); await this.fillUserForm(data, companyName, personName); const result = await this.submitForm(); await this.waitForDialogClosed(); return result; } /** * 编辑用户(完整流程) * @param username 用户名 * @param data 更新的用户数据 * @param companyName 公司名称(当用户类型为 EMPLOYER 时必须提供) * @param personName 残疾人姓名(当用户类型为 TALENT 时必须提供) * @returns 表单提交结果 */ async editUser(username: string, data: UserUpdateData, companyName?: string, personName?: string): Promise { await this.openEditDialog(username); await this.fillEditUserForm(data, companyName, personName); const result = await this.submitForm(); await this.waitForDialogClosed(); return result; } /** * 删除用户(使用 API 直接删除,绕过 UI) * @param username 用户名 * @returns 是否成功删除 */ async deleteUser(username: string): Promise { try { // 使用 API 直接删除,添加超时保护 const result = await Promise.race([ this.page.evaluate(async ({ username, apiGetAll, apiDelete }) => { // 尝试获取 token(使用标准键名) const token = localStorage.getItem('token') || localStorage.getItem('auth_token') || localStorage.getItem('accessToken'); if (!token) { return { success: false, noToken: true }; } try { // 先获取用户列表,找到用户的 ID(限制 100 条) const listResponse = await fetch(`${apiGetAll}?skip=0&take=100`, { headers: { 'Authorization': `Bearer ${token}` } }); if (!listResponse.ok) { return { success: false, notFound: false }; } const listData = await listResponse.json(); // 根据用户名查找用户 ID const user = listData.data?.find((u: { username: string }) => u.username === username ); if (!user) { // 用户不在列表中,可能已被删除或在其他页 return { success: false, notFound: true }; } // 使用用户 ID 删除 - DELETE 方法 const deleteResponse = await fetch(`${apiDelete}/${user.id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); if (!deleteResponse.ok && deleteResponse.status !== 204) { return { success: false, notFound: false }; } return { success: true }; } catch (_error) { return { success: false, notFound: false }; } }, { username, apiGetAll: UserManagementPage.API_GET_ALL_USERS, apiDelete: UserManagementPage.API_DELETE_USER }), // 10 秒超时 new Promise((resolve) => setTimeout(() => resolve({ success: false, timeout: true }), 10000)) ]) as Promise<{ success: boolean; notFound?: boolean; timeout?: boolean }>; // 如果超时:打印警告但返回 true(允许测试继续) if (result.timeout) { console.debug(`删除用户 "${username}" 超时,但允许测试继续`); return true; } // 如果用户找不到:认为删除成功(可能已被其他测试删除) if (result.notFound) { console.debug(`删除用户 "${username}": 用户不存在,认为已删除`); return true; } if (result.noToken) { console.debug('删除用户失败: 未找到认证 token'); return false; } if (!result.success) { console.debug(`删除用户 "${username}" 失败: 未知错误`); return false; } // 删除成功后刷新页面,确保列表更新 await this.page.reload(); await this.page.waitForLoadState('domcontentloaded'); return true; } catch (error) { console.debug(`删除用户 "${username}" 异常:`, error); // 发生异常时返回 true,避免阻塞测试 return true; } } // ===== 搜索和验证方法 ===== /** * 按用户名搜索 * @param keyword 搜索关键词 * @returns 搜索结果是否包含目标用户 */ async searchUsers(keyword: string): Promise { await this.searchInput.fill(keyword); await this.searchButton.click(); await this.page.waitForLoadState('domcontentloaded'); await this.page.waitForTimeout(TIMEOUTS.LONG); // 验证搜索结果 return await this.userExists(keyword); } /** * 验证用户是否存在(使用精确匹配) * @param username 用户名 * @returns 用户是否存在 */ async userExists(username: string): Promise { const userRow = this.userTable.locator('tbody tr').filter({ hasText: username }); const count = await userRow.count(); if (count === 0) return false; // 进一步验证用户名列的文本是否完全匹配 // 表格列顺序:头像(0), 用户名(1), 昵称(2), 邮箱(3), 真实姓名(4), ... const nameCell = userRow.locator('td').nth(1); const actualText = await nameCell.textContent(); return actualText?.trim() === username; } /** * 获取用户数量 * @returns 用户数量 */ async getUserCount(): Promise { const rows = await this.userTable.locator('tbody tr').count(); return rows; } /** * 根据用户名获取用户行 * @param username 用户名 * @returns 用户行定位器 */ getUserByUsername(username: string): Locator { return this.userTable.locator('tbody tr').filter({ hasText: username }); } /** * 期望用户存在 * @param username 用户名 */ async expectUserExists(username: string): Promise { const exists = await this.userExists(username); if (!exists) { throw new Error(`期望用户 "${username}" 存在,但未找到`); } } /** * 期望用户不存在 * @param username 用户名 */ async expectUserNotExists(username: string): Promise { const exists = await this.userExists(username); if (exists) { throw new Error(`期望用户 "${username}" 不存在,但找到了`); } } }