import { Page, Locator, expect } from '@playwright/test'; export class ActivityManagementPage { readonly page: Page; readonly pageTitle: Locator; readonly createActivityButton: Locator; readonly searchInput: Locator; readonly searchButton: Locator; readonly activityTable: Locator; readonly editButtons: Locator; readonly deleteButtons: Locator; readonly statusToggleButtons: Locator; readonly pagination: Locator; readonly typeFilter: Locator; constructor(page: Page) { this.page = page; this.pageTitle = page.locator('[data-testid="activity-management-title"]'); this.createActivityButton = page.locator('[data-testid="create-activity-button"]'); this.searchInput = page.locator('[data-testid="activity-search-input"]'); this.searchButton = page.getByRole('button', { name: '搜索' }); this.activityTable = page.locator('[data-testid="activity-table"]'); this.editButtons = page.locator('[data-testid^="edit-activity-"]'); this.deleteButtons = page.locator('[data-testid^="delete-activity-"]'); this.statusToggleButtons = page.locator('[data-testid^="toggle-activity-"]'); this.pagination = page.locator('[data-slot="pagination"]'); this.typeFilter = page.locator('[data-testid="activity-type-filter"]'); } async goto() { // 直接导航到活动管理页面 await this.page.goto('/admin/activities'); // 等待页面完全加载 await this.page.waitForLoadState('domcontentloaded'); // 等待活动管理标题出现 await this.page.waitForSelector('h1:has-text("活动管理")', { state: 'visible', timeout: 15000 }); // 等待表格数据加载完成 await this.page.waitForSelector('table tbody tr', { state: 'visible', timeout: 20000 }); await this.expectToBeVisible(); } async expectToBeVisible() { // 等待页面完全加载 await expect(this.pageTitle).toBeVisible({ timeout: 15000 }); await expect(this.createActivityButton).toBeVisible({ timeout: 10000 }); // 等待至少一行活动数据加载完成 await expect(this.activityTable.locator('tbody tr').first()).toBeVisible({ timeout: 20000 }); } async searchActivities(keyword: string) { await this.searchInput.fill(keyword); // 等待防抖搜索完成(300ms + 网络请求时间) await this.page.waitForTimeout(500); await this.page.waitForLoadState('networkidle'); } async filterByType(type: '去程' | '返程') { await this.typeFilter.click(); await this.page.getByRole('option', { name: type }).click(); await this.page.waitForLoadState('networkidle'); } async createActivity(activityData: { name: string; description?: string; type: '去程' | '返程'; startDate: string; endDate: string; }) { await this.createActivityButton.click(); // 填写活动表单 await this.page.getByLabel('活动名称').fill(activityData.name); if (activityData.description) { await this.page.getByLabel('活动描述').fill(activityData.description); } // 选择活动类型 await this.page.getByLabel('活动类型').click(); await this.page.getByRole('option', { name: activityData.type }).click(); // 填写开始日期和结束日期 await this.page.getByLabel('开始日期').fill(activityData.startDate); await this.page.getByLabel('结束日期').fill(activityData.endDate); // 提交表单 - 使用模态框中的创建按钮 await this.page.locator('[role="dialog"]').getByRole('button', { name: '创建活动' }).click(); // 等待模态框关闭 await this.page.waitForSelector('[role="dialog"]', { state: 'hidden', timeout: 10000 }); await this.page.waitForLoadState('networkidle'); // 等待活动创建结果提示 try { await Promise.race([ this.page.waitForSelector('text=创建成功', { timeout: 10000 }), this.page.waitForSelector('text=创建失败', { timeout: 10000 }) ]); // 检查是否有错误提示 const errorVisible = await this.page.locator('text=创建失败').isVisible().catch(() => false); if (errorVisible) { console.log('创建活动失败:前端显示创建失败提示'); return; } // 如果是创建成功,刷新页面 await this.page.waitForTimeout(2000); await this.page.reload(); await this.page.waitForLoadState('networkidle'); await this.expectToBeVisible(); } catch (error) { // 如果没有提示出现,继续执行 console.log('创建操作没有显示提示信息,继续执行'); await this.page.waitForTimeout(2000); await this.page.waitForLoadState('networkidle'); } } async getActivityCount(): Promise { const rows = await this.activityTable.locator('tbody tr').count(); // 如果只有一行且显示"暂无活动数据",则返回0 if (rows === 1) { const firstRowText = await this.activityTable.locator('tbody tr').first().textContent(); if (firstRowText?.includes('暂无活动数据')) { return 0; } } return rows; } async getActivityByName(name: string): Promise { const activityRow = this.activityTable.locator('tbody tr').filter({ hasText: name }).first(); return (await activityRow.count()) > 0 ? activityRow : null; } async activityExists(name: string): Promise { const activityRow = this.activityTable.locator('tbody tr').filter({ hasText: name }).first(); return (await activityRow.count()) > 0; } async editActivity(name: string, updates: { name?: string; description?: string; type?: '去程活动' | '返程活动'; startDate?: string; endDate?: string; }) { const activityRow = await this.getActivityByName(name); if (!activityRow) throw new Error(`Activity ${name} not found`); // 使用data-testid定位编辑按钮 const editButton = activityRow.locator('[data-testid^="edit-activity-"]'); await editButton.waitFor({ state: 'visible', timeout: 10000 }); await editButton.click(); // 等待编辑模态框出现 await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 10000 }); // 更新字段 if (updates.name) { await this.page.getByLabel('活动名称').fill(updates.name); } if (updates.description) { await this.page.getByLabel('活动描述').fill(updates.description); } if (updates.type) { await this.page.getByLabel('活动类型').click(); await this.page.getByRole('option', { name: updates.type }).click(); } if (updates.startDate) { await this.page.getByLabel('开始日期').fill(updates.startDate); } if (updates.endDate) { await this.page.getByLabel('结束日期').fill(updates.endDate); } // 提交更新 await this.page.locator('[role="dialog"]').getByRole('button', { name: '更新活动' }).click(); await this.page.waitForLoadState('networkidle'); // 等待操作完成 await this.page.waitForTimeout(1000); } async deleteActivity(name: string) { const activityRow = await this.getActivityByName(name); if (!activityRow) throw new Error(`Activity ${name} not found`); // 使用data-testid定位删除按钮 const deleteButton = activityRow.locator('[data-testid^="delete-activity-"]'); await deleteButton.waitFor({ state: 'visible', timeout: 10000 }); await deleteButton.click(); // 等待删除确认对话框出现 - 使用data-testid await this.page.waitForSelector('[data-testid="delete-confirm-dialog"]', { state: 'visible', timeout: 10000 }); // 确认删除 - 点击删除按钮 await this.page.locator('[data-testid="delete-confirm-dialog"]').getByRole('button', { name: '删除' }).click(); // 等待删除操作完成 try { await Promise.race([ this.page.waitForSelector('text=删除成功', { timeout: 10000 }), this.page.waitForSelector('text=删除失败', { timeout: 10000 }) ]); const errorVisible = await this.page.locator('text=删除失败').isVisible().catch(() => false); if (errorVisible) { throw new Error('删除操作失败:前端显示删除失败提示'); } } catch (error) { console.log('删除操作没有显示提示信息,继续执行'); } // 等待页面状态稳定,不需要强制刷新 await this.page.waitForLoadState('networkidle'); } async toggleActivityStatus(name: string) { const activityRow = await this.getActivityByName(name); if (!activityRow) throw new Error(`Activity ${name} not found`); // 使用data-testid定位状态切换按钮 const statusButton = activityRow.locator('[data-testid^="toggle-activity-"]'); await statusButton.waitFor({ state: 'visible', timeout: 10000 }); // 获取当前状态(从状态单元格获取,不是按钮文本) const currentStatus = await this.getActivityStatus(name); await statusButton.click(); // 等待状态切换确认对话框出现 - 使用data-testid await this.page.waitForSelector('[data-testid="status-confirm-dialog"]', { state: 'visible', timeout: 10000 }); // 确认状态切换 - 点击禁用或启用按钮 // 注意:按钮文本是当前要执行的操作,不是当前状态 const actionText = currentStatus === '启用' ? '禁用' : '启用'; await this.page.locator('[data-testid="status-confirm-dialog"]').getByRole('button', { name: actionText }).click(); // 等待操作完成 await this.page.waitForLoadState('networkidle'); await this.page.waitForTimeout(1000); return currentStatus; } async expectActivityExists(name: string) { const exists = await this.activityExists(name); expect(exists).toBe(true); } async expectActivityNotExists(name: string) { const exists = await this.activityExists(name); expect(exists).toBe(false); } async getActivityStatus(name: string): Promise { const activityRow = await this.getActivityByName(name); if (!activityRow) return null; // 状态文本在第五列(索引4) const statusCell = activityRow.locator('td').nth(4); return await statusCell.textContent(); } }