Puppeteer 备忘录

概述

Puppeteer 是由Google开发的Node.js库,用于通过开发者工具协议控制无头 Chrome Chromium。它允许您自动化 UI 测试、网页抓取、截图测试等操作。

1
2
3
4
5
6
7
8
9
10
const puppeteer = require('puppeteer');

(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('<https://example.com>');
await page.screenshot({path: 'example.png'});

await browser.close();
})();

启动浏览器

启动一个无头浏览器实例:

1
const browser = await puppeteer.launch();

启动完整版本的 Chrome:

1
2
3
const browser = await puppeteer.launch({
headless: false
});

使用自定义参数启动浏览器:

1
2
3
const browser = await puppeteer.launch({
args: ['--start-maximized']
});

自定义启动选项:

1
2
3
4
puppeteer.launch({
executablePath: '/path/to/Chrome', // 自定义 Chrome 二进制文件路径
product: 'firefox' // 启动 Firefox
});

创建页面

创建新页面:

1
const page = await browser.newPage();

创建匿名页面:

1
2
const context = await browser.createIncogniteBrowserContext();
const page = await context.newPage();

访问已有页面:

1
2
const pages = await browser.pages();
const page = pages[0];

标签

在标签之间切换/将它们置于前台:

1
2
await page1.bringToFront();
await page2.bringToFront();

操作

导航到 URL:

1
await page.goto('https://example.com');

点击元素:

1
await page.click('#element');

输入内容:

1
await page.type('#input', 'Text');

按键盘键:

1
await page.keyboard.press('Shift');

上传文件:

1
await page.setInputFiles('#upload', ['/path/to/file1', '/path/to/file2']);

在页面上执行 JavaScript 代码:

1
2
3
const result = await page.evaluate(() => {
return document.querySelector('#result').textContent;
});

悬停在元素上:

1
await page.hover('#element');

捕获截图:

1
await page.screenshot({path: 'screenshot.png'});

模拟移动设备:

1
await page.emulate(puppeteer.devices['iPhone 6']);

滚动至视图:

1
await page.evaluate(el => el.scrollIntoView(), await page.$('.item'));

在 iframe 中输入:

1
2
const frame = page.frames().find(f => f.name() === 'frame');
await frame.$eval('#input', el => el.value = 'Text');

在移动设备上点击元素:

1
await page.touchscreen.tap(200, 75);

触发拖放:

1
2
3
await page.mouse.down();
await page.mouse.move(0, 100);
await page.mouse.up();

选择器

通过 CSS 选择器获取元素:

1
const nav = await page.$('nav');

获取多个元素:

1
const items = await page.$$('.item');

使用 XPath 选择器:

1
const button = await page.$x('//*[@id="button"]');

获取文本内容:

1
const text = await page.textContent('.results');

高级选择器

使用文本选择器:

1
const link = await page.$('a:text("Next")');

可见性选择器:

1
const hidden = await page.$('.element:hidden');

属性选择器:

1
const checkbox = await page.$('input[type="checkbox"]');

XPath 选择器:

1
const submit = await page.$x('//button[@type="submit"]');

通过文本内容获取:

1
const p = await page.$eval('p', el => el.innerText === 'Hello');

查询 Shadow DOM:

1
2
const shadow = await page.$('.element/shadow-root');
const text = await shadow.$eval('.text', el => el.textContent);

辅助测试

检查问题:

1
2
3
4
5
const issues = await page.accessibility.audit({
runA11yChecks: true
});

expect(issues).toHaveLength(0);

检查颜色对比度:

1
2
3
4
5
6
const contrastratio = await page.$eval('.button', button => {
const bgColor = window.getComputedStyle(button).backgroundColor;
// 计算对比度比率
});

expect(contrastratio).toBeGreaterThan(4.5);

选项卡焦点顺序:

1
2
3
4
await page.keyboard.press('Tab');

const active = await page.evaluate(() => document.activeElement.id);
expect(active).toBe('username');

调试与报告

跟踪控制台错误:

1
2
3
4
5
page.on('console', msg => {
if (msg.type() === 'error') {
console.error(msg.text());
}
});

生成 HTML 报告:

