route-management.page.ts 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. import { Page, Locator, expect } from '@playwright/test';
  2. export class RouteManagementPage {
  3. readonly page: Page;
  4. readonly pageTitle: Locator;
  5. readonly createRouteButton: Locator;
  6. readonly searchInput: Locator;
  7. readonly routeTable: Locator;
  8. readonly editButtons: Locator;
  9. readonly deleteButtons: Locator;
  10. readonly statusToggleButtons: Locator;
  11. readonly pagination: Locator;
  12. readonly vehicleTypeFilter: Locator;
  13. constructor(page: Page) {
  14. this.page = page;
  15. this.pageTitle = page.locator('[data-testid="route-management-title"]');
  16. this.createRouteButton = page.locator('[data-testid="create-route-button"]');
  17. this.searchInput = page.locator('[data-testid="route-search-input"]');
  18. this.routeTable = page.locator('[data-testid="route-table"]');
  19. this.editButtons = page.locator('[data-testid^="edit-route-"]');
  20. this.deleteButtons = page.locator('[data-testid^="delete-route-"]');
  21. this.statusToggleButtons = page.locator('[data-testid^="toggle-route-"]');
  22. this.pagination = page.locator('[data-slot="pagination"]');
  23. this.vehicleTypeFilter = page.locator('[data-testid="route-vehicle-type-filter"]');
  24. }
  25. async goto() {
  26. // 直接导航到路线管理页面
  27. await this.page.goto('/admin/routes');
  28. // 等待页面完全加载
  29. await this.page.waitForLoadState('domcontentloaded');
  30. // 等待路线管理标题出现
  31. await this.page.waitForSelector('[data-testid="route-management-title"]', { state: 'visible', timeout: 15000 });
  32. // 等待表格数据加载完成
  33. await this.page.waitForSelector('[data-testid="route-table"] tbody tr', { state: 'visible', timeout: 20000 });
  34. await this.expectToBeVisible();
  35. }
  36. async expectToBeVisible() {
  37. // 等待页面完全加载
  38. await expect(this.pageTitle).toBeVisible({ timeout: 15000 });
  39. await expect(this.createRouteButton).toBeVisible({ timeout: 10000 });
  40. // 等待至少一行路线数据加载完成
  41. await expect(this.routeTable.locator('tbody tr').first()).toBeVisible({ timeout: 20000 });
  42. }
  43. async searchRoutes(keyword: string) {
  44. await this.searchInput.fill(keyword);
  45. // 防抖搜索,等待网络请求完成
  46. await this.page.waitForTimeout(500); // 等待防抖延迟
  47. await this.page.waitForLoadState('networkidle');
  48. }
  49. async filterByVehicleType(type: '大巴' | '中巴' | '小车') {
  50. await this.vehicleTypeFilter.click();
  51. await this.page.getByRole('option', { name: type }).click();
  52. await this.page.waitForLoadState('networkidle');
  53. }
  54. async createRoute(routeData: {
  55. name: string;
  56. startPoint: string;
  57. endPoint: string;
  58. vehicleType: 'bus' | 'van' | 'car';
  59. price: number;
  60. seatCount: number;
  61. departureTime: string;
  62. activityId: number;
  63. }) {
  64. await this.createRouteButton.click();
  65. // 填写路线表单
  66. await this.page.getByLabel('路线名称').fill(routeData.name);
  67. await this.page.getByLabel('出发地').fill(routeData.startPoint);
  68. await this.page.getByLabel('目的地').fill(routeData.endPoint);
  69. // 选择车型
  70. await this.page.getByLabel('车型').click();
  71. const vehicleTypeMap = {
  72. 'bus': '大巴',
  73. 'van': '中巴',
  74. 'car': '小车'
  75. };
  76. await this.page.getByRole('option', { name: vehicleTypeMap[routeData.vehicleType] }).click();
  77. // 填写价格和座位数
  78. await this.page.getByLabel('价格').fill(routeData.price.toString());
  79. await this.page.getByLabel('座位数').fill(routeData.seatCount.toString());
  80. // 填写出发时间
  81. await this.page.getByLabel('出发时间').fill(routeData.departureTime);
  82. // 选择活动
  83. await this.page.getByLabel('关联活动').click();
  84. await this.page.getByRole('option').first().click();
  85. // 提交表单 - 使用模态框中的创建按钮
  86. await this.page.locator('[role="dialog"]').getByRole('button', { name: '创建路线' }).click();
  87. await this.page.waitForLoadState('networkidle');
  88. // 等待路线创建结果提示
  89. try {
  90. await Promise.race([
  91. this.page.waitForSelector('text=创建成功', { timeout: 10000 }),
  92. this.page.waitForSelector('text=创建失败', { timeout: 10000 })
  93. ]);
  94. // 检查是否有错误提示
  95. const errorVisible = await this.page.locator('text=创建失败').isVisible().catch(() => false);
  96. if (errorVisible) {
  97. return;
  98. }
  99. // 如果是创建成功,刷新页面
  100. await this.page.waitForTimeout(1000);
  101. await this.page.reload();
  102. await this.page.waitForLoadState('networkidle');
  103. await this.expectToBeVisible();
  104. } catch (error) {
  105. // 如果没有提示出现,继续执行
  106. console.log('创建操作没有显示提示信息,继续执行');
  107. await this.page.reload();
  108. await this.page.waitForLoadState('networkidle');
  109. await this.expectToBeVisible();
  110. }
  111. }
  112. async getRouteCount(): Promise<number> {
  113. const rows = await this.routeTable.locator('tbody tr').count();
  114. // 如果只有一行且包含"暂无路线数据",则返回0
  115. if (rows === 1) {
  116. const firstRow = this.routeTable.locator('tbody tr').first();
  117. const rowText = await firstRow.textContent();
  118. if (rowText && rowText.includes('暂无路线数据')) {
  119. return 0;
  120. }
  121. }
  122. return rows;
  123. }
  124. async getRouteByName(name: string): Promise<Locator | null> {
  125. const routeRow = this.routeTable.locator('tbody tr').filter({ hasText: name }).first();
  126. return (await routeRow.count()) > 0 ? routeRow : null;
  127. }
  128. async routeExists(name: string): Promise<boolean> {
  129. const routeRow = this.routeTable.locator('tbody tr').filter({ hasText: name }).first();
  130. return (await routeRow.count()) > 0;
  131. }
  132. async editRoute(name: string, updates: {
  133. name?: string;
  134. startPoint?: string;
  135. endPoint?: string;
  136. vehicleType?: 'bus' | 'van' | 'car';
  137. price?: number;
  138. seatCount?: number;
  139. departureTime?: string;
  140. }) {
  141. const routeRow = await this.getRouteByName(name);
  142. if (!routeRow) throw new Error(`Route ${name} not found`);
  143. // 编辑按钮
  144. const editButton = routeRow.locator('[data-testid^="edit-route-"]');
  145. await editButton.waitFor({ state: 'visible', timeout: 10000 });
  146. await editButton.click();
  147. // 等待编辑模态框出现
  148. await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 10000 });
  149. // 更新字段
  150. if (updates.name) {
  151. await this.page.getByLabel('路线名称').fill(updates.name);
  152. }
  153. if (updates.startPoint) {
  154. await this.page.getByLabel('出发地').fill(updates.startPoint);
  155. }
  156. if (updates.endPoint) {
  157. await this.page.getByLabel('目的地').fill(updates.endPoint);
  158. }
  159. if (updates.vehicleType) {
  160. await this.page.getByLabel('车型').click();
  161. const vehicleTypeMap = {
  162. 'bus': '大巴',
  163. 'van': '中巴',
  164. 'car': '小车'
  165. };
  166. await this.page.getByRole('option', { name: vehicleTypeMap[updates.vehicleType] }).click();
  167. }
  168. if (updates.price) {
  169. await this.page.getByLabel('价格').fill(updates.price.toString());
  170. }
  171. if (updates.seatCount) {
  172. await this.page.getByLabel('座位数').fill(updates.seatCount.toString());
  173. }
  174. if (updates.departureTime) {
  175. await this.page.getByLabel('出发时间').fill(updates.departureTime);
  176. }
  177. // 提交更新
  178. await this.page.locator('[role="dialog"]').getByRole('button', { name: '更新路线' }).click();
  179. await this.page.waitForLoadState('networkidle');
  180. // 等待操作完成
  181. await this.page.waitForTimeout(1000);
  182. }
  183. async deleteRoute(name: string) {
  184. const routeRow = await this.getRouteByName(name);
  185. if (!routeRow) throw new Error(`Route ${name} not found`);
  186. // 删除按钮
  187. const deleteButton = routeRow.locator('[data-testid^="delete-route-"]');
  188. await deleteButton.waitFor({ state: 'visible', timeout: 10000 });
  189. await deleteButton.click();
  190. // 确认删除对话框
  191. await this.page.getByRole('button', { name: '删除' }).click();
  192. // 等待删除操作完成
  193. try {
  194. await Promise.race([
  195. this.page.waitForSelector('text=删除成功', { timeout: 10000 }),
  196. this.page.waitForSelector('text=删除失败', { timeout: 10000 })
  197. ]);
  198. const errorVisible = await this.page.locator('text=删除失败').isVisible().catch(() => false);
  199. if (errorVisible) {
  200. throw new Error('删除操作失败:前端显示删除失败提示');
  201. }
  202. } catch (error) {
  203. console.log('删除操作没有显示提示信息,继续执行');
  204. }
  205. // 刷新页面确认路线是否被删除
  206. await this.page.reload();
  207. await this.page.waitForLoadState('networkidle');
  208. await this.expectToBeVisible();
  209. }
  210. async toggleRouteStatus(name: string) {
  211. const routeRow = await this.getRouteByName(name);
  212. if (!routeRow) throw new Error(`Route ${name} not found`);
  213. // 状态切换按钮
  214. const statusButton = routeRow.locator('[data-testid^="toggle-route-"]');
  215. await statusButton.waitFor({ state: 'visible', timeout: 10000 });
  216. const currentStatus = await statusButton.textContent();
  217. await statusButton.click();
  218. // 确认状态切换对话框
  219. await this.page.getByRole('button', { name: '确认' }).click();
  220. // 等待操作完成
  221. await this.page.waitForLoadState('networkidle');
  222. await this.page.waitForTimeout(1000);
  223. return currentStatus;
  224. }
  225. async expectRouteExists(name: string) {
  226. const exists = await this.routeExists(name);
  227. expect(exists).toBe(true);
  228. }
  229. async expectRouteNotExists(name: string) {
  230. const exists = await this.routeExists(name);
  231. expect(exists).toBe(false);
  232. }
  233. async getRouteStatus(name: string): Promise<string | null> {
  234. const routeRow = await this.getRouteByName(name);
  235. if (!routeRow) return null;
  236. const statusButton = routeRow.locator('[data-testid^="toggle-route-"]');
  237. return await statusButton.textContent();
  238. }
  239. }