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
A presentation at Nordic.js in September 2018 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