Playwright复用Firebase认证状态:IndexedDB持久化方案咨询
问题
我希望每次运行Playwright测试时无需重复登录被测平台,因此尝试实现「复用状态」与「复用认证」功能。但被测平台基于Firebase构建,Firebase使用IndexedDB存储认证数据,而Playwright的storageState方法无法持久化IndexedDB数据,导致复用状态后仍需重新登录,求解决方案。
现有global-setup.ts代码
import { Browser, chromium, expect, Page } from "@playwright/test"; async function globalSetup(){ const browser = await chromium.launch({headless: false}); const context = await browser.newContext(); const page: Page = await context.newPage(); await page.goto("<'url'>") await page.getByLabel('Email').fill("<'email'>"); await page.getByLabel('Password').fill("<'password'>"); await page.getByRole('button', { name: 'Sign in' }).click(); await expect (page.getByRole('link', { name: 'logo' })).toBeVisible({ timeout: 30000 }); // 保存状态 await page.context().storageState({ path: "./LoginAuth.json" }); await browser.close(); } export default globalSetup;
解决方案
方法1:使用持久化浏览器上下文
Playwright的持久化上下文会完整保存包括IndexedDB在内的所有浏览器存储数据,是适配Firebase认证场景的最简方案。
修改global-setup.ts为创建持久化上下文:
import { chromium } from "@playwright/test"; import * as path from "path"; async function globalSetup() { // 指定持久化存储目录 const userDataDir = path.join(__dirname, ".playwright-user-data"); const browser = await chromium.launchPersistentContext(userDataDir, { headless: false, }); const page = await browser.newPage(); await page.goto("<'url'>"); await page.getByLabel('Email').fill("<'email'>"); await page.getByLabel('Password').fill("<'password'>"); await page.getByRole('button', { name: 'Sign in' }).click(); // 确保登录完成 await page.waitForSelector('role=link[name="logo"]', { timeout: 30000 }); await browser.close(); } export default globalSetup;
在playwright.config.ts中配置测试用例复用该上下文:
import { defineConfig } from "@playwright/test"; export default defineConfig({ use: { // 复用持久化用户数据目录 userDataDir: "./.playwright-user-data", }, });
方法2:手动导出/导入IndexedDB数据
若不想使用持久化上下文,可通过evaluate方法手动读写IndexedDB数据,实现状态复用。
步骤1:在全局初始化中导出IndexedDB数据
修改global-setup.ts,添加导出逻辑:
import { Browser, chromium, expect, Page } from "@playwright/test"; import * as fs from "fs"; async function globalSetup(){ const browser = await chromium.launch({headless: false}); const context = await browser.newContext(); const page: Page = await context.newPage(); await page.goto("<'url'>") await page.getByLabel('Email').fill("<'email'>"); await page.getByLabel('Password').fill("<'password'>"); await page.getByRole('button', { name: 'Sign in' }).click(); await expect(page.getByRole('link', { name: 'logo' })).toBeVisible({ timeout: 30000 }); // 保存Cookie、SessionStorage等状态 await page.context().storageState({ path: "./LoginAuth.json" }); // 导出Firebase的IndexedDB数据(通常库名为firebaseLocalStorageDb) const indexedDbData = await page.evaluate(async () => { const dbName = "firebaseLocalStorageDb"; return new Promise((resolve, reject) => { const request = indexedDB.open(dbName); request.onsuccess = (event) => { const db = (event.target as IDBOpenDBRequest).result; const objStoreNames = db.objectStoreNames; const data: Record<string, any[]> = {}; const transactions = Array.from(objStoreNames).map((storeName) => { return new Promise((res) => { const transaction = db.transaction(storeName, "readonly"); const store = transaction.objectStore(storeName); const getAllRequest = store.getAll(); getAllRequest.onsuccess = () => { data[storeName] = getAllRequest.result; res(null); }; }); }); Promise.all(transactions).then(() => { db.close(); resolve({ dbName, data }); }); }; request.onerror = () => reject(request.error); }); }); // 保存IndexedDB数据到文件 fs.writeFileSync("./IndexedDbAuth.json", JSON.stringify(indexedDbData)); await browser.close(); } export default globalSetup;
步骤2:在测试前导入IndexedDB数据
在测试文件的beforeAll中添加导入逻辑:
import { test } from "@playwright/test"; import * as fs from "fs"; test.beforeAll(async ({ browser }) => { const storageState = JSON.parse(fs.readFileSync("./LoginAuth.json", "utf-8")); const indexedDbData = JSON.parse(fs.readFileSync("./IndexedDbAuth.json", "utf-8")); const context = await browser.newContext({ storageState }); const page = await context.newPage(); await page.goto("<'url'>"); // 先打开站点初始化IndexedDB环境 // 导入IndexedDB数据 await page.evaluate((dbData) => { return new Promise((resolve, reject) => { const request = indexedDB.open(dbData.dbName); request.onsuccess = (event) => { const db = (event.target as IDBOpenDBRequest).result; const transactions = Object.entries(dbData.data).map(([storeName, items]) => { return new Promise((res) => { const transaction = db.transaction(storeName, "readwrite"); const store = transaction.objectStore(storeName); store.clear(); // 清空现有数据(可选) items.forEach(item => store.add(item)); transaction.oncomplete = () => res(null); }); }); Promise.all(transactions).then(() => { db.close(); resolve(null); }); }; request.onerror = () => reject(request.error); }); }, indexedDbData); await context.close(); });
方法3:使用Firebase自定义认证令牌
若有权限访问Firebase Admin SDK,可直接生成自定义令牌实现无UI登录,效率更高且不依赖浏览器存储:
import { test } from "@playwright/test"; import * as admin from "firebase-admin"; test.beforeAll(async () => { // 初始化Firebase Admin SDK(需提前配置服务账号密钥) admin.initializeApp({ credential: admin.credential.cert("./service-account-key.json"), }); // 生成自定义认证令牌 const uid = "目标用户UID"; const customToken = await admin.auth().createCustomToken(uid); // 在测试上下文注入令牌完成登录 test.use({ contextOptions: { async beforePage(page) { await page.addInitScript((token) => { window.firebase.auth().signInWithCustomToken(token); }, customToken); }, }, }); });
内容的提问来源于stack exchange,提问作者Benjamin Iriberri
相关产品推荐
相关产品推荐

