activity-management.page.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. import { Page, Locator, expect } from '@playwright/test';
  2. export class ActivityManagementPage {
  3. readonly page: Page;
  4. readonly pageTitle: Locator;
  5. readonly createActivityButton: Locator;
  6. readonly searchInput: Locator;
  7. readonly searchButton: Locator;
  8. readonly activityTable: Locator;
  9. readonly editButtons: Locator;
  10. readonly deleteButtons: Locator;
  11. readonly statusToggleButtons: Locator;
  12. readonly pagination: Locator;
  13. readonly typeFilter: Locator;
  14. constructor(page: Page) {
  15. this.page = page;
  16. this.pageTitle = page.locator('[data-testid="activity-management-title"]');
  17. this.createActivityButton = page.locator('[data-testid="create-activity-button"]');
  18. this.searchInput = page.locator('[data-testid="activity-search-input"]');
  19. this.searchButton = page.getByRole('button', { name: '搜索' });
  20. this.activityTable = page.locator('[data-testid="activity-table"]');
  21. this.editButtons = page.locator('[data-testid^="edit-activity-"]');
  22. this.deleteButtons = page.locator('[data-testid^="delete-activity-"]');
  23. this.statusToggleButtons = page.locator('[data-testid^="toggle-activity-"]');
  24. this.pagination = page.locator('[data-slot="pagination"]');
  25. this.typeFilter = page.locator('[data-testid="activity-type-filter"]');
  26. }
  27. async goto() {
  28. // 直接导航到活动管理页面
  29. await this.page.goto('/admin/activities');
  30. // 等待页面完全加载
  31. await this.page.waitForLoadState('domcontentloaded');
  32. // 等待活动管理标题出现
  33. await this.page.waitForSelector('h1:has-text("活动管理")', { state: 'visible', timeout: 15000 });
  34. // 等待表格数据加载完成
  35. await this.page.waitForSelector('table tbody tr', { state: 'visible', timeout: 20000 });
  36. await this.expectToBeVisible();
  37. }
  38. async expectToBeVisible() {
  39. // 等待页面完全加载
  40. await expect(this.pageTitle).toBeVisible({ timeout: 15000 });
  41. await expect(this.createActivityButton).toBeVisible({ timeout: 10000 });
  42. // 等待至少一行活动数据加载完成
  43. await expect(this.activityTable.locator('tbody tr').first()).toBeVisible({ timeout: 20000 });
  44. }
  45. async searchActivities(keyword: string) {
  46. await this.searchInput.fill(keyword);
  47. // 等待防抖搜索完成(300ms + 网络请求时间)
  48. await this.page.waitForTimeout(500);
  49. await this.page.waitForLoadState('networkidle');
  50. }
  51. async filterByType(type: '去程' | '返程') {
  52. await this.typeFilter.click();
  53. await this.page.getByRole('option', { name: type }).click();
  54. await this.page.waitForLoadState('networkidle');
  55. }
  56. async createActivity(activityData: {
  57. name: string;
  58. description?: string;
  59. type: '去程' | '返程';
  60. startDate: string;
  61. endDate: string;
  62. }) {
  63. await this.createActivityButton.click();
  64. // 填写活动表单
  65. await this.page.getByLabel('活动名称').fill(activityData.name);
  66. if (activityData.description) {
  67. await this.page.getByLabel('活动描述').fill(activityData.description);
  68. }
  69. // 选择活动类型
  70. await this.page.getByLabel('活动类型').click();
  71. await this.page.getByRole('option', { name: activityData.type }).click();
  72. // 填写开始日期和结束日期
  73. await this.page.getByLabel('开始日期').fill(activityData.startDate);
  74. await this.page.getByLabel('结束日期').fill(activityData.endDate);
  75. // 提交表单 - 使用模态框中的创建按钮
  76. await this.page.locator('[role="dialog"]').getByRole('button', { name: '创建活动' }).click();
  77. // 等待模态框关闭
  78. await this.page.waitForSelector('[role="dialog"]', { state: 'hidden', timeout: 10000 });
  79. await this.page.waitForLoadState('networkidle');
  80. // 等待活动创建结果提示
  81. try {
  82. await Promise.race([
  83. this.page.waitForSelector('text=创建成功', { timeout: 10000 }),
  84. this.page.waitForSelector('text=创建失败', { timeout: 10000 })
  85. ]);
  86. // 检查是否有错误提示
  87. const errorVisible = await this.page.locator('text=创建失败').isVisible().catch(() => false);
  88. if (errorVisible) {
  89. console.log('创建活动失败:前端显示创建失败提示');
  90. return;
  91. }
  92. // 如果是创建成功,刷新页面
  93. await this.page.waitForTimeout(2000);
  94. await this.page.reload();
  95. await this.page.waitForLoadState('networkidle');
  96. await this.expectToBeVisible();
  97. } catch (error) {
  98. // 如果没有提示出现,继续执行
  99. console.log('创建操作没有显示提示信息,继续执行');
  100. await this.page.waitForTimeout(2000);
  101. await this.page.waitForLoadState('networkidle');
  102. }
  103. }
  104. async getActivityCount(): Promise<number> {
  105. const rows = await this.activityTable.locator('tbody tr').count();
  106. // 如果只有一行且显示"暂无活动数据",则返回0
  107. if (rows === 1) {
  108. const firstRowText = await this.activityTable.locator('tbody tr').first().textContent();
  109. if (firstRowText?.includes('暂无活动数据')) {
  110. return 0;
  111. }
  112. }
  113. return rows;
  114. }
  115. async getActivityByName(name: string): Promise<Locator | null> {
  116. const activityRow = this.activityTable.locator('tbody tr').filter({ hasText: name }).first();
  117. return (await activityRow.count()) > 0 ? activityRow : null;
  118. }
  119. async activityExists(name: string): Promise<boolean> {
  120. const activityRow = this.activityTable.locator('tbody tr').filter({ hasText: name }).first();
  121. return (await activityRow.count()) > 0;
  122. }
  123. async editActivity(name: string, updates: {
  124. name?: string;
  125. description?: string;
  126. type?: '去程活动' | '返程活动';
  127. startDate?: string;
  128. endDate?: string;
  129. }) {
  130. const activityRow = await this.getActivityByName(name);
  131. if (!activityRow) throw new Error(`Activity ${name} not found`);
  132. // 使用data-testid定位编辑按钮
  133. const editButton = activityRow.locator('[data-testid^="edit-activity-"]');
  134. await editButton.waitFor({ state: 'visible', timeout: 10000 });
  135. await editButton.click();
  136. // 等待编辑模态框出现
  137. await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 10000 });
  138. // 更新字段
  139. if (updates.name) {
  140. await this.page.getByLabel('活动名称').fill(updates.name);
  141. }
  142. if (updates.description) {
  143. await this.page.getByLabel('活动描述').fill(updates.description);
  144. }
  145. if (updates.type) {
  146. await this.page.getByLabel('活动类型').click();
  147. await this.page.getByRole('option', { name: updates.type }).click();
  148. }
  149. if (updates.startDate) {
  150. await this.page.getByLabel('开始日期').fill(updates.startDate);
  151. }
  152. if (updates.endDate) {
  153. await this.page.getByLabel('结束日期').fill(updates.endDate);
  154. }
  155. // 提交更新
  156. await this.page.locator('[role="dialog"]').getByRole('button', { name: '更新活动' }).click();
  157. await this.page.waitForLoadState('networkidle');
  158. // 等待操作完成
  159. await this.page.waitForTimeout(1000);
  160. }
  161. async deleteActivity(name: string) {
  162. const activityRow = await this.getActivityByName(name);
  163. if (!activityRow) throw new Error(`Activity ${name} not found`);
  164. // 使用data-testid定位删除按钮
  165. const deleteButton = activityRow.locator('[data-testid^="delete-activity-"]');
  166. await deleteButton.waitFor({ state: 'visible', timeout: 10000 });
  167. await deleteButton.click();
  168. // 等待删除确认对话框出现 - 使用data-testid
  169. await this.page.waitForSelector('[data-testid="delete-confirm-dialog"]', { state: 'visible', timeout: 10000 });
  170. // 确认删除 - 点击删除按钮
  171. await this.page.locator('[data-testid="delete-confirm-dialog"]').getByRole('button', { name: '删除' }).click();
  172. // 等待删除操作完成
  173. try {
  174. await Promise.race([
  175. this.page.waitForSelector('text=删除成功', { timeout: 10000 }),
  176. this.page.waitForSelector('text=删除失败', { timeout: 10000 })
  177. ]);
  178. const errorVisible = await this.page.locator('text=删除失败').isVisible().catch(() => false);
  179. if (errorVisible) {
  180. throw new Error('删除操作失败:前端显示删除失败提示');
  181. }
  182. } catch (error) {
  183. console.log('删除操作没有显示提示信息,继续执行');
  184. }
  185. // 等待页面状态稳定,不需要强制刷新
  186. await this.page.waitForLoadState('networkidle');
  187. }
  188. async toggleActivityStatus(name: string) {
  189. const activityRow = await this.getActivityByName(name);
  190. if (!activityRow) throw new Error(`Activity ${name} not found`);
  191. // 使用data-testid定位状态切换按钮
  192. const statusButton = activityRow.locator('[data-testid^="toggle-activity-"]');
  193. await statusButton.waitFor({ state: 'visible', timeout: 10000 });
  194. // 获取当前状态(从状态单元格获取,不是按钮文本)
  195. const currentStatus = await this.getActivityStatus(name);
  196. await statusButton.click();
  197. // 等待状态切换确认对话框出现 - 使用data-testid
  198. await this.page.waitForSelector('[data-testid="status-confirm-dialog"]', { state: 'visible', timeout: 10000 });
  199. // 确认状态切换 - 点击禁用或启用按钮
  200. // 注意:按钮文本是当前要执行的操作,不是当前状态
  201. const actionText = currentStatus === '启用' ? '禁用' : '启用';
  202. await this.page.locator('[data-testid="status-confirm-dialog"]').getByRole('button', { name: actionText }).click();
  203. // 等待操作完成
  204. await this.page.waitForLoadState('networkidle');
  205. await this.page.waitForTimeout(1000);
  206. return currentStatus;
  207. }
  208. async expectActivityExists(name: string) {
  209. const exists = await this.activityExists(name);
  210. expect(exists).toBe(true);
  211. }
  212. async expectActivityNotExists(name: string) {
  213. const exists = await this.activityExists(name);
  214. expect(exists).toBe(false);
  215. }
  216. async getActivityStatus(name: string): Promise<string | null> {
  217. const activityRow = await this.getActivityByName(name);
  218. if (!activityRow) return null;
  219. // 状态文本在第五列(索引4)
  220. const statusCell = activityRow.locator('td').nth(4);
  221. return await statusCell.textContent();
  222. }
  223. }