零度AI
高级22 分钟阅读

Claude Code E2E测试

使用 Claude Code 配置 Playwright E2E 测试,模拟真实用户操作验证完整流程

Claude CodeE2E测试Playwright端到端自动化测试

E2E 测试概述

端到端(E2E)测试模拟真实用户操作,验证整个应用的正确性。Claude Code 可以帮你配置 Playwright 进行 E2E 测试。

E2E 测试范围

完整的用户流程:从登录到完成业务操作,验证前端、后端和数据库的集成。

Playwright 配置

项目初始化

bash
帮我创建一个 Playwright E2E 测试配置。
javascript
// playwright.config.js
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
  testDir: "./e2e",
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: [
    ["html", { outputFolder: "playwright-report" }],
    ["json", { outputFile: "playwright-results.json" }],
  ],
  use: {
    baseURL: process.env.BASE_URL || "http://localhost:3000",
    trace: "on-first-retry",
    screenshot: "only-on-failure",
    video: "retain-on-failure",
  },
  projects: [
    {
      name: "chromium",
      use: { ...devices["Desktop Chrome"] },
    },
    {
      name: "firefox",
      use: { ...devices["Desktop Firefox"] },
    },
    {
      name: "webkit",
      use: { ...devices["Desktop Safari"] },
    },
    {
      name: "Mobile Chrome",
      use: { ...devices["Pixel 5"] },
    },
    {
      name: "Mobile Safari",
      use: { ...devices["iPhone 12"] },
    },
  ],
  webServer: process.env.CI
    ? {
        command: "npm run start",
        url: "http://localhost:3000",
        reuseExistingServer: false,
        timeout: 120 * 1000,
      }
    : undefined,
});

登录 E2E 测试

javascript
// e2e/auth/login.spec.js
import { test, expect } from "@playwright/test";
import { faker } from "@faker-js/faker";

test.describe("登录功能", () => {
  test.beforeEach(async ({ page }) => {
    await page.goto("/login");
  });

  test("应该成功登录", async ({ page }) => {
    // 填写登录表单
    await page.fill('input[name="email"]', "test@example.com");
    await page.fill('input[name="password"]', "Password123!");

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

    // 等待登录成功
    await expect(page).toHaveURL("/dashboard");
    await expect(page.locator("h1")).toContainText("欢迎回来");
  });

  test("应该显示错误提示 - 错误密码", async ({ page }) => {
    await page.fill('input[name="email"]', "test@example.com");
    await page.fill('input[name="password"]', "WrongPassword!");
    await page.click('button[type="submit"]');

    await expect(page.locator(".error-message")).toContainText("邮箱或密码错误");
    await expect(page).toHaveURL("/login");
  });

  test("应该显示错误提示 - 不存在的用户", async ({ page }) => {
    await page.fill('input[name="email"]', "nonexistent@example.com");
    await page.fill('input[name="password"]', "Password123!");
    await page.click('button[type="submit"]');

    await expect(page.locator(".error-message")).toContainText("用户不存在");
  });

  test("表单验证 - 邮箱格式错误", async ({ page }) => {
    await page.fill('input[name="email"]', "invalid-email");
    await page.fill('input[name="password"]', "Password123!");
    await page.click('button[type="submit"]');

    await expect(page.locator('input[name="email"]')).toHaveAttribute(
      "aria-invalid",
      "true"
    );
  });

  test("表单验证 - 密码为空", async ({ page }) => {
    await page.fill('input[name="email"]', "test@example.com");
    await page.click('button[type="submit"]');

    await expect(page.locator('input[name="password"]')).toHaveAttribute(
      "aria-invalid",
      "true"
    );
  });

  test("应该支持记住登录状态", async ({ page }) => {
    await page.fill('input[name="email"]', "test@example.com");
    await page.fill('input[name="password"]', "Password123!");
    await page.check('input[name="remember"]');
    await page.click('button[type="submit"]');

    await expect(page).toHaveURL("/dashboard");

    // 关闭浏览器,重新打开应该保持登录状态
    await page.context().storageState({ path: "./auth-state.json" });
  });
});

注册 E2E 测试

javascript
// e2e/auth/register.spec.js
import { test, expect } from "@playwright/test";
import { faker } from "@faker-js/faker";

test.describe("注册功能", () => {
  test.beforeEach(async ({ page }) => {
    await page.goto("/register");
  });

  test("应该成功注册新用户", async ({ page }) => {
    const userData = {
      username: faker.internet.username(),
      email: faker.internet.email(),
      password: "Password123!",
    };

    await page.fill('input[name="username"]', userData.username);
    await page.fill('input[name="email"]', userData.email);
    await page.fill('input[name="password"]', userData.password);
    await page.fill('input[name="confirmPassword"]', userData.password);
    await page.check('input[name="terms"]');
    await page.click('button[type="submit"]');

    // 等待注册成功,跳转到验证邮箱页面
    await expect(page).toHaveURL("/verify-email");
    await expect(page.locator("h1")).toContainText("验证邮箱");
  });

  test("密码不匹配时应该显示错误", async ({ page }) => {
    await page.fill('input[name="username"]', faker.internet.username());
    await page.fill('input[name="email"]', faker.internet.email());
    await page.fill('input[name="password"]', "Password123!");
    await page.fill('input[name="confirmPassword"]', "DifferentPassword!");
    await page.click('button[type="submit"]');

    await expect(page.locator(".error-message")).toContainText("密码不匹配");
  });

  test("用户名已存在时应该显示错误", async ({ page }) => {
    // 使用一个已存在的用户名
    await page.fill('input[name="username"]', "existinguser");
    await page.fill('input[name="email"]', faker.internet.email());
    await page.fill('input[name="password"]', "Password123!");
    await page.fill('input[name="confirmPassword"]', "Password123!");
    await page.click('button[type="submit"]');

    await expect(page.locator(".error-message")).toContainText("用户名已被使用");
  });
});

