Powerful Automation with the Chrome DevTools Protocol

A presentation at Nordic.js in September 2018 in Stockholm, Sweden by Trent Willis

Slide 1

Slide 1

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

Slide 2

Slide 2

Hej Hello, Nordic.js! @TRENTMWILLIS #NORDICJS

Slide 3

Slide 3

Pretty cool chat app @TRENTMWILLIS #NORDICJS

Slide 4

Slide 4

This. Is. BORING. @TRENTMWILLIS #NORDICJS

Slide 5

Slide 5

manual testing This. Is. BORING. @TRENTMWILLIS #NORDICJS

Slide 6

Slide 6

Powerful Automation with the Chrome DevTools Protocol @TRENTMWILLIS #NORDICJS

Slide 7

Slide 7

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

Slide 8

Slide 8

Netflix Mainly here to have fun @trentmwillis QUnit Lead Not associated with the Chrome project @TRENTMWILLIS #NORDICJS

Slide 9

Slide 9

CHITCHAT The Chat app you build in an interview. chitchat.glitch.me @TRENTMWILLIS #NORDICJS

Slide 10

Slide 10

Pretty cool chat app @TRENTMWILLIS #NORDICJS

Slide 11

Slide 11

Record screenshots Track memory Use Audit accessibility @TRENTMWILLIS #NORDICJS

Slide 12

Slide 12

! r o t c e p s n i e h t t c e p Ins github.com/ChromeDevTools/devtools-frontend @TRENTMWILLIS #NORDICJS

Slide 13

Slide 13

Chrome DevTools Protocol chromedevtools.github.io/devtools-protocol/ @TRENTMWILLIS #NORDICJS

Slide 14

Slide 14

chrome --remote-debugging-port=<port> @TRENTMWILLIS #NORDICJS

Slide 15

Slide 15

A high-level API to control Chrome @TRENTMWILLIS #NORDICJS

Slide 16

Slide 16

@TRENTMWILLIS #NORDICJS

Slide 17

Slide 17

@TRENTMWILLIS #NORDICJS

Slide 18

Slide 18

@TRENTMWILLIS #NORDICJS

Slide 19

Slide 19

H ! s s e l d a e @TRENTMWILLIS #NORDICJS

Slide 20

Slide 20

All code will be available online @TRENTMWILLIS #NORDICJS

Slide 21

Slide 21

npm install puppeteer Please don’t do this on the conference WiFi. K? Thx. @TRENTMWILLIS #NORDICJS

Slide 22

Slide 22

npm install qunit Please don’t do this on the conference WiFi. K? Thx. @TRENTMWILLIS #NORDICJS

Slide 23

Slide 23

const puppeteer = require('puppeteer'); const QUnit = require('qunit'); const { module: testModule, test } = QUnit; @TRENTMWILLIS #NORDICJS

Slide 24

