소스 검색

✅ test(admin): 添加管理后台集成测试和e2e测试

- 新增组件测试工作流配置【ci】
- 添加仪表盘页面集成测试【test】
- 添加登录页面集成测试【test】
- 添加用户管理页面集成测试【test】
- 添加登录功能e2e测试【test】
- 添加用户管理e2e测试【test】
- 更新集成测试工作流配置【ci】
- 添加测试脚本到package.json【chore】
yourname 2 달 전
부모
커밋
4467e9fe31

+ 90 - 0
.github/workflows/component-tests.yml

@@ -0,0 +1,90 @@
+name: Component Tests
+
+on:
+  push:
+    branches: [ main, develop ]
+    paths:
+      - 'src/client/**'
+      - 'src/client/__integration_tests__/**'
+      - 'vitest.config.components.ts'
+      - '.github/workflows/component-tests.yml'
+  pull_request:
+    branches: [ main ]
+    paths:
+      - 'src/client/**'
+      - 'src/client/__integration_tests__/**'
+      - 'vitest.config.components.ts'
+      - '.github/workflows/component-tests.yml'
+  workflow_dispatch:
+
+jobs:
+  component-tests:
+    runs-on: ubuntu-latest
+    timeout-minutes: 10
+
+    steps:
+    - name: Checkout code
+      uses: actions/checkout@v4
+
+    - name: Setup Node.js
+      uses: actions/setup-node@v4
+      with:
+        node-version: '20'
+        cache: 'pnpm'
+
+    - name: Install pnpm
+      uses: pnpm/action-setup@v2
+      with:
+        version: 8
+
+    - name: Install dependencies
+      run: pnpm install --frozen-lockfile
+
+    - name: Run component tests
+      run: pnpm test:components
+
+    - name: Upload test results
+      if: always()
+      uses: actions/upload-artifact@v4
+      with:
+        name: component-test-results
+        path: |
+          test-results/
+          coverage/
+        retention-days: 7
+
+    - name: Generate coverage report
+      if: success()
+      run: pnpm test:components:coverage
+
+    - name: Upload coverage to Codecov
+      if: success()
+      uses: codecov/codecov-action@v3
+      with:
+        file: ./coverage/coverage-final.json
+        flags: component-tests
+
+    - name: Generate test summary
+      if: always()
+      uses: test-summary/action@v2
+      with:
+        paths: test-results/junit.xml
+
+    - name: Notify on failure
+      if: failure()
+      uses: 8398a7/action-slack@v3
+      with:
+        status: ${{ job.status }}
+        channel: '#ci-notifications'
+        webhook_url: ${{ secrets.SLACK_WEBHOOK }}
+      env:
+        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
+
+    - name: Send test results to GitHub
+      if: always()
+      uses: dorny/test-reporter@v1
+      with:
+        name: Component Tests
+        path: test-results/junit.xml
+        reporter: jest-junit
+        fail-on-error: false

+ 2 - 0
.github/workflows/integration-tests.yml

@@ -6,12 +6,14 @@ on:
     paths:
       - 'src/server/api/**'
       - 'src/server/__test_utils__/**'
+      - 'src/client/__integration_tests__/**'
       - '.github/workflows/integration-tests.yml'
   pull_request:
     branches: [ main ]
     paths:
       - 'src/server/api/**'
       - 'src/server/__test_utils__/**'
+      - 'src/client/__integration_tests__/**'
       - '.github/workflows/integration-tests.yml'
   workflow_dispatch:
 

+ 1 - 0
package.json

@@ -13,6 +13,7 @@
     "test:coverage": "npm run test:api:coverage && npm run test:components:coverage",
     "test:api": "vitest",
     "test:components": "vitest --config=vitest.config.components.ts",
+    "test:integration": "npm run test:components",
     "test:api:coverage": "vitest --coverage",
     "test:components:coverage": "vitest --coverage --config=vitest.config.components.ts",
     "test:e2e": "playwright test --config=tests/e2e/playwright.config.ts",

+ 124 - 0
src/client/__integration_tests__/admin/dashboard.test.tsx

