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
相关产品推荐
相关产品推荐