1
2
const html = '<h1>测试报告</h1>';
fs.writeFileSync('report.html', html);

跟踪测试覆盖率:

1
2
3
4
5
const coverage = await page.coverage.startJSCoverage();

// 运行测试

const results = await coverage.stopJSCoverage();

等待

等待导航:

1
await page.waitForNavigation();

等待选择器:

1
await page.waitForSelector('div.loaded');

等待固定时间:

1
await page.waitFor(1000); // 等待 1 秒

等待函数结果:

1
await page.waitForFunction(() => window.fetchDone);

等待 XHR 请求:

1
await page.waitForRequest(request => request.url() === 'data.json');

带超时的导航:

1
await page.waitForNavigation({timeout: 60000});

元素等待 30 秒:

1
await page.waitForSelector('.item', {timeout: 30000});

框架

获取页面框架:

1
const frames = page.mainFrame().childFrames();

设置当前框架:

1
2
3
4
const frame = page.frames().find(f => f.name() === 'frame');
await frame.evaluate(() => {
// 在框架内运行代码
});

输入

获取元素的 HTML/文本/属性:

1
2
3
4
5
const html = await page.$eval('.item', el => el.outerHTML);

const text = await page.$eval('.item', el => el.innerText);

const class = await page.$eval('.item', el => el.getAttribute('class'));

填写并提交表单:

1
2
await page.type('#input', 'Text');
await page.click('#submitButton');

采样

对元素截图:

1
2
3
4
const el = await

page.$('.element');
await el.screenshot({path: 'element.png'});

模拟设备和视口:

1
2
3
4
5
const devices = puppeteer.devices;
const iPhone = devices['iPhone 6'];

await page.emulate(iPhone);
await page.setViewport(iPhone.viewport);

获取资源定时数据:

1
2
const metrics = await page.metrics();
const requests = metrics.requestfinished;

PDF

生成 PDF 报告:

1
2
3
4
await page.pdf({
path: 'page.pdf',
format: 'A4'
});

横向定向 PDF:

1
2
3
4
await page.pdf({
path: 'page.pdf',
landscape: true
});

事件

页面加载事件:

1
2
3
page.once('load', () => {
// 页面完全加载
});

网络请求失败事件:

1
2
3
page.on('requestfailed', request => {
console.log(request.url + ' ' + request.failure().errorText);
});

控制台消息事件:

1
2
3
page.on('console', msg => {
console.log(`${msg.type()} ${msg.text()}`);
});

认证

设置用户代理:

1
await page.setUserAgent('CustomAgent');

设置自定义标头:

1
2
3
await page.setExtraHTTPHeaders({
'Accept-Language': 'en-US'
});

设置 cookies:

等待页面设置 cookie:

1
await page.setCookie({name: 'session', value: '1234'});

设置凭据:

1
2
3
4
await page.authenticate({
username: 'user',
password: 'pass'
});

设置绕过 CSP:

1
await browser.launch({ignoreHTTPSErrors: true});

使用代理服务器:

1
await page.authenticate({username: 'user', password: 'pass'});

网络

禁用缓存:

1
await page.setCacheEnabled(false);

设置节流率:

1
2
3
4
5
6
await page.setRequestInterception(true);
page.on('request', request => {
request.continue({
throttling: 0.5 // 慢 50%
});
});

模拟响应:

1
2
3
4
5
6
page.on('request', interceptedRequest => {
interceptedRequest.respond({
contentType: 'text/html',
body: '<html>模拟页面</html>'
});
});

模拟重定向响应:

1
2
3
await page.route('**/*', route => {
route.continue({ url: '/mock-page.html' });
});

模拟 404 状态:

1
2
3
page.on('request', route => {
route.abort('notfound');
});

高级用法

等待更复杂的条件:

1
2
3
4
5
6
7
// 等待文本内容更改
await page.waitForFunction(selector => {
return document.querySelector(selector).textContent === 'Updated';
}, {}, selector);

// 等待 500ms 内无网络请求
await page.waitForTimeout(500);

处理弹出窗口和新标签页:

1
2
3
4
5
6
7
8
9
10
page.on('dialog', dialog => {
dialog.accept(); // 或 dismiss()
});

