| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830 |
- 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<string, string>;
- 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: 15000 });
- // 等待树形结构加载
- await this.treeContainer.waitFor({ state: 'visible', timeout: 20000 });
- await this.expectToBeVisible();
- }
- /**
- * 验证页面关键元素可见
- */
- async expectToBeVisible() {
- await this.pageTitle.waitFor({ state: 'visible', timeout: 15000 });
- await this.addProvinceButton.waitFor({ state: 'visible', timeout: 10000 });
- }
- /**
- * 打开新增省对话框
- */
- async openCreateProvinceDialog() {
- await this.addProvinceButton.click();
- // 等待对话框出现
- await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 });
- }
- /**
- * 打开新增子区域对话框
- * @param parentName 父级区域名称
- * @param childType 子区域类型('市'、'区' 或 '街道')
- */
- async openAddChildDialog(parentName: string, childType: '市' | '区' | '街道') {
- // 首先确保父级节点可见
- const parentText = this.treeContainer.getByText(parentName);
- await parentText.waitFor({ state: 'visible', timeout: 5000 });
- // 找到父级节点并悬停,使操作按钮可见
- 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: 3000 });
- await button.click({ timeout: 5000 });
- // 等待对话框出现
- await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 });
- }
- /**
- * 打开编辑区域对话框
- * @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(300);
- }
- } 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(500);
- const regionRow = regionText.locator('xpath=ancestor::div[contains(@class, "group")][1]');
- await regionRow.hover();
- await this.page.waitForTimeout(300);
- const button = regionRow.getByRole('button', { name: '编辑' });
- await button.waitFor({ state: 'visible', timeout: 3000 });
- await button.click({ timeout: 5000 });
- await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 });
- }
- /**
- * 打开删除确认对话框
- * @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(300);
- }
- } 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(300);
- }
- } 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(500);
- const regionRow = regionText.locator('xpath=ancestor::div[contains(@class, "group")][1]');
- await regionRow.hover();
- await this.page.waitForTimeout(300);
- const button = regionRow.getByRole('button', { name: '删除' });
- await button.waitFor({ state: 'visible', timeout: 3000 });
- await button.click({ timeout: 5000 });
- // 等待删除确认对话框出现
- await this.page.waitForSelector('[role="alertdialog"]', { state: 'visible', timeout: 5000 });
- }
- /**
- * 打开状态切换确认对话框
- * @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: 5000 });
- // 滚动到元素位置
- await regionText.scrollIntoViewIfNeeded();
- await this.page.waitForTimeout(300);
- const regionRow = regionText.locator('xpath=ancestor::div[contains(@class, "group")][1]');
- await regionRow.hover();
- await this.page.waitForTimeout(300);
- // 在区域行内查找"启用"或"禁用"按钮(操作按钮组中的状态切换按钮)
- const statusButton = regionRow.getByRole('button', { name: /^(启用|禁用)$/ });
- await statusButton.waitFor({ state: 'visible', timeout: 3000 });
- await statusButton.click({ timeout: 5000 });
- // 等待状态切换确认对话框出现
- await this.page.waitForSelector('[role="alertdialog"]', { state: 'visible', timeout: 5000 });
- }
- /**
- * 填写区域表单
- * @param data 区域数据
- */
- async fillRegionForm(data: RegionData) {
- // 等待表单出现
- await this.page.waitForSelector('form', { state: 'visible', timeout: 5000 });
- // 填写区域名称
- if (data.name) {
- await this.page.getByLabel('区域名称').fill(data.name);
- }
- // 填写行政区划代码
- if (data.code) {
- await this.page.getByLabel('行政区划代码').fill(data.code);
- }
- }
- /**
- * 提交表单
- * @returns 表单提交结果
- */
- async submitForm(): Promise<FormSubmitResult> {
- // 收集网络响应
- 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: 5000 });
- } catch {
- // domcontentloaded 也可能失败,继续执行
- }
- // 额外等待,给 API 响应一些时间
- await this.page.waitForTimeout(1000);
- // 移除监听器
- this.page.off('response', responseHandler);
- // 等待对话框关闭或错误出现
- await this.page.waitForTimeout(1500);
- // 检查 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: 5000 }).catch(() => {});
- await this.page.waitForTimeout(500);
- }
- /**
- * 确认删除操作
- */
- async confirmDelete() {
- const confirmButton = this.page.getByRole('button', { name: /^确认删除$/ });
- await confirmButton.click();
- // 等待确认对话框关闭和网络请求完成
- await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: 5000 }).catch(() => {});
- // 使用更宽松的等待策略
- try {
- await this.page.waitForLoadState('domcontentloaded', { timeout: 5000 });
- } catch {
- // 继续执行
- }
- await this.page.waitForTimeout(1000);
- }
- /**
- * 取消删除操作
- */
- async cancelDelete() {
- const cancelButton = this.page.locator('[role="alertdialog"]').getByRole('button', { name: '取消' });
- await cancelButton.click();
- await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: 5000 }).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(2000);
- 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: 5000 }).catch(() => {});
- // 使用更宽松的等待策略
- try {
- await this.page.waitForLoadState('domcontentloaded', { timeout: 5000 });
- } catch {
- // 继续执行
- }
- await this.page.waitForTimeout(1000);
- }
- /**
- * 取消状态切换操作
- */
- async cancelToggleStatus() {
- const cancelButton = this.page.locator('[role="alertdialog"]').getByRole('button', { name: '取消' });
- await cancelButton.click();
- await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: 5000 }).catch(() => {});
- }
- /**
- * 验证区域是否存在
- * @param regionName 区域名称
- * @returns 区域是否存在
- */
- async regionExists(regionName: string): Promise<boolean> {
- console.debug(`regionExists: 查找 "${regionName}"`);
- try {
- // 等待元素出现,最多等待 5 秒
- const regionElement = this.treeContainer.getByText(regionName, { exact: true });
- await regionElement.waitFor({ state: 'attached', timeout: 5000 });
- 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: 5000 });
- // 滚动到元素位置
- await regionText.scrollIntoViewIfNeeded();
- await this.page.waitForTimeout(300);
- // 找到区域节点的展开按钮
- 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(200);
- // 点击展开按钮
- await expandButton.click({ timeout: 5000 });
- // 等待懒加载的子节点出现
- await this.page.waitForTimeout(500);
- 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(500);
- }
- }
- /**
- * 获取区域的状态
- * @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(500);
- // 根据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<FormSubmitResult> {
- 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<FormSubmitResult> {
- 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<FormSubmitResult> {
- 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<boolean> {
- await this.openDeleteDialog(regionName);
- await this.confirmDelete();
- // 等待并检查 Toast 消息
- await this.page.waitForTimeout(1000);
- 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<boolean> {
- // 先确保区域在树中可见
- 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(1000);
- const successToast = this.page.locator('[data-sonner-toast][data-type="success"]');
- const hasSuccess = await successToast.count() > 0;
- // 等待树形结构刷新以显示更新后的状态
- try {
- await this.page.waitForLoadState('domcontentloaded', { timeout: 5000 });
- } catch {
- // 继续执行
- }
- await this.waitForTreeLoaded();
- console.debug(`toggleRegionStatus 完成: 区域="${regionName}", hasSuccess=${hasSuccess}`);
- return hasSuccess;
- }
- /**
- * 等待树形结构加载完成
- */
- async waitForTreeLoaded() {
- await this.treeContainer.waitFor({ state: 'visible', timeout: 20000 });
- // 等待加载文本消失(使用更健壮的选择器)
- // 加载文本位于 CardContent 中,带有 text-muted-foreground 类
- await this.page.locator('.text-muted-foreground', { hasText: '加载中' }).waitFor({ state: 'hidden', timeout: 10000 }).catch(() => {});
- }
- /**
- * 刷新树形结构(用于创建新区域后强制刷新)
- * 由于树组件使用懒加载缓存,新创建的区域不会自动显示
- * 此方法通过重新导航到页面来强制刷新树数据
- */
- async refreshTree() {
- console.debug('刷新树形结构以显示新创建的区域');
- // 重新导航到当前页面,强制刷新所有数据
- await this.page.reload();
- // 等待页面加载完成
- await this.page.waitForLoadState('domcontentloaded', { timeout: 15000 });
- // 等待树形结构加载完成
- await this.treeContainer.waitFor({ state: 'visible', timeout: 20000 });
- await this.pageTitle.waitFor({ state: 'visible', timeout: 15000 });
- // 等待懒加载完成
- await this.page.locator('.text-muted-foreground', { hasText: '加载中' }).waitFor({ state: 'hidden', timeout: 10000 }).catch(() => {});
- console.debug('树形结构刷新完成');
- }
- }
|