完整用户流程测试

博客文章管理流程

javascript
// e2e/blog/article-management.spec.js
import { test, expect } from "@playwright/test";
import { faker } from "@faker-js/faker";

test.describe("文章管理完整流程", () => {
  let authToken;

  test.beforeAll(async ({ browser }) => {
    // 创建测试用户并登录
    const context = await browser.newContext();
    const page = await context.newPage();

    // 注册
    await page.goto("/register");
    await page.fill('input[name="username"]', faker.internet.username());
    await page.fill('input[name="email"]', faker.internet.email());
    await page.fill('input[name="password"]', "Password123!");
    await page.fill('input[name="confirmPassword"]', "Password123!");
    await page.check('input[name="terms"]');
    await page.click('button[type="submit"]');

    // 保存认证状态
    authToken = await context.storageState();
    await context.close();
  });

  test("创建 -> 编辑 -> 发布 -> 删除 文章", async ({ page }) => {
    // 使用已认证的状态
    const context = await browser.newContext({
      storageState: authToken,
    });
    page.context().setAuthToken?.(authToken);
    await page.goto("/dashboard");

    // 1. 创建新文章
    await page.click('button:has-text("新建文章")');
    await expect(page).toHaveURL("/editor/new");

    const title = faker.lorem.sentence();
    const content = faker.lorem.paragraphs(3);

    await page.fill('input[name="title"]', title);
    await page.fill(".editor-content", content);
    await page.fill('input[name="tags"]', "JavaScript,React");

    await page.click('button:has-text("保存草稿")');
    await expect(page.locator(".success-message")).toContainText("草稿已保存");

    // 2. 验证文章出现在草稿列表
    await page.goto("/dashboard/posts?status=draft");
    await expect(page.locator("article").first()).toContainText(title);

    // 3. 编辑文章
    await page.click(`article:has-text("${title}") >> text=编辑`);
    await expect(page).toHaveURL(/\/editor\/\w+/);

    const newTitle = title + " - 更新版";
    await page.fill('input[name="title"]', newTitle);
    await page.click('button:has-text("保存更新")');

    // 4. 发布文章
    await page.click('button:has-text("发布")');
    await expect(page.locator(".success-message")).toContainText("发布成功");
    await expect(page.locator(".post-status")).toContainText("已发布");

    // 5. 验证文章出现在博客列表
    await page.goto("/blog");
    await expect(page.locator(".post-card")).toContainText(newTitle);

    // 6. 验证文章详情页
    await page.click(`.post-card:has-text("${newTitle}")`);
    await expect(page.locator("h1")).toContainText(newTitle);
    await expect(page.locator(".post-content")).toContainText(content);

    // 7. 删除文章
    await page.click('button:has-text("删除")');
    await page.click('button:has-text("确认删除")');
    await expect(page.locator(".success-message")).toContainText("删除成功");

    await page.goto("/blog");
    await expect(page.locator(".post-card")).not.toContainText(newTitle);
  });
});

购物车流程

javascript
// e2e/shopping/cart.spec.js
import { test, expect } from "@playwright/test";

test.describe("购物车功能", () => {
  test.beforeEach(async ({ page }) => {
    await page.goto("/");
    await page.click('a:has-text("登录")');
    await page.fill('input[name="email"]', "shopper@example.com");
    await page.fill('input[name="password"]', "Password123!");
    await page.click('button[type="submit"]');
  });

  test("添加商品到购物车", async ({ page }) => {
    // 浏览商品
    await page.goto("/products");

    // 点击第一个商品
    await page.click(".product-card:first-child");
    await expect(page).toHaveURL(/\/products\/\w+/);

    // 添加到购物车
    const productName = await page.locator("h1").textContent();
    await page.click('button:has-text("加入购物车")');

    // 验证购物车更新
    await expect(page.locator(".cart-badge")).toContainText("1");

    // 打开购物车
    await page.click(".cart-icon");
    await expect(page.locator(".cart-item")).toContainText(productName);
  });

  test("修改商品数量", async ({ page }) => {
    await page.goto("/cart");

    // 增加数量
    await page.click('.cart-item:first-child >> button:has-text("+")');
    await expect(page.locator('.cart-item:first-child input[type="number"]')).toHaveValue(
      "2"
    );

    // 减少数量
    await page.click('.cart-item:first-child >> button:has-text("-")');
    await expect(page.locator('.cart-item:first-child input[type="number"]')).toHaveValue(
      "1"
    );
  });

  test("删除商品", async ({ page }) => {
    await page.goto("/cart");

    const initialCount = await page.locator(".cart-item").count();

    await page.hover(".cart-item:first-child");
    await page.click('.cart-item:first-child >> button:has-text("删除")');

    await expect(page.locator(".cart-item")).toHaveCount(initialCount - 1);
  });

  test("结算流程", async ({ page }) => {
    await page.goto("/cart");

    // 确认购物车有商品
    await expect(page.locator(".cart-item")).not.toHaveCount(0);

    // 点击结算
    await page.click('button:has-text("去结算")');
    await expect(page).toHaveURL("/checkout");

    // 选择地址
    await page.click('input[name="address"][value="saved-address"]');

    // 选择支付方式
    await page.click('input[name="payment"][value="alipay"]');

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

    // 验证跳转到支付页面
    await expect(page).toHaveURL(/\/order\/\w+\/payment/);
  });
});

