A presentation at Nordic.js in in Stockholm, Sweden by Trent Willis
n u F g n i v a H d n a m o d e r Curing Bo Powerful Automation with the Chrome DevTools Protocol @TRENTMWILLIS #NORDICJS
Hej Hello, Nordic.js! @TRENTMWILLIS #NORDICJS
Pretty cool chat app @TRENTMWILLIS #NORDICJS
This. Is. BORING. @TRENTMWILLIS #NORDICJS
manual testing This. Is. BORING. @TRENTMWILLIS #NORDICJS
Powerful Automation with the Chrome DevTools Protocol @TRENTMWILLIS #NORDICJS
n u F g n i v a H d n a m o d e r Curing Bo Powerful Automation with the Chrome DevTools Protocol @TRENTMWILLIS #NORDICJS
Netflix Mainly here to have fun @trentmwillis QUnit Lead Not associated with the Chrome project @TRENTMWILLIS #NORDICJS
CHITCHAT The Chat app you build in an interview. chitchat.glitch.me @TRENTMWILLIS #NORDICJS
Pretty cool chat app @TRENTMWILLIS #NORDICJS
Record screenshots Track memory Use Audit accessibility @TRENTMWILLIS #NORDICJS
! r o t c e p s n i e h t t c e p Ins github.com/ChromeDevTools/devtools-frontend @TRENTMWILLIS #NORDICJS
Chrome DevTools Protocol chromedevtools.github.io/devtools-protocol/ @TRENTMWILLIS #NORDICJS
chrome --remote-debugging-port=<port> @TRENTMWILLIS #NORDICJS
A high-level API to control Chrome @TRENTMWILLIS #NORDICJS
@TRENTMWILLIS #NORDICJS
@TRENTMWILLIS #NORDICJS
@TRENTMWILLIS #NORDICJS
H ! s s e l d a e @TRENTMWILLIS #NORDICJS
All code will be available online @TRENTMWILLIS #NORDICJS
npm install puppeteer Please don’t do this on the conference WiFi. K? Thx. @TRENTMWILLIS #NORDICJS
npm install qunit Please don’t do this on the conference WiFi. K? Thx. @TRENTMWILLIS #NORDICJS
const puppeteer = require('puppeteer'); const QUnit = require('qunit'); const { module: testModule, test } = QUnit; @TRENTMWILLIS #NORDICJS
const puppeteer = require('puppeteer'); const QUnit = require('qunit'); const { module: testModule, test } = QUnit; testModule('End-To-End Tests', function(hooks) { hooks.before(async function() { Before test }); module starts hooks.after(async function() { }); test('basic', async }); }); After test function(assert) module ends @TRENTMWILLIS #NORDICJS {
const puppeteer = require('puppeteer'); const QUnit = require('qunit'); const { module: testModule, test } = QUnit; testModule('End-To-End Tests', function(hooks) { hooks.before(async function() { this.browser = await puppeteer.launch(); }); hooks.after(async function() { }); test('basic', async function(assert) { }); }); @TRENTMWILLIS #NORDICJS
const puppeteer = require('puppeteer'); const QUnit = require('qunit'); const { module: testModule, test } = QUnit; testModule('End-To-End Tests', function(hooks) { hooks.before(async function() { this.browser = await puppeteer.launch() }); hooks.after(async function() { await this.browser.close(); }); test('basic', async function(assert) { }); }); @TRENTMWILLIS #NORDICJS
const puppeteer = require('puppeteer'); const QUnit = require('qunit'); const { module: testModule, test } = QUnit; testModule('End-To-End Tests', function(hooks) { hooks.before(async function() { this.browser = await puppeteer.launch() }); hooks.after(async function() { await this.browser.close(); }); test('basic', async function(assert) { }); }); @TRENTMWILLIS #NORDICJS
test('basic', async function(assert) { const page = await this.browser.newPage(); }); @TRENTMWILLIS #NORDICJS
test('basic', async function(assert) { const page = await this.browser.newPage(); await page.goto('https://chitchat.glitch.me/'); }); @TRENTMWILLIS #NORDICJS
test('basic', async function(assert) { const page = await this.browser.newPage(); await page.goto('https://chitchat.glitch.me/'); const numClients = await page.waitFor( ‘#num-clients:not(:empty)’ ); }); @TRENTMWILLIS #NORDICJS
test('basic', async function(assert) { const page = await this.browser.newPage(); await page.goto('https://chitchat.glitch.me/'); const numClients = await page.waitFor( ‘#num-clients:not(:empty)’ ); const text = await page.evaluate( el => el.textContent, numClients ); assert.equal(numClients, '1'); }); @TRENTMWILLIS #NORDICJS
test('basic', async function(assert) { const page = await this.browser.newPage(); await page.goto('https://chitchat.glitch.me/'); const numClients = await page.waitFor( ‘#num-clients:not(:empty)’ ); const text = await page.evaluate( el => el.textContent, numClients ); assert.equal(numClients, '1'); await page.close(); }); @TRENTMWILLIS #NORDICJS
Our first end-to-end test! Woohoo! /workspace/chit-chat/demos/1.gif @TRENTMWILLIS #NORDICJS
test('basic', async function(assert) { // Visit page and check clients connected const message = 'Hello, Nordic.js!'; await page.type('#message', message); }); @TRENTMWILLIS #NORDICJS
test('basic', async function(assert) { // Visit page and check clients connected const message = 'Hello, Nordic.js!'; await page.type('#message', message); await page.click('#send'); }); @TRENTMWILLIS #NORDICJS
test('basic', async function(assert) { // Visit page and check clients connected const message = 'Hello, Nordic.js!'; await page.type('#message', message); await page.click('#send'); const displayedText = await waitAndGetText( page, '.message' ); assert.equal(displayedText, message); }); @TRENTMWILLIS #NORDICJS
test('basic', async function(assert) { // Visit page and check clients connected const message = 'Hello, Nordic.js!'; await page.type('#message', message); await page.click('#send'); const displayedText = await waitAndGetText( page, '.message' ); assert.equal(displayedText, message); await page.close(); }); @TRENTMWILLIS #NORDICJS
This is basically the same video from before… O B A Y ! G N I R @TRENTMWILLIS #NORDICJS
@TRENTMWILLIS #NORDICJS
But what about Selenium? Cypress? <insert e2e solution>? @TRENTMWILLIS #NORDICJS
Ease of Flexibility @TRENTMWILLIS #NORDICJS
Visual Regression Testing In 20 lines of code! @TRENTMWILLIS #NORDICJS
OMG, so easy! await page.screenshot({path: filePath}); @TRENTMWILLIS #NORDICJS
npm install looks-same @TRENTMWILLIS #NORDICJS
const fs = require('fs'); const looksSame = require('looks-same'); const screenshot = async (page, title) => { const filePath = ./screenshots/${title}.png
; if (fs.existsSync(filePath)) { const newFilePath = ./screenshots/${title}-new.png
; await page.screenshot({ path: newFilePath, fullPage: true }); const result = await new Promise(resolve => { looksSame(filePath, newFilePath, (err, equal) => resolve(equal)); }); fs.unlinkSync(newFilePath); return result; } else { await page.screenshot({ path: filePath, fullPage: true }); return true; } }; @TRENTMWILLIS #NORDICJS
test('basic', async function(assert) { // Visit page, check clients connected and test // sending messages assert.ok(await screenshot(page, 'demo.4')); await page.close(); }); @TRENTMWILLIS #NORDICJS
Even has focus! @TRENTMWILLIS #NORDICJS
npm install jest @TRENTMWILLIS #NORDICJS
Jest, if you’re into that expect(await page.content()).toMatchSnapshot(); @TRENTMWILLIS #NORDICJS
Functional and visually verified. We’re just scratching the surface! @TRENTMWILLIS #NORDICJS
By prototype By heap Memory Leak Detection @TRENTMWILLIS #NORDICJS
p a e H y B const startMetrics = await page.metrics(); @TRENTMWILLIS #NORDICJS
{ } Timestamp: 888864.298237, Documents: 2, Frames: 1, JSEventListeners: 3, Nodes: 186, LayoutCount: 287, RecalcStyleCount: 523, LayoutDuration: 0.073111, RecalcStyleDuration: 0.092844, ScriptDuration: 0.028183, TaskDuration: 0.567313, JSHeapUsedSize: 1653264, JSHeapTotalSize: 6033408 @TRENTMWILLIS #NORDICJS
p a e H y B const startMetrics = await page.metrics(); @TRENTMWILLIS #NORDICJS
p a e H y B const startMetrics = await page.metrics(); for (let i = 0; i < 20; i++) { await sendMessage(page, Message #${i}
); }
@TRENTMWILLIS #NORDICJS
p a e H y B const startMetrics = await page.metrics(); for (let i = 0; i < 20; i++) { await sendMessage(page, Message #${i}
); } const endMetrics = await page.metrics(); assert.ok( endMetrics.JSHeapUsedSize < startMetrics.JSHeapUsedSize * 1.1 );
@TRENTMWILLIS #NORDICJS
p a e H y B
a t o n y l b a b o a r e P d i d o o g
const startMetrics = await page.metrics();
for (let i = 0; i < 20; i++) { await sendMessage(page, Message #${i}
); } const endMetrics = await page.metrics();
assert.ok( endMetrics.JSHeapUsedSize < startMetrics.JSHeapUsedSize * 1.1 );
@TRENTMWILLIS #NORDICJS
e p y t o t o r P By for (let i = 0; i < 20; i++) { await sendMessage(page, Message #${i}
); }
@TRENTMWILLIS #NORDICJS
e p y t o t o r P By for (let i = 0; i < 20; i++) { await sendMessage(page, Message #${i}
); } const formDataProto = await page.evaluateHandle(() => FormData.prototype);
@TRENTMWILLIS #NORDICJS
e p y t o t o r P By for (let i = 0; i < 20; i++) { await sendMessage(page, Message #${i}
); } const formDataProto = await page.evaluateHandle(() => FormData.prototype); const formDataInstances = await page.queryObjects(formDataProto);
@TRENTMWILLIS #NORDICJS
e p y t o t o r P By
for (let i = 0; i < 20; i++) { await sendMessage(page, Message #${i}
); } const formDataProto = await page.evaluateHandle(() => FormData.prototype); const formDataInstances = await page.queryObjects(formDataProto); const count = await page.evaluate( formDatas => formDatas.length, formDataInstances ); assert.equal(count, 0);
@TRENTMWILLIS #NORDICJS
No more unused stuff User Flow Code Coverage @TRENTMWILLIS #NORDICJS
await page.coverage.startJSCoverage(); // Do stuff... const jsCoverage = await page.coverage.stopJSCoverage(); @TRENTMWILLIS #NORDICJS
[ ] { } url: 'https://chitchat.glitch.me/client.js', ranges: [ { start: 0, end: 894 }, { start: 932, end: 1408 } ], text: 'long string of file contents' @TRENTMWILLIS #NORDICJS
puppeteer-to-istanbul github.com/istanbuljs/puppeteer-to-istanbul @TRENTMWILLIS #NORDICJS
await page.coverage.startJSCoverage(); // Do stuff... const jsCoverage = await page.coverage.stopJSCoverage(); @TRENTMWILLIS #NORDICJS
const pti = require('puppeteer-to-istanbul'); // Later in your test... await page.coverage.startJSCoverage(); // Do stuff... const jsCoverage = await page.coverage.stopJSCoverage(); @TRENTMWILLIS #NORDICJS
const pti = require('puppeteer-to-istanbul'); // Later in your test... await page.coverage.startJSCoverage(); // Do stuff... const jsCoverage = await page.coverage.stopJSCoverage(); pti.write(jsCoverage); @TRENTMWILLIS #NORDICJS
@TRENTMWILLIS #NORDICJS
@TRENTMWILLIS #NORDICJS
(or, Browser) Multi-User, End-To-End Testing @TRENTMWILLIS #NORDICJS
test('can send and receive messages', async function(assert) { const page1 = await openChat(this.browser); const page2 = await openChat(this.browser); Extract testing await sendMessage(page1, 'Hello, Nordic.js!'); await sendMessage(page2, 'Hello, Trent!');
code into functions
await page1.waitForXPath(//*/li[text()="Hello, Trent!"]
); await page2.waitForXPath(//*/li[text()="Hello, Nordic.js!"]
); });
@TRENTMWILLIS #NORDICJS
@TRENTMWILLIS #NORDICJS
const puppeteer = require('puppeteer'); const pti = require('puppeteer-to-istanbul'); const QUnit = require('qunit');
const writeCoverage = async (page) => { const jsCoverage = await page.coverage.stopJSCoverage(); pti.write(jsCoverage); };
const { module: testModule, test } = QUnit; const sendMessage = async (page, messageText) => { await page.type('#message', messageText); await page.click('#send'); await page.waitForXPath(//*/li[@class="message sender"][text()="${messageText}"]
); };
testModule('End-To-End Tests', function(hooks) { hooks.before(async function() { this.browser = await puppeteer.launch(); }); hooks.after(async function() { await this.browser.close(); });
const openChat = async (browser) => { const page = await browser.newPage();
test('application loads and shows number of online users', async function(assert) { const page1 = await openChat(this.browser); const page2 = await openChat(this.browser);
await page.coverage.startJSCoverage(); await page.goto('https://chitchat.glitch.me/');
await sendMessage(page1, 'Hello, Nordic.js!'); await sendMessage(page2, 'Hello, Trent!');
const numClients = await page.waitForSelector('#num-clients'); await page.waitForFunction(numClients => numClients.textContent.trim(), {}, numClients);
await page1.waitForXPath(//*/li[@class="message recipient"][text()="Hello, Trent!"]
); await page2.waitForXPath(//*/li[@class="message recipient"][text()="Hello, Nordic.js!"]
);
return page; }; const checkA11y = async (assert, page) => { await page.addScriptTag({ url: 'https://cdnjs.cloudflare.com/ajax/libs/axe-core/3.0.3/ axe.min.js' }); const results = await page.evaluate(() => axe.run(document)); assert.equal(results.violations.length, 0); }; const checkMemory = async (assert, page) => { const formDataProto = await page.evaluateHandle(() => FormData.prototype); const formDataInstances = await page.queryObjects(formDataProto); const count = await page.evaluate(formDatas => formDatas.length, formDataInstances); assert.equal(count, 0); };
await checkA11y(assert, page1); await checkA11y(assert, page2); await checkMemory(assert, page1); await checkMemory(assert, page2); await writeCoverage(page1); await writeCoverage(page2); }); });
@TRENTMWILLIS #NORDICJS
Testing is just one form of automation. We can do more! @TRENTMWILLIS #NORDICJS
APIs for workflow automation APIs for static sites RESTful APIs “Application Puppeteer Interface” @TRENTMWILLIS #NORDICJS
@TRENTMWILLIS #NORDICJS
cat pictures Important charts Screenshot Emailer SaaS: Screenshots as a Service @TRENTMWILLIS #NORDICJS
@TRENTMWILLIS #NORDICJS
Record + Playback Service A.K.A., reproduction service @TRENTMWILLIS #NORDICJS
@TRENTMWILLIS #NORDICJS
Awesome! puppeteer-recorder github.com/checkly/puppeteer-recorder @TRENTMWILLIS #NORDICJS
Chaos UI Testing Unleash the monkeys! @TRENTMWILLIS #NORDICJS
Experiment! ! n u f e v a And, h github.com/trentmwillis/devtools-protocol-demos Share your ideas with me @trentmwillis @TRENTMWILLIS #NORDICJS
Many developers are familiar with the powerful Chrome DevTools that help us inspect, profile, and debug our web applications. But, did you know that Chrome exposes virtually all the same information and capabilities via an API that you can use in JavaScript?
In this talk, we’ll look at how you can start using the Chrome DevTools Protocol today to unlock powerful automation techniques for your web application. We’ll cover how to start using the protocol through Puppeteer and then dive into some of the really exciting possibilities that have opened up when using this in conjunction with headless browsers, such as holistic end-to-end testing, the collection of code usage metrics, and new service paradigms.
Here’s what was said about this presentation on social media.
This is why we can't have nice things. #nordicjs @trentmwillis @nordicjs pic.twitter.com/ufGZowIpuP
— 🌍 🌳 🐝 🌊 🏳️🌈 (@reimertz) September 6, 2018
Thanks for a really interesting talk! Looking forward to explore more of puppeteer.
— Ryrholm (@Ryrholm) September 6, 2018
Awesome talk Trent! 🔥 Looking forward to the recording/slides.
— Andrey Lushnikov (@aslushnikov) September 6, 2018
@trentmwillis loved your talk! we're using Puppeteer for @nodeconfar's website to check that social cards are displayed correctly pic.twitter.com/4GgFMUXB3c
— Alejandro Oviedo 🇸🇪 Nordic.js (@a0viedo) September 6, 2018
@trentmwillis: mission accoplished.
— Ben (@BenedekGagyi) September 6, 2018
I'm pretty sure I'm going to spend the night testing all the dev tools magic you showed today :)
@trentmwillis Thanks for awesome talk. I recently used puppeteer to contribute OGP metadata generation with featured slide screenshots to reveal-md presentation tool https://t.co/anX1eccFgX
— Juhamatti Niemelä (@iiska) September 6, 2018
Chrome Devtools Protocol by @trentmwillis at #nordicjs @nordicjs as #sketchnotes pic.twitter.com/MpqHTbGVRN
— Norman Wehrle (@normanwehrle) September 6, 2018
Thanks for your talk. I am motivated to find usecases just to play around with puppeteer.
— Norman Wehrle (@normanwehrle) September 6, 2018
Great talk! I gave a talk with some similar ideas you might find interesting https://t.co/LkWxVoVPnB
— Johnny (@cowchimp) December 12, 2018
and also a library that aims to wrap several DevTools Protocol APIs https://t.co/QgmLNQL1JV (still in its infancy)
Great stuff about browser automation and testing with #Puppeteer https://t.co/ZpnLqberDW
— Chema Fdez Varela (@jmfvarela) December 12, 2018