Procházet zdrojové kódy

✅ test(admin): 添加活动管理和路线管理页面的端到端测试

- 为活动管理页面添加data-testid属性【页面元素】
- 为路线管理页面添加data-testid属性【页面元素】
- 创建活动管理页面对象模型【页面对象】
- 创建路线管理页面对象模型【页面对象】
- 添加活动管理端到端测试用例【测试用例】
- 添加路线管理端到端测试用例【测试用例】
- 在测试配置中注册新的页面对象【测试配置】
yourname před 4 měsíci
rodič
revize
21a79be569

+ 8 - 4
src/client/admin/pages/Activities.tsx

@@ -211,12 +211,12 @@ export const ActivitiesPage: React.FC = () => {
     <div className="p-6">
       <div className="flex items-center justify-between mb-6">
         <div>
-          <h1 className="text-3xl font-bold tracking-tight">活动管理</h1>
+          <h1 className="text-3xl font-bold tracking-tight" data-testid="activity-management-title">活动管理</h1>
           <p className="text-muted-foreground">
             管理旅行活动,包括去程和返程活动
           </p>
         </div>
-        <Button onClick={handleCreate}>
+        <Button onClick={handleCreate} data-testid="create-activity-button">
           <Plus className="h-4 w-4 mr-2" />
           新建活动
         </Button>
@@ -239,11 +239,12 @@ export const ActivitiesPage: React.FC = () => {
                   placeholder="搜索活动名称或描述..."
                   className="pl-8"
                   onChange={(e) => debouncedSearch(e.target.value)}
+                  data-testid="activity-search-input"
                 />
               </div>
               {/* 类型筛选 */}
               <Select value={typeFilter} onValueChange={setTypeFilter}>
-                <SelectTrigger className="w-32">
+                <SelectTrigger className="w-32" data-testid="activity-type-filter">
                   <Filter className="h-4 w-4 mr-2" />
                   <SelectValue placeholder="类型" />
                 </SelectTrigger>
@@ -290,7 +291,7 @@ export const ActivitiesPage: React.FC = () => {
           )}
 
           <div className="rounded-md border">
-            <Table>
+            <Table data-testid="activity-table">
               <TableHeader>
                 <TableRow>
                   <TableHead>活动名称</TableHead>
@@ -348,6 +349,7 @@ export const ActivitiesPage: React.FC = () => {
                             size="sm"
                             onClick={() => handleToggleStatus(activity)}
                             disabled={toggleStatusMutation.isPending}
+                            data-testid={`toggle-activity-${activity.id}-status`}
                           >
                             <Power className="h-4 w-4 mr-1" />
                             {activity.isDisabled === 0 ? '禁用' : '启用'}
@@ -356,6 +358,7 @@ export const ActivitiesPage: React.FC = () => {
                             variant="outline"
                             size="sm"
                             onClick={() => handleEdit(activity)}
+                            data-testid={`edit-activity-${activity.id}`}
                           >
                             <Edit className="h-4 w-4" />
                           </Button>
@@ -367,6 +370,7 @@ export const ActivitiesPage: React.FC = () => {
                                 deleteMutation.mutate(activity.id);
                               }
                             }}
+                            data-testid={`delete-activity-${activity.id}`}
                           >
                             <Trash2 className="h-4 w-4" />
                           </Button>

+ 8 - 4
src/client/admin/pages/Routes.tsx

@@ -211,12 +211,12 @@ export const RoutesPage: React.FC = () => {
     <div className="p-6">
       <div className="flex items-center justify-between mb-6">
         <div>
-          <h1 className="text-3xl font-bold tracking-tight">路线管理</h1>
+          <h1 className="text-3xl font-bold tracking-tight" data-testid="route-management-title">路线管理</h1>
           <p className="text-muted-foreground">
             管理旅行路线,包括出发地、目的地、车型和价格等信息
           </p>
         </div>
-        <Button onClick={handleCreate}>
+        <Button onClick={handleCreate} data-testid="create-route-button">
           <Plus className="h-4 w-4 mr-2" />
           新建路线
         </Button>
@@ -239,11 +239,12 @@ export const RoutesPage: React.FC = () => {
                   placeholder="搜索路线名称、地点或车型..."
                   className="pl-8"
                   onChange={(e) => debouncedSearch(e.target.value)}
+                  data-testid="route-search-input"
                 />
               </div>
               {/* 车型筛选 */}
               <Select value={vehicleTypeFilter} onValueChange={setVehicleTypeFilter}>
-                <SelectTrigger className="w-32">
+                <SelectTrigger className="w-32" data-testid="route-vehicle-type-filter">
                   <Filter className="h-4 w-4 mr-2" />
                   <SelectValue placeholder="车型" />
                 </SelectTrigger>
@@ -291,7 +292,7 @@ export const RoutesPage: React.FC = () => {
           )}
 
           <div className="rounded-md border">
-            <Table>
+            <Table data-testid="route-table">
               <TableHeader>
                 <TableRow>
                   <TableHead>路线名称</TableHead>
@@ -361,6 +362,7 @@ export const RoutesPage: React.FC = () => {
                             size="sm"
                             onClick={() => handleToggleStatus(route)}
                             disabled={toggleStatusMutation.isPending}
+                            data-testid={`toggle-route-${route.id}-status`}
                           >
                             <Power className="h-4 w-4 mr-1" />
                             {route.isDisabled === 0 ? '禁用' : '启用'}
@@ -369,6 +371,7 @@ export const RoutesPage: React.FC = () => {
                             variant="outline"
                             size="sm"
                             onClick={() => handleEdit(route)}
+                            data-testid={`edit-route-${route.id}`}
                           >
                             <Edit className="h-4 w-4" />
                           </Button>
@@ -380,6 +383,7 @@ export const RoutesPage: React.FC = () => {
                                 deleteMutation.mutate(route.id);
                               }
                             }}
+                            data-testid={`delete-route-${route.id}`}
                           >
                             <Trash2 className="h-4 w-4" />
                           </Button>

+ 252 - 0
tests/e2e/pages/admin/activity-management.page.ts

@@ -0,0 +1,252 @@
+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);
+    await this.searchButton.click();
+    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.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) {
+        return;
+      }
+
+      // 如果是创建成功,刷新页面
+      await this.page.waitForTimeout(1000);
+      await this.page.reload();
+      await this.page.waitForLoadState('networkidle');
+      await this.expectToBeVisible();
+    } catch (error) {
+      // 如果没有提示出现,继续执行
+      console.log('创建操作没有显示提示信息,继续执行');
+      await this.page.reload();
+      await this.page.waitForLoadState('networkidle');
+      await this.expectToBeVisible();
+    }
+  }
+
+  async getActivityCount(): Promise<number> {
+    const rows = await this.activityTable.locator('tbody tr').count();
+    return rows;
+  }
+
+  async getActivityByName(name: string): Promise<Locator | null> {
+    const activityRow = this.activityTable.locator('tbody tr').filter({ hasText: name }).first();
+    return (await activityRow.count()) > 0 ? activityRow : null;
+  }
+
+  async activityExists(name: string): Promise<boolean> {
+    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`);
+
+    // 编辑按钮是图标按钮
+    const editButton = activityRow.locator('button').first();
+    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`);
+
+    // 删除按钮是图标按钮(第二个按钮是删除)
+    const deleteButton = activityRow.locator('button').nth(1);
+    await deleteButton.waitFor({ state: 'visible', timeout: 10000 });
+    await deleteButton.click();
+
+    // 确认删除对话框
+    await this.page.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.reload();
+    await this.page.waitForLoadState('networkidle');
+    await this.expectToBeVisible();
+  }
+
+  async toggleActivityStatus(name: string) {
+    const activityRow = await this.getActivityByName(name);
+    if (!activityRow) throw new Error(`Activity ${name} not found`);
+
+    // 状态切换按钮(第三个按钮)
+    const statusButton = activityRow.locator('button').nth(2);
+    await statusButton.waitFor({ state: 'visible', timeout: 10000 });
+
+    const currentStatus = await statusButton.textContent();
+    await statusButton.click();
+
+    // 确认状态切换对话框
+    await this.page.getByRole('button', { name: '确认' }).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<string | null> {
+    const activityRow = await this.getActivityByName(name);
+    if (!activityRow) return null;
+
+    const statusButton = activityRow.locator('button').nth(2);
+    return await statusButton.textContent();
+  }
+}