组件交互测试

模态框测试

javascript
// e2e/components/modal.spec.js
import { test, expect } from "@playwright/test";

test.describe("模态框组件", () => {
  test("点击按钮应该打开模态框", async ({ page }) => {
    await page.goto("/dashboard");

    await page.click('button:has-text("新建项目")');

    await expect(page.locator(".modal-overlay")).toBeVisible();
    await expect(page.locator(".modal-title")).toContainText("新建项目");
  });

  test("点击关闭按钮应该关闭模态框", async ({ page }) => {
    await page.goto("/dashboard");
    await page.click('button:has-text("新建项目")');

    await expect(page.locator(".modal-overlay")).toBeVisible();

    await page.click('.modal-overlay >> button:has-text("取消")');

    await expect(page.locator(".modal-overlay")).not.toBeVisible();
  });

  test("点击遮罩层应该关闭模态框", async ({ page }) => {
    await page.goto("/dashboard");
    await page.click('button:has-text("新建项目")');

    await expect(page.locator(".modal-overlay")).toBeVisible();

    await page.click(".modal-overlay", { position: { x: 10, y: 10 } });

    await expect(page.locator(".modal-overlay")).not.toBeVisible();
  });

  test("按 ESC 键应该关闭模态框", async ({ page }) => {
    await page.goto("/dashboard");
    await page.click('button:has-text("新建项目")');

    await expect(page.locator(".modal-overlay")).toBeVisible();

    await page.keyboard.press("Escape");

    await expect(page.locator(".modal-overlay")).not.toBeVisible();
  });

  test("表单提交后应该关闭模态框", async ({ page }) => {
    await page.goto("/dashboard");
    await page.click('button:has-text("新建项目")');

    await page.fill('input[name="projectName"]', "我的新项目");
    await page.click('.modal-actions >> button:has-text("创建")');

    await expect(page.locator(".modal-overlay")).not.toBeVisible();
    await expect(page.locator(".success-message")).toContainText("创建成功");
  });
});

表单测试

javascript
// e2e/components/form.spec.js
import { test, expect } from "@playwright/test";

test.describe("表单组件", () => {
  test("实时验证应该正常工作", async ({ page }) => {
    await page.goto("/profile/edit");

    const emailInput = page.locator('input[name="email"]');

    // 输入无效邮箱
    await emailInput.fill("invalid-email");
    await emailInput.blur();

    await expect(page.locator('input[name="email"] + .error')).toContainText(
      "请输入有效的邮箱地址"
    );

    // 输入有效邮箱后错误消失
    await emailInput.fill("valid@example.com");
    await expect(page.locator('input[name="email"] + .error')).not.toBeVisible();
  });

  test("密码强度指示应该实时更新", async ({ page }) => {
    await page.goto("/register");

    const passwordInput = page.locator('input[name="password"]');
    const strengthIndicator = page.locator(".password-strength");

    await passwordInput.fill("weak");
    await expect(strengthIndicator).toHaveClass(/strength-weak/);

    await passwordInput.fill("Medium123!");
    await expect(strengthIndicator).toHaveClass(/strength-medium/);

    await passwordInput.fill("StrongPassword123!");
    await expect(strengthIndicator).toHaveClass(/strength-strong/);
  });
});

CI 集成

GitHub Actions

yaml
# .github/workflows/e2e.yml
name: E2E Tests

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  e2e:
    timeout-minutes: 30
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright Browsers
        run: npx playwright install --with-deps chromium

      - name: Run Playwright tests
        run: npx playwright test

      - name: Upload Playwright Report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

      - name: Upload Test Results
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-results
          path: playwright-results.json

E2E 测试最佳实践

E2E 测试耗时较长,应该只覆盖核心用户流程,使用测试账号隔离数据,确保测试的稳定性。

总结

使用 Claude Code 配置 E2E 测试:

  • 使用 Playwright 实现跨浏览器测试
  • 编写完整的用户流程测试
  • 测试组件交互和表单验证
  • 配置 CI/CD 自动化执行
  • 生成详细的测试报告
  • 遵循 E2E 测试最佳实践