|
@@ -1,5 +1,27 @@
|
|
|
import { Page, Locator } from '@playwright/test';
|
|
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;
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 区域数据接口
|
|
* 区域数据接口
|
|
|
*/
|
|
*/
|
|
@@ -9,22 +31,41 @@ export interface RegionData {
|
|
|
/** 行政区划代码 */
|
|
/** 行政区划代码 */
|
|
|
code?: string;
|
|
code?: string;
|
|
|
/** 区域层级(1=省, 2=市, 3=区) */
|
|
/** 区域层级(1=省, 2=市, 3=区) */
|
|
|
- level?: 1 | 2 | 3;
|
|
|
|
|
|
|
+ level?: RegionLevel;
|
|
|
/** 父级区域ID */
|
|
/** 父级区域ID */
|
|
|
parentId?: number | null;
|
|
parentId?: number | null;
|
|
|
/** 状态(0=启用, 1=禁用) */
|
|
/** 状态(0=启用, 1=禁用) */
|
|
|
- isDisabled?: 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 {
|
|
export interface FormSubmitResult {
|
|
|
|
|
+ /** 提交是否成功 */
|
|
|
success: boolean;
|
|
success: boolean;
|
|
|
|
|
+ /** 是否有错误 */
|
|
|
hasError: boolean;
|
|
hasError: boolean;
|
|
|
|
|
+ /** 是否有成功消息 */
|
|
|
hasSuccess: boolean;
|
|
hasSuccess: boolean;
|
|
|
|
|
+ /** 错误消息 */
|
|
|
errorMessage?: string;
|
|
errorMessage?: string;
|
|
|
|
|
+ /** 成功消息 */
|
|
|
successMessage?: string;
|
|
successMessage?: string;
|
|
|
|
|
+ /** 网络响应列表 */
|
|
|
|
|
+ responses?: NetworkResponse[];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -50,9 +91,13 @@ export class RegionManagementPage {
|
|
|
|
|
|
|
|
constructor(page: Page) {
|
|
constructor(page: Page) {
|
|
|
this.page = page;
|
|
this.page = page;
|
|
|
- this.pageTitle = page.getByText('省市区树形管理');
|
|
|
|
|
- this.addProvinceButton = page.getByRole('button', { name: '新增省' });
|
|
|
|
|
- this.treeContainer = page.locator('.border.rounded-lg.bg-background');
|
|
|
|
|
|
|
+ // 使用精确文本匹配获取页面标题
|
|
|
|
|
+ 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();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -137,11 +182,14 @@ export class RegionManagementPage {
|
|
|
*/
|
|
*/
|
|
|
async openToggleStatusDialog(regionName: string) {
|
|
async openToggleStatusDialog(regionName: string) {
|
|
|
// 找到区域节点并点击"启用"或"禁用"按钮
|
|
// 找到区域节点并点击"启用"或"禁用"按钮
|
|
|
- const button = this.treeContainer.getByText(regionName)
|
|
|
|
|
- .locator('../../..')
|
|
|
|
|
- .locator('button', { hasText: /^(启用|禁用)$/ });
|
|
|
|
|
|
|
+ // 使用更精确的选择器:在节点行内查找操作按钮组中的状态切换按钮
|
|
|
|
|
+ const regionRow = this.treeContainer.getByText(regionName, { exact: true }).locator('xpath=ancestor::div[contains(@class, "group")][1]');
|
|
|
|
|
+ // 在操作按钮组中查找状态切换按钮(第3个按钮:编辑、状态切换、删除)
|
|
|
|
|
+ const statusButton = regionRow.getByRole('button').filter({ hasText: /^(启用|禁用)$/ }).and(
|
|
|
|
|
+ regionRow.locator('xpath=./div[contains(@class, "flex") and contains(@class, "gap")]//button[position()=3]')
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
- await button.click();
|
|
|
|
|
|
|
+ await statusButton.click();
|
|
|
// 等待状态切换确认对话框出现
|
|
// 等待状态切换确认对话框出现
|
|
|
await this.page.waitForSelector('[role="alertdialog"]', { state: 'visible', timeout: 5000 });
|
|
await this.page.waitForSelector('[role="alertdialog"]', { state: 'visible', timeout: 5000 });
|
|
|
}
|
|
}
|
|
@@ -170,6 +218,36 @@ export class RegionManagementPage {
|
|
|
* @returns 表单提交结果
|
|
* @returns 表单提交结果
|
|
|
*/
|
|
*/
|
|
|
async submitForm(): Promise<FormSubmitResult> {
|
|
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: /^(创建|更新)$/ });
|
|
const submitButton = this.page.getByRole('button', { name: /^(创建|更新)$/ });
|
|
|
await submitButton.click();
|
|
await submitButton.click();
|
|
@@ -177,6 +255,9 @@ export class RegionManagementPage {
|
|
|
// 等待网络请求完成
|
|
// 等待网络请求完成
|
|
|
await this.page.waitForLoadState('networkidle', { timeout: 10000 });
|
|
await this.page.waitForLoadState('networkidle', { timeout: 10000 });
|
|
|
|
|
|
|
|
|
|
+ // 移除监听器
|
|
|
|
|
+ this.page.off('response', responseHandler);
|
|
|
|
|
+
|
|
|
// 等待 Toast 消息显示
|
|
// 等待 Toast 消息显示
|
|
|
await this.page.waitForTimeout(2000);
|
|
await this.page.waitForTimeout(2000);
|
|
|
|
|
|
|
@@ -187,8 +268,8 @@ export class RegionManagementPage {
|
|
|
const hasError = await errorToast.count() > 0;
|
|
const hasError = await errorToast.count() > 0;
|
|
|
const hasSuccess = await successToast.count() > 0;
|
|
const hasSuccess = await successToast.count() > 0;
|
|
|
|
|
|
|
|
- let errorMessage = null;
|
|
|
|
|
- let successMessage = null;
|
|
|
|
|
|
|
+ let errorMessage: string | null = null;
|
|
|
|
|
+ let successMessage: string | null = null;
|
|
|
|
|
|
|
|
if (hasError) {
|
|
if (hasError) {
|
|
|
errorMessage = await errorToast.first().textContent();
|
|
errorMessage = await errorToast.first().textContent();
|
|
@@ -203,6 +284,7 @@ export class RegionManagementPage {
|
|
|
hasSuccess,
|
|
hasSuccess,
|
|
|
errorMessage: errorMessage ?? undefined,
|
|
errorMessage: errorMessage ?? undefined,
|
|
|
successMessage: successMessage ?? undefined,
|
|
successMessage: successMessage ?? undefined,
|
|
|
|
|
+ responses,
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -283,15 +365,14 @@ export class RegionManagementPage {
|
|
|
* @param regionName 区域名称
|
|
* @param regionName 区域名称
|
|
|
*/
|
|
*/
|
|
|
async expandNode(regionName: string) {
|
|
async expandNode(regionName: string) {
|
|
|
- // 找到区域节点的展开按钮(向右的箭头图标)
|
|
|
|
|
- const expandButton = this.treeContainer.getByText(regionName)
|
|
|
|
|
- .locator('../../..')
|
|
|
|
|
- .locator('button')
|
|
|
|
|
- .filter({ has: this.page.locator('svg[data-lucide="chevron-right"]') });
|
|
|
|
|
|
|
+ // 找到区域节点的展开按钮
|
|
|
|
|
+ // 使用更健壮的选择器:在节点行内查找第一个小尺寸按钮(展开/收起按钮总是第一个)
|
|
|
|
|
+ const regionRow = this.treeContainer.getByText(regionName, { exact: true }).locator('xpath=ancestor::div[contains(@class, "group")][1]');
|
|
|
|
|
+ const expandButton = regionRow.locator('button').filter({ has: regionRow.locator('svg') }).first();
|
|
|
|
|
|
|
|
const count = await expandButton.count();
|
|
const count = await expandButton.count();
|
|
|
if (count > 0) {
|
|
if (count > 0) {
|
|
|
- await expandButton.first().click();
|
|
|
|
|
|
|
+ await expandButton.click();
|
|
|
await this.page.waitForTimeout(500);
|
|
await this.page.waitForTimeout(500);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -301,15 +382,13 @@ export class RegionManagementPage {
|
|
|
* @param regionName 区域名称
|
|
* @param regionName 区域名称
|
|
|
*/
|
|
*/
|
|
|
async collapseNode(regionName: string) {
|
|
async collapseNode(regionName: string) {
|
|
|
- // 找到区域节点的收起按钮(向下的箭头图标)
|
|
|
|
|
- const collapseButton = this.treeContainer.getByText(regionName)
|
|
|
|
|
- .locator('../../..')
|
|
|
|
|
- .locator('button')
|
|
|
|
|
- .filter({ has: this.page.locator('svg[data-lucide="chevron-down"]') });
|
|
|
|
|
|
|
+ // 找到区域节点的收起按钮
|
|
|
|
|
+ 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();
|
|
const count = await collapseButton.count();
|
|
|
if (count > 0) {
|
|
if (count > 0) {
|
|
|
- await collapseButton.first().click();
|
|
|
|
|
|
|
+ await collapseButton.click();
|
|
|
await this.page.waitForTimeout(500);
|
|
await this.page.waitForTimeout(500);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -320,14 +399,16 @@ export class RegionManagementPage {
|
|
|
* @returns 区域状态('启用' 或 '禁用')
|
|
* @returns 区域状态('启用' 或 '禁用')
|
|
|
*/
|
|
*/
|
|
|
async getRegionStatus(regionName: string): Promise<'启用' | '禁用' | null> {
|
|
async getRegionStatus(regionName: string): Promise<'启用' | '禁用' | null> {
|
|
|
- const regionRow = this.treeContainer.getByText(regionName).locator('../../..');
|
|
|
|
|
|
|
+ const regionRow = this.treeContainer.getByText(regionName, { exact: true }).locator('xpath=ancestor::div[contains(@class, "group")][1]');
|
|
|
|
|
+ // 使用更精确的选择器:查找包含"启用"或"禁用"文本的 Badge
|
|
|
|
|
+ // 根据 Badge 变体:启用=variant="default",禁用=variant="secondary"
|
|
|
const statusBadge = regionRow.locator('.badge').filter({ hasText: /^(启用|禁用)$/ });
|
|
const statusBadge = regionRow.locator('.badge').filter({ hasText: /^(启用|禁用)$/ });
|
|
|
|
|
|
|
|
const count = await statusBadge.count();
|
|
const count = await statusBadge.count();
|
|
|
if (count === 0) return null;
|
|
if (count === 0) return null;
|
|
|
|
|
|
|
|
const text = await statusBadge.first().textContent();
|
|
const text = await statusBadge.first().textContent();
|
|
|
- return (text === '启用' || text === '禁用') ? text : null;
|
|
|
|
|
|
|
+ return text === '启用' || text === '禁用' ? text : null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -415,7 +496,8 @@ export class RegionManagementPage {
|
|
|
*/
|
|
*/
|
|
|
async waitForTreeLoaded() {
|
|
async waitForTreeLoaded() {
|
|
|
await this.treeContainer.waitFor({ state: 'visible', timeout: 20000 });
|
|
await this.treeContainer.waitFor({ state: 'visible', timeout: 20000 });
|
|
|
- // 等待加载文本消失
|
|
|
|
|
- await this.page.waitForSelector('text=加载中...', { state: 'hidden', timeout: 10000 }).catch(() => {});
|
|
|
|
|
|
|
+ // 等待加载文本消失(使用更健壮的选择器)
|
|
|
|
|
+ // 加载文本位于 CardContent 中,带有 text-muted-foreground 类
|
|
|
|
|
+ await this.page.locator('.text-muted-foreground', { hasText: '加载中' }).waitFor({ state: 'hidden', timeout: 10000 }).catch(() => {});
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|