+ 278 - 0
tests/e2e/pages/admin/route-management.page.ts

@@ -0,0 +1,278 @@
+import { Page, Locator, expect } from '@playwright/test';
+
+export class RouteManagementPage {
+  readonly page: Page;
+  readonly pageTitle: Locator;
+  readonly createRouteButton: Locator;
+  readonly searchInput: Locator;
+  readonly searchButton: Locator;
+  readonly routeTable: Locator;
+  readonly editButtons: Locator;
+  readonly deleteButtons: Locator;
+  readonly statusToggleButtons: Locator;
+  readonly pagination: Locator;
+  readonly vehicleTypeFilter: Locator;
+
+  constructor(page: Page) {
+    this.page = page;
+    this.pageTitle = page.locator('[data-testid="route-management-title"]');
+    this.createRouteButton = page.locator('[data-testid="create-route-button"]');
+    this.searchInput = page.locator('[data-testid="route-search-input"]');
+    this.searchButton = page.getByRole('button', { name: '搜索' });
+    this.routeTable = page.locator('[data-testid="route-table"]');
+    this.editButtons = page.locator('[data-testid^="edit-route-"]');
+    this.deleteButtons = page.locator('[data-testid^="delete-route-"]');
+    this.statusToggleButtons = page.locator('[data-testid^="toggle-route-"]');
+    this.pagination = page.locator('[data-slot="pagination"]');
+    this.vehicleTypeFilter = page.locator('[data-testid="route-vehicle-type-filter"]');
+  }
+
+  async goto() {
+    // 直接导航到路线管理页面
+    await this.page.goto('/admin/routes');
+
+    // 等待页面完全加载
+    await this.page.waitForLoadState('domcontentloaded');
+
+    // 等待路线管理标题出现
+    await this.page.waitForSelector('[data-testid="route-management-title"]', { state: 'visible', timeout: 15000 });
+
+    // 等待表格数据加载完成
+    await this.page.waitForSelector('[data-testid="route-table"] tbody tr', { state: 'visible', timeout: 20000 });
+
+    await this.expectToBeVisible();
+  }
+
+  async expectToBeVisible() {
+    // 等待页面完全加载
+    await expect(this.pageTitle).toBeVisible({ timeout: 15000 });
+    await expect(this.createRouteButton).toBeVisible({ timeout: 10000 });
+
+    // 等待至少一行路线数据加载完成
+    await expect(this.routeTable.locator('tbody tr').first()).toBeVisible({ timeout: 20000 });
+  }
+
+  async searchRoutes(keyword: string) {
+    await this.searchInput.fill(keyword);
+    await this.searchButton.click();
+    await this.page.waitForLoadState('networkidle');
+  }
+
+  async filterByVehicleType(type: '大巴' | '中巴' | '小车') {
+    await this.vehicleTypeFilter.click();
+    await this.page.getByRole('option', { name: type }).click();
+    await this.page.waitForLoadState('networkidle');
+  }
+
+  async createRoute(routeData: {
+    name: string;
+    startPoint: string;
+    endPoint: string;
+    vehicleType: 'bus' | 'van' | 'car';
+    price: number;
+    seatCount: number;
+    departureTime: string;
+    activityId: number;
+  }) {
+    await this.createRouteButton.click();
+
+    // 填写路线表单
+    await this.page.getByLabel('路线名称').fill(routeData.name);
+    await this.page.getByLabel('出发地').fill(routeData.startPoint);
+    await this.page.getByLabel('目的地').fill(routeData.endPoint);
+
+    // 选择车型
+    await this.page.getByLabel('车型').click();
+    const vehicleTypeMap = {
+      'bus': '大巴',
+      'van': '中巴',
+      'car': '小车'
+    };
+    await this.page.getByRole('option', { name: vehicleTypeMap[routeData.vehicleType] }).click();
+
+    // 填写价格和座位数
+    await this.page.getByLabel('价格').fill(routeData.price.toString());
+    await this.page.getByLabel('座位数').fill(routeData.seatCount.toString());
+
+    // 填写出发时间
+    await this.page.getByLabel('出发时间').fill(routeData.departureTime);
+
+    // 选择活动
+    await this.page.getByLabel('关联活动').click();
+    await this.page.getByRole('option').first().click();
+
+    // 提交表单 - 使用模态框中的创建按钮
+    await this.page.locator('[role="dialog"]').getByRole('button', { name: '创建路线' }).click();
+    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) {
+        return;
+      }
+
+      // 如果是创建成功,刷新页面
+      await this.page.waitForTimeout(1000);
+      await this.page.reload();
+      await this.page.waitForLoadState('networkidle');
+      await this.expectToBeVisible();
+    } catch (error) {
+      // 如果没有提示出现,继续执行
+      console.log('创建操作没有显示提示信息,继续执行');
+      await this.page.reload();
+      await this.page.waitForLoadState('networkidle');
+      await this.expectToBeVisible();
+    }
+  }
+
+  async getRouteCount(): Promise<number> {
+    const rows = await this.routeTable.locator('tbody tr').count();
+    return rows;
+  }
+
+  async getRouteByName(name: string): Promise<Locator | null> {
+    const routeRow = this.routeTable.locator('tbody tr').filter({ hasText: name }).first();
+    return (await routeRow.count()) > 0 ? routeRow : null;
+  }
+
+  async routeExists(name: string): Promise<boolean> {
+    const routeRow = this.routeTable.locator('tbody tr').filter({ hasText: name }).first();
+    return (await routeRow.count()) > 0;
+  }
+
+  async editRoute(name: string, updates: {
+    name?: string;
+    startPoint?: string;
+    endPoint?: string;
+    vehicleType?: 'bus' | 'van' | 'car';
+    price?: number;
+    seatCount?: number;
+    departureTime?: string;
+  }) {
+    const routeRow = await this.getRouteByName(name);
+    if (!routeRow) throw new Error(`Route ${name} not found`);
+
+    // 编辑按钮
+    const editButton = routeRow.locator('[data-testid^="edit-route-"]');
+    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.startPoint) {
+      await this.page.getByLabel('出发地').fill(updates.startPoint);
+    }
+    if (updates.endPoint) {
+      await this.page.getByLabel('目的地').fill(updates.endPoint);
+    }
+    if (updates.vehicleType) {
+      await this.page.getByLabel('车型').click();
+      const vehicleTypeMap = {
+        'bus': '大巴',
+        'van': '中巴',
+        'car': '小车'
+      };
+      await this.page.getByRole('option', { name: vehicleTypeMap[updates.vehicleType] }).click();
+    }
+    if (updates.price) {
+      await this.page.getByLabel('价格').fill(updates.price.toString());
+    }
+    if (updates.seatCount) {
+      await this.page.getByLabel('座位数').fill(updates.seatCount.toString());
+    }
+    if (updates.departureTime) {
+      await this.page.getByLabel('出发时间').fill(updates.departureTime);
+    }
+
+    // 提交更新
+    await this.page.locator('[role="dialog"]').getByRole('button', { name: '更新路线' }).click();
+    await this.page.waitForLoadState('networkidle');
+
+    // 等待操作完成
+    await this.page.waitForTimeout(1000);
+  }
+
+  async deleteRoute(name: string) {
+    const routeRow = await this.getRouteByName(name);
+    if (!routeRow) throw new Error(`Route ${name} not found`);
+
+    // 删除按钮
+    const deleteButton = routeRow.locator('[data-testid^="delete-route-"]');
+    await deleteButton.waitFor({ state: 'visible', timeout: 10000 });
+    await deleteButton.click();
+
+    // 确认删除对话框
+    await this.page.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.reload();
+    await this.page.waitForLoadState('networkidle');
+    await this.expectToBeVisible();
+  }
+
+  async toggleRouteStatus(name: string) {
+    const routeRow = await this.getRouteByName(name);
+    if (!routeRow) throw new Error(`Route ${name} not found`);
+
+    // 状态切换按钮
+    const statusButton = routeRow.locator('[data-testid^="toggle-route-"]');
+    await statusButton.waitFor({ state: 'visible', timeout: 10000 });
+
+    const currentStatus = await statusButton.textContent();
+    await statusButton.click();
+
+    // 确认状态切换对话框
+    await this.page.getByRole('button', { name: '确认' }).click();
+
+    // 等待操作完成
+    await this.page.waitForLoadState('networkidle');
+    await this.page.waitForTimeout(1000);
+
+    return currentStatus;
+  }
+
+  async expectRouteExists(name: string) {
+    const exists = await this.routeExists(name);
+    expect(exists).toBe(true);
+  }
+
+  async expectRouteNotExists(name: string) {
+    const exists = await this.routeExists(name);
+    expect(exists).toBe(false);
+  }
+
+  async getRouteStatus(name: string): Promise<string | null> {
+    const routeRow = await this.getRouteByName(name);
+    if (!routeRow) return null;
+
+    const statusButton = routeRow.locator('[data-testid^="toggle-route-"]');
+    return await statusButton.textContent();
+  }
+}

