7.Cookie、存储与状态管理
在 Web 自动化测试中,状态管理是一个绕不开的话题。登录态保持、用户偏好设置、会话信息恢复……这些场景背后都离不开 Cookie、本地存储和浏览器权限的支持。Puppeteer 作为直接操作 Chrome 浏览器的工具,提供了非常底层的 API 来管理这些状态数据。本章将深入探讨如何在 Puppeteer 中读取、设置、删除 Cookie,管理本地存储,以及控制浏览器权限。
读取页面 Cookie 信息
Cookie 是服务器留在浏览器里的小纸条,记录着用户的身份凭证、偏好设置等信息。在自动化测试中,我们经常需要读取这些信息来验证登录状态或追踪用户行为。
从浏览器上下文获取 Cookie
Puppeteer 提供了两种读取 Cookie 的方式:通过 Browser 对象或 BrowserContext 对象。默认情况下,Browser 对象的方法操作的是默认浏览器上下文。
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
// 通过页面脚本设置一个测试 Cookie
await page.evaluate(() => {
document.cookie = 'myCookie=MyCookieValue; path=/';
});
// 获取所有可用的 Cookie
const cookies = await browser.cookies();
console.log(cookies);
这段代码首先启动浏览器并打开一个页面,然后通过 page.evaluate 在页面上下文中执行 JavaScript 代码来设置 Cookie。最后调用 browser.cookies() 获取当前浏览器上下文中所有的 Cookie。返回的 Cookie 是一个数组,每个元素都是一个包含 name、value、domain、path 等属性的对象。
需要注意的是,browser.cookies() 获取的是浏览器存储中的所有 Cookie,不仅限于当前页面。如果只想获取特定域名的 Cookie,可以传递 URL 参数:
// 只获取 example.com 域下的 Cookie
const domainCookies = await browser.cookies('https://example.com');
console.log(domainCookies);
从页面上下文直接读取
有时候我们需要在页面执行过程中实时获取 Cookie,这时可以直接在 page.evaluate 中读取 document.cookie:
// 获取当前页面所有 Cookie 的字符串形式
const cookieString = await page.evaluate(() => {
return document.cookie;
});
console.log(cookieString); // "myCookie=MyCookieValue; anotherCookie=123"
// 解析为对象形式
const cookies = await page.evaluate(() => {
return document.cookie.split(';').reduce((acc, cookie) => {
const [name, value] = cookie.trim().split('=');
acc[name] = value;
return acc;
}, {} as Record<string, string>);
});
console.log(cookies); // { myCookie: 'MyCookieValue', anotherCookie: '123' }
这种方式获取的是当前页面可访问的 Cookie,受 HttpOnly 标志的限制——标记为 HttpOnly 的 Cookie 无法通过 JavaScript 读取,这是浏览器的重要安全机制。因此,如果需要获取完整的 Cookie 信息,包括 HttpOnly 类型的,必须使用 browser.cookies() 方法。
处理跨域 Cookie
在现代 Web 应用中,跨域请求和第三方 Cookie 很常见。Puppeteer 默认会返回所有匹配的 Cookie,包括第三方 Cookie。如果需要区分,可以通过 domain 属性进行过滤:
const allCookies = await browser.cookies();
const firstPartyCookies = allCookies.filter(cookie =>
cookie.domain === '.example.com' || cookie.domain === 'example.com'
);
const thirdPartyCookies = allCookies.filter(cookie =>
!cookie.domain.includes('example.com')
);
console.log('第一方 Cookie:', firstPartyCookies);
console.log('第三方 Cookie:', thirdPartyCookies);
这段代码展示了如何区分第一方和第三方 Cookie。第一方 Cookie 的域名与当前页面域名一致,而第三方 Cookie 来自其他域名,通常是广告追踪或分析服务设置的。
设置与删除 Cookie
读取 Cookie 只是第一步,更强大的能力在于主动设置和删除 Cookie。这在模拟登录状态、测试不同用户场景时特别有用。
设置 Cookie 的完整语法
Puppeteer 的 browser.setCookie() 方法允许我们精确控制 Cookie 的每个属性:
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
// 设置两个测试 Cookie
await browser.setCookie(
{
name: 'session_id',
value: 'abc123xyz',
domain: 'localhost',
path: '/',
expires: -1, // -1 表示会话 Cookie,浏览器关闭后失效
httpOnly: true, // 禁止 JavaScript 访问
secure: false, // 允许 HTTP 传输
sameSite: 'Lax', // 防止 CSRF 攻击
},
{
name: 'user_preference',
value: 'dark_mode',
domain: 'localhost',
path: '/settings',
expires: Date.now() / 1000 + 86400 * 30, // 30 天后过期
httpOnly: false,
secure: true, // 仅 HTTPS 传输
sameSite: 'Strict',
},
);
console.log(await browser.cookies());
这段代码展示了设置 Cookie 的核心参数。每个参数都有特定用途:
name和value:Cookie 的名称和值,这是唯一必需的参数domain:Cookie 所属的域名,必须以点号开头或完全匹配path:Cookie 生效的路径,默认为/expires:过期时间,以秒为单位的 Unix 时间戳,-1 表示会话 CookiehttpOnly:如果为 true,Cookie 无法通过 JavaScript 访问,能有效防止 XSS 攻击secure:如果为 true,Cookie 只能通过 HTTPS 传输sameSite:防止 CSRF 攻击,可选值为Strict、Lax或NonesameParty:Chrome 的 First-Party Sets 功能相关,通常设为 false
在特定上下文中设置 Cookie
如果使用了多个浏览器上下文,需要在特定上下文中设置 Cookie:
const context = await browser.createBrowserContext();
const page = await context.newPage();
await context.setCookie({
name: 'context_specific',
value: 'value123',
domain: '.example.com',
});
// 这个 Cookie 只在当前 context 中可见
console.log(await context.cookies());
这种方式实现了 Cookie 的隔离,不同上下文之间的 Cookie 互不干扰,非常适合多用户并发测试场景。
删除 Cookie 的精确控制
删除 Cookie 需要精确匹配其属性,不仅仅是名称:
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
// 删除特定 Cookie
await browser.deleteCookie(
{
name: 'session_id',
domain: 'localhost',
path: '/',
},
{
name: 'user_preference',
domain: 'localhost',
path: '/settings',
},
);
console.log(await browser.cookies()); // 确认删除结果
browser.deleteCookie() 方法需要指定 Cookie 的 name、domain 和 path 才能准确定位并删除。如果提供的属性不匹配,删除操作会静默失败,不会抛出错误。因此,删除前最好先读取确认 Cookie 的完整信息。
批量清空 Cookie
测试结束后,通常需要清理环境。可以批量删除所有 Cookie:
// 获取并删除所有 Cookie
const cookies = await browser.cookies();
for (const cookie of cookies) {
await browser.deleteCookie({
name: cookie.name,
domain: cookie.domain,
path: cookie.path,
});
}
// 验证清空结果
console.log('剩余 Cookie 数量:', (await browser.cookies()).length); // 0
这段代码先获取所有 Cookie,然后遍历删除。注意删除操作是异步的,但 Puppeteer 内部会按顺序处理,不需要额外等待。
实际应用场景:登录状态保持
在自动化测试中,最常见的用途是保持登录状态,避免每次测试都重复登录流程:
async function saveLoginState(browser: puppeteer.Browser, filePath: string) {
const cookies = await browser.cookies();
await fs.promises.writeFile(filePath, JSON.stringify(cookies, null, 2));
}
async function restoreLoginState(browser: puppeteer.Browser, filePath: string) {
const cookieData = await fs.promises.readFile(filePath, 'utf-8');
const cookies = JSON.parse(cookieData);
for (const cookie of cookies) {
await browser.setCookie(cookie);
}
}
// 使用示例
// 第一次运行:执行登录并保存状态
await page.goto('https://example.com/login');
await page.locator('#username').fill('testuser');
await page.locator('#password').fill('password');
await page.locator('#login-btn').click();
await page.waitForNavigation();
await saveLoginState(browser, './cookies.json');
// 后续运行:直接恢复状态
await restoreLoginState(browser, './cookies.json');
await page.goto('https://example.com/dashboard');
// 已处于登录状态
这个模式将登录后的 Cookie 持久化到文件,后续测试直接加载,大幅提升了测试效率。需要注意的是,Cookie 可能有过期时间,需要定期更新保存的登录状态。
管理本地存储与 Session
除了 Cookie,现代 Web 应用还广泛使用 localStorage 和 sessionStorage 来存储客户端数据。Puppeteer 通过 page.evaluate 提供了对这些存储的完全控制。
操作 localStorage
localStorage 用于持久化存储键值对数据,除非手动清除,否则长期有效:
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
// 设置 localStorage 数据
await page.evaluate(() => {
localStorage.setItem('theme', 'dark');
localStorage.setItem('language', 'zh-CN');
localStorage.setItem('userId', 'user123');
});
// 读取 localStorage 数据
const theme = await page.evaluate(() => {
return localStorage.getItem('theme');
});
console.log('当前主题:', theme); // dark
// 批量读取所有数据
const allStorage = await page.evaluate(() => {
const items: Record<string, string> = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key) {
items[key] = localStorage.getItem(key) || '';
}
}
return items;
});
console.log('所有 localStorage 数据:', allStorage);
// 删除特定项
await page.evaluate(() => {
localStorage.removeItem('theme');
});
// 清空所有 localStorage
await page.evaluate(() => {
localStorage.clear();
});
这段代码展示了 localStorage 的完整操作流程。由于 localStorage 是页面域名的隔离存储,必须在页面加载完成后才能访问。如果尝试在 about:blank 页面或其他域名页面访问,会抛出安全错误。
操作 sessionStorage
sessionStorage 与 localStorage API 类似,但数据只在当前会话有效,关闭标签页后自动清除:
// 设置 sessionStorage
await page.evaluate(() => {
sessionStorage.setItem('tempData', 'someValue');
sessionStorage.setItem('sessionId', 'sess123');
});
// 读取 sessionStorage
const sessionData = await page.evaluate(() => {
const items: Record<string, string> = {};
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
if (key) {
items[key] = sessionStorage.getItem(key) || '';
}
}
return items;
});
console.log('sessionStorage 数据:', sessionData);
sessionStorage 的使用场景通常是临时数据存储,比如表单草稿、页面间传递的临时参数等。在自动化测试中,我们可能需要模拟这些临时数据的设置和读取。
持久化本地存储状态
与 Cookie 类似,本地存储的状态也可以持久化并在后续测试中恢复:
import fs from 'fs';
import puppeteer from 'puppeteer';
async function saveStorageState(page: puppeteer.Page, filePath: string) {
const storageState = await page.evaluate(() => {
const local: Record<string, string> = {};
const session: Record<string, string> = {};
// 保存 localStorage
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key) {
local[key] = localStorage.getItem(key) || '';
}
}
// 保存 sessionStorage
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
if (key) {
session[key] = sessionStorage.getItem(key) || '';
}
}
return { local, session };
});
await fs.promises.writeFile(filePath, JSON.stringify(storageState, null, 2));
}
async function restoreStorageState(page: puppeteer.Page, filePath: string) {
const storageData = await fs.promises.readFile(filePath, 'utf-8');
const { local, session } = JSON.parse(storageData);
await page.evaluate((data) => {
// 恢复 localStorage
Object.entries(data.local).forEach(([key, value]) => {
localStorage.setItem(key, value);
});
// 恢复 sessionStorage
Object.entries(data.session).forEach(([key, value]) => {
sessionStorage.setItem(key, value);
});
}, { local, session });
}
// 使用示例
await page.goto('https://example.com');
// ... 执行一些操作,产生本地存储数据
await saveStorageState(page, './storage-state.json');
// 在新会话中恢复
const newPage = await browser.newPage();
await newPage.goto('https://example.com');
await restoreStorageState(newPage, './storage-state.json');
这个模式对于测试依赖本地存储的应用非常有用,比如单页应用的用户设置、缓存数据等。
IndexedDB 的高级操作
对于复杂的数据存储需求,现代应用会使用 IndexedDB。Puppeteer 同样可以通过 page.evaluate 操作 IndexedDB,但 API 较为复杂:
// 读取 IndexedDB 数据
const dbData = await page.evaluate(() => {
return new Promise((resolve, reject) => {
const request = indexedDB.open('MyDatabase', 1);
request.onsuccess = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const getAllRequest = store.getAll();
getAllRequest.onsuccess = () => {
resolve(getAllRequest.result);
};
getAllRequest.onerror = () => {
reject(getAllRequest.error);
};
};
request.onerror = () => {
reject(request.error);
};
});
});
console.log('IndexedDB 数据:', dbData);
由于 IndexedDB 操作是异步的,需要在 page.evaluate 中返回 Promise。Puppeteer 会自动等待 Promise resolve。这种方式可以读取、修改甚至清空 IndexedDB 中的数据,但代码相对复杂,需要处理数据库版本、事务等概念。
浏览器权限控制
现代浏览器提供了多种权限 API,如地理位置、通知、摄像头等。在自动化测试中,我们经常需要模拟用户授予或拒绝这些权限。
权限控制基础
Puppeteer 通过 BrowserContext.overridePermissions 方法控制权限:
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const context = browser.defaultBrowserContext();
// 授予地理位置权限
await context.overridePermissions('https://example.com', ['geolocation']);
// 授予多个权限
await context.overridePermissions('https://example.com', [
'notifications',
'camera',
'microphone'
]);
// 撤销所有权限
await context.clearPermissionOverrides();
权限设置是针对整个浏览器上下文的,会影响该上下文中的所有页面。权限一旦设置,会持续生效直到被清除或浏览器关闭。
常见权限类型
Puppeteer 支持的权限类型包括:
// 地理位置权限
await context.overridePermissions('https://maps.example.com', ['geolocation']);
// 通知权限
await context.overridePermissions('https://chat.example.com', ['notifications']);
// 媒体设备权限
await context.overridePermissions('https://video.example.com', [
'camera',
'microphone'
]);
// 剪贴板访问权限
await context.overridePermissions('https://docs.example.com', [
'clipboard-read',
'clipboard-write'
]);
// 传感器权限
await context.overridePermissions('https://game.example.com', [
'accelerometer',
'gyroscope'
]);
每个权限字符串对应浏览器的一种功能。设置权限后,页面调用相关 API 时不会弹出授权对话框,而是直接获得授权状态。
实际应用:测试地理定位功能
假设要测试一个依赖地理位置的 Web 应用:
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const context = browser.defaultBrowserContext();
const page = await browser.newPage();
// 1. 授予地理位置权限
await context.overridePermissions('https://weather.example.com', ['geolocation']);
// 2. 模拟地理位置坐标
await page.setGeolocation({
latitude: 39.9042, // 北京纬度
longitude: 116.4074, // 北京经度
accuracy: 100 // 精度,单位为米
});
// 3. 访问页面并测试
await page.goto('https://weather.example.com');
await page.waitForSelector('.location-info');
// 4. 验证位置信息
const locationText = await page.locator('.location-info').textContent();
console.log('检测到的位置:', locationText); // 应显示北京相关信息
// 5. 测试拒绝权限的场景
await context.overridePermissions('https://weather.example.com', []);
await page.reload();
const errorMessage = await page.locator('.error-message').textContent();
console.log('错误信息:', errorMessage); // 应显示权限被拒绝的提示
这个例子展示了完整的权限测试流程:授权、模拟数据、验证功能,以及测试拒绝权限的异常情况。page.setGeolocation 用于模拟具体的地理位置坐标,与权限授权配合使用。
权限与存储的关系
权限设置和存储管理经常配合使用。例如,测试 PWA 应用时,可能需要同时设置存储权限和初始化 IndexedDB:
// 为 PWA 设置必要权限
await context.overridePermissions('https://pwa.example.com', [
'notifications',
'persistent-storage' // 持久化存储权限
]);
// 初始化应用数据
await page.goto('https://pwa.example.com');
await page.evaluate(() => {
// 初始化 IndexedDB
const request = indexedDB.open('PWA_DB', 1);
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains('settings')) {
db.createObjectStore('settings');
}
};
});
// 设置初始配置
await page.evaluate(() => {
const dbRequest = indexedDB.open('PWA_DB', 1);
dbRequest.onsuccess = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
const transaction = db.transaction(['settings'], 'readwrite');
const store = transaction.objectStore('settings');
store.put('dark', 'theme');
store.put(true, 'notificationsEnabled');
};
});
这种模式确保 PWA 应用在测试环境中拥有正确的权限和初始数据,避免测试过程中的不确定行为。
状态管理的最佳实践
在实际项目中,状态管理代码需要良好的组织才能维护。
封装状态管理类
import puppeteer from 'puppeteer';
import fs from 'fs';
class BrowserStateManager {
private browser: puppeteer.Browser;
constructor(browser: puppeteer.Browser) {
this.browser = browser;
}
async saveAllState(page: puppeteer.Page, filePath: string) {
const [cookies, storage] = await Promise.all([
this.browser.cookies(),
page.evaluate(() => {
const local: Record<string, string> = {};
const session: Record<string, string> = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key) local[key] = localStorage.getItem(key) || '';
}
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
if (key) session[key] = sessionStorage.getItem(key) || '';
}
return { local, session };
})
]);
await fs.promises.writeFile(filePath, JSON.stringify({ cookies, storage }, null, 2));
}
async restoreAllState(page: puppeteer.Page, filePath: string) {
const data = JSON.parse(await fs.promises.readFile(filePath, 'utf-8'));
// 恢复 Cookie
for (const cookie of data.cookies) {
await this.browser.setCookie(cookie);
}
// 恢复存储
await page.evaluate((storage) => {
Object.entries(storage.local).forEach(([k, v]) => localStorage.setItem(k, v));
Object.entries(storage.session).forEach(([k, v]) => sessionStorage.setItem(k, v));
}, data.storage);
}
async clearAllState(page: puppeteer.Page) {
// 清除 Cookie
const cookies = await this.browser.cookies();
for (const cookie of cookies) {
await this.browser.deleteCookie({
name: cookie.name,
domain: cookie.domain,
path: cookie.path
});
}
// 清除存储
await page.evaluate(() => {
localStorage.clear();
sessionStorage.clear();
});
}
}
// 使用示例
const browser = await puppeteer.launch();
const page = await browser.newPage();
const stateManager = new BrowserStateManager(browser);
await page.goto('https://example.com');
// ... 执行操作
await stateManager.saveAllState(page, './full-state.json');
// 清理环境
await stateManager.clearAllState(page);
// 恢复状态
await stateManager.restoreAllState(page, './full-state.json');
这个封装类提供了统一的状态管理接口,将 Cookie、localStorage 和 sessionStorage 的操作整合在一起,简化了测试代码。
状态隔离策略
在并行测试中,状态隔离至关重要。每个测试用例应该使用独立的浏览器上下文:
import puppeteer from 'puppeteer';
async function runTest(userId: string) {
const browser = await puppeteer.launch();
const context = await browser.createBrowserContext(); // 创建独立上下文
const page = await context.newPage();
// 设置该用户特有的状态
await context.setCookie({
name: 'user_id',
value: userId,
domain: '.example.com'
});
await page.evaluate((id) => {
localStorage.setItem('currentUser', id);
}, userId);
// 执行测试
await page.goto('https://example.com/dashboard');
// ... 断言和验证
// 清理:关闭上下文,所有状态自动清除
await context.close();
await browser.close();
}
// 并行运行多个测试
await Promise.all([
runTest('user1'),
runTest('user2'),
runTest('user3')
]);
这种方式确保每个测试用例在完全隔离的环境中运行,避免了状态污染导致的 flaky tests。
敏感数据处理
处理包含敏感信息的状态时,需要注意数据安全:
// 不推荐:直接保存包含密码的 Cookie
const cookies = await browser.cookies();
await fs.promises.writeFile('./cookies.json', JSON.stringify(cookies));
// 推荐:过滤敏感信息后再保存
const safeCookies = cookies.filter(cookie =>
!cookie.name.toLowerCase().includes('password') &&
!cookie.name.toLowerCase().includes('token')
);
await fs.promises.writeFile('./cookies-safe.json', JSON.stringify(safeCookies));
// 或者对敏感值进行加密
import crypto from 'crypto';
const encrypt = (text: string) => {
const cipher = crypto.createCipher('aes-256-cbc', 'secret-key');
return cipher.update(text, 'utf8', 'hex') + cipher.final('hex');
};
const encryptedCookies = cookies.map(cookie => ({
...cookie,
value: cookie.httpOnly ? encrypt(cookie.value) : cookie.value
}));
这些措施可以防止敏感信息泄露到日志文件或版本控制系统中。
总结
本章深入探讨了 Puppeteer 中的状态管理技术。Cookie 的读取、设置和删除提供了模拟用户身份的能力;localStorage 和 sessionStorage 的管理让我们能够控制客户端持久化数据;浏览器权限控制则为测试现代 Web API 提供了基础。
这些技术在实际项目中往往需要组合使用。例如,测试一个完整的电商流程可能需要:设置登录 Cookie、初始化用户偏好 localStorage、授予地理位置权限来定位最近门店。通过合理封装和状态隔离,可以构建出稳定可靠的自动化测试体系。
在下一章中,我们将探讨如何将页面内容保存为截图和 PDF,以及如何处理文件上传下载等媒体操作,进一步扩展 Puppeteer 的实战能力。