# Testing With Cypress vs Playwright: What Is the Difference?

### Sr Director of Engineering, Mercari US

Heavy smoke fills the air as people cross 34th Street in Herald Square in New York City on June 6, 2023.

## survival is possible* but we need to act now

• join an organization

# Agenda

• JavaScript vs The World
• JavaScript vs JavaScript vs JavaScript
• PW vs Cy syntax
• PW vs Cy architecture

## Speaker: Gleb Bahmutov PhD

C / C++ / C# / Java / CoffeeScript / JavaScript / Node / Angular / Vue / Cycle.js / functional programming / testing

# Lots Of 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``````

# procedural / imperative JS

``````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``````
• clear "multiply then print" semantics

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()
})
})``````

# Promises

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);
};
};``````

### Sleep, mul then print using promises

``````numbers.map(pauseMulAndPrint)
.reduce(Q.when, Q())
.done();
// ... 6 ... 2 ... 14``````
``````it('computes', () => {
return compute().then(x => {
expect(x).to.deep.equal(...)
})
})``````

# Async to the rescue

``````it('computes', async () => {
const x = await compute()
expect(x).to.deep.equal(...)
})``````

# Async to the rescue

``````<body>
<h1>Fruits</h1>
<ul>
<li>Apples</li>
<li>Grapes</li>
<li>Kiwi</li>
</ul>
</body>``````

# Static page

``````test('has 3 fruits', async ({ page }) => {
await page.goto(index)
await expect(page.getByRole('listitem')).toHaveText([
'Apples',
'Grapes',
'Kiwi',
])
})``````

Playwright (pw)

# Dynamic page

``````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

# With prices

``````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)

# I want a value!

``````const X = elements
const strings = X.innerText
const prices = ...
expect(prices).to.deep.equal(...)``````

## Promises were wrapped values

``````getElements()
.then(els => els.innerText)
.then(parsePrices)
.then(prices => expect(prices)...)
.catch(e => ...)``````

# In functional programming

``````const make = (input) =>
Option.fromNullable(input)
.map(parseInput)
.flatMap((input) => Option.fromNullable(transform(input)))
.map(print)
.map(prettify)
.getWithDefault("fallback");``````

# In functional reactive programming

• Everything is a source of asynchronous events.
• Your code is a pipeline reacting to one or more input streams and outputting another stream of events

# In functional reactive programming

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

Observable

async / await

🛑

# I want a value!

``````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

• What if we accept that almost everything is async?
• What if we optimize for data transformations and assertions?
``````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 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

## The Syntax

// pw

• imperative syntax
• promise-based
• declarative syntax
• reactive stream
``````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

## The "Tinder" data flow

// pw

• imperative syntax
• promise-based
• declarative syntax
• reactive stream

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

## for-loop vs Array.forEach

// 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

## Data flows left vs 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``````

## Data flows left vs right

// 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>``````

# Static page

``````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...

# Ok, I give up.

``````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`

# Need For Speed 🏎️

``````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

# Playwright

• runs outside the browser
• controls the browser via Chrome Debugger Protocol (FAST)

# Cypress

• runs inside the browser
• wraps a lot of browser elements using JavaScript
• creates user interface (SLOW)

# Need For Speed 🏎️

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``

# Replaying Tests: The New Hotness

Let me take a look. Is this why my test has failed?

👏 Thank You 👏

By Gleb Bahmutov

# Testing With Cypress vs Playwright: What Is the Difference?

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.

• 689