@@ -0,0 +1,124 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { DashboardPage } from '@/client/admin/pages/Dashboard';
+
+// Mock 导航功能
+const mockNavigate = vi.fn();
+vi.mock('react-router', () => ({
+  useNavigate: () => mockNavigate,
+}));
+
+describe('DashboardPage 集成测试', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it('应该正确渲染核心指标卡片', async () => {
+    render(<DashboardPage />);
+
+    // 验证统计卡片显示
+    expect(screen.getByText('活跃用户')).toBeInTheDocument();
+    expect(screen.getByText('112,893')).toBeInTheDocument();
+    expect(screen.getByText('较昨日增长 12.5%')).toBeInTheDocument();
+
+    expect(screen.getByText('系统消息')).toBeInTheDocument();
+    expect(screen.getByText('93')).toBeInTheDocument();
+
+    expect(screen.getByText('在线用户')).toBeInTheDocument();
+    expect(screen.getByText('1,128')).toBeInTheDocument();
+  });
+
+  it('应该正确渲染系统性能指标', async () => {
+    render(<DashboardPage />);
+
+    // 验证系统性能部分
+    expect(screen.getByText('系统性能')).toBeInTheDocument();
+    expect(screen.getByText('当前系统各项资源的使用情况')).toBeInTheDocument();
+
+    expect(screen.getByText('CPU使用率')).toBeInTheDocument();
+    expect(screen.getByText('65%')).toBeInTheDocument();
+
+    expect(screen.getByText('内存使用率')).toBeInTheDocument();
+    expect(screen.getByText('78%')).toBeInTheDocument();
+
+    expect(screen.getByText('磁盘使用率')).toBeInTheDocument();
+    expect(screen.getByText('45%')).toBeInTheDocument();
+
+    expect(screen.getByText('网络使用率')).toBeInTheDocument();
+    expect(screen.getByText('32%')).toBeInTheDocument();
+  });
+
+  it('应该正确渲染最近活动列表', async () => {
+    render(<DashboardPage />);
+
+    // 验证最近活动部分
+    expect(screen.getByText('最近活动')).toBeInTheDocument();
+    expect(screen.getByText('系统最新操作记录')).toBeInTheDocument();
+
+    expect(screen.getByText('张三 登录系统')).toBeInTheDocument();
+    expect(screen.getByText('2分钟前')).toBeInTheDocument();
+
+    expect(screen.getByText('李四 创建了新用户')).toBeInTheDocument();
+    expect(screen.getByText('5分钟前')).toBeInTheDocument();
+
+    expect(screen.getByText('王五 删除了用户')).toBeInTheDocument();
+    expect(screen.getByText('10分钟前')).toBeInTheDocument();
+
+    expect(screen.getByText('赵六 修改了配置')).toBeInTheDocument();
+    expect(screen.getByText('15分钟前')).toBeInTheDocument();
+  });
+
+  it('应该正确渲染快捷操作卡片', async () => {
+    render(<DashboardPage />);
+
+    // 验证快捷操作部分
+    expect(screen.getByText('快捷操作')).toBeInTheDocument();
+    expect(screen.getByText('常用的管理功能')).toBeInTheDocument();
+
+    expect(screen.getByText('用户管理')).toBeInTheDocument();
+    expect(screen.getByText('查看和管理所有用户')).toBeInTheDocument();
+
+    expect(screen.getByText('系统设置')).toBeInTheDocument();
+    expect(screen.getByText('配置系统参数')).toBeInTheDocument();
+
+    expect(screen.getByText('数据备份')).toBeInTheDocument();
+    expect(screen.getByText('执行数据备份操作')).toBeInTheDocument();
+
+    expect(screen.getByText('日志查看')).toBeInTheDocument();
+    expect(screen.getByText('查看系统日志')).toBeInTheDocument();
+  });
+
+  it('应该正确渲染页面标题和描述', async () => {
+    render(<DashboardPage />);
+
+    expect(screen.getByText('仪表盘')).toBeInTheDocument();
+    expect(screen.getByText('欢迎回来!这里是系统概览和关键指标。')).toBeInTheDocument();
+  });
+
+  it('应该具有正确的响应式布局类名', async () => {
+    const { container } = render(<DashboardPage />);
+
+    // 验证网格布局类名
+    const gridElements = container.querySelectorAll('.grid');
+    expect(gridElements.length).toBeGreaterThan(0);
+
+    // 验证响应式类名存在 - 使用更宽松的检查
+    const hasResponsiveClasses = container.innerHTML.includes('md:grid-cols-2') ||
+                                container.innerHTML.includes('lg:grid-cols-3');
+    expect(hasResponsiveClasses).toBe(true);
+  });
+
+  it('应该包含正确的图标和视觉元素', async () => {
+    render(<DashboardPage />);
+
+    // 验证卡片标题存在
+    expect(screen.getByText('活跃用户')).toBeInTheDocument();
+    expect(screen.getByText('系统消息')).toBeInTheDocument();
+    expect(screen.getByText('在线用户')).toBeInTheDocument();
+
+    // 验证进度条存在 - 使用更通用的检查
+    const progressElements = document.querySelectorAll('[role="progressbar"]');
+    expect(progressElements.length).toBeGreaterThan(0);
+  });
+});

+ 183 - 0
src/client/__integration_tests__/admin/login.test.tsx