const [popup] = await Promise.all([
new Promise(resolve => browser.once('targetcreated', target => resolve(target.page()))),
page.click('#open-popup'), // 单击打开弹出窗口的按钮
]);

await popup.waitForSelector('h1'); // 等待弹出窗口内容

使用自动重试稳定不稳定的测试:

1
2
3
4
5
// 自动重试失败步骤最多 4 次
const autoRetry = require('puppeteer-autoretry');
autoRetry.setDefaults({ retries: 4 });

await autoRetry(page).type('#input', 'Text');

触摸交互

在元素上点击:

1
await page.tap('button');

在移动设备上滚动:

1
await page.touchscreen.scroll(50, 100);

拖放:

1
2
3
await page.touchscreen.down();
await page.touchscreen.move(50, 100);
await page.touchscreen.up();

地理位置和权限

设置地理位置:

1
await page.setGeolocation({latitude: 0, longitude: 0});

授予摄像头访问权限:

1
await page.grantPermissions(['camera']);

高级用例

提交表单和上传文件:

1
2
3
4
5
6
7
// 提交表单
await page.type('#email', 'test@example.com');
await page.click('#submit');

// 上传文件
const input = await page.$('input#file');
input.uploadFile('/path/to/file.txt');

从网站中抓取内容:

1
2
3
4
// 从所有 p 元素中提取文本
const texts = await page.$$eval('p', elements => {
return elements.map(el => el.textContent);
});

跨浏览器视觉测试:

1
2
3
4
5
6
7
const devices = puppeteer.devices;

for (const browserType of ['chromium', 'firefox', 'webkit']) {
const browser = await puppeteer.launch({browserType});

// 模拟设备并进行测试
}

视觉回归测试

比较截图:

1
2
3
4
const screenshot = await page.screenshot();

const diff = await visualDiff.compare(screenshot, 'baseline.png');
expect(diff.misMatchPercentage).toBeLessThan(0.01);

并行测试

并行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
const browser = await puppeteer.launch();
const pagePromises = [
browser.newPage(),
browser.newPage()
];

const pages = await Promise.all(pagePromises);

// 并行运行测试
await Promise.all([
pages[0].goto('url1'),
pages[1].goto('url2')
])

使用技巧

通过持久化上下文加速执行:

1
2
3
4
5
6
7
8
// 持久化浏览器上下文
const browserContext = await browser.createIncogniteBrowserContext();

await browserContext.close();
await browserContext.waitForTarget(page => page.url() === 'about:blank');

// 恢复上下文
const page = await browserContext.newPage();

在测试期间分析 CPU 使用情况:

1
2
3
4
5
await page.profiling.start({path: 'trace.json'});

// 运行 CPU 密集任务

await page.profiling.stop();

使用隐私模式上下文:

1
2
const context = await browser.createIncogniteBrowserContext();
const page = await context.newPage();

在上下文之间传输 cookie:

1
2
3
4
5
6
const context1 = await browser.createIncogniteBrowserContext();
const context2 = await browser.createIncogniteBrowserContext();

await context1.addCookies([cookieObj1, cookieObj2]);

const transferred = await context2.transferCookies(context1);

使用持久性上下文:

1
2
3
4
5
const context = await browser.createPersistentContext();
await context.close();

// 以后恢复
const page = await context.newPage();

测试自动化策略

可重用的页面对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class LoginPage {

constructor(page) {
this.page = page;
}

async login(username, password) {
await this.page.type('#username', username);
await this.page.type('#password', password);
await this.page.click('#submit');
}

}

// 使用方式:
const loginPage = new LoginPage(page);
await loginPage.login('user1', '123456');

同步测试序列:

1
2
3
4
5
6
7
const [response] = await Promise.all([
page.click('#submit'), // 请求发送后返回
page.waitForNavigation() // 页面加载后解析
]);

// 在导航后断言响应
expect(response.status()).toBe(200);

重试失败的测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
for (let retry = 0; retry < 3; retry++) {
try {
await loginPage.login('invalid', 'password');
break; // 测试通过,因此我们中断
} catch (error) {
if (retry === 2) {
throw error; // 3 次重试后失败
}


// 否则重试测试
}
}