E2E_AUTH_GUIDE.md 12.3 KB

E2E 测试认证指南

📋 概述

本文档介绍如何在 E2E 测试中处理需要登录认证的场景。

🌐 测试环境

架构说明

  • 本地: http://localhost:5173 - Vite 开发服务器
  • 测试服务器: http://oa-dev.onwall.cn - 通过反向代理访问
  • 代理前缀: /srv/ - API 请求前缀

访问方式

Playwright → localhost:5173 → /srv/api/* → oa-dev.onwall.cn/srv/api/*

详细说明: 参见 E2E 代理配置

🔑 测试账号

测试环境配置

测试手机号: 13761653761 固定验证码: 888888 测试服务器: http://oa-dev.onwall.cn(通过代理)

特点:

  • ✅ 点击"发送验证码"后无需等待,直接输入 888888 即可
  • ✅ 无需真实接收短信
  • ✅ 可以重复使用

📁 文件结构

e2e/
├── helpers/
│   └── auth.js           # 认证辅助工具
├── auth.spec.js         # 认证测试示例
├── courses.spec.js      # 课程功能测试(使用认证)
└── example.spec.js      # 其他示例测试

🚀 快速开始

1. 基础登录

import { login } from './helpers/auth'

test('测试需要登录的功能', async ({ page }) => {
  // 登录
  await login(page)

  // 访问需要登录的页面
  await page.goto('/profile')

  // 验证
  await expect(page).toHaveURL(/\/profile/)
})

2. 使用自动登录的 Fixture

import { test } from './helpers/auth'

test.describe('需要登录的测试组', () => {
  // 使用 authenticatedPage fixture 会自动登录
  test('测试功能', async ({ authenticatedPage }) => {
    // 页面已经登录,直接使用
    await authenticatedPage.goto('/profile')
  })
})

🛠️ 认证工具 API

login(page, account?)

执行完整的登录流程。

参数:

  • page - Playwright page 对象
  • account - 可选的账号信息(默认使用测试账号)

示例:

import { login, TEST_ACCOUNT } from './helpers/auth'

// 使用默认测试账号
await login(page)

// 使用自定义账号
await login(page, {
  phone: '13800138000',
  code: '123456',
})

登录流程:

  1. 访问 /login
  2. 输入手机号
  3. 点击"发送验证码"
  4. 输入验证码
  5. 点击登录
  6. 等待跳转到首页

quickLogin(page, token?)

使用 localStorage 快速登录(跳过 UI 流程)。

参数:

  • page - Playwright page 对象
  • token - 可选的 token 字符串

示例:

// 首次使用(会执行正常登录获取 token)
await quickLogin(page)

// 复用已知 token
await quickLogin(page, 'your-token-here')

适用场景:

  • 需要跳过登录流程,节省时间
  • 多个测试使用同一个 token
  • 不需要测试登录功能本身

logout(page)

清除登录状态。

示例:

import { logout } from './helpers/auth'

test('测试登出', async ({ page }) => {
  await login(page)
  await logout(page)

  // 验证已登出
  await expect(page).toHaveURL(/\/login/)
})

isLoggedIn(page)

检查当前是否已登录。

返回: Promise<boolean>

示例:

import { isLoggedIn } from './helpers/auth'

test('检查登录状态', async ({ page }) => {
  const loggedIn = await isLoggedIn(page)
  expect(loggedIn).toBe(true)
})

📝 测试场景示例

场景 1: 测试受保护的页面

test('未登录访问个人中心应跳转登录页', async ({ page }) => {
  // 确保未登录
  await logout(page)

  // 访问需要登录的页面
  await page.goto('/profile')

  // 验证跳转到登录页
  await expect(page).toHaveURL(/\/login/)
})

test('登录后可以访问个人中心', async ({ page }) => {
  // 登录
  await login(page)

  // 访问个人中心
  await page.goto('/profile')

  // 验证访问成功
  await expect(page).toHaveURL(/\/profile/)
})

场景 2: 测试购买流程

test('登录后购买课程', async ({ page }) => {
  // 登录
  await login(page)

  // 访问课程详情
  await page.goto('/courses/123')

  // 点击购买
  await page.click('button:has-text("购买")')

  // 验证跳转到结算页
  await expect(page).toHaveURL(/\/checkout/)

  // 验证结算页内容
  const checkoutForm = page.locator('.checkout')
  await expect(checkoutForm).toBeVisible()
})

场景 3: 测试打卡功能

test('提交打卡', async ({ page }) => {
  // 登录
  await login(page)

  // 访问打卡页面
  await page.goto('/checkin')

  // 填写打卡内容
  await page.fill('textarea', '今天的打卡内容')

  // 提交
  await page.click('button:has-text("提交")')

  // 验证成功提示
  const successToast = page.locator('.van-toast--success')
  await expect(successToast).toBeVisible()
})

场景 4: 测试学习进度

test('查看学习进度', async ({ page }) => {
  // 登录
  await login(page)

  // 访问学习进度页
  await page.goto('/study/progress')

  // 等待加载
  await page.waitForSelector('.progress-item')

  // 验证进度显示
  const progressItems = page.locator('.progress-item')
  const count = await progressItems.count()

  expect(count).toBeGreaterThan(0)
})

🎯 最佳实践

1. 使用 beforeEach 确保初始状态

test.describe('用户功能', () => {
  test.beforeEach(async ({ page }) => {
    // 每个测试前确保登录
    await login(page)
  })

  test('功能 A', async ({ page }) => {
    // 页面已登录
  })

  test('功能 B', async ({ page }) => {
    // 页面已登录
  })
})

2. 测试前先登出(避免状态污染)

test.describe('认证相关', () => {
  test.beforeEach(async ({ page }) => {
    // 确保未登录状态
    await logout(page)
  })

  test('测试登录', async ({ page }) => {
    // 干净的登录状态
  })
})

3. 使用 Page Object Model

// pages/LoginPage.js
export class LoginPage {
  constructor(page) {
    this.page = page
    this.phoneInput = page.locator('input[name="phone"]')
    this.codeInput = page.locator('input[name="code"]')
    this.submitButton = page.locator('button[type="submit"]')
  }

  async login(phone, code) {
    await this.phoneInput.fill(phone)
    await this.codeInput.fill(code)
    await this.submitButton.click()
  }
}

// 测试中使用
import { LoginPage } from './pages/LoginPage'

test('使用 Page Object 登录', async ({ page }) => {
  const loginPage = new LoginPage(page)
  await page.goto('/login')
  await loginPage.login('13761653761', '888888')
})

4. 合理使用快速登录

// ✓ GOOD - 需要测试登录本身
test('登录流程', async ({ page }) => {
  await page.goto('/login')
  await login(page)
  await expect(page).toHaveURL(/\/home/)
})

// ✓ GOOD - 不需要测试登录,使用快速登录
test('查看个人资料', async ({ page }) => {
  await quickLogin(page) // 跳过登录流程
  await page.goto('/profile')
  // 测试个人资料功能...
})

5. 复用登录状态

test.describe('多个测试共享登录', () => {
  let sharedToken = null

  test('第一个测试获取 token', async ({ page }) => {
    await login(page)

    // 获取 token
    sharedToken = await page.evaluate(() => {
      const userInfo = localStorage.getItem('user_info')
      return JSON.parse(userInfo).token
    })
  })

  test('第二个测试复用 token', async ({ page }) => {
    // 使用 token 快速登录
    await quickLogin(page, sharedToken)

    // 进行测试...
  })
})

🔧 高级用法

1. 拦截登录 API(Mock)

test('使用 Mock 登录', async ({ page }) => {
  // 拦截登录 API
  await page.route('**/api/login', route => {
    route.fulfill({
      status: 200,
      body: JSON.stringify({
        code: 1,
        data: {
          token: 'mock-token-123',
          userId: 'test-user',
        },
      }),
    })
  })

  // 访问登录页并提交
  await page.goto('/login')
  await login(page)

  // 验证使用 Mock 数据
  const token = await page.evaluate(() => {
    const userInfo = localStorage.getItem('user_info')
    return JSON.parse(userInfo).token
  })

  expect(token).toBe('mock-token-123')
})