+ 264 - 0
tests/e2e/specs/admin/activities.spec.ts

@@ -0,0 +1,264 @@
+import { test, expect } from '../../utils/test-setup';
+
+test.describe.serial('活动管理 E2E 测试', () => {
+  test.beforeEach(async ({ adminLoginPage, activityManagementPage }) => {
+    // 以管理员身份登录后台
+    await adminLoginPage.goto();
+    await adminLoginPage.login('admin', 'admin123');
+    await activityManagementPage.goto();
+  });
+
+  test('查看活动列表', async ({ activityManagementPage }) => {
+    await activityManagementPage.expectToBeVisible();
+    const activityCount = await activityManagementPage.getActivityCount();
+    expect(activityCount).toBeGreaterThan(0);
+  });
+
+  test('搜索活动', async ({ activityManagementPage }) => {
+    // 先获取所有活动
+    const initialCount = await activityManagementPage.getActivityCount();
+
+    // 搜索存在的活动
+    await activityManagementPage.searchActivities('活动');
+    const searchedCount = await activityManagementPage.getActivityCount();
+    expect(searchedCount).toBeGreaterThan(0);
+    expect(searchedCount).toBeLessThanOrEqual(initialCount);
+
+    // 搜索不存在的活动
+    await activityManagementPage.searchActivities('nonexistentactivity12345');
+    const emptyCount = await activityManagementPage.getActivityCount();
+    expect(emptyCount).toBe(0);
+
+    // 清除搜索
+    await activityManagementPage.searchActivities('');
+    const normalCount = await activityManagementPage.getActivityCount();
+    expect(normalCount).toBeGreaterThan(0);
+  });
+
+  test('按活动类型筛选', async ({ activityManagementPage }) => {
+    // 筛选去程活动
+    await activityManagementPage.filterByType('去程活动');
+    const departureCount = await activityManagementPage.getActivityCount();
+    expect(departureCount).toBeGreaterThan(0);
+
+    // 筛选返程活动
+    await activityManagementPage.filterByType('返程活动');
+    const returnCount = await activityManagementPage.getActivityCount();
+    expect(returnCount).toBeGreaterThan(0);
+
+    // 重置筛选
+    await activityManagementPage.filterByType('全部类型');
+    const allCount = await activityManagementPage.getActivityCount();
+    expect(allCount).toBeGreaterThan(0);
+  });
+
+  test('创建新活动', async ({ activityManagementPage }) => {
+    const testActivityName = `测试活动_${Date.now()}`;
+
+    await activityManagementPage.createActivity({
+      name: testActivityName,
+      description: '这是一个测试活动',
+      type: '去程活动',
+      startDate: '2025-01-01 08:00',
+      endDate: '2025-01-01 18:00'
+    });
+
+    // 验证活动创建成功
+    await activityManagementPage.expectActivityExists(testActivityName);
+  });
+
+  test('编辑活动信息', async ({ activityManagementPage }) => {
+    const testActivityName = `编辑活动_${Date.now()}`;
+
+    // 先创建测试活动
+    await activityManagementPage.createActivity({
+      name: testActivityName,
+      description: '原始描述',
+      type: '去程活动',
+      startDate: '2025-01-01 08:00',
+      endDate: '2025-01-01 18:00'
+    });
+
+    // 编辑活动信息
+    await activityManagementPage.editActivity(testActivityName, {
+      name: `${testActivityName}_更新`,
+      description: '更新后的描述',
+      type: '返程活动',
+      startDate: '2025-01-02 09:00',
+      endDate: '2025-01-02 19:00'
+    });
+
+    // 验证活动信息已更新
+    await activityManagementPage.expectActivityExists(`${testActivityName}_更新`);
+  });
+
+  test('删除活动', async ({ activityManagementPage }) => {
+    const testActivityName = `删除活动_${Date.now()}`;
+
+    // 先创建测试活动
+    await activityManagementPage.createActivity({
+      name: testActivityName,
+      description: '待删除活动',
+      type: '去程活动',
+      startDate: '2025-01-01 08:00',
+      endDate: '2025-01-01 18:00'
+    });
+
+    // 验证活动存在
+    await activityManagementPage.expectActivityExists(testActivityName);
+
+    // 删除活动
+    await activityManagementPage.deleteActivity(testActivityName);
+
+    // 验证活动已被删除
+    await activityManagementPage.expectActivityNotExists(testActivityName);
+  });
+
+  test('启用/禁用活动', async ({ activityManagementPage }) => {
+    const testActivityName = `状态活动_${Date.now()}`;
+
+    // 先创建测试活动
+    await activityManagementPage.createActivity({
+      name: testActivityName,
+      description: '状态测试活动',
+      type: '去程活动',
+      startDate: '2025-01-01 08:00',
+      endDate: '2025-01-01 18:00'
+    });
+
+    // 验证活动存在
+    await activityManagementPage.expectActivityExists(testActivityName);
+
+    // 获取初始状态
+    const initialStatus = await activityManagementPage.getActivityStatus(testActivityName);
+    expect(initialStatus).toMatch(/启用|禁用/);
+
+    // 切换状态
+    await activityManagementPage.toggleActivityStatus(testActivityName);
+
+    // 验证状态已切换
+    const newStatus = await activityManagementPage.getActivityStatus(testActivityName);
+    expect(newStatus).not.toBe(initialStatus);
+
+    // 再次切换状态
+    await activityManagementPage.toggleActivityStatus(testActivityName);
+
+    // 验证状态恢复
+    const finalStatus = await activityManagementPage.getActivityStatus(testActivityName);
+    expect(finalStatus).toBe(initialStatus);
+  });
+
+  test('活动分页功能', async ({ activityManagementPage }) => {
+    // 确保有足够多的活动来测试分页
+    const initialCount = await activityManagementPage.getActivityCount();
+
+    if (initialCount < 10) {
+      // 创建一些测试活动
+      for (let i = 0; i < 5; i++) {
+        await activityManagementPage.createActivity({
+          name: `分页测试活动_${Date.now()}_${i}`,
+          description: `分页测试活动 ${i}`,
+          type: i % 2 === 0 ? '去程活动' : '返程活动',
+          startDate: '2025-01-01 08:00',
+          endDate: '2025-01-01 18:00'
+        });
+      }
+    }
+
+    // 验证分页控件可见
+    await expect(activityManagementPage.pagination).toBeVisible();
+  });
+
+  test('完整活动管理工作流 - 登录→创建→编辑→启用/禁用→删除', async ({ adminLoginPage, activityManagementPage, page }) => {
+    const testActivityName = `工作流活动_${Date.now()}`;
+
+    // 1. 登录系统
+    await adminLoginPage.goto();
+    await adminLoginPage.login('admin', 'admin123');
+
+    // 验证登录成功,跳转到Dashboard
+    await expect(page).toHaveURL(/.*dashboard/);
+    await expect(page.locator('text=仪表盘')).toBeVisible();
+
+    // 2. 导航到活动管理页面
+    await activityManagementPage.goto();
+    await activityManagementPage.expectToBeVisible();
+
+    // 3. 创建新活动
+    await activityManagementPage.createActivity({
+      name: testActivityName,
+      description: '工作流测试活动',
+      type: '去程活动',
+      startDate: '2025-01-01 08:00',
+      endDate: '2025-01-01 18:00'
+    });
+
+    // 验证活动创建成功
+    await activityManagementPage.expectActivityExists(testActivityName);
+
+    // 4. 搜索并编辑活动
+    await activityManagementPage.searchActivities(testActivityName);
+    await activityManagementPage.editActivity(testActivityName, {
+      name: `${testActivityName}_更新`,
+      description: '更新后的工作流活动',
+      type: '返程活动',
+      startDate: '2025-01-02 09:00',
+      endDate: '2025-01-02 19:00'
+    });
+
+    // 验证活动信息已更新
+    await activityManagementPage.expectActivityExists(`${testActivityName}_更新`);
+
+    // 5. 启用/禁用活动
+    await activityManagementPage.toggleActivityStatus(`${testActivityName}_更新`);
+    const statusAfterToggle = await activityManagementPage.getActivityStatus(`${testActivityName}_更新`);
+    expect(statusAfterToggle).toMatch(/启用|禁用/);
+
+    // 6. 删除活动
+    await activityManagementPage.deleteActivity(`${testActivityName}_更新`);
+
+    // 验证活动已被删除
+    await activityManagementPage.expectActivityNotExists(`${testActivityName}_更新`);
+
+    // 7. 返回Dashboard验证状态
+    await page.goto('/admin/dashboard');
+    await expect(page.locator('text=仪表盘')).toBeVisible();
+    await expect(page.locator('text=欢迎回来')).toBeVisible();
+  });
+
+  test('边界条件测试 - 空搜索和特殊字符', async ({ activityManagementPage }) => {
+    // 测试空搜索
+    await activityManagementPage.searchActivities('');
+    const normalCount = await activityManagementPage.getActivityCount();
+    expect(normalCount).toBeGreaterThan(0);
+
+    // 测试特殊字符搜索
+    await activityManagementPage.searchActivities('@#$%');
+    const specialCharCount = await activityManagementPage.getActivityCount();
+    expect(specialCharCount).toBe(0);
+
+    // 清除搜索
+    await activityManagementPage.searchActivities('');
+    const restoredCount = await activityManagementPage.getActivityCount();
+    expect(restoredCount).toBeGreaterThan(0);
+  });
+
+  test('响应式布局 - 桌面端', async ({ activityManagementPage, page }) => {
+    await page.setViewportSize({ width: 1200, height: 800 });
+    await activityManagementPage.expectToBeVisible();
+
+    // 验证桌面端布局元素
+    await expect(activityManagementPage.searchInput).toBeVisible();
+    await expect(activityManagementPage.createActivityButton).toBeVisible();
+    await expect(activityManagementPage.activityTable).toBeVisible();
+  });
+
+  test('响应式布局 - 移动端', async ({ activityManagementPage, page }) => {
+    await page.setViewportSize({ width: 375, height: 667 });
+    await activityManagementPage.expectToBeVisible();
+
+    // 验证移动端布局
+    await expect(activityManagementPage.pageTitle).toBeVisible();
+    await expect(activityManagementPage.searchInput).toBeVisible();
+  });
+});