@@ -0,0 +1,183 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { LoginPage } from '@/client/admin/pages/Login';
+import { TestWrapper } from '@/client/__test_utils__/test-render';
+
+// Mock 认证钩子和导航
+const mockLogin = vi.fn();
+const mockNavigate = vi.fn();
+
+vi.mock('../hooks/AuthProvider', () => ({
+  useAuth: () => ({
+    login: mockLogin,
+  }),
+}));
+
+vi.mock('react-router', () => ({
+  useNavigate: () => mockNavigate,
+}));
+
+// Mock toast
+vi.mock('sonner', () => ({
+  toast: {
+    success: vi.fn(),
+    error: vi.fn(),
+  }
+}));
+
+describe('LoginPage 集成测试', () => {
+  const user = userEvent.setup();
+
+  beforeEach(() => {
+    vi.clearAllMocks();
+    mockLogin.mockResolvedValue(undefined);
+  });
+
+  it('应该正确渲染登录页面标题和品牌元素', async () => {
+    render(<LoginPage />);
+
+    expect(screen.getByText('管理后台登录')).toBeInTheDocument();
+    expect(screen.getByText('请输入您的账号和密码继续操作')).toBeInTheDocument();
+    expect(screen.getByText('欢迎登录')).toBeInTheDocument();
+  });
+
+  it('应该包含用户名和密码输入字段', async () => {
+    render(<LoginPage />);
+
+    expect(screen.getByLabelText('用户名')).toBeInTheDocument();
+    expect(screen.getByLabelText('密码')).toBeInTheDocument();
+    expect(screen.getByPlaceholderText('请输入用户名')).toBeInTheDocument();
+    expect(screen.getByPlaceholderText('请输入密码')).toBeInTheDocument();
+  });
+
+  it('应该显示密码可见性切换按钮', async () => {
+    render(<LoginPage />);
+
+    const passwordInput = screen.getByPlaceholderText('请输入密码');
+    expect(passwordInput).toHaveAttribute('type', 'password');
+
+    const toggleButton = screen.getByRole('button', { name: /toggle password visibility/i });
+    await user.click(toggleButton);
+
+    expect(passwordInput).toHaveAttribute('type', 'text');
+  });
+
+  it('应该验证表单必填字段', async () => {
+    render(<LoginPage />);
+
+    const submitButton = screen.getByRole('button', { name: '登录' });
+    await user.click(submitButton);
+
+    // 验证错误消息显示
+    expect(screen.getByText('请输入用户名')).toBeInTheDocument();
+    expect(screen.getByText('请输入密码')).toBeInTheDocument();
+  });
+
+  it('应该成功处理登录表单提交', async () => {
+    render(<LoginPage />);
+
+    // 填写表单
+    await user.type(screen.getByLabelText('用户名'), 'admin');
+    await user.type(screen.getByLabelText('密码'), 'admin123');
+
+    const submitButton = screen.getByRole('button', { name: '登录' });
+    await user.click(submitButton);
+
+    // 验证登录函数被调用
+    await waitFor(() => {
+      expect(mockLogin).toHaveBeenCalledWith('admin', 'admin123');
+    });
+
+    // 验证导航发生
+    expect(mockNavigate).toHaveBeenCalledWith('/admin/dashboard');
+  });
+
+  it('应该处理登录失败场景', async () => {
+    const errorMessage = '认证失败';
+    mockLogin.mockRejectedValue(new Error(errorMessage));
+
+    render(<LoginPage />);
+
+    // 填写表单
+    await user.type(screen.getByLabelText('用户名'), 'admin');
+    await user.type(screen.getByLabelText('密码'), 'wrongpassword');
+
+    const submitButton = screen.getByRole('button', { name: '登录' });
+    await user.click(submitButton);
+
+    // 验证错误 toast 被调用
+    await waitFor(() => {
+      const { error } = vi.mocked(require('sonner').toast);
+      expect(error).toHaveBeenCalledWith(errorMessage);
+    });
+  });
+
+  it('应该显示加载状态当登录进行中', async () => {
+    // 模拟异步登录
+    mockLogin.mockImplementation(() => new Promise(resolve => setTimeout(resolve, 100)));
+
+    render(<LoginPage />);
+
+    // 填写表单并提交
+    await user.type(screen.getByLabelText('用户名'), 'admin');
+    await user.type(screen.getByLabelText('密码'), 'admin123');
+
+    const submitButton = screen.getByRole('button', { name: '登录' });
+    await user.click(submitButton);
+
+    // 验证加载状态显示
+    expect(screen.getByText('登录中...')).toBeInTheDocument();
+    expect(submitButton).toBeDisabled();
+
+    // 等待登录完成
+    await waitFor(() => {
+      expect(screen.queryByText('登录中...')).not.toBeInTheDocument();
+    });
+  });
+
+  it('应该显示测试账号信息', async () => {
+    render(<LoginPage />);
+
+    expect(screen.getByText('测试账号:')).toBeInTheDocument();
+    expect(screen.getByText('admin')).toBeInTheDocument();
+    expect(screen.getByText('admin123')).toBeInTheDocument();
+  });
+
+  it('应该包含版权信息和联系链接', async () => {
+    render(<LoginPage />);
+
+    const currentYear = new Date().getFullYear();
+    expect(screen.getByText(`© ${currentYear} 管理系统. 保留所有权利.`)).toBeInTheDocument();
+    expect(screen.getByText('遇到问题?')).toBeInTheDocument();
+    expect(screen.getByText('联系管理员')).toBeInTheDocument();
+  });
+
+  it('应该具有正确的样式类名', async () => {
+    const { container } = render(<LoginPage />);
+
+    // 验证背景渐变
+    const bgGradient = container.querySelector('.bg-gradient-to-br');
+    expect(bgGradient).toBeInTheDocument();
+
+    // 验证卡片阴影
+    const shadowCard = container.querySelector('.shadow-xl');
+    expect(shadowCard).toBeInTheDocument();
+
+    // 验证响应式 padding
+    const responsivePadding = container.querySelector('.px-4.sm\:px-6.lg\:px-8');
+    expect(responsivePadding).toBeInTheDocument();
+  });
+
+  it('应该包含正确的图标', async () => {
+    render(<LoginPage />);
+
+    // 验证用户图标
+    const userIcons = screen.getAllByRole('img', { hidden: true });
+    expect(userIcons.length).toBeGreaterThan(0);
+
+    // 验证锁图标
+    expect(screen.getByLabelText('username-icon')).toBeInTheDocument();
+    expect(screen.getByLabelText('password-icon')).toBeInTheDocument();
+  });
+});