2. 测试多种登录方式

test.describe('多种登录方式', () => {
  test('手机号验证码登录', async ({ page }) => {
    await page.goto('/login')

    // 选择验证码登录 tab
    await page.click('text=验证码登录')

    // 输入手机号
    await page.fill('input[name="phone"]', '13761653761')

    // 点击发送验证码
    await page.click('text=发送验证码')

    // 输入验证码
    await page.fill('input[name="code"]', '888888')

    // 点击登录
    await page.click('button[type="submit"]')

    // 验证
    await expect(page).toHaveURL(/\/home/)
  })

  test('密码登录(如果支持)', async ({ page }) => {
    await page.goto('/login')

    // 选择密码登录 tab
    await page.click('text=密码登录')

    // 输入手机号和密码
    await page.fill('input[name="phone"]', '13761653761')
    await page.fill('input[name="password"]', 'password123')

    // 点击登录
    await page.click('button[type="submit"]')

    // 验证
    await expect(page).toHaveURL(/\/home/)
  })
})

3. 测试权限控制

test.describe('权限控制', () => {
  test('普通用户无法访问管理员页面', async ({ page }) => {
    // 登录普通用户
    await login(page)

    // 尝试访问管理员页面
    await page.goto('/admin')

    // 验证跳转到 403 或首页
    await expect(page).toHaveURL(/(403|home)/)
  })

  test('VIP 用户可以访问 VIP 课程', async ({ page }) => {
    // 登录 VIP 用户(使用不同 token)
    await quickLogin(page, 'vip-user-token')

    // 访问 VIP 课程
    await page.goto('/courses/vip/123')

    // 验证可以访问
    await expect(page).toHaveURL(/\/courses\/vip\/123/)
  })
})