+ 345 - 0
tests/e2e/specs/admin/routes.spec.ts

@@ -0,0 +1,345 @@
+import { test, expect } from '../../utils/test-setup';
+
+test.describe.serial('路线管理 E2E 测试', () => {
+  test.beforeEach(async ({ adminLoginPage, routeManagementPage }) => {
+    // 以管理员身份登录后台
+    await adminLoginPage.goto();
+    await adminLoginPage.login('admin', 'admin123');
+    await routeManagementPage.goto();
+  });
+
+  test('查看路线列表', async ({ routeManagementPage }) => {
+    await routeManagementPage.expectToBeVisible();
+    const routeCount = await routeManagementPage.getRouteCount();
+    expect(routeCount).toBeGreaterThan(0);
+  });
+
+  test('搜索路线', async ({ routeManagementPage }) => {
+    // 先获取所有路线
+    const initialCount = await routeManagementPage.getRouteCount();
+
+    // 搜索存在的路线
+    await routeManagementPage.searchRoutes('路线');
+    const searchedCount = await routeManagementPage.getRouteCount();
+    expect(searchedCount).toBeGreaterThan(0);
+    expect(searchedCount).toBeLessThanOrEqual(initialCount);
+
+    // 搜索不存在的路线
+    await routeManagementPage.searchRoutes('nonexistentroute12345');
+    const emptyCount = await routeManagementPage.getRouteCount();
+    expect(emptyCount).toBe(0);
+
+    // 清除搜索
+    await routeManagementPage.searchRoutes('');
+    const normalCount = await routeManagementPage.getRouteCount();
+    expect(normalCount).toBeGreaterThan(0);
+  });
+
+  test('按车型筛选', async ({ routeManagementPage }) => {
+    // 筛选大巴车型
+    await routeManagementPage.filterByVehicleType('大巴');
+    const busCount = await routeManagementPage.getRouteCount();
+    expect(busCount).toBeGreaterThanOrEqual(0);
+
+    // 筛选中巴车型
+    await routeManagementPage.filterByVehicleType('中巴');
+    const vanCount = await routeManagementPage.getRouteCount();
+    expect(vanCount).toBeGreaterThanOrEqual(0);
+
+    // 筛选小车车型
+    await routeManagementPage.filterByVehicleType('小车');
+    const carCount = await routeManagementPage.getRouteCount();
+    expect(carCount).toBeGreaterThanOrEqual(0);
+
+    // 重置筛选
+    await routeManagementPage.filterByVehicleType('全部车型');
+    const allCount = await routeManagementPage.getRouteCount();
+    expect(allCount).toBeGreaterThan(0);
+  });
+
+  test('创建新路线', async ({ routeManagementPage }) => {
+    const testRouteName = `测试路线_${Date.now()}`;
+
+    await routeManagementPage.createRoute({
+      name: testRouteName,
+      startPoint: '北京',
+      endPoint: '上海',
+      vehicleType: 'bus',
+      price: 200,
+      seatCount: 40,
+      departureTime: '2025-01-01 08:00',
+      activityId: 1
+    });
+
+    // 验证路线创建成功
+    await routeManagementPage.expectRouteExists(testRouteName);
+  });
+
+  test('编辑路线信息', async ({ routeManagementPage }) => {
+    const testRouteName = `编辑路线_${Date.now()}`;
+
+    // 先创建测试路线
+    await routeManagementPage.createRoute({
+      name: testRouteName,
+      startPoint: '北京',
+      endPoint: '上海',
+      vehicleType: 'bus',
+      price: 200,
+      seatCount: 40,
+      departureTime: '2025-01-01 08:00',
+      activityId: 1
+    });
+
+    // 编辑路线信息
+    await routeManagementPage.editRoute(testRouteName, {
+      name: `${testRouteName}_更新`,
+      startPoint: '广州',
+      endPoint: '深圳',
+      vehicleType: 'van',
+      price: 150,
+      seatCount: 20,
+      departureTime: '2025-01-02 09:00'
+    });
+
+    // 验证路线信息已更新
+    await routeManagementPage.expectRouteExists(`${testRouteName}_更新`);
+  });
+
+  test('删除路线', async ({ routeManagementPage }) => {
+    const testRouteName = `删除路线_${Date.now()}`;
+
+    // 先创建测试路线
+    await routeManagementPage.createRoute({
+      name: testRouteName,
+      startPoint: '北京',
+      endPoint: '上海',
+      vehicleType: 'bus',
+      price: 200,
+      seatCount: 40,
+      departureTime: '2025-01-01 08:00',
+      activityId: 1
+    });
+
+    // 验证路线存在
+    await routeManagementPage.expectRouteExists(testRouteName);
+
+    // 删除路线
+    await routeManagementPage.deleteRoute(testRouteName);
+
+    // 验证路线已被删除
+    await routeManagementPage.expectRouteNotExists(testRouteName);
+  });
+
+  test('启用/禁用路线', async ({ routeManagementPage }) => {
+    const testRouteName = `状态路线_${Date.now()}`;
+
+    // 先创建测试路线
+    await routeManagementPage.createRoute({
+      name: testRouteName,
+      startPoint: '北京',
+      endPoint: '上海',
+      vehicleType: 'bus',
+      price: 200,
+      seatCount: 40,
+      departureTime: '2025-01-01 08:00',
+      activityId: 1
+    });
+
+    // 验证路线存在
+    await routeManagementPage.expectRouteExists(testRouteName);
+
+    // 获取初始状态
+    const initialStatus = await routeManagementPage.getRouteStatus(testRouteName);
+    expect(initialStatus).toMatch(/启用|禁用/);
+
+    // 切换状态
+    await routeManagementPage.toggleRouteStatus(testRouteName);
+
+    // 验证状态已切换
+    const newStatus = await routeManagementPage.getRouteStatus(testRouteName);
+    expect(newStatus).not.toBe(initialStatus);
+
+    // 再次切换状态
+    await routeManagementPage.toggleRouteStatus(testRouteName);
+
+    // 验证状态恢复
+    const finalStatus = await routeManagementPage.getRouteStatus(testRouteName);
+    expect(finalStatus).toBe(initialStatus);
+  });
+
+  test('路线分页功能', async ({ routeManagementPage }) => {
+    // 确保有足够多的路线来测试分页
+    const initialCount = await routeManagementPage.getRouteCount();
+
+    if (initialCount < 10) {
+      // 创建一些测试路线
+      for (let i = 0; i < 5; i++) {
+        await routeManagementPage.createRoute({
+          name: `分页测试路线_${Date.now()}_${i}`,
+          startPoint: `城市${i}`,
+          endPoint: `城市${i + 1}`,
+          vehicleType: i % 3 === 0 ? 'bus' : i % 3 === 1 ? 'van' : 'car',
+          price: 100 + i * 20,
+          seatCount: 30 + i * 5,
+          departureTime: `2025-01-01 0${8 + i}:00`,
+          activityId: 1
+        });
+      }
+    }
+
+    // 验证分页控件可见
+    await expect(routeManagementPage.pagination).toBeVisible();
+  });
+
+  test('完整路线管理工作流 - 登录→创建→编辑→启用/禁用→删除', async ({ adminLoginPage, routeManagementPage, page }) => {
+    const testRouteName = `工作流路线_${Date.now()}`;
+
+    // 1. 登录系统
+    await adminLoginPage.goto();
+    await adminLoginPage.login('admin', 'admin123');
+
+    // 验证登录成功,跳转到Dashboard
+    await expect(page).toHaveURL(/.*dashboard/);
+    await expect(page.locator('text=仪表盘')).toBeVisible();
+
+    // 2. 导航到路线管理页面
+    await routeManagementPage.goto();
+    await routeManagementPage.expectToBeVisible();
+
+    // 3. 创建新路线
+    await routeManagementPage.createRoute({
+      name: testRouteName,
+      startPoint: '北京',
+      endPoint: '上海',
+      vehicleType: 'bus',
+      price: 200,
+      seatCount: 40,
+      departureTime: '2025-01-01 08:00',
+      activityId: 1
+    });
+
+    // 验证路线创建成功
+    await routeManagementPage.expectRouteExists(testRouteName);
+
+    // 4. 搜索并编辑路线
+    await routeManagementPage.searchRoutes(testRouteName);
+    await routeManagementPage.editRoute(testRouteName, {
+      name: `${testRouteName}_更新`,
+      startPoint: '广州',
+      endPoint: '深圳',
+      vehicleType: 'van',
+      price: 150,
+      seatCount: 20,
+      departureTime: '2025-01-02 09:00'
+    });
+
+    // 验证路线信息已更新
+    await routeManagementPage.expectRouteExists(`${testRouteName}_更新`);
+
+    // 5. 启用/禁用路线
+    await routeManagementPage.toggleRouteStatus(`${testRouteName}_更新`);
+    const statusAfterToggle = await routeManagementPage.getRouteStatus(`${testRouteName}_更新`);
+    expect(statusAfterToggle).toMatch(/启用|禁用/);
+
+    // 6. 删除路线
+    await routeManagementPage.deleteRoute(`${testRouteName}_更新`);
+
+    // 验证路线已被删除
+    await routeManagementPage.expectRouteNotExists(`${testRouteName}_更新`);
+
+    // 7. 返回Dashboard验证状态
+    await page.goto('/admin/dashboard');
+    await expect(page.locator('text=仪表盘')).toBeVisible();
+    await expect(page.locator('text=欢迎回来')).toBeVisible();
+  });
+
+  test('边界条件测试 - 空搜索和特殊字符', async ({ routeManagementPage }) => {
+    // 测试空搜索
+    await routeManagementPage.searchRoutes('');
+    const normalCount = await routeManagementPage.getRouteCount();
+    expect(normalCount).toBeGreaterThan(0);
+
+    // 测试特殊字符搜索
+    await routeManagementPage.searchRoutes('@#$%');
+    const specialCharCount = await routeManagementPage.getRouteCount();
+    expect(specialCharCount).toBe(0);
+
+    // 清除搜索
+    await routeManagementPage.searchRoutes('');
+    const restoredCount = await routeManagementPage.getRouteCount();
+    expect(restoredCount).toBeGreaterThan(0);
+  });
+
+  test('响应式布局 - 桌面端', async ({ routeManagementPage, page }) => {
+    await page.setViewportSize({ width: 1200, height: 800 });
+    await routeManagementPage.expectToBeVisible();
+
+    // 验证桌面端布局元素
+    await expect(routeManagementPage.searchInput).toBeVisible();
+    await expect(routeManagementPage.createRouteButton).toBeVisible();
+    await expect(routeManagementPage.routeTable).toBeVisible();
+  });
+
+  test('响应式布局 - 移动端', async ({ routeManagementPage, page }) => {
+    await page.setViewportSize({ width: 375, height: 667 });
+    await routeManagementPage.expectToBeVisible();
+
+    // 验证移动端布局
+    await expect(routeManagementPage.pageTitle).toBeVisible();
+    await expect(routeManagementPage.searchInput).toBeVisible();
+  });
+
+  test('价格和座位数验证', async ({ routeManagementPage }) => {
+    const testRouteName = `验证路线_${Date.now()}`;
+
+    // 创建路线并验证价格和座位数显示
+    await routeManagementPage.createRoute({
+      name: testRouteName,
+      startPoint: '北京',
+      endPoint: '上海',
+      vehicleType: 'bus',
+      price: 250,
+      seatCount: 45,
+      departureTime: '2025-01-01 08:00',
+      activityId: 1
+    });
+
+    // 验证路线存在
+    await routeManagementPage.expectRouteExists(testRouteName);
+
+    // 验证价格和座位数在表格中正确显示
+    const routeRow = await routeManagementPage.getRouteByName(testRouteName);
+    expect(routeRow).not.toBeNull();
+    await expect(routeRow!).toContainText('¥250');
+    await expect(routeRow!).toContainText('45');
+  });
+
+  test('车型显示验证', async ({ routeManagementPage }) => {
+    const testRouteName = `车型路线_${Date.now()}`;
+
+    // 创建不同车型的路线
+    const vehicleTypes = ['bus', 'van', 'car'];
+    const vehicleTypeNames = ['大巴', '中巴', '小车'];
+
+    for (let i = 0; i < vehicleTypes.length; i++) {
+      const routeName = `${testRouteName}_${vehicleTypes[i]}`;
+
+      await routeManagementPage.createRoute({
+        name: routeName,
+        startPoint: '城市A',
+        endPoint: '城市B',
+        vehicleType: vehicleTypes[i] as 'bus' | 'van' | 'car',
+        price: 100,
+        seatCount: 30,
+        departureTime: '2025-01-01 08:00',
+        activityId: 1
+      });
+
+      // 验证车型正确显示
+      await routeManagementPage.expectRouteExists(routeName);
+      const routeRow = await routeManagementPage.getRouteByName(routeName);
+      expect(routeRow).not.toBeNull();
+      await expect(routeRow!).toContainText(vehicleTypeNames[i]);
+    }
+  });
+});

+ 10 - 0
tests/e2e/utils/test-setup.ts

@@ -2,11 +2,15 @@ import { test as base } from '@playwright/test';
 import { AdminLoginPage } from '../pages/admin/login.page';
 import { DashboardPage } from '../pages/admin/dashboard.page';
 import { UserManagementPage } from '../pages/admin/user-management.page';
+import { ActivityManagementPage } from '../pages/admin/activity-management.page';
+import { RouteManagementPage } from '../pages/admin/route-management.page';
 
 type Fixtures = {
   adminLoginPage: AdminLoginPage;
   dashboardPage: DashboardPage;
   userManagementPage: UserManagementPage;
+  activityManagementPage: ActivityManagementPage;
+  routeManagementPage: RouteManagementPage;
 };
 
 export const test = base.extend<Fixtures>({
@@ -19,6 +23,12 @@ export const test = base.extend<Fixtures>({
   userManagementPage: async ({ page }, use) => {
     await use(new UserManagementPage(page));
   },
+  activityManagementPage: async ({ page }, use) => {
+    await use(new ActivityManagementPage(page));
+  },
+  routeManagementPage: async ({ page }, use) => {
+    await use(new RouteManagementPage(page));
+  },
 });
 
 export { expect } from '@playwright/test';