+ 262 - 0
src/client/__integration_tests__/admin/users.test.tsx

@@ -0,0 +1,262 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { UsersPage } from '@/client/admin/pages/Users';
+import { TestWrapper } from '@/client/__test_utils__/test-render';
+
+// Mock API 客户端
+vi.mock('@/client/api', () => ({
+  userClient: {
+    $get: vi.fn().mockResolvedValue({
+      status: 200,
+      ok: true,
+      json: async () => ({
+        data: [
+          {
+            id: 1,
+            username: 'admin',
+            nickname: '管理员',
+            email: 'admin@example.com',
+            phone: '13800138000',
+            name: '系统管理员',
+            isDisabled: 0,
+            createdAt: '2024-01-01T00:00:00.000Z',
+            roles: [{ id: 1, name: 'admin' }]
+          }
+        ],
+        pagination: {
+          total: 1,
+          current: 1,
+          pageSize: 10
+        }
+      })
+    }),
+    $post: vi.fn().mockResolvedValue({
+      status: 201,
+      ok: true,
+      json: async () => ({ message: '用户创建成功' })
+    }),
+    ':id': {
+      $put: vi.fn().mockResolvedValue({
+        status: 200,
+        ok: true,
+        json: async () => ({ message: '用户更新成功' })
+      }),
+      $delete: vi.fn().mockResolvedValue({
+        status: 204,
+        ok: true
+      })
+    }
+  }
+}));
+
+// Mock toast 和 react-hook-form
+vi.mock('sonner', () => ({
+  toast: {
+    success: vi.fn(),
+    error: vi.fn(),
+  }
+}));
+
+vi.mock('react-hook-form', async () => {
+  const actual = await vi.importActual('react-hook-form');
+  return {
+    ...actual,
+    useForm: vi.fn().mockReturnValue({
+      control: {},
+      handleSubmit: vi.fn((fn) => fn),
+      reset: vi.fn(),
+      formState: { errors: {} }
+    })
+  };
+});
+
+describe('UsersPage 集成测试', () => {
+  const user = userEvent.setup();
+
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it('应该正确渲染用户管理页面标题', async () => {
+    render(
+      <TestWrapper>
+        <UsersPage />
+      </TestWrapper>
+    );
+
+    expect(screen.getByText('用户管理')).toBeInTheDocument();
+    expect(screen.getByText('创建用户')).toBeInTheDocument();
+  });
+
+  it('应该显示用户列表和搜索功能', async () => {
+    render(
+      <TestWrapper>
+        <UsersPage />
+      </TestWrapper>
+    );
+
+    // 等待数据加载
+    await waitFor(() => {
+      expect(screen.getByPlaceholderText('搜索用户名、昵称或邮箱...')).toBeInTheDocument();
+    });
+
+    expect(screen.getByText('搜索')).toBeInTheDocument();
+    expect(screen.getByText('高级筛选')).toBeInTheDocument();
+  });
+
+  it('应该处理搜索功能', async () => {
+    render(<UsersPage />);
+
+    const searchInput = screen.getByPlaceholderText('搜索用户名、昵称或邮箱...');
+    const searchButton = screen.getByText('搜索');
+
+    // 输入搜索关键词
+    await user.type(searchInput, 'testuser');
+    await user.click(searchButton);
+
+    // 验证搜索参数被设置
+    expect(searchInput).toHaveValue('testuser');
+  });
+
+  it('应该显示高级筛选功能', async () => {
+    render(<UsersPage />);
+
+    const filterButton = screen.getByText('高级筛选');
+    await user.click(filterButton);
+
+    // 验证筛选表单显示
+    expect(screen.getByText('用户状态')).toBeInTheDocument();
+    expect(screen.getByText('用户角色')).toBeInTheDocument();
+    expect(screen.getByText('创建时间')).toBeInTheDocument();
+  });
+
+  it('应该处理用户状态筛选', async () => {
+    render(<UsersPage />);
+
+    const filterButton = screen.getByText('高级筛选');
+    await user.click(filterButton);
+
+    const statusSelect = screen.getByText('选择状态');
+    await user.click(statusSelect);
+
+    // 这里需要模拟选择操作,但由于 Select 组件的复杂性,我们验证选项存在
+    expect(screen.getByText('全部状态')).toBeInTheDocument();
+    expect(screen.getByText('启用')).toBeInTheDocument();
+    expect(screen.getByText('禁用')).toBeInTheDocument();
+  });
+
+  it('应该显示创建用户按钮并打开模态框', async () => {
+    render(<UsersPage />);
+
+    const createButton = screen.getByText('创建用户');
+    await user.click(createButton);
+
+    // 验证模态框标题
+    expect(screen.getByText('创建用户')).toBeInTheDocument();
+  });
+
+  it('应该显示分页组件', async () => {
+    render(
+      <TestWrapper>
+        <UsersPage />
+      </TestWrapper>
+    );
+
+    // 验证分页控件存在
+    await waitFor(() => {
+      expect(screen.getByText(/共 \d+ 位用户/)).toBeInTheDocument();
+    });
+  });
+
+  it('应该处理表格数据加载状态', async () => {
+    render(
+      <TestWrapper>
+        <UsersPage />
+      </TestWrapper>
+    );
+
+    // 验证骨架屏或加载状态
+    const skeletonElements = document.querySelectorAll('[data-slot="skeleton"]');
+    expect(skeletonElements.length).toBeGreaterThan(0);
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      expect(screen.getByText('admin')).toBeInTheDocument();
+    });
+  });
+
+  it('应该显示正确的表格列标题', async () => {
+    render(
+      <TestWrapper>
+        <UsersPage />
+      </TestWrapper>
+    );
+
+    // 等待数据加载
+    await waitFor(() => {
+      expect(screen.getByText('用户名')).toBeInTheDocument();
+      expect(screen.getByText('昵称')).toBeInTheDocument();
+      expect(screen.getByText('邮箱')).toBeInTheDocument();
+      expect(screen.getByText('真实姓名')).toBeInTheDocument();
+      expect(screen.getByText('角色')).toBeInTheDocument();
+      expect(screen.getByText('状态')).toBeInTheDocument();
+      expect(screen.getByText('创建时间')).toBeInTheDocument();
+      expect(screen.getByText('操作')).toBeInTheDocument();
+    });
+  });
+
+  it('应该包含编辑和删除操作按钮', async () => {
+    render(
+      <TestWrapper>
+        <UsersPage />
+      </TestWrapper>
+    );
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      expect(screen.getByText('admin')).toBeInTheDocument();
+
+      // 查找操作按钮
+      const editButtons = document.querySelectorAll('[aria-label*="edit"], [aria-label*="编辑"]');
+      const deleteButtons = document.querySelectorAll('[aria-label*="delete"], [aria-label*="删除"]');
+
+      expect(editButtons.length).toBeGreaterThan(0);
+      expect(deleteButtons.length).toBeGreaterThan(0);
+    });
+  });
+
+  it('应该处理响应式布局', async () => {
+    const { container } = render(
+      <TestWrapper>
+        <UsersPage />
+      </TestWrapper>
+    );
+
+    // 等待数据加载
+    await waitFor(() => {
+      expect(screen.getByText('admin')).toBeInTheDocument();
+    });
+
+    // 验证响应式网格类名
+    const gridElements = container.querySelectorAll('.grid');
+    expect(gridElements.length).toBeGreaterThan(0);
+
+    // 验证响应式类名存在
+    const hasResponsiveClasses = container.innerHTML.includes('md:grid-cols-3');
+    expect(hasResponsiveClasses).toBe(true);
+  });
+
+  it('应该显示用户总数信息', async () => {
+    render(
+      <TestWrapper>
+        <UsersPage />
+      </TestWrapper>
+    );
+
+    // 验证用户总数显示
+    await waitFor(() => {
+      expect(screen.getByText(/共 \d+ 位用户/)).toBeInTheDocument();
+    });
+  });
+});

