import { TIMEOUTS } from '../../utils/timeouts'; import { Page, Locator } from '@playwright/test'; /** * 区域层级常量 */ export const REGION_LEVEL = { PROVINCE: 1, CITY: 2, DISTRICT: 3, } as const; /** * 区域层级类型 */ export type RegionLevel = typeof REGION_LEVEL[keyof typeof REGION_LEVEL]; /** * 区域状态常量 */ export const REGION_STATUS = { ENABLED: 0, DISABLED: 1, } as const; /** * 区域数据接口 */ export interface RegionData { /** 区域名称 */ name: string; /** 行政区划代码 */ code?: string; /** 区域层级(1=省, 2=市, 3=区) */ level?: RegionLevel; /** 父级区域ID */ parentId?: number | null; /** 状态(0=启用, 1=禁用) */ isDisabled?: typeof REGION_STATUS[keyof typeof REGION_STATUS]; } /** * 网络响应数据 */ export interface NetworkResponse { 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/areas * * @example * ```typescript * const regionPage = new RegionManagementPage(page); * await regionPage.goto(); * await regionPage.createProvince({ name: '测试省', code: '110000' }); * ``` */ export class RegionManagementPage { readonly page: Page; // 页面级选择器 readonly pageTitle: Locator; readonly addProvinceButton: Locator; readonly treeContainer: Locator; constructor(page: Page) { this.page = page; // 使用精确文本匹配获取页面标题 this.pageTitle = page.getByText('省市区树形管理', { exact: true }); // 使用 role + name 组合获取新增按钮(比单独 text 更健壮) this.addProvinceButton = page.getByRole('button', { name: '新增省', exact: true }); // 使用 Card 组件的结构来定位树形容器(比 Tailwind 类更健壮) // 根据实际 DOM: Card > CardContent > AreaTreeAsync > div.border.rounded-lg.bg-background this.treeContainer = page.locator('.border.rounded-lg').first(); } /** * 导航到区域管理页面 */ async goto() { await this.page.goto('/admin/areas'); await this.page.waitForLoadState('domcontentloaded'); // 等待页面标题出现 await this.pageTitle.waitFor({ state: 'visible', timeout: TIMEOUTS.PAGE_LOAD }); // 等待树形结构加载 await this.treeContainer.waitFor({ state: 'visible', timeout: TIMEOUTS.PAGE_LOAD_LONG }); await this.expectToBeVisible(); } /** * 验证页面关键元素可见 */ async expectToBeVisible() { await this.pageTitle.waitFor({ state: 'visible', timeout: TIMEOUTS.PAGE_LOAD }); await this.addProvinceButton.waitFor({ state: 'visible', timeout: TIMEOUTS.TABLE_LOAD }); } /** * 打开新增省对话框 */ async openCreateProvinceDialog() { await this.addProvinceButton.click(); // 等待对话框出现 await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: TIMEOUTS.DIALOG }); } /** * 打开新增子区域对话框 * @param parentName 父级区域名称 * @param childType 子区域类型('市'、'区' 或 '街道') */ async openAddChildDialog(parentName: string, childType: '市' | '区' | '街道') { // 首先确保父级节点可见 const parentText = this.treeContainer.getByText(parentName); await parentText.waitFor({ state: 'visible', timeout: TIMEOUTS.DIALOG }); // 找到父级节点并悬停,使操作按钮可见 const regionRow = parentText.locator('xpath=ancestor::div[contains(@class, "group")][1]'); await regionRow.hover(); // 找到对应的"新增市"、"新增区"或"新增街道"按钮 const buttonName = childType === '市' ? '新增市' : childType === '区' ? '新增区' : '新增街道'; const button = regionRow.getByRole('button', { name: buttonName }); // 等待按钮可见并可点击 await button.waitFor({ state: 'visible', timeout: TIMEOUTS.ELEMENT_VISIBLE_SHORT }); await button.click({ timeout: TIMEOUTS.DIALOG }); // 等待对话框出现 await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: TIMEOUTS.DIALOG }); } /** * 打开编辑区域对话框 * @param regionName 区域名称 */ async openEditDialog(regionName: string) { await this.waitForTreeLoaded(); // 先尝试直接查找区域 const allRegions = this.treeContainer.getByText(regionName, { exact: true }); let count = await allRegions.count(); // 如果找不到,可能区域在折叠的父节点下 // 对于市级或区级区域,需要展开对应的省级节点 if (count === 0 && (regionName.includes('市') || regionName.includes('区'))) { console.debug(`区域 "${regionName}" 未找到,尝试展开所有可能的省级节点`); // 查找所有省级节点(以"省"结尾的区域名称) const provinceTexts = this.treeContainer.getByText(/省$/); const provinceCount = await provinceTexts.count(); console.debug(`找到 ${provinceCount} 个省级节点`); // 展开所有省级节点(不提前退出) for (let i = 0; i < provinceCount; i++) { try { const provinceName = await provinceTexts.nth(i).textContent(); if (provinceName) { const trimmedName = provinceName.trim(); console.debug(`尝试展开省节点: ${trimmedName}`); await this.expandNode(trimmedName); await this.page.waitForTimeout(TIMEOUTS.SHORT); } } catch (error) { // 忽略展开失败,继续下一个 } } // 再次检查目标区域 count = await allRegions.count(); console.debug(`展开所有省节点后,目标区域 "${regionName}" 数量: ${count}`); } if (count === 0) { throw new Error(`区域 "${regionName}" 未找到,即使展开所有省级节点后`); } // 找到目标区域(如果有多个,使用最后一个) const targetIndex = count - 1; const regionText = allRegions.nth(targetIndex >= 0 ? targetIndex : 0); await regionText.scrollIntoViewIfNeeded(); await this.page.waitForTimeout(TIMEOUTS.MEDIUM); const regionRow = regionText.locator('xpath=ancestor::div[contains(@class, "group")][1]'); await regionRow.hover(); await this.page.waitForTimeout(TIMEOUTS.SHORT); const button = regionRow.getByRole('button', { name: '编辑' }); await button.waitFor({ state: 'visible', timeout: TIMEOUTS.ELEMENT_VISIBLE_SHORT }); await button.click({ timeout: TIMEOUTS.DIALOG }); await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: TIMEOUTS.DIALOG }); } /** * 打开删除确认对话框 * @param regionName 区域名称 */ async openDeleteDialog(regionName: string) { await this.waitForTreeLoaded(); // 先尝试直接查找区域 const allRegions = this.treeContainer.getByText(regionName, { exact: true }); let count = await allRegions.count(); // 如果找不到,可能区域在折叠的父节点下 // 对于市级或区级区域,需要展开对应的省级节点 if (count === 0 && (regionName.includes('市') || regionName.includes('区') || regionName.includes('街道'))) { console.debug(`区域 "${regionName}" 未找到,尝试展开所有可能的省级节点`); // 查找所有省级节点(以"省"结尾的区域名称) const provinceTexts = this.treeContainer.getByText(/省$/); const provinceCount = await provinceTexts.count(); console.debug(`找到 ${provinceCount} 个省级节点`); // 展开所有省级节点(不提前退出) for (let i = 0; i < provinceCount; i++) { try { const provinceName = await provinceTexts.nth(i).textContent(); if (provinceName) { const trimmedName = provinceName.trim(); console.debug(`尝试展开省节点: ${trimmedName}`); await this.expandNode(trimmedName); await this.page.waitForTimeout(TIMEOUTS.SHORT); } } catch (error) { // 忽略展开失败,继续下一个 } } // 再次检查目标区域 count = await allRegions.count(); console.debug(`展开所有省节点后,目标区域 "${regionName}" 数量: ${count}`); } // 如果还是找不到,尝试展开市级节点(用于区级和街道) if (count === 0 && (regionName.includes('区') || regionName.includes('街道'))) { console.debug(`区域 "${regionName}" 仍未找到,尝试展开所有市级节点`); const cityTexts = this.treeContainer.getByText(/市$/); const cityCount = await cityTexts.count(); console.debug(`找到 ${cityCount} 个市级节点`); for (let i = 0; i < cityCount; i++) { try { const cityName = await cityTexts.nth(i).textContent(); if (cityName) { const trimmedName = cityName.trim(); console.debug(`尝试展开市节点: ${trimmedName}`); await this.expandNode(trimmedName); await this.page.waitForTimeout(TIMEOUTS.SHORT); } } catch (error) { // 忽略展开失败,继续下一个 } } count = await allRegions.count(); console.debug(`展开所有市节点后,目标区域 "${regionName}" 数量: ${count}`); } if (count === 0) { throw new Error(`区域 "${regionName}" 未找到,即使展开所有节点后`); } // 找到目标区域(如果有多个,使用最后一个) const targetIndex = count - 1; const regionText = allRegions.nth(targetIndex >= 0 ? targetIndex : 0); await regionText.scrollIntoViewIfNeeded(); await this.page.waitForTimeout(TIMEOUTS.MEDIUM); const regionRow = regionText.locator('xpath=ancestor::div[contains(@class, "group")][1]'); await regionRow.hover(); await this.page.waitForTimeout(TIMEOUTS.SHORT); const button = regionRow.getByRole('button', { name: '删除' }); await button.waitFor({ state: 'visible', timeout: TIMEOUTS.ELEMENT_VISIBLE_SHORT }); await button.click({ timeout: TIMEOUTS.DIALOG }); // 等待删除确认对话框出现 await this.page.waitForSelector('[role="alertdialog"]', { state: 'visible', timeout: TIMEOUTS.DIALOG }); } /** * 打开状态切换确认对话框 * @param regionName 区域名称 */ async openToggleStatusDialog(regionName: string) { // 等待树形结构加载完成 await this.waitForTreeLoaded(); // 找到所有匹配的区域文本,使用最后一个(最新创建的) const allRegions = this.treeContainer.getByText(regionName, { exact: true }); const count = await allRegions.count(); if (count === 0) { throw new Error(`区域 "${regionName}" 未找到`); } // 使用最后一个匹配项(最新创建的) const targetIndex = count - 1; const regionText = allRegions.nth(targetIndex); await regionText.waitFor({ state: 'visible', timeout: TIMEOUTS.DIALOG }); // 滚动到元素位置 await regionText.scrollIntoViewIfNeeded(); await this.page.waitForTimeout(TIMEOUTS.SHORT); const regionRow = regionText.locator('xpath=ancestor::div[contains(@class, "group")][1]'); await regionRow.hover(); await this.page.waitForTimeout(TIMEOUTS.SHORT); // 在区域行内查找"启用"或"禁用"按钮(操作按钮组中的状态切换按钮) const statusButton = regionRow.getByRole('button', { name: /^(启用|禁用)$/ }); await statusButton.waitFor({ state: 'visible', timeout: TIMEOUTS.ELEMENT_VISIBLE_SHORT }); await statusButton.click({ timeout: TIMEOUTS.DIALOG }); // 等待状态切换确认对话框出现 await this.page.waitForSelector('[role="alertdialog"]', { state: 'visible', timeout: TIMEOUTS.DIALOG }); } /** * 填写区域表单 * @param data 区域数据 */ async fillRegionForm(data: RegionData) { // 等待表单出现 await this.page.waitForSelector('form', { state: 'visible', timeout: TIMEOUTS.DIALOG }); // 填写区域名称 if (data.name) { await this.page.getByLabel('区域名称').fill(data.name); } // 填写行政区划代码 if (data.code) { await this.page.getByLabel('行政区划代码').fill(data.code); } } /** * 提交表单 * @returns 表单提交结果 */ async submitForm(): Promise { // 收集网络响应 const responses: NetworkResponse[] = []; // 监听所有网络请求 const responseHandler = async (response: Response) => { const url = response.url(); // 监听区域管理相关的 API 请求 if (url.includes('/areas') || url.includes('area')) { const requestBody = response.request()?.postData(); const responseBody = await response.text().catch(() => ''); let jsonBody = null; try { jsonBody = JSON.parse(responseBody); } catch { // 不是 JSON 响应 } responses.push({ url, method: response.request()?.method() ?? 'UNKNOWN', status: response.status(), ok: response.ok(), responseHeaders: await response.allHeaders().catch(() => ({})), responseBody: jsonBody || responseBody, }); } }; this.page.on('response', responseHandler); // 点击提交按钮(创建或更新) const submitButton = this.page.getByRole('button', { name: /^(创建|更新)$/ }); await submitButton.click(); // 等待网络请求完成 - 使用更宽松的策略 // networkidle 可能因后台轮询而失败,使用 domcontentloaded 代替 try { await this.page.waitForLoadState('domcontentloaded', { timeout: TIMEOUTS.DIALOG }); } catch { // domcontentloaded 也可能失败,继续执行 } // 额外等待,给 API 响应一些时间 await this.page.waitForTimeout(TIMEOUTS.LONG); // 移除监听器 this.page.off('response', responseHandler); // 等待对话框关闭或错误出现 await this.page.waitForTimeout(TIMEOUTS.LONGER); // 检查 Toast 消息 const errorToast = this.page.locator('[data-sonner-toast][data-type="error"]'); const successToast = this.page.locator('[data-sonner-toast][data-type="success"]'); const hasError = await errorToast.count() > 0; const hasSuccess = await successToast.count() > 0; let errorMessage: string | null = null; let successMessage: string | null = null; if (hasError) { errorMessage = await errorToast.first().textContent(); } if (hasSuccess) { successMessage = await successToast.first().textContent(); } return { success: hasSuccess || (!hasError && !hasSuccess), hasError, hasSuccess, errorMessage: errorMessage ?? undefined, successMessage: successMessage ?? undefined, responses, }; } /** * 取消对话框 */ async cancelDialog() { const cancelButton = this.page.getByRole('button', { name: '取消' }); await cancelButton.click(); await this.waitForDialogClosed(); } /** * 等待对话框关闭 */ async waitForDialogClosed() { const dialog = this.page.locator('[role="dialog"]'); await dialog.waitFor({ state: 'hidden', timeout: TIMEOUTS.DIALOG }).catch(() => {}); await this.page.waitForTimeout(TIMEOUTS.MEDIUM); } /** * 确认删除操作 */ async confirmDelete() { const confirmButton = this.page.getByRole('button', { name: /^确认删除$/ }); await confirmButton.click(); // 等待确认对话框关闭和网络请求完成 await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: TIMEOUTS.DIALOG }).catch(() => {}); // 使用更宽松的等待策略 try { await this.page.waitForLoadState('domcontentloaded', { timeout: TIMEOUTS.DIALOG }); } catch { // 继续执行 } await this.page.waitForTimeout(TIMEOUTS.LONG); } /** * 取消删除操作 */ async cancelDelete() { 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(() => {}); } /** * 确认状态切换操作 */ async confirmToggleStatus() { // 监听 API 响应 let apiResponse: any = null; const responseHandler = async (response: Response) => { const url = response.url(); if (url.includes('/areas') || url.includes('area')) { try { const responseBody = await response.text(); apiResponse = { url, status: response.status(), body: responseBody, }; console.debug(`API 响应: ${url}, status=${response.status()}`); } catch { // ignore } } }; this.page.on('response', responseHandler); const confirmButton = this.page.locator('[role="alertdialog"]').getByRole('button', { name: '确认' }); await confirmButton.click(); // 等待 API 响应 await this.page.waitForTimeout(TIMEOUTS.VERY_LONG); this.page.off('response', responseHandler); if (apiResponse) { console.debug(`状态切换 API 响应: status=${apiResponse.status}, body=${apiResponse.body}`); } // 等待确认对话框关闭和网络请求完成 await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: TIMEOUTS.DIALOG }).catch(() => {}); // 使用更宽松的等待策略 try { await this.page.waitForLoadState('domcontentloaded', { timeout: TIMEOUTS.DIALOG }); } catch { // 继续执行 } await this.page.waitForTimeout(TIMEOUTS.LONG); } /** * 取消状态切换操作 */ async cancelToggleStatus() { 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(() => {}); } /** * 验证区域是否存在 * @param regionName 区域名称 * @returns 区域是否存在 */ async regionExists(regionName: string): Promise { console.debug(`regionExists: 查找 "${regionName}"`); try { // 等待元素出现,最多等待 5 秒 const regionElement = this.treeContainer.getByText(regionName, { exact: true }); await regionElement.waitFor({ state: 'attached', timeout: TIMEOUTS.DIALOG }); console.debug(`regionExists: 找到 "${regionName}"`); return true; } catch (error) { console.debug(`regionExists: 未找到 "${regionName}", error:`, error); // 元素未出现 return false; } } /** * 展开区域节点 * @param regionName 区域名称 */ async expandNode(regionName: string) { console.debug(`expandNode: 尝试展开 "${regionName}"`); // 找到区域文本元素 - 使用更精确的定位策略 // 使用 filter 确保只匹配完全独立的文本节点,而不是包含按钮的元素 const allRegionTexts = this.treeContainer.getByText(regionName, { exact: true }); const count = await allRegionTexts.count(); console.debug(`expandNode: 找到 ${count} 个匹配 "${regionName}" 的元素`); if (count === 0) { throw new Error(`区域 "${regionName}" 未找到`); } // 使用第一个匹配项 const regionText = allRegionTexts.first(); await regionText.waitFor({ state: 'visible', timeout: TIMEOUTS.DIALOG }); // 滚动到元素位置 await regionText.scrollIntoViewIfNeeded(); await this.page.waitForTimeout(TIMEOUTS.SHORT); // 找到区域节点的展开按钮 const regionRow = regionText.locator('xpath=ancestor::div[contains(@class, "group")][1]'); const expandButton = regionRow.locator('button').filter({ has: regionRow.locator('svg') }).first(); const buttonCount = await expandButton.count(); console.debug(`expandNode: 展开按钮数量: ${buttonCount}`); if (buttonCount > 0) { // 悬停以确保按钮可见 await regionRow.hover(); await this.page.waitForTimeout(TIMEOUTS.VERY_SHORT); // 点击展开按钮 await expandButton.click({ timeout: TIMEOUTS.DIALOG }); // 等待懒加载的子节点出现 await this.page.waitForTimeout(TIMEOUTS.MEDIUM); console.debug(`expandNode: 成功展开 "${regionName}"`); } else { console.debug(`expandNode: 没有找到展开按钮,可能已经是展开状态或没有子节点`); } } /** * 收起区域节点 * @param regionName 区域名称 */ async collapseNode(regionName: string) { // 找到区域节点的收起按钮 const regionRow = this.treeContainer.getByText(regionName, { exact: true }).locator('xpath=ancestor::div[contains(@class, "group")][1]'); const collapseButton = regionRow.locator('button').filter({ has: regionRow.locator('svg') }).first(); const count = await collapseButton.count(); if (count > 0) { await collapseButton.click(); await this.page.waitForTimeout(TIMEOUTS.MEDIUM); } } /** * 获取区域的状态 * @param regionName 区域名称 * @returns 区域状态('启用' 或 '禁用') */ async getRegionStatus(regionName: string): Promise<'启用' | '禁用' | null> { // 等待树形结构加载完成 await this.waitForTreeLoaded(); // 找到所有匹配的区域文本 const allRegions = this.treeContainer.getByText(regionName, { exact: true }); const count = await allRegions.count(); console.debug(`getRegionStatus: 查找 "${regionName}", 找到 ${count} 个匹配`); if (count === 0) return null; // 如果有多个匹配,尝试找到最后一个(最新创建的) // 因为新创建的区域通常在列表的末尾 const targetIndex = count - 1; const regionText = allRegions.nth(targetIndex); // 确保元素可见 await regionText.scrollIntoViewIfNeeded(); await this.page.waitForTimeout(TIMEOUTS.MEDIUM); // 根据DOM结构,状态是区域名称后的第4个 generic 元素 // regionName 的父级 generic 下有: name, level, code, status const regionNameParent = regionText.locator('xpath=..'); // 获取所有子元素 const children = regionNameParent.locator('xpath=./generic'); const childCount = await children.count(); console.debug(`getRegionStatus: regionNameParent 下有 ${childCount} 个子元素`); // 状态通常是最后一个子元素或倒数第二个 // 根据DOM结构,状态在第4个位置(索引3,从0开始) if (childCount >= 4) { const statusElement = children.nth(3); const statusText = await statusElement.textContent(); console.debug(`getRegionStatus: 从位置3获取状态: "${statusText}"`); if (statusText === '启用' || statusText === '禁用') { return statusText; } } // 如果上述方法失败,尝试在父级内查找状态 const enabledText = regionNameParent.getByText('启用', { exact: true }); const disabledText = regionNameParent.getByText('禁用', { exact: true }); const hasEnabled = await enabledText.count() > 0; const hasDisabled = await disabledText.count() > 0; console.debug(`getRegionStatus: "${regionName}" hasEnabled=${hasEnabled}, hasDisabled=${hasDisabled}`); if (hasEnabled && !hasDisabled) return '启用'; if (hasDisabled && !hasEnabled) return '禁用'; // 如果两者都有,需要更精确的选择器 // 状态在操作按钮之前,所以应该先找到状态元素 const allTexts = await regionNameParent.allTextContents(); console.debug(`getRegionStatus: 所有文本内容:`, allTexts); // 检查最后一个非空文本是否是状态 for (const text of allTexts) { if (text === '启用') return '启用'; if (text === '禁用') return '禁用'; } return null; } /** * 创建省 * @param data 省份数据 * @returns 表单提交结果 */ async createProvince(data: RegionData): Promise { await this.openCreateProvinceDialog(); await this.fillRegionForm(data); const result = await this.submitForm(); await this.waitForDialogClosed(); return result; } /** * 创建子区域(市、区或街道) * @param parentName 父级区域名称 * @param childType 子区域类型('市'、'区' 或 '街道') * @param data 子区域数据 * @returns 表单提交结果 */ async createChildRegion( parentName: string, childType: '市' | '区' | '街道', data: RegionData ): Promise { await this.openAddChildDialog(parentName, childType); await this.fillRegionForm(data); const result = await this.submitForm(); await this.waitForDialogClosed(); return result; } /** * 编辑区域 * @param regionName 区域名称 * @param data 更新的区域数据 * @returns 表单提交结果 */ async editRegion(regionName: string, data: RegionData): Promise { await this.openEditDialog(regionName); await this.fillRegionForm(data); const result = await this.submitForm(); await this.waitForDialogClosed(); return result; } /** * 删除区域 * @param regionName 区域名称 * @returns 是否成功(true = 成功删除, false = 删除失败或取消) */ async deleteRegion(regionName: string): Promise { await this.openDeleteDialog(regionName); await this.confirmDelete(); // 等待并检查 Toast 消息 await this.page.waitForTimeout(TIMEOUTS.LONG); const successToast = this.page.locator('[data-sonner-toast][data-type="success"]'); const hasSuccess = await successToast.count() > 0; return hasSuccess; } /** * 切换区域状态(启用/禁用) * @param regionName 区域名称 * @returns 是否成功 */ async toggleRegionStatus(regionName: string): Promise { // 先确保区域在树中可见 await this.waitForTreeLoaded(); const exists = await this.regionExists(regionName); if (!exists) { console.debug(`⚠️ toggleRegionStatus: 区域 "${regionName}" 在树中未找到`); } await this.openToggleStatusDialog(regionName); await this.confirmToggleStatus(); // 等待并检查 Toast 消息 await this.page.waitForTimeout(TIMEOUTS.LONG); const successToast = this.page.locator('[data-sonner-toast][data-type="success"]'); const hasSuccess = await successToast.count() > 0; // 等待树形结构刷新以显示更新后的状态 try { await this.page.waitForLoadState('domcontentloaded', { timeout: TIMEOUTS.DIALOG }); } catch { // 继续执行 } await this.waitForTreeLoaded(); console.debug(`toggleRegionStatus 完成: 区域="${regionName}", hasSuccess=${hasSuccess}`); return hasSuccess; } /** * 等待树形结构加载完成 */ async waitForTreeLoaded() { await this.treeContainer.waitFor({ state: 'visible', timeout: TIMEOUTS.PAGE_LOAD_LONG }); // 等待加载文本消失(使用更健壮的选择器) // 加载文本位于 CardContent 中,带有 text-muted-foreground 类 await this.page.locator('.text-muted-foreground', { hasText: '加载中' }).waitFor({ state: 'hidden', timeout: TIMEOUTS.TABLE_LOAD }).catch(() => {}); } /** * 刷新树形结构(用于创建新区域后强制刷新) * 由于树组件使用懒加载缓存,新创建的区域不会自动显示 * 此方法通过重新导航到页面来强制刷新树数据 */ async refreshTree() { console.debug('刷新树形结构以显示新创建的区域'); // 重新导航到当前页面,强制刷新所有数据 await this.page.reload(); // 等待页面加载完成 await this.page.waitForLoadState('domcontentloaded', { timeout: TIMEOUTS.PAGE_LOAD }); // 等待树形结构加载完成 await this.treeContainer.waitFor({ state: 'visible', timeout: TIMEOUTS.PAGE_LOAD_LONG }); await this.pageTitle.waitFor({ state: 'visible', timeout: TIMEOUTS.PAGE_LOAD }); // 等待懒加载完成 await this.page.locator('.text-muted-foreground', { hasText: '加载中' }).waitFor({ state: 'hidden', timeout: TIMEOUTS.TABLE_LOAD }).catch(() => {}); console.debug('树形结构刷新完成'); } }