⚠️ 常见问题

1. 登录超时

问题: 登录测试经常超时失败

解决方案:

// 增加等待时间
await page.waitForURL(/\/home/, { timeout: 15000 })

// 或者使用更明确的等待
await page.waitForSelector('.home-content', { timeout: 10000 })

2. Token 过期

问题: 测试运行时提示 token 过期

解决方案:

// 每次测试前重新登录
test.beforeEach(async ({ page }) => {
  await logout(page)
  await login(page)
})

3. 登录状态污染

问题: 前一个测试的登录状态影响下一个测试

解决方案:

// 在 afterEach 中清理
test.afterEach(async ({ page }) => {
  await logout(page)
})

// 或者使用测试隔离
test.describe.serial('需要连续状态的测试', () => {
  // 这些测试会按顺序执行
})

4. 元素选择器不稳定

问题: 登录按钮的选择器经常变化

解决方案:

// 使用多个备选选择器
const submitButton = page
  .locator('button[type="submit"]')
  .or(page.locator('button:has-text("登录")'))
  .or(page.locator('.login-button'))

// 或者使用 data-testid
await page.click('[data-testid="login-submit"]')

📊 性能优化

1. 使用全局登录(全局钩子)

// e2e/hooks.js
import { login } from './helpers/auth'

export const mochaGlobalSetup = async () => {
  // 在所有测试前登录一次,获取 token
  // 将 token 保存到文件或环境变量
  // 后续测试直接使用 token
}

2. 并行测试处理登录

Playwright 默认并行运行测试,每个测试会独立登录。

// playwright.config.js
export default defineConfig({
  // 减少并行数量,避免登录冲突
  workers: 2,

  // 每个测试使用独立的浏览器上下文
  use: {
    // 每个测试有独立的 localStorage
  },
})

🎉 总结

关键点:

  • ✅ 使用 login() 执行完整登录流程
  • ✅ 使用 quickLogin() 跳过 UI 流程
  • ✅ 使用 authenticatedPage fixture 自动登录
  • ✅ 测试前确保干净的登录状态
  • ✅ 合理复用 token

测试账号:

  • 手机号: 13761653761
  • 验证码: 888888

下一步:

  • 为所有需要登录的功能编写测试
  • 使用 Page Object Model 组织代码
  • 配置 CI/CD 自动运行 E2E 测试

享受高效的 E2E 测试开发!🚀