Playwright共享认证状态仅在fullyParallel模式下生效问题咨询
问题现象
使用Playwright编写测试,按官方文档配置了setup项目生成认证状态文件,用于跨测试用例共享登录态。但**非全并行模式(或worker数量少于测试用例数)**运行时,只有第一个测试用例能正常继承登录状态,第二个测试用例会丢失认证;开启fullyParallel后所有测试均可正常运行。开发阶段希望保持串行运行,需解决该问题。
相关代码片段
auth.setup.js
import { test as setup, expect } from "@playwright/test"; const username = process.env.USER_NAME; const password = process.env.PASSWORD; setup("Log into the web app", async ({ page }) => { await page.goto("https://www.example.com/"); await page .getByRole("textbox", { name: /email address/i, }) .fill(username); await page .getByRole("button", { name: /continue/i, }) .click(); await page.getByLabel("Password").fill(password); await page .getByRole("button", { name: /continue/i, }) .click(); await expect(page).toHaveTitle(/example.com/); await page.context().storageState({ path: "playwright/.auth/user.json" }); });
playwright.config.js
export default defineConfig({ // ... fullyParallel: !!process.env.CI, // ... projects: [ { name: "setup", testMatch: /.*\.setup\.js/, }, { name: "chromium", use: { ...devices["Desktop Chrome"], baseURL: "https://example.com", storageState: "playwright/.auth/user.json", }, dependencies: ["setup"], }, // ... ], });
example.spec.js
test.describe("Website", () => { test("Homepage", async ({ page }) => { await test.step("See homepage fields", async () => { await page.goto("/"); await expect(page).toHaveTitle(/homepage/i); }); }); test("Info page", async ({ page }) => { await test.step("See users", async () => { await page.goto("/info"); await expect(page).toHaveTitle(/info/i); }); }); });
原因分析
当测试串行运行(或worker数不足)时,Playwright会在同一个浏览器上下文中依次运行多个测试用例。第一个测试用例加载storageState后,后续操作可能会修改上下文的Cookie、localStorage等认证数据(比如页面跳转后的会话更新、页面状态变更),导致第二个测试用例启动时,上下文已经不是初始的认证状态。
而开启fullyParallel后,每个测试用例会被分配到独立的浏览器上下文,每个上下文都会重新加载storageState文件,因此不会互相干扰,所有测试都能正常获取认证状态。
解决方法(保持串行运行的前提下)
方法1:全局配置强制每个上下文加载初始状态
修改playwright.config.js中chromium项目的配置,通过contextOptions指定storageState,确保每个新创建的浏览器上下文都从文件加载干净的认证状态:
{ name: "chromium", use: { ...devices["Desktop Chrome"], baseURL: "https://example.com", contextOptions: { storageState: "playwright/.auth/user.json", }, }, dependencies: ["setup"], }
注:原配置中的
storageState是给页面层级用的,contextOptions.storageState会确保每个测试用例启动时都创建新上下文并加载初始状态,避免串行时的状态污染。
方法2:测试前置钩子重置上下文
在测试描述块中添加beforeEach钩子,每次测试前清空当前上下文的状态并重新加载storageState:
test.describe("Website", () => { test.beforeEach(async ({ context }) => { await context.clearCookies(); await context.storageState({ path: "playwright/.auth/user.json" }); }); test("Homepage", async ({ page }) => { await test.step("See homepage fields", async () => { await page.goto("/"); await expect(page).toHaveTitle(/homepage/i); }); }); test("Info page", async ({ page }) => { await test.step("See users", async () => { await page.goto("/info"); await expect(page).toHaveTitle(/info/i); }); }); });
方法3:手动为每个测试创建独立上下文
在测试用例中手动创建新的浏览器上下文并加载storageState,替代默认的共享上下文:
test.describe("Website", () => { test("Homepage", async ({ browser }) => { const context = await browser.newContext({ storageState: "playwright/.auth/user.json", ...devices["Desktop Chrome"], }); const page = await context.newPage(); await page.goto("/"); await expect(page).toHaveTitle(/homepage/i); await context.close(); }); test("Info page", async ({ browser }) => { const context = await browser.newContext({ storageState: "playwright/.auth/user.json", ...devices["Desktop Chrome"], }); const page = await context.newPage(); await page.goto("/info"); await expect(page).toHaveTitle(/info/i); await context.close(); }); });
内容的提问来源于stack exchange,提问作者justinw

