9

web浏览器自动化之playwright

 2 years ago
source link: https://jeremyxu2010.github.io/2020/11/web%E6%B5%8F%E8%A7%88%E5%99%A8%E8%87%AA%E5%8A%A8%E5%8C%96%E4%B9%8Bplaywright/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

web浏览器自动化之playwright

2020-11-01 约 2316 字 预计阅读 5 分钟

工作中经常需要在几个后台管理站点上进行一些重复性工作。作为一个会偷懒的程序员,自然要想点主意。

之前就使用selenium-webdriver做过一些web浏览器自动化的工作,在github上找了下,发现microsoft又开源了一个playwright,这个比以前的selenium-webdriver使用起来更简单了,这里就用它试一试。

由于是playwright是一个node库,这里最好新建一个目录,并在这个目录中安装npm依赖库。

$ mkdir playwright-sandbox
$ cd playwright-sandbox
$ npm i -D playwright
$ npm i -D playwright-cli

安装时它会自动下载当前平台上3个浏览器二进制文件,可以通过一系统环境变量控制这种下载行为,参见Advanced installation

安装完毕后可以写个小脚本测试一下:

test.js:

const { webkit } = require('playwright');
(async () => {
const browser = await webkit.launch();
const page = await browser.newPage();
await page.goto('http://whatsmyuseragent.org/');
await page.screenshot({ path: `example.png` });
await browser.close();
})();

可以简单地执行一下看看效果:

$ node ./test.js

像上面这样写脚本跟selenium-webdriver也差不多,实在有些无趣。因此playwright还提供了杀手级辅助工具,可以直接录制用户的交互行为生成代码。

$ npx playwright-cli codegen wikipedia.org

这个就方便太多了。

脚本都可以生成了,写脚本就不再是问题了,真正要花脑力的地方变成了如何修改生成的脚本。在可以随心所欲地修改脚本前,有必要了解下playwright脚本里的一些核心概念。

Browser

Browser代表的是一个Chromium、Firefox或WebKit浏览器实例。Playwright脚本通常会启动一个浏览器实例,最后会将这个实例关闭。Browser可以以headless模式或headful模式启动,分别是无GUI和有GUI模式。

const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'.
const browser = await chromium.launch({ headless: false });
await browser.close();

因为启动一个浏览器实例成本很高,因此在playwright脚本里一般会复用浏览器实例,通过多个浏览器上下文在同一个浏览器实例里达到完成不同的逻辑。

Browser context

Browser context是一个隔离地有点类似于session的浏览器实例上下文。

const browser = await chromium.launch();
const context = await browser.newContext();

Browser context可以用来模拟多个页面场景,每个场景可以有不同的权限、本地化、设备等。

