You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Playwright E2E测试本地正常但GitHub Actions失败(Next.js14 App Router)

Playwright E2E测试本地正常但GitHub Actions中认证失败

环境信息

本地环境

  • Node.js: v20.x
  • Next.js: 14.x
  • Playwright: 1.x
  • 操作系统:macOS/Linux

GitHub Actions环境

  • 运行环境:ubuntu-latest
  • Node.js: 20.x

相关代码

认证设置脚本(setup.ts)

import { test, expect } from "@playwright/test";
import { mkdirSync } from "fs";
import { dirname } from "path";

const authFile = "playwright/.auth/user.json";

test("test", async ({ page }) => {
  mkdirSync(dirname(authFile), { recursive: true });

  await page.goto("http://localhost:3000/api/auth/signin?csrf=true");
  await page.getByRole("button", { name: "Sign in with Azure Active" }).click();
  await page.getByPlaceholder("Email, phone, or Skype").click();
  await page
    .getByPlaceholder("Email, phone, or Skype")
    .fill(process.env.PLAYWRIGHT_EMAIL ?? "");
  await page.getByRole("button", { name: "Next" }).click();
  await page.getByPlaceholder("Password").click();
  await page
    .getByPlaceholder("Password")
    .fill(process.env.PLAYWRIGHT_PASSWORD ?? "");
  await page.getByRole("button", { name: "Sign in" }).click();
  try {
    const yesButton = page.getByRole("button", { name: "Yes" });
    await yesButton.waitFor({ state: "visible", timeout: 10000 });
    await yesButton.click();
  } catch (error) {
    console.log(
      "The 'Yes' button was not found or not clickable within the time limit."
    );
  }
  await page.goto("http://localhost:3000/home");

  await page.context().storageState({ path: authFile });
});

路由测试脚本(redirect.spec.ts)

import { test, expect } from "@playwright/test";

test("Authenticated root route redirects to home page", async ({ page }) => {
  await page.goto("http://localhost:3000/home");
  await expect(page).toHaveURL("http://localhost:3000/home");
  await page.goto("http://localhost:3000/");
  await expect(page).toHaveURL("http://localhost:3000/home");
});

test("Unauthenticated access redirects to login page for root and home routes", async ({ page }) => {
  await page.context().clearCookies();
  await page.goto("http://localhost:3000/");
  await expect(page).toHaveURL("http://localhost:3000/login");
  await page.goto("http://localhost:3000/home");
  await expect(page).toHaveURL("http://localhost:3000/login");
});

Playwright配置(playwright.config.ts)

import { defineConfig, devices } from "@playwright/test";
import dotenv from "dotenv";
import path from "path";

dotenv.config({ path: path.resolve(__dirname, ".env.local") });

export default defineConfig({
  testDir: "./e2e-tests",
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: "html",
  use: {
    trace: "on-first-retry",
  },
  projects: [
    { name: "setup", testMatch: "setup.ts" },
    {
      name: "chromium",
      use: {
        ...devices["Desktop Chrome"],
        storageState: "playwright/.auth/user.json",
      },
      dependencies: ["setup"],
    },
    {
      name: "firefox",
      use: {
        ...devices["Desktop Firefox"],
        storageState: "playwright/.auth/user.json",
      },
      dependencies: ["setup"],
    },
    {
      name: "webkit",
      use: {
        ...devices["Desktop Safari"],
        storageState: "playwright/.auth/user.json",
      },
      dependencies: ["setup"],
    },
  ],
  webServer: {
    command: "npm run dev",
    url: "http://127.0.0.1:3000",
    reuseExistingServer: !process.env.CI,
  },
});

GitHub Actions工作流(playwright.yml)

name: Playwright Tests

on:
  pull_request:

jobs:
  end-to-end-test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [20.x]

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}

      - name: Install dependencies
        run: npm install

      - name: Install Playwright browsers
        run: npx playwright install --with-deps

      - name: Run Playwright tests
        env:
          PLAYWRIGHT_EMAIL: ${{ secrets.PLAYWRIGHT_EMAIL }}
          PLAYWRIGHT_PASSWORD: ${{ secrets.PLAYWRIGHT_PASSWORD }}
          AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID}}
          AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET}}
          AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID}}
          NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET}}
          NEXTAUTH_URL: ${{ secrets.NEXTAUTH_URL}}
        run: npm run test:e2e
      - uses: actions/upload-artifact@v4
        if: ${{ !cancelled() }}
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

问题现象

测试在GitHub Actions中失败,报错如下:

Error: Timed out 5000ms waiting for expect(locator).toHaveURL(expected)
Expected string: "***/home"
Received string: "***/login"

CI环境中认证步骤未生效,但本地执行完全正常。

已排查步骤

  • 确认GitHub Actions中的环境变量已正确配置
  • 所有依赖和浏览器版本已更新至最新
  • 尝试增加超时时间,问题依旧

解决方案建议

1. 验证认证流程在CI中的执行完整性

在setup.ts中添加断言和日志,确保每个步骤成功完成:

  • 在填写邮箱/密码后,添加断言确认输入内容正确:
    await expect(page.getByPlaceholder("Email, phone, or Skype")).toHaveValue(process.env.PLAYWRIGHT_EMAIL ?? "");
    
  • 每次点击操作后等待页面跳转完成:
    await page.getByRole("button", { name: "Sign in with Azure Active" }).click();
    await page.waitForNavigation(); // 等待跳转至Azure登录页
    
  • 在Actions中添加步骤检查认证文件是否生成:
    - name: Check auth file
      if: always()
      run: cat playwright/.auth/user.json
    

2. 优化webServer启动等待策略

Next.js App Router在CI环境可能需要更长初始化时间,修改playwright.config.ts中的webServer配置:

webServer: {
  command: "npm run dev",
  url: "http://127.0.0.1:3000",
  reuseExistingServer: !process.env.CI,
  timeout: 120000, // 延长超时至2分钟
  waitForHTTPResponse: {
    url: "http://127.0.0.1:3000",
    timeout: 120000,
  },
},

3. 排查Azure AD认证的CI特殊情况

  • 确认测试账号未启用MFA,Azure AD可能在陌生环境触发额外验证导致流程中断
  • 检查NEXTAUTH_URL是否设置为http://127.0.0.1:3000,CI环境应使用本地地址而非生产域名
  • 捕获Azure登录过程中的错误,添加更详细的日志:
    try {
      await page.getByRole("button", { name: "Sign in" }).click();
      await page.waitForNavigation();
    } catch (err) {
      console.error("Login failed:", err);
      await page.screenshot({ path: "login-failure.png" });
      throw err;
    }
    

4. 确保storageState路径正确

在配置中使用绝对路径加载认证状态,避免相对路径问题:

projects: [
  { name: "setup", testMatch: "setup.ts" },
  {
    name: "chromium",
    use: {
      ...devices["Desktop Chrome"],
      storageState: path.resolve(__dirname, "playwright/.auth/user.json"),
    },
    dependencies: ["setup"],
  },
  // 其他浏览器项目同理
],

5. 利用Playwright Trace定位问题

修改配置启用全量trace记录,失败后下载回放:

use: {
  trace: "on", // 每次测试都生成trace
},

在Actions中下载playwright-report artifact,通过playwright show-trace命令本地回放认证流程,定位具体失败环节。

内容的提问来源于stack exchange,提问作者TedRed

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 02:39:50