+ 212 - 0
tests/e2e/specs/admin/login.spec.ts

@@ -0,0 +1,212 @@
+import { test, expect } from '../../utils/test-setup';
+import testUsers from '../../fixtures/test-users.json' with { type: 'json' };
+
+test.describe('登录页面 E2E 测试', () => {
+  test.beforeEach(async ({ page, adminLoginPage }) => {
+    await adminLoginPage.goto();
+  });
+
+  test('登录页面加载', async ({ adminLoginPage }) => {
+    await adminLoginPage.expectToBeVisible();
+    await expect(adminLoginPage.pageTitle).toHaveText('管理后台登录');
+    await expect(adminLoginPage.welcomeText).toBeVisible();
+  });
+
+  test('成功登录', async ({ adminLoginPage, dashboardPage }) => {
+    // 使用有效凭据登录
+    await adminLoginPage.login(testUsers.admin.username, testUsers.admin.password);
+
+    // 验证跳转到仪表盘
+    await dashboardPage.expectToBeVisible();
+    await expect(dashboardPage.pageTitle).toHaveText('仪表盘');
+
+    // 验证成功 toast 显示
+    await expect(adminLoginPage.successToast).toBeVisible();
+    await expect(adminLoginPage.successToast).toContainText('登录成功');
+  });
+
+  test('登录失败 - 错误密码', async ({ adminLoginPage }) => {
+    // 使用错误密码尝试登录
+    await adminLoginPage.login(testUsers.admin.username, 'wrongpassword');
+
+    // 验证错误消息显示
+    await expect(adminLoginPage.errorToast).toBeVisible();
+    await expect(adminLoginPage.errorToast).toContainText('登录失败');
+
+    // 验证仍然在登录页面
+    await adminLoginPage.expectToBeVisible();
+  });
+
+  test('登录失败 - 不存在的用户', async ({ adminLoginPage }) => {
+    // 使用不存在的用户尝试登录
+    await adminLoginPage.login('nonexistentuser', 'anypassword');
+
+    // 验证错误消息显示
+    await expect(adminLoginPage.errorToast).toBeVisible();
+    await expect(adminLoginPage.errorToast).toContainText('登录失败');
+
+    // 验证仍然在登录页面
+    await adminLoginPage.expectToBeVisible();
+  });
+
+  test('表单验证 - 空用户名', async ({ adminLoginPage }) => {
+    // 不填写用户名直接提交
+    await adminLoginPage.passwordInput.fill('password');
+    await adminLoginPage.submitButton.click();
+
+    // 验证验证错误显示
+    await expect(adminLoginPage.usernameError).toBeVisible();
+    await expect(adminLoginPage.usernameError).toContainText('请输入用户名');
+  });
+
+  test('表单验证 - 空密码', async ({ adminLoginPage }) => {
+    // 不填写密码直接提交
+    await adminLoginPage.usernameInput.fill('admin');
+    await adminLoginPage.submitButton.click();
+
+    // 验证验证错误显示
+    await expect(adminLoginPage.passwordError).toBeVisible();
+    await expect(adminLoginPage.passwordError).toContainText('请输入密码');
+  });
+
+  test('密码可见性切换', async ({ adminLoginPage }) => {
+    // 初始状态密码应该被隐藏
+    await expect(adminLoginPage.passwordInput).toHaveAttribute('type', 'password');
+
+    // 点击显示密码按钮
+    await adminLoginPage.togglePasswordButton.click();
+
+    // 验证密码可见
+    await expect(adminLoginPage.passwordInput).toHaveAttribute('type', 'text');
+
+    // 再次点击隐藏密码
+    await adminLoginPage.togglePasswordButton.click();
+
+    // 验证密码隐藏
+    await expect(adminLoginPage.passwordInput).toHaveAttribute('type', 'password');
+  });
+
+  test('测试账号信息显示', async ({ adminLoginPage }) => {
+    // 验证测试账号信息存在
+    await expect(adminLoginPage.testAccountInfo).toBeVisible();
+    await expect(adminLoginPage.testAccountInfo).toContainText('admin');
+    await expect(adminLoginPage.testAccountInfo).toContainText('admin123');
+  });
+
+  test('使用测试账号登录', async ({ adminLoginPage, dashboardPage }) => {
+    // 使用测试账号信息登录
+    await adminLoginPage.usernameInput.fill('admin');
+    await adminLoginPage.passwordInput.fill('admin123');
+    await adminLoginPage.submitButton.click();
+
+    // 验证登录成功
+    await dashboardPage.expectToBeVisible();
+    await expect(dashboardPage.pageTitle).toHaveText('仪表盘');
+  });
+
+  test('登录页面样式和布局', async ({ adminLoginPage, page }) => {
+    // 验证背景渐变
+    await expect(adminLoginPage.backgroundElement).toHaveClass(/bg-gradient/);
+
+    // 验证卡片阴影
+    await expect(adminLoginPage.loginCard).toHaveClass(/shadow/);
+
+    // 验证响应式设计
+    await page.setViewportSize({ width: 375, height: 667 });
+    await adminLoginPage.expectToBeVisible();
+
+    // 移动端布局验证
+    await expect(adminLoginPage.pageTitle).toBeVisible();
+    await expect(adminLoginPage.usernameInput).toBeVisible();
+    await expect(adminLoginPage.passwordInput).toBeVisible();
+  });
+
+  test('登录后刷新保持登录状态', async ({ adminLoginPage, dashboardPage, page }) => {
+    // 先登录
+    await adminLoginPage.login(testUsers.admin.username, testUsers.admin.password);
+    await dashboardPage.expectToBeVisible();
+
+    // 刷新页面
+    await page.reload();
+
+    // 验证仍然保持登录状态
+    await dashboardPage.expectToBeVisible();
+    await expect(dashboardPage.pageTitle).toHaveText('仪表盘');
+  });
+
+  test('登出后重定向到登录页', async ({ adminLoginPage, dashboardPage, page }) => {
+    // 先登录
+    await adminLoginPage.login(testUsers.admin.username, testUsers.admin.password);
+    await dashboardPage.expectToBeVisible();
+
+    // 执行登出
+    await dashboardPage.logout();
+
+    // 验证重定向到登录页
+    await adminLoginPage.expectToBeVisible();
+    await expect(adminLoginPage.pageTitle).toHaveText('管理后台登录');
+
+    // 验证不能直接访问受保护页面
+    await page.goto('/admin/dashboard');
+    await adminLoginPage.expectToBeVisible(); // 应该重定向回登录页
+  });
+
+  test('多标签页登录状态同步', async ({ adminLoginPage, dashboardPage, context }) => {
+    // 在第一个标签页登录
+    await adminLoginPage.login(testUsers.admin.username, testUsers.admin.password);
+    await dashboardPage.expectToBeVisible();
+
+    // 打开第二个标签页
+    const newPage = await context.newPage();
+    await newPage.goto('/admin/dashboard');
+
+    // 验证第二个标签页也自动登录
+    const newDashboardPage = dashboardPage.clone(newPage);
+    await newDashboardPage.expectToBeVisible();
+
+    // 在第一个标签页登出
+    await dashboardPage.logout();
+    await adminLoginPage.expectToBeVisible();
+
+    // 验证第二个标签页也登出
+    await newPage.reload();
+    const newLoginPage = adminLoginPage.clone(newPage);
+    await newLoginPage.expectToBeVisible();
+  });
+
+  test('登录加载状态显示', async ({ adminLoginPage }) => {
+    // 填写登录信息
+    await adminLoginPage.usernameInput.fill('admin');
+    await adminLoginPage.passwordInput.fill('admin123');
+
+    // 提交表单
+    await adminLoginPage.submitButton.click();
+
+    // 验证加载状态显示
+    await expect(adminLoginPage.loadingSpinner).toBeVisible();
+    await expect(adminLoginPage.submitButton).toBeDisabled();
+
+    // 等待加载完成
+    await expect(adminLoginPage.loadingSpinner).not.toBeVisible();
+  });
+
+  test('浏览器返回按钮行为', async ({ adminLoginPage, dashboardPage, page }) => {
+    // 先登录
+    await adminLoginPage.login(testUsers.admin.username, testUsers.admin.password);
+    await dashboardPage.expectToBeVisible();
+
+    // 点击浏览器返回按钮
+    await page.goBack();
+
+    // 验证不会返回到登录页(应该停留在仪表盘或重定向)
+    try {
+      await adminLoginPage.expectToBeVisible({ timeout: 2000 });
+      // 如果看到登录页,再次前进
+      await page.goForward();
+      await dashboardPage.expectToBeVisible();
+    } catch {
+      // 如果没看到登录页,说明行为正确
+      await dashboardPage.expectToBeVisible();
+    }
+  });
+});