const { devices } = require('playwright');
const iPhone = devices['iPhone 11 Pro'];
const context = await browser.newContext({
...iPhone,
permissions: ['geolocation'],
geolocation: { latitude: 52.52, longitude: 13.39},
colorScheme: 'dark',
locale: 'de-DE'

Page and frame

Page代表着一个浏览器tab页面,它应该被用于导航到URL。脚本中的绝大部分用户交互行为都是在页面内容上执行的。

// Create a page.
const page = await context.newPage();
// Navigate explicitly, similar to entering a URL in the browser.
await page.goto('http://example.com');
// Fill an input.
await page.fill('#search', 'query');
// Navigate implicitly by clicking a link.
await page.click('#submit');
// Expect a new url.
console.log(page.url());
// Page can navigate from the script - this will be picked up by Playwright.
window.location.href = 'https://example.com';

跟正常的html页面一样,一个页面中可以有多个frame,同样可以用脚本操作frame

// Get frame using the frame's name attribute
const frame = page.frame('frame-login');
// Get frame using frame's URL
const frame = page.frame({ url: /.*domain.*/ });
// Get frame using any other selector
const frameElementHandle = await page.$('.frame-class');
const frame = await frameElementHandle.contentFrame();
// Interact with the frame
await frame.fill('#username-input', 'John');

Selector

要对web页面进行操作,首先就要定位到页面元素。playwright同样支持CSS selectors, XPath selectors, HTML attributes text content方式定位页面元素。

// Using data-test-id= selector engine
await page.click('data-test-id=foo');
// CSS and XPath selector engines are automatically detected
await page.click('div');
await page.click('//html/body/div');
// Find node by text substring
await page.click('text=Hello w');
// Explicit CSS and XPath notation
await page.click('css=div');
await page.click('xpath=//html/body/div');
// Only search light DOM, outside WebComponent shadow DOM:
await page.click('css:light=div');
// Click an element with text 'Sign Up' inside of a #free-month-promo.
await page.click('#free-month-promo >> text=Sign Up');
// Capture textContent of a section that contains an element with text 'Selectors'.
const sectionText = await page.$eval('*css=section >> text=Selectors', e => e.textContent);

Execution contexts: Node.js and Browser

除了操作web页面上已有的界面元素,还可以直接在浏览器上下文中执行自定义脚本,这个就相当于在页面的开发者工具Console中执行脚本。

const status = await page.evaluate(async () => {
const response = await fetch(location.href);
return response.status;
const data = { text: 'some data', value: 1 };
// Pass |data| as a parameter.
const result = await page.evaluate(data => {
window.myApp.use(data);
}, data);

主要核心概念就上面这些了,看起来不难。另外官方还很贴心地提供了一系列指引,参考这些应该可以解决编写脚本过程中遇到的各类问题。

脚本写好后,需要简单调试一下,以确保脚本逻辑的正确性。官方文档也写得比较清楚了,只要会用chrome的开发者工具应该可以快速上手,这里就不赘述了。

很多web页面都有认证鉴权,只有通过认证,获得合法的认证状态才能进行进一步的操作,因此需要特意说明一下。

如果是简单的用户名密码认证,可以直接在脚本中完成登录。

const page = await context.newPage();
await page.goto('https://github.com/login');
// Interact with login form
await page.click('text=Login');
await page.fill('input[name="login"]', USERNAME);
await page.fill('input[name="password"]', PASSWORD);
await page.click('text=Submit');
// Verify app is logged in

利用认证状态

有些web应用安全性做得比较好,采取的是双因素认证。这时可以先将认证状态保存下来,然后在脚本中利用该认证状态。

下面的例子假设认证状态是保存在cookies中的。

const { webkit } = require('playwright');
const fs = require('fs').promises;
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
(async () => {
const browser = await webkit.launch({ headless: false });
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://www.google.com.hk/');
# 这时手动完成web页面上的登录流程
await sleep(60000);
# 登录完毕后将上下文中的cookies写入文件
const cookies = await context.cookies();
await fs.writeFile('cookies.json', JSON.stringify(cookies));
// Close page
await page.close();
await context.close();
await browser.close();
})();

接下来可以利用认证状态。

const { webkit } = require('playwright');
const fs = require('fs').promises;
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
(async () => {
const browser = await webkit.launch({ headless: false });
const context = await browser.newContext();
const cookiesStr = await fs.readFile('cookies.json', 'utf8');
await context.addCookies(JSON.parse(cookiesStr));
const page = await context.newPage();
await page.goto('https://www.google.com.hk/');
// 在这里执行正常的逻辑
// Close page
await page.close();
await context.close();
await browser.close();
})();

playwright支持CookiesLocal storageSession storage多种保存认证状态的方式。

所有脚本逻辑都写在一起不便于关注点分离,因此官方建议对于逻辑较复杂的脚本,可以将在一个页面内的各个逻辑代码段封装为一个个类中的方法。

// models/Search.js
class SearchPage {
constructor(page) {
this.page = page;
async navigate() {
await this.page.goto('https://bing.com');
async search(text) {
await this.page.fill('[aria-label="Enter your search term"]', text);
await this.page.keyboard.press('Enter');
module.exports = { SearchPage };
// search.spec.js
const { SearchPage } = require('./models/Search');
// In the test
const page = await browser.newPage();
const searchPage = new SearchPage(page);
await searchPage.navigate();
await searchPage.search('search query');

进一步思考

playwright的介绍差不多就上面这些了,可以看出来这些功能之前selenium-webdriver也都可以提供,但无疑playwright做得更加易用了,特别是可以自动录制用户的交互行为,直接生成脚本,这个就很强了。

再想一想,平时开发过程中Android UI自动化测试脚本的编写也是比较程式化的,这个是否可以有一个像playwright之类的框架自动生成脚本?看了下暂时是没有的,这是个方向。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK