| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421 |
- import { Page, Locator } from '@playwright/test';
- /**
- * 区域数据接口
- */
- export interface RegionData {
- /** 区域名称 */
- name: string;
- /** 行政区划代码 */
- code?: string;
- /** 区域层级(1=省, 2=市, 3=区) */
- level?: 1 | 2 | 3;
- /** 父级区域ID */
- parentId?: number | null;
- /** 状态(0=启用, 1=禁用) */
- isDisabled?: 0 | 1;
- }
- /**
- * 表单提交结果
- */
- export interface FormSubmitResult {
- success: boolean;
- hasError: boolean;
- hasSuccess: boolean;
- errorMessage?: string;
- successMessage?: string;
- }
- /**
- * 区域管理 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('省市区树形管理');
- this.addProvinceButton = page.getByRole('button', { name: '新增省' });
- this.treeContainer = page.locator('.border.rounded-lg.bg-background');
- }
- /**
- * 导航到区域管理页面
- */
- 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 button = this.treeContainer.getByText(parentName)
- .locator('../../..')
- .getByRole('button', { name: childType === '市' ? '新增市' : '新增区' });
- await button.click();
- // 等待对话框出现
- await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 });
- }
- /**
- * 打开编辑区域对话框
- * @param regionName 区域名称
- */
- async openEditDialog(regionName: string) {
- // 找到区域节点并点击"编辑"按钮
- const button = this.treeContainer.getByText(regionName)
- .locator('../../..')
- .getByRole('button', { name: '编辑' });
- await button.click();
- // 等待对话框出现
- await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 });
- }
- /**
- * 打开删除确认对话框
- * @param regionName 区域名称
- */
- async openDeleteDialog(regionName: string) {
- // 找到区域节点并点击"删除"按钮
- const button = this.treeContainer.getByText(regionName)
- .locator('../../..')
- .getByRole('button', { name: '删除' });
- await button.click();
- // 等待删除确认对话框出现
- await this.page.waitForSelector('[role="alertdialog"]', { state: 'visible', timeout: 5000 });
- }
- /**
- * 打开状态切换确认对话框
- * @param regionName 区域名称
- */
- async openToggleStatusDialog(regionName: string) {
- // 找到区域节点并点击"启用"或"禁用"按钮
- const button = this.treeContainer.getByText(regionName)
- .locator('../../..')
- .locator('button', { hasText: /^(启用|禁用)$/ });
- await button.click();
- // 等待状态切换确认对话框出现
- 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 submitButton = this.page.getByRole('button', { name: /^(创建|更新)$/ });
- await submitButton.click();
- // 等待网络请求完成
- await this.page.waitForLoadState('networkidle', { timeout: 10000 });
- // 等待 Toast 消息显示
- await this.page.waitForTimeout(2000);
- // 检查 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 = null;
- let successMessage = 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,
- };
- }
- /**
- * 取消对话框
- */
- 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(() => {});
- await this.page.waitForLoadState('networkidle', { timeout: 10000 });
- await this.page.waitForTimeout(1000);
- }
- /**
- * 取消删除操作
- */
- async cancelDelete() {
- const cancelButton = this.page.getByRole('button', { name: '取消' }).and(
- this.page.locator('[role="alertdialog"]')
- );
- await cancelButton.click();
- await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: 5000 }).catch(() => {});
- }
- /**
- * 确认状态切换操作
- */
- async confirmToggleStatus() {
- const confirmButton = this.page.locator('[role="alertdialog"]').getByRole('button', { name: '确认' });
- await confirmButton.click();
- // 等待确认对话框关闭和网络请求完成
- await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: 5000 }).catch(() => {});
- await this.page.waitForLoadState('networkidle', { timeout: 10000 });
- 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> {
- const regionElement = this.treeContainer.getByText(regionName);
- return (await regionElement.count()) > 0;
- }
- /**
- * 展开区域节点
- * @param regionName 区域名称
- */
- async expandNode(regionName: string) {
- // 找到区域节点的展开按钮(向右的箭头图标)
- const expandButton = this.treeContainer.getByText(regionName)
- .locator('../../..')
- .locator('button')
- .filter({ has: this.page.locator('svg[data-lucide="chevron-right"]') });
- const count = await expandButton.count();
- if (count > 0) {
- await expandButton.first().click();
- await this.page.waitForTimeout(500);
- }
- }
- /**
- * 收起区域节点
- * @param regionName 区域名称
- */
- async collapseNode(regionName: string) {
- // 找到区域节点的收起按钮(向下的箭头图标)
- const collapseButton = this.treeContainer.getByText(regionName)
- .locator('../../..')
- .locator('button')
- .filter({ has: this.page.locator('svg[data-lucide="chevron-down"]') });
- const count = await collapseButton.count();
- if (count > 0) {
- await collapseButton.first().click();
- await this.page.waitForTimeout(500);
- }
- }
- /**
- * 获取区域的状态
- * @param regionName 区域名称
- * @returns 区域状态('启用' 或 '禁用')
- */
- async getRegionStatus(regionName: string): Promise<'启用' | '禁用' | null> {
- const regionRow = this.treeContainer.getByText(regionName).locator('../../..');
- const statusBadge = regionRow.locator('.badge').filter({ hasText: /^(启用|禁用)$/ });
- const count = await statusBadge.count();
- if (count === 0) return null;
- const text = await statusBadge.first().textContent();
- return (text === '启用' || text === '禁用') ? text : 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.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;
- return hasSuccess;
- }
- /**
- * 等待树形结构加载完成
- */
- async waitForTreeLoaded() {
- await this.treeContainer.waitFor({ state: 'visible', timeout: 20000 });
- // 等待加载文本消失
- await this.page.waitForSelector('text=加载中...', { state: 'hidden', timeout: 10000 }).catch(() => {});
- }
- }
|