木偶也是有心的~
😜
背景
目前负责的几十多个页面管理系统平台,产品发版迭代频繁,除了本业务线会修改逻辑代码,其他业务线也时有涉及(通用逻辑),受测试资源所限,不是每次改动都能做到全面回归验证,时常会在线上暴露出一些问题。
需求
期望有个自动化测试工具,能在发版进行主页面回归验证。因管理平台用户量级较小,主要兼容Chrome浏览器,前端页面使用Nodejs开发,顾经过选型,选择了Puppeteer,Puppeteer是Chrome官方出品,有后台背景,使用的也是JS脚本,在解决问题的同时,也可以提高对前端代码的认知水平。一举两得,何乐而不为之。😆
Puppeteer是什么?
Puppeteer,“木偶”,顾名思义,可以用它来操纵网页。是Chrome推出headless(无界面)模式之后,由官方出品的通过DevTools协议控制headless Chrome的Node库,可以通过Puppeteer的提供的API直接控制Chrome模拟大部分用户操作来进行自动化测试或者作为爬虫访问页面来收集数据。最大特点:操作Dom可以完全在内存中进行模拟既在V8引擎中处理而不用打开浏览器。
Headless Browser
Headless Browser(无头浏览器,在Chrome59中发布)是浏览器的无界面状态,可以在不打开浏览器GUI的情况下,使用浏览器支持的性能。
Chrome Headless相比于其他的浏览器,可以更便捷的运行web自动化,编写爬虫、截图等。通常是由编程或者命令行来控制的。可以加快UI自动化测试的执行时间,对于UI自动化测试,少了真实浏览器加载css,js以及渲染页面的工作。无头测试要比真实浏览器快的多。可以在无界面的服务器或CI上运行测试,减少了外界的干扰,使自动化测试更稳定。
终端命令使用(Mac使用,需要指定别名)
alias chrome="/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome"
启动官网
chrome --headless --disable-gpu --remote-debugging-port=8080 https://maimai.cn
1. --headless
无浏览器模式
2. --disable-gpu
禁用GPU加速
3. --remote-debugging-port=8080
指定端口
4. https://maimai.cn
指定网址
浏览器中输入http://127.0.0.1:8080,可以打开官网
打印DOM
chrome --headless --disable-gpu --dump-dom https://maimai.cn
DOM结构
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="description" content="8000万人都在用的职场社交平台,基于“实名职业认证”和“人脉网络引擎”帮助职场人拓展人脉、交流合作、求职招聘,收获更多机遇。">
<meta name="keywords" content="脉脉,脉脉官网,脉脉APP,maimai,招聘,找人,合作,商务,升职,加薪,找工作,职言,社交,交友">
<meta name="baidu-site-verification" content="tTbhbyypfH">
<meta name="renderer" content="webkit">
<title>脉脉-成就职业梦想</title>
<link rel="stylesheet" type="text/css" href="/static/styles/website/pc/index.css?v=24">
</head>
</html>
创建PDF文件
chrome --headless --disable-gpu --print-to-pdf https://maimai.cn
截屏
chrome --headless --disable-gpu --window-size=1280,1696 --screenshot https://maimai.cn
1. --window-size=1280,1696
设置尺寸
Puppeteer能做什么?
- 生成网页截图或者 PDF
- 高级爬虫,可以爬取网页
- 模拟键盘输入、表单自动提交、登录网页等,实现 UI 自动化测试
- 捕获站点的时间线,以便追踪你的网站,帮助分析网站性能问题
安装
Puppeteer依赖Node,为了异步超级好用的async/await(ES6的规范),推荐使用7.6版本以上的Node。
#npm install puppeteer
or #yarn add puppeteer
如何创建实例
创建demo.js文件
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://maimai.cn');
await page.screenshot({path: 'maimai.png'});
await page.pdf({path: 'maimai.pdf', format: 'A4'});
await browser.close();
})();
1. puppeteer.launch()
创建一个浏览器实例 Browser 对象
2. Browser
对象创建页面 Page 对象
3. page.goto()
跳转到指定的页面
4. page.screenshot()
对页面进行截图
5. page.pdf()
创建pdf文件
6. browser.close()
关闭浏览器
执行:
node demo.js
调试技巧
无界模式
const browser = await puppeteer.launch({headless: false}); //关闭无界模式,默认开启
指定浏览器路径,默认自带的chromium
const browser = await puppeteer.launch({executablePath: '/path/to/Chrome'}); //指定chrome,默认自带
减慢操作速度(毫秒)
const browser = await puppeteer.launch({
headless: false,
slowMo: 250 // 延迟250ms
});
开启devtools
const browser = await puppeteer.launch({devtools: true});
Puppeteer API 分层结构
Puppeteer通过使用Chrome DevTools Protocol(CDP)协议与浏览器进行通信,Browser对应一个浏览器实例,拥有浏览器上下文,一个Browser可以包含多个BrowserContext。Page表示一个Tab页面,一个BrowserContext可以包含多个Page。每个页面都有一个主的Frame,ExecutionContext是Frame提供的一个JavasSript执行环境
Browser
: 对应一个浏览器实例,一个 Browser 可以包含多个 BrowserContext
BrowserContext
: 对应浏览器一个上下文会话,就像我们打开一个普通的 Chrome 之后又打开一个隐身模式的浏览器一样,BrowserContext 具有独立的 Session(cookie 和 cache 独立不共享),一个 BrowserContext 可以包含多个 Page
Page
:表示一个 Tab 页面,通过 browserContext.newPage()/browser.newPage() 创建,browser.newPage() 创建页面时会使用默认的 BrowserContext,一个 Page 可以包含多个 Frame
Frame
: 一个框架,每个页面有一个主框架(page.MainFrame()),也可以多个子框架,主要由 iframe 标签创建产生的
Worker
: 具有单一执行上下文,以便于和 WebWorkers 交互
常用API
页面导航
page.goto
:打开新页面
page.goBack
:回退到上一个页面
page.goForward
:前进到下一个页面
page.reload
:重新加载页面
page.waitForNavigation
:等待页面跳转
基本上所有的操作都是异步的,以上几个 API 都涉及到关于打开一个页面,什么情况下才能判断这个函数执行完毕呢,这些函数都提供了两个参数 waitUtil 和 timeout,waitUtil 表示直到什么出现就算执行完毕,timeout 表示如果超过这个时间还没有结束就抛出异常。
await page.goto('https://www.baidu.com', {
timeout: 30 * 1000,
waitUntil: [
'load', //等待 “load” 事件触发
'domcontentloaded', //等待 “domcontentloaded” 事件触发
'networkidle0', //在 500ms 内没有任何网络连接
'networkidle2' //在 500ms 内网络连接个数不超过 2 个
]
});
等待元素、请求、响应
page.waitForXPath
:等待 xPath 对应的元素出现,返回对应的实例
page.waitForSelector
:等待选择器对应的元素出现,返回对应的实例
page.waitForResponse
:等待某个响应结束,返回 Response 实例
page.waitForRequest
:等待某个请求出现,返回 Request 实例
await page.waitForXPath('//img');
await page.waitForSelector('#uniqueId');
await page.waitForResponse('https://d.youdata.netease.com/api/dash/hello');
await page.waitForRequest('https://d.youdata.netease.com/api/dash/hello');
自定义等待
page.waitForFunction
:等待在页面中自定义函数的执行结果,返回实例
page.waitFor
:设置等待时间,实在没办法的做法
获取元素
page.$('#uniqueId')
:获取某个选择器对应的第一个元素
page.$$('div')
:获取某个选择器对应的所有元素
page.$x('//img')
:获取某个 xPath 对应的所有元素
page.waitForXPath('//img')
:等待某个 xPath 对应的元素出现
page.waitForSelector('#uniqueId')
:等待某个选择器对应的元素出现
ElementHandle
elementHandle.click()
:点击某个元素
elementHandle.tap()
:模拟手指触摸点击
elementHandle.focus()
:聚焦到某个元素
elementHandle.hover()
:鼠标 hover 到某个元素上
elementHandle.type('hello')
:在输入框输入文本
好几个栗子~~ 🌰
login
(async () => {
const browser = await puppeteer.launch({
slowMo: 100, //放慢速度
headless: false,
defaultViewport: {width: 1440, height: 780},
ignoreHTTPSErrors: false, //忽略 https 报错
args: ['--start-fullscreen'] //全屏打开页面
});
const page = await browser.newPage();
await page.goto('https://maimai.cn/login/');
await page.type('input[class=loginPhoneInput]', '136xxxxxxxx',{delay: 20}); //输入手机号
await page.type('input[id=login_pw]', '123456', {delay: 20});//输入密码
await page.tap('input[class=loginBtn]');//点击登录
page.waitForNavigation()
console.log('登录成功');
await page.waitFor(5*1000);
await page.close();
await browser.close();
})();
spider
(async () => {
const browser = await puppeteer.launch({headless:false});
const page = await browser.newPage();
await page.goto('https://maimai.cn/',{waitUntil:'networkidle2'});//500毫秒内网络连接数不超过2个
await page.waitFor('body > div > div.website-navbar > div > div.website-navbar__links > div.website-links');
const result = await page.evaluate(() => {
let data = []; // 初始化空数组来存储数据
let elements = document.querySelectorAll('body > div > div.website-navbar > div > div.website-navbar__links > div.website-links > a'); // 获取selector下的所有<a>标签元素
for (var element of elements){
let title = element.innerText; // 获取标题
let url = element.href;//获取网址
data.push({title,url}); // 存入数组
}
return data;
});
console.log(result);
await page.waitFor(3000);
await browser.close();
})();
trace
在 DevTools 的 Performance 可以上传对应的 json 文件并查看分析结果
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.tracing.start({path: './trace.json'});
await page.goto('https://maimai.cn');
await page.tracing.stop();
browser.close();
})();
参考文献
DOM及页面渲染
更多api
中文文档
Puppeteer Github
结合项目来谈谈puppeteer