Slide 24

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 {

Slide 25

Slide 25

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

Slide 26

Slide 26

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

Slide 27

Slide 27

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

Slide 28

Slide 28

test('basic', async function(assert) { const page = await this.browser.newPage(); }); @TRENTMWILLIS #NORDICJS

Slide 29

Slide 29

test('basic', async function(assert) { const page = await this.browser.newPage(); await page.goto('https://chitchat.glitch.me/'); }); @TRENTMWILLIS #NORDICJS

Slide 30

Slide 30

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

Slide 31

Slide 31

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

Slide 32

Slide 32

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

Slide 33

Slide 33

Our first end-to-end test! Woohoo! /workspace/chit-chat/demos/1.gif @TRENTMWILLIS #NORDICJS

Slide 34

Slide 34

test('basic', async function(assert) { // Visit page and check clients connected const message = 'Hello, Nordic.js!'; await page.type('#message', message); }); @TRENTMWILLIS #NORDICJS

Slide 35

Slide 35

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

Slide 36

Slide 36

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

Slide 37

Slide 37

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

Slide 38

Slide 38

This is basically the same video from before… O B A Y ! G N I R @TRENTMWILLIS #NORDICJS

Slide 39

Slide 39

@TRENTMWILLIS #NORDICJS

Slide 40

Slide 40

But what about Selenium? Cypress? <insert e2e solution>? @TRENTMWILLIS #NORDICJS

Slide 41

Slide 41

Ease of Flexibility @TRENTMWILLIS #NORDICJS

Slide 42

Slide 42

Visual Regression Testing In 20 lines of code! @TRENTMWILLIS #NORDICJS

Slide 43

Slide 43

OMG, so easy! await page.screenshot({path: filePath}); @TRENTMWILLIS #NORDICJS

Slide 44

Slide 44

npm install looks-same @TRENTMWILLIS #NORDICJS

Slide 45

Slide 45

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

Slide 46

Slide 46

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

Slide 47

Slide 47

Even has focus! @TRENTMWILLIS #NORDICJS

Slide 48

Slide 48

npm install jest @TRENTMWILLIS #NORDICJS

Slide 49

Slide 49

Jest, if you’re into that expect(await page.content()).toMatchSnapshot(); @TRENTMWILLIS #NORDICJS

Slide 50

Slide 50

Functional and visually verified. We’re just scratching the surface! @TRENTMWILLIS #NORDICJS

Slide 51

Slide 51

By prototype By heap Memory Leak Detection @TRENTMWILLIS #NORDICJS

Slide 52

Slide 52

p a e H y B const startMetrics = await page.metrics(); @TRENTMWILLIS #NORDICJS

Slide 53

Slide 53

{ } 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

Slide 54

Slide 54

p a e H y B const startMetrics = await page.metrics(); @TRENTMWILLIS #NORDICJS

Slide 55

Slide 55

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

Slide 56

Slide 56

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

Slide 57

Slide 57

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

Slide 58

Slide 58

e p y t o t o r P By for (let i = 0; i < 20; i++) { await sendMessage(page, Message #${i}); } @TRENTMWILLIS #NORDICJS

Slide 59

Slide 59

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

Slide 60

Slide 60

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

Slide 61

Slide 61

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

Slide 62

Slide 62

No more unused stuff User Flow Code Coverage @TRENTMWILLIS #NORDICJS

Slide 63

Slide 63

await page.coverage.startJSCoverage(); // Do stuff... const jsCoverage = await page.coverage.stopJSCoverage(); @TRENTMWILLIS #NORDICJS

Slide 64

Slide 64

[ ] { } url: 'https://chitchat.glitch.me/client.js', ranges: [ { start: 0, end: 894 }, { start: 932, end: 1408 } ], text: 'long string of file contents' @TRENTMWILLIS #NORDICJS

Slide 65

Slide 65

puppeteer-to-istanbul github.com/istanbuljs/puppeteer-to-istanbul @TRENTMWILLIS #NORDICJS

Slide 66

Slide 66

await page.coverage.startJSCoverage(); // Do stuff... const jsCoverage = await page.coverage.stopJSCoverage(); @TRENTMWILLIS #NORDICJS

Slide 67

Slide 67

const pti = require('puppeteer-to-istanbul'); // Later in your test... await page.coverage.startJSCoverage(); // Do stuff... const jsCoverage = await page.coverage.stopJSCoverage(); @TRENTMWILLIS #NORDICJS

Slide 68

Slide 68

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

Slide 69

Slide 69

@TRENTMWILLIS #NORDICJS

Slide 70

Slide 70

@TRENTMWILLIS #NORDICJS

Slide 71

Slide 71

(or, Browser) Multi-User, End-To-End Testing @TRENTMWILLIS #NORDICJS

Slide 72

Slide 72

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

Slide 73

Slide 73

@TRENTMWILLIS #NORDICJS

Slide 74

Slide 74

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

Slide 75

Slide 75

Testing is just one form of automation. We can do more! @TRENTMWILLIS #NORDICJS

Slide 76

Slide 76

APIs for workflow automation APIs for static sites RESTful APIs “Application Puppeteer Interface” @TRENTMWILLIS #NORDICJS

Slide 77

Slide 77

@TRENTMWILLIS #NORDICJS

Slide 78

Slide 78

cat pictures Important charts Screenshot Emailer SaaS: Screenshots as a Service @TRENTMWILLIS #NORDICJS

Slide 79

Slide 79

@TRENTMWILLIS #NORDICJS

Slide 80

Slide 80

Record + Playback Service A.K.A., reproduction service @TRENTMWILLIS #NORDICJS

Slide 81

Slide 81

@TRENTMWILLIS #NORDICJS

Slide 82

Slide 82

Awesome! puppeteer-recorder github.com/checkly/puppeteer-recorder @TRENTMWILLIS #NORDICJS

Slide 83

Slide 83

Chaos UI Testing Unleash the monkeys! @TRENTMWILLIS #NORDICJS

Slide 84

Slide 84

Experiment! ! n u f e v a And, h github.com/trentmwillis/devtools-protocol-demos Share your ideas with me @trentmwillis @TRENTMWILLIS #NORDICJS