+ 184 - 0
tests/e2e/specs/admin/users.spec.ts

@@ -0,0 +1,184 @@
+import { test, expect } from '../../utils/test-setup';
+import testUsers from '../../fixtures/test-users.json' with { type: 'json' };
+
+test.describe('用户管理 E2E 测试', () => {
+  test.beforeEach(async ({ adminLoginPage, userManagementPage }) => {
+    // 以管理员身份登录
+    await adminLoginPage.goto();
+    await adminLoginPage.login(testUsers.admin.username, testUsers.admin.password);
+    await userManagementPage.goto();
+    await userManagementPage.expectToBeVisible();
+  });
+
+  test('用户列表页面加载', async ({ userManagementPage }) => {
+    await userManagementPage.expectToBeVisible();
+    await expect(userManagementPage.pageTitle).toHaveText('用户管理');
+  });
+
+  test('搜索用户功能', async ({ userManagementPage }) => {
+    // 在搜索框中输入关键词
+    await userManagementPage.searchInput.fill('test');
+    await userManagementPage.searchButton.click();
+
+    // 验证搜索结果
+    await expect(userManagementPage.userTable).toBeVisible();
+    const userRows = await userManagementPage.getUserRows();
+    expect(userRows.length).toBeGreaterThan(0);
+  });
+
+  test('创建新用户', async ({ userManagementPage }) => {
+    const testUser = {
+      username: `testuser_${Date.now()}`,
+      password: 'Test123!',
+      email: `test${Date.now()}@example.com`
+    };
+
+    // 打开创建用户对话框
+    await userManagementPage.createUserButton.click();
+    await expect(userManagementPage.createUserDialog).toBeVisible();
+
+    // 填写用户信息
+    await userManagementPage.usernameInput.fill(testUser.username);
+    await userManagementPage.passwordInput.fill(testUser.password);
+    await userManagementPage.emailInput.fill(testUser.email);
+
+    // 提交表单
+    await userManagementPage.submitCreateUserButton.click();
+
+    // 验证用户创建成功
+    await expect(userManagementPage.successToast).toBeVisible();
+    await expect(userManagementPage.successToast).toContainText('创建成功');
+  });
+
+  test('编辑用户信息', async ({ userManagementPage }) => {
+    // 获取第一个用户
+    const firstUserRow = userManagementPage.getUserRow(0);
+    await expect(firstUserRow).toBeVisible();
+
+    // 点击编辑按钮
+    await userManagementPage.getEditButton(0).click();
+    await expect(userManagementPage.editUserDialog).toBeVisible();
+
+    // 修改用户信息
+    const newEmail = `updated${Date.now()}@example.com`;
+    await userManagementPage.emailInput.fill(newEmail);
+
+    // 提交更新
+    await userManagementPage.submitEditUserButton.click();
+
+    // 验证更新成功
+    await expect(userManagementPage.successToast).toBeVisible();
+    await expect(userManagementPage.successToast).toContainText('更新成功');
+  });
+
+  test('删除用户', async ({ userManagementPage }) => {
+    // 获取用户行数
+    const initialUserCount = (await userManagementPage.getUserRows()).length;
+
+    if (initialUserCount > 0) {
+      // 点击第一个用户的删除按钮
+      await userManagementPage.getDeleteButton(0).click();
+      await expect(userManagementPage.deleteConfirmDialog).toBeVisible();
+
+      // 确认删除
+      await userManagementPage.confirmDeleteButton.click();
+
+      // 验证删除成功
+      await expect(userManagementPage.successToast).toBeVisible();
+      await expect(userManagementPage.successToast).toContainText('删除成功');
+
+      // 验证用户数量减少
+      const finalUserCount = (await userManagementPage.getUserRows()).length;
+      expect(finalUserCount).toBe(initialUserCount - 1);
+    }
+  });
+
+  test('用户列表分页', async ({ userManagementPage }) => {
+    // 验证分页控件存在
+    await expect(userManagementPage.pagination).toBeVisible();
+
+    // 如果有第二页,测试翻页
+    if (await userManagementPage.nextPageButton.isVisible()) {
+      await userManagementPage.nextPageButton.click();
+      await expect(userManagementPage.currentPageIndicator).toContainText('2');
+
+      // 返回第一页
+      await userManagementPage.previousPageButton.click();
+      await expect(userManagementPage.currentPageIndicator).toContainText('1');
+    }
+  });
+
+  test('用户状态筛选', async ({ userManagementPage }) => {
+    // 打开高级筛选
+    await userManagementPage.advancedFilterButton.click();
+    await expect(userManagementPage.filterPanel).toBeVisible();
+
+    // 选择启用状态
+    await userManagementPage.statusFilter.selectOption('0');
+    await userManagementPage.applyFilterButton.click();
+
+    // 验证筛选结果
+    const userRows = await userManagementPage.getUserRows();
+    expect(userRows.length).toBeGreaterThan(0);
+
+    // 验证用户状态
+    for (const row of userRows) {
+      const statusBadge = row.locator('[data-testid="user-status"]');
+      await expect(statusBadge).toHaveText('启用');
+    }
+  });
+
+  test('重置筛选条件', async ({ userManagementPage }) => {
+    // 应用筛选条件
+    await userManagementPage.advancedFilterButton.click();
+    await userManagementPage.statusFilter.selectOption('1');
+    await userManagementPage.applyFilterButton.click();
+
+    // 重置筛选
+    await userManagementPage.resetFilterButton.click();
+
+    // 验证所有用户显示
+    const userRows = await userManagementPage.getUserRows();
+    expect(userRows.length).toBeGreaterThan(0);
+  });
+
+  test('用户列表排序', async ({ userManagementPage }) => {
+    // 点击用户名排序
+    await userManagementPage.usernameHeader.click();
+
+    // 获取排序后的用户名
+    const firstUsername = await userManagementPage.getUsername(0);
+    const secondUsername = await userManagementPage.getUsername(1);
+
+    // 验证排序(升序)
+    expect(firstUsername.localeCompare(secondUsername)).toBeLessThanOrEqual(0);
+
+    // 再次点击进行降序排序
+    await userManagementPage.usernameHeader.click();
+
+    const firstUsernameDesc = await userManagementPage.getUsername(0);
+    const secondUsernameDesc = await userManagementPage.getUsername(1);
+
+    // 验证降序排序
+    expect(firstUsernameDesc.localeCompare(secondUsernameDesc)).toBeGreaterThanOrEqual(0);
+  });
+
+  test('响应式布局 - 桌面端', async ({ userManagementPage, page }) => {
+    await page.setViewportSize({ width: 1200, height: 800 });
+    await userManagementPage.expectToBeVisible();
+
+    // 验证桌面端布局元素
+    await expect(userManagementPage.searchInput).toBeVisible();
+    await expect(userManagementPage.createUserButton).toBeVisible();
+    await expect(userManagementPage.userTable).toBeVisible();
+  });
+
+  test('响应式布局 - 移动端', async ({ userManagementPage, page }) => {
+    await page.setViewportSize({ width: 375, height: 667 });
+    await userManagementPage.expectToBeVisible();
+
+    // 验证移动端布局
+    await expect(userManagementPage.pageTitle).toBeVisible();
+    await expect(userManagementPage.mobileMenuButton).toBeVisible();
+  });
+});