Gleb Bahmutov PRO
JavaScript ninja, image processing expert, software quality fanatic
Heavy smoke fills the air as people cross 34th Street in Herald Square in New York City on June 6, 2023.
source: https://time.com/6285871/wildfire-smoke-photos-us-canada/
Why doesn't this work
It used to work
C / C++ / C# / Java / CoffeeScript / JavaScript / Node / Angular / Vue / Cycle.js / functional programming / testing
Edit and view HTML documents, 1991
JavaScript!
Problem: given an array of numbers, multiply each number by a constant and print the result.
var numbers = [3, 1, 7];
var constant = 2;
// 6 2 14
var numbers = [3, 1, 7];
var constant = 2;
var k = 0;
for(k = 0; k < numbers.length; k += 1) {
console.log(numbers[k] * constant);
}
// 6 2 14
Arrays: object-oriented + functional
var numbers = [3, 1, 7];
var constant = 2;
function mul(a, b) {
return a * b;
}
function print(n) {
console.log(n);
}
var mulBy = mul.bind(null, constant);
numbers
.map(mulBy)
.forEach(print);
// 6 2 14
functional bits
Object-oriented methods
Arrays: object-oriented + functional
numbers
.map(mulBy)
.forEach(print);
JS was influenced by Self and Scheme
numbers
.map(mulBy)
.forEach(print);
// same as
const a1 = numbers.map(mulBy);
a1.forEach(print)
numbers
.map(mulBy)
.filter(predicate)
.map(...)
.reduce(...)
Fluent and chained
I want a value!
const X = numbers
.map(mulBy)
.filter(predicate)
.map(...)
.reduce(...)
// use x
"But what if my computation is asynchronous?"
compute((x) => {
// x is the value
})
// runs before callback
// cannot access x
callback
const X = compute()
expect(X).to.deep.equal([6, 2, 14])
"But what if my computation is asynchronous?"
it('computes', (done) => {
compute((x) => {
expect(x).to.deep.equal(...)
done()
})
})
Single async operation with a value or an error
const p = new Promise((resolve, reject) => {
...
resolve(42)
})
// after promise successfully completes
p.then(...)
.catch(...)
var sleepSecond = _.partial(Q.delay, 1000);
var pauseMulAndPrint = function (n) {
return function () {
return sleepSecond()
.then(_.partial(byConstant, n))
.then(print);
};
};
numbers.map(pauseMulAndPrint)
.reduce(Q.when, Q())
.done();
// ... 6 ... 2 ... 14
it('computes', () => {
return compute().then(x => {
expect(x).to.deep.equal(...)
})
})
it('computes', async () => {
const x = await compute()
expect(x).to.deep.equal(...)
})
<body>
<h1>Fruits</h1>
<ul>
<li>Apples</li>
<li>Grapes</li>
<li>Kiwi</li>
</ul>
</body>
test('has 3 fruits', async ({ page }) => {
await page.goto(index)
await expect(page.getByRole('listitem')).toHaveText([
'Apples',
'Grapes',
'Kiwi',
])
})
Playwright (pw)
test('has 3 fruits', async ({ page }) => {
await page.goto(index)
await expect(page.getByRole('listitem')).toHaveText([
'Apples',
'Grapes',
'Kiwi',
])
})
Playwright (pw)
Microsoft Playwright automatically retries the assertion
test('has 3 fruits', async ({ page }) => {
await page.goto(index)
const elements = page.getByRole('listitem')
await expect(async () => {
const strings = await elements.allInnerTexts()
const prices = strings.map((s) => s.split('$')[1])
.map(Number)
expect(prices).toEqual([1, 2, 3])
}).toPass()
})
Playwright (pw)
test('has 3 fruits', async ({ page }) => {
await page.goto(index)
const elements = page.getByRole('listitem')
await expect(async () => {
const strings = await elements.allInnerTexts()
const prices = strings.map((s) => s.split('$')[1])
.map(Number)
expect(prices).toEqual([1, 2, 3])
}).toPass()
})
Playwright (pw)
test('has 3 fruits', async ({ page }) => {
await expect(async () => {
}).toPass()
})
retry async fn
Playwright (pw)
const X = elements
const strings = X.innerText
const prices = ...
expect(prices).to.deep.equal(...)
getElements()
.then(els => els.innerText)
.then(parsePrices)
.then(prices => expect(prices)...)
.catch(e => ...)
const make = (input) =>
Option.fromNullable(input)
.map(parseInput)
.flatMap((input) => Option.fromNullable(transform(input)))
.map(print)
.map(prettify)
.getWithDefault("fallback");
User click events
var Rx = require('rx');
var clickEvents = Rx.Observable
.fromEvent(document.querySelector('#btn'), 'click')
var numberEvents = Rx.Observable
.fromArray(numbers);
function pickSecond(c, n) { return n; }
Rx.Observable.zip(clickEvents, numberEvents,
pickSecond)
.map(byConstant)
.subscribe(print);
// prints a number on each button click
Promise
Task, Option, Result
Observable
async / await
🛑
test('has 3 fruits', async ({ page }) => {
await page.goto(index)
const elements = page.getByRole('listitem')
await expect(async () => {
const strings = await elements.allInnerTexts()
const prices = strings.map((s) => s.split('$')[1])
.map(Number)
expect(prices).toEqual([1, 2, 3])
}).toPass()
})
Retry
import 'cypress-map'
it('has 3 fruits', () => {
cy.visit('public/index.html')
cy.get('#fruits li')
.map('innerText')
.mapInvoke('split', '$')
.mapInvoke('at', 1)
.map(Number)
.should('deep.equal', [1, 2, 3])
})
Cypress (cy)
import 'cypress-map'
it('has 3 fruits', () => {
cy.visit('public/index.html')
cy.get('#fruits li')
.map('innerText')
.mapInvoke('split', '$')
.mapInvoke('at', 1)
.map(Number)
.should('deep.equal', [1, 2, 3])
})
import 'cypress-map'
it('has 3 fruits', () => {
cy.visit('public/index.html')
cy.get('#fruits li')
.map('innerText')
.mapInvoke('split', '$')
.mapInvoke('at', 1)
.map(Number)
.should('deep.equal', [1, 2, 3])
})
Elements to numbers
pipeline
retry
import 'cypress-map'
it('has 3 fruits', () => {
cy.visit('public/index.html')
cy.get('#fruits li')
.map('innerText')
.mapInvoke('split', '$')
.mapInvoke('at', 1)
.map(Number)
.should('deep.equal', [1, 2, 3])
})
Elements to numbers
pipeline
test('has 3 fruits', async ({ page }) => {
await page.goto(index)
const elements = page.getByRole('listitem')
await expect(async () => {
const strings = await elements.allInnerTexts()
const prices = strings.map((s) => s.split('$')[1])
.map(Number)
expect(prices).toEqual([1, 2, 3])
}).toPass()
})
Retry
Vs
retry
cy
pw
it('has 3 fruits', () => {
cy.visit('public/index.html')
})
test('has 3 fruits', async ({ page }) => {
await page.goto(index)
})
Of course the test is async
Of course loading the page is async
Of course the test must wait for the command to finish
cy
pw
import { test, expect } from '@playwright/test'
test('adds todos', async ({ page, request }) => {
await request.post('/reset', { data: { todos: [] } })
await page.goto('/')
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?')
await newTodo.fill('one')
await newTodo.press('Enter')
await newTodo.fill('two')
await newTodo.press('Enter')
const todoItems = page.locator('li.todo')
await expect(todoItems).toHaveCount(2)
})
it('adds todos', () => {
cy.request('POST', '/reset', { todos: [] })
cy.visit('/')
cy.get('.new-todo').type('one{enter}').type('two{enter}')
cy.get('li.todo').should('have.length', 2)
})
// cy
// pw
import { test, expect } from '@playwright/test'
test('adds todos', async ({ page, request }) => {
await request.post('/reset', { data: { todos: [] } })
await page.goto('/')
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?')
await newTodo.fill('one')
await newTodo.press('Enter')
await newTodo.fill('two')
await newTodo.press('Enter')
const todoItems = page.locator('li.todo')
await expect(todoItems).toHaveCount(2)
})
it('adds todos', () => {
cy.request('POST', '/reset', { todos: [] })
cy.visit('/')
cy.get('.new-todo').type('one{enter}').type('two{enter}')
cy.get('li.todo').should('have.length', 2)
})
// cy
// pw
Swipe right
(data flows to the right)
Swipe left
(data is assigned to the left)
var k = 0;
for(k = 0; k < numbers.length; k += 1) {
console.log(numbers[k] * constant);
}
// 6 2 14
// _ is Lodash / Ramda
_(numbers)
.map(_.partial(mul, constant))
.forEach(print);
// 6 2 14
// functional
// imperative
Swipe right
(data flows to the right)
Swipe left
(data is assigned to the left)
const n = Number(await locator.getText())
// n is set
cy.get('#count')
.invoke('text')
.then(Number)
.then(n => {
// n is set
})
// right
// left
Swipe right
(data flows to the right)
Swipe left
(data is assigned to the left)
const n = Number(await locator.getText())
// n is set
// left
What do you want to check on the page?
cy.get('#count')
.invoke('text')
.then(Number)
.then(n => {
// n is set
})
// right
cy.get('li.todo').should('have.length', 2)
There should be 2 items
cy.get('li.todo')
.should('satisfy', $li => $li.length % 2 === 0)
There should be an even number of items
cy.wait('@load').its('response.body.length')
.then(n => {
cy.get('li.todo').should('have.length', n)
})
There should be the same number of items as returned by the server
cy.intercept('GET', '/todos', { fixture: 'three.json' })
cy.get('li.todo').should('have.length', 3)
If you can control the data in your tests, then the test syntax collapses into a simple and elegant fluent chain
$todo
test('has 3 fruits', async ({ page }) => {
await page.goto(index)
await expect(page.getByRole('listitem')).toHaveText([
'Apples',
'Grapes',
'Kiwi',
])
})
<body>
<h1>Fruits</h1>
<ul>
<li>Apples</li>
<li>Grapes</li>
<li>Kiwi</li>
</ul>
</body>
test('has 3 fruits', async ({ page }) => {
await page.goto(index)
await expect(page.getByRole('listitem')).toHaveText([
'Apples',
'Grapes',
'Kiwi',
])
})
import 'cypress-map'
it('has 3 fruits', () => {
cy.visit(index)
cy.get('#fruits li')
.map('innerText')
.should('deep.equal', ['Apples', 'Grapes', 'Kiwi'])
})
functional reactive bits
if you do not have "toHaveText" assertion...
If the test looks weird or complicated...
const n = Number(await locator.getText())
// n is set
cy.get('#count')
.invoke('text')
.then(Number)
.then(n => {
// n is set
})
// cy
// pw
npm i -D cypress-await
const n = Number(await locator.getText())
// n is set
const n = await
cy.get('#count')
.invoke('text')
.then(Number)
// cy + cypress-awit
// pw
npm i -D cypress-await
const n = Number(await locator.getText())
// n is set
const n = cy
.get('#count')
.invoke('text')
.then(Number)
// cy + cypress-awit
// pw
npm i -D cypress-await
import { test } from '@playwright/test'
test('logs hello', async () => {
console.log('hello, world')
})
it('logs hello', () => {
console.log('hello, world')
})
// pw
// cy
// pw
// cy
Cypress iframes the app and its specs
Node.js
Playwright test code
Browser
application
Chrome Debugger Protocol
Node.js
Config / plugins file
Browser
application
Cypress test code
WebSocket
for
cy.task
I do not care how long the tests take to run.
I care how long it takes to debug a failing test
Time-travel to inspect the result of each command ⏳
$ npx playwright test --trace on
$ npx playwright test --ui
Cypress Test Replays https://on.cypress.io/test-replay
Let me take a look. Is this why my test has failed?
👏 Thank You 👏
By Gleb Bahmutov
In this presentation, I will show how the two most popular modern web application testing tools, Cypress and Playwright, approach the same problem in very different ways. We will see how to write end-to-end, API, and component tests using both tools and how to execute them on a continuous integration system. While other comparisons often focus on finding a single winner, I would like to list the main advantages of each tool to help you make an informed decision depending on your use case. Presented at iJS Sept 2023 in NYC, 30 minutes.
JavaScript ninja, image processing expert, software quality fanatic