Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Connect.Tech 2020: Advanced Cypress Testing
Search
Jeremy Fairbank
September 28, 2020
Programming
250
1
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Connect.Tech 2020: Advanced Cypress Testing
Jeremy Fairbank
September 28, 2020
More Decks by Jeremy Fairbank
See All by Jeremy Fairbank
CodeMash 2020: Solving the Boolean Identity Crisis
jfairbank
1
210
CodeMash 2020: Practical Functional Programming
jfairbank
1
360
Connect.Tech 2019: Practical Functional Programming
jfairbank
0
420
Connect.Tech 2019: Solving the Boolean Identity Crisis
jfairbank
0
240
Lambda Squared 2019: Solving the Boolean Identity Crisis
jfairbank
0
180
All Things Open 2018: Practical Functional Programming
jfairbank
2
280
Connect.Tech 2018: Effective React Testing
jfairbank
1
210
Fluent Conf 2018: Building web apps with Elm Tutorial
jfairbank
2
930
Codestock 2018: Building a Resilient, API-driven Front-End with Elm
jfairbank
0
300
Other Decks in Programming
See All in Programming
Strategic Design in the Frontend: Moduliths & Micro Frontends @DDDEurope
manfredsteyer
PRO
0
100
その問い、本当に正しいですか?AI時代のエンジニアに必要な哲学と認知科学 / ai-philosophy-cognitive-science
minodriven
9
5.1k
AI 時代のソフトウェア設計の学び方
masuda220
PRO
29
12k
DynamoDBには集計系のクエリがないけどなんとかしたい
musan
1
140
RTSPクライアントを自作してみた話
simotin13
0
610
Vue × Nuxt × Oxc どこまで使える?実運用の現在地
andpad
0
250
AI時代のUIはどこへ行く?その2!
yusukebe
21
7.2k
A2UI という光を覗いてみる
satohjohn
1
140
Dataformのリポジトリを立ち上げるときにまずやること / dataform-day0-2026
snhryt
0
160
ローカルLLMでどこまでコードが書けるか -拡張版 / How much code can be written on a local LLM Extended
kishida
11
4.1k
Composerを使ったサプライチェーン攻撃の様子を眺めてみる #phpstudy
o0h
PRO
2
250
ふつうのFeature Flag実践入門
irof
7
4k
Featured
See All Featured
The Language of Interfaces
destraynor
162
27k
Principles of Awesome APIs and How to Build Them.
keavy
128
18k
Helping Users Find Their Own Way: Creating Modern Search Experiences
danielanewman
31
3.2k
How to optimise 3,500 product descriptions for ecommerce in one day using ChatGPT
katarinadahlin
PRO
1
3.6k
End of SEO as We Know It (SMX Advanced Version)
ipullrank
3
4.2k
世界の人気アプリ100個を分析して見えたペイウォール設計の心得
akihiro_kokubo
PRO
71
40k
ラッコキーワード サービス紹介資料
rakko
1
3.7M
XXLCSS - How to scale CSS and keep your sanity
sugarenia
250
1.3M
Abbi's Birthday
coloredviolet
2
8.1k
Are puppies a ranking factor?
jonoalderson
1
3.6k
Discover your Explorer Soul
emna__ayadi
2
1.1k
For a Future-Friendly Web
brad_frost
183
10k
Transcript
Testing Advanced Cypress Testing Jeremy Fairbank @elpapapollo
@testdouble helps improves how the world build software. testdouble.com
Available in print or e-book programming-elm.com
Testing anti-patterns
Assumptions
Assumptions
Assumptions End-to-End
Assumptions End-to-End Real API
Assumptions End-to-End Real API Test Data
The App bit.ly/act-repo
Selector Syndrome
import albums from '../../../server/jazz-albums-test-pristine.json' describe('Home page', function () { it('albums
can be viewed on home page', function () { cy.get('.album-list-item').should('have.length', albums.length) albums.forEach((album) => { cy.get('.album-list-item h2') .contains(album.title) .parent('.album-list-item') .should('exist') .find('h3') .should('contain', album.artists.join(' - ')) }) }) })
import albums from '../../../server/jazz-albums-test-pristine.json' describe('Home page', function () { it('albums
can be viewed on home page', function () { cy.get('.album-list-item').should('have.length', albums.length) albums.forEach((album) => { cy.get('.album-list-item h2') .contains(album.title) .parent('.album-list-item') .should('exist') .find('h3') .should('contain', album.artists.join(' - ')) }) }) })
import albums from '../../../server/jazz-albums-test-pristine.json' describe('Home page', function () { it('albums
can be viewed on home page', function () { cy.get('.album-list-item').should('have.length', albums.length) albums.forEach((album) => { cy.get('.album-list-item h2') .contains(album.title) .parent('.album-list-item') .should('exist') .find('h3') .should('contain', album.artists.join(' - ')) }) }) })
import albums from '../../../server/jazz-albums-test-pristine.json' describe('Home page', function () { it('albums
can be viewed on home page', function () { cy.get('.album-list-item').should('have.length', albums.length) albums.forEach((album) => { cy.get('.album-list-item h2') .contains(album.title) .parent('.album-list-item') .should('exist') .find('h3') .should('contain', album.artists.join(' - ')) }) }) })
import albums from '../../../server/jazz-albums-test-pristine.json' describe('Home page', function () { it('albums
can be viewed on home page', function () { cy.get('.album-list-item').should('have.length', albums.length) albums.forEach((album) => { cy.get('.album-list-item h2') .contains(album.title) .parent('.album-list-item') .should('exist') .find('h3') .should('contain', album.artists.join(' - ')) }) }) })
import albums from '../../../server/jazz-albums-test-pristine.json' describe('Home page', function () { it('albums
can be viewed on home page', function () { cy.get('.album-list-item').should('have.length', albums.length) albums.forEach((album) => { cy.get('.album-list-item h2') .contains(album.title) .parent('.album-list-item') .should('exist') .find('h3') .should('contain', album.artists.join(' - ')) }) }) })
import albums from '../../../server/jazz-albums-test-pristine.json' describe('Home page', function () { it('albums
can be viewed on home page', function () { cy.get('.album-list-item').should('have.length', albums.length) albums.forEach((album) => { cy.get('.album-list-item h2') .contains(album.title) .parent('.album-list-item') .should('exist') .find('h3') .should('contain', album.artists.join(' - ')) }) }) })
it('sorts albums', function () { function albumsSortedLike(albumList) { albumList.forEach((album, index)
=> { cy.get('.album-list-item') .eq(index) .find('h2') .should('contain', album.title) }) } cy.contains('.filter-section', 'Sort By').find('select').select('Title') albumsSortedLike(albumsSortedByTitle) cy.contains('.filter-section', 'Sort By').find('select').select('Artist') albumsSortedLike(albumsSortedByArtists) cy.contains('.filter-section', 'Sort By').find('select').select('Default') albumsSortedLike(albumsSortedById) })
it('sorts albums', function () { function albumsSortedLike(albumList) { albumList.forEach((album, index)
=> { cy.get('.album-list-item') .eq(index) .find('h2') .should('contain', album.title) }) } cy.contains('.filter-section', 'Sort By').find('select').select('Title') albumsSortedLike(albumsSortedByTitle) cy.contains('.filter-section', 'Sort By').find('select').select('Artist') albumsSortedLike(albumsSortedByArtists) cy.contains('.filter-section', 'Sort By').find('select').select('Default') albumsSortedLike(albumsSortedById) })
it('sorts albums', function () { function albumsSortedLike(albumList) { albumList.forEach((album, index)
=> { cy.get('.album-list-item') .eq(index) .find('h2') .should('contain', album.title) }) } cy.contains('.filter-section', 'Sort By').find('select').select('Title') albumsSortedLike(albumsSortedByTitle) cy.contains('.filter-section', 'Sort By').find('select').select('Artist') albumsSortedLike(albumsSortedByArtists) cy.contains('.filter-section', 'Sort By').find('select').select('Default') albumsSortedLike(albumsSortedById) })
it('sorts albums', function () { function albumsSortedLike(albumList) { albumList.forEach((album, index)
=> { cy.get('.album-list-item') .eq(index) .find('h2') .should('contain', album.title) }) } cy.contains('.filter-section', 'Sort By').find('select').select('Title') albumsSortedLike(albumsSortedByTitle) cy.contains('.filter-section', 'Sort By').find('select').select('Artist') albumsSortedLike(albumsSortedByArtists) cy.contains('.filter-section', 'Sort By').find('select').select('Default') albumsSortedLike(albumsSortedById) })
it('sorts albums', function () { function albumsSortedLike(albumList) { albumList.forEach((album, index)
=> { cy.get('.album-list-item') .eq(index) .find('h2') .should('contain', album.title) }) } cy.contains('.filter-section', 'Sort By').find('select').select('Title') albumsSortedLike(albumsSortedByTitle) cy.contains('.filter-section', 'Sort By').find('select').select('Artist') albumsSortedLike(albumsSortedByArtists) cy.contains('.filter-section', 'Sort By').find('select').select('Default') albumsSortedLike(albumsSortedById) })
it('sorts albums', function () { function albumsSortedLike(albumList) { albumList.forEach((album, index)
=> { cy.get('.album-list-item') .eq(index) .find('h2') .should('contain', album.title) }) } cy.contains('.filter-section', 'Sort By').find('select').select('Title') albumsSortedLike(albumsSortedByTitle) cy.contains('.filter-section', 'Sort By').find('select').select('Artist') albumsSortedLike(albumsSortedByArtists) cy.contains('.filter-section', 'Sort By').find('select').select('Default') albumsSortedLike(albumsSortedById) })
it('sorts albums', function () { function albumsSortedLike(albumList) { albumList.forEach((album, index)
=> { cy.get('.album-list-item') .eq(index) .find('h2') .should('contain', album.title) }) } cy.contains('.filter-section', 'Sort By').find('select').select('Title') albumsSortedLike(albumsSortedByTitle) cy.contains('.filter-section', 'Sort By').find('select').select('Artist') albumsSortedLike(albumsSortedByArtists) cy.contains('.filter-section', 'Sort By').find('select').select('Default') albumsSortedLike(albumsSortedById) })
it('searches for artists', function () { cy.contains('.filter-section', 'Search Artists') .find('input')
.type('John Coltrane') cy.get('.album-list-item') .should('have.length', 2) .find('h2') .should('contain', 'A Love Supreme') .and('contain', 'Blue Train') .parent('.album-list-item') .find('h3') .should('contain', 'John Coltrane') })
it('searches for artists', function () { cy.contains('.filter-section', 'Search Artists') .find('input')
.type('John Coltrane') cy.get('.album-list-item') .should('have.length', 2) .find('h2') .should('contain', 'A Love Supreme') .and('contain', 'Blue Train') .parent('.album-list-item') .find('h3') .should('contain', 'John Coltrane') })
it('searches for artists', function () { cy.contains('.filter-section', 'Search Artists') .find('input')
.type('John Coltrane') cy.get('.album-list-item') .should('have.length', 2) .find('h2') .should('contain', 'A Love Supreme') .and('contain', 'Blue Train') .parent('.album-list-item') .find('h3') .should('contain', 'John Coltrane') })
it('searches for artists', function () { cy.contains('.filter-section', 'Search Artists') .find('input')
.type('John Coltrane') cy.get('.album-list-item') .should('have.length', 2) .find('h2') .should('contain', 'A Love Supreme') .and('contain', 'Blue Train') .parent('.album-list-item') .find('h3') .should('contain', 'John Coltrane') })
it('searches for artists', function () { cy.contains('.filter-section', 'Search Artists') .find('input')
.type('John Coltrane') cy.get('.album-list-item') .should('have.length', 2) .find('h2') .should('contain', 'A Love Supreme') .and('contain', 'Blue Train') .parent('.album-list-item') .find('h3') .should('contain', 'John Coltrane') })
it('searches for artists', function () { cy.contains('.filter-section', 'Search Artists') .find('input')
.type('John Coltrane') cy.get('.album-list-item') .should('have.length', 2) .find('h2') .should('contain', 'A Love Supreme') .and('contain', 'Blue Train') .parent('.album-list-item') .find('h3') .should('contain', 'John Coltrane') })
it('searches for artists', function () { cy.contains('.filter-section', 'Search Artists') .find('input')
.type('John Coltrane') cy.get('.album-list-item') .should('have.length', 2) .find('h2') .should('contain', 'A Love Supreme') .and('contain', 'Blue Train') .parent('.album-list-item') .find('h3') .should('contain', 'John Coltrane') })
Markup or CSS changes?
const AlbumListItem = ({ album }) => ( <Link className="album-list-item"
to={`/albums/${album.id}`} > <img src={album.coverUrl} alt="" /> <RatingIcon rating={album.rating} /> <h2>{album.title}</h2> <h3>{album.artists.join(' - ')}</h3> </Link> )
const AlbumListItem = ({ album }) => ( <Link className="album"
to={`/albums/${album.id}`} > <img src={album.coverUrl} alt="" /> <RatingIcon rating={album.rating} /> <h2>{album.title}</h2> <h3>{album.artists.join(' - ')}</h3> </Link> )
const AlbumListItem = ({ album }) => ( <Link className="album"
to={`/albums/${album.id}`} > <img src={album.coverUrl} alt="" /> <RatingIcon rating={album.rating} /> <h3>{album.title}</h3> <h4>{album.artists.join(' - ')}</h4> </Link> )
None
const AlbumListItem = ({ album }) => ( <Link className="album-list-item"
to={`/albums/${album.id}`} > <img src={album.coverUrl} alt="" /> <RatingIcon rating={album.rating} /> <h2>{album.title}</h2> <h3>{album.artists.join(' - ')}</h3> </Link> )
const AlbumListItemSelectors = ({ album }) => ( <Link className="album-list-item"
to={`/albums/${album.id}`} data-test="album-list-item" > <img src={album.coverUrl} alt="" /> <RatingIcon rating={album.rating} /> <h2 data-test="title">{album.title}</h2> <h3 data-test="artists">{album.artists.join(' - ')}</h3> </Link> )
const AlbumListItemSelectors = ({ album }) => ( <Link className="album-list-item"
to={`/albums/${album.id}`} data-test="album-list-item" > <img src={album.coverUrl} alt="" /> <RatingIcon rating={album.rating} /> <h2 data-test="title">{album.title}</h2> <h3 data-test="artists">{album.artists.join(' - ')}</h3> </Link> )
it('albums can be viewed on home page', function () {
cy.get('[data-test="album-list-item"]') .should('have.length', albums.length) albums.forEach((album) => { cy.get('[data-test="title"]') .contains(album.title) .parent('[data-test="album-list-item"]') .should('exist') .find('[data-test="artists"]') .should('contain', album.artists.join(' - ')) }) })
it('albums can be viewed on home page', function () {
cy.get('[data-test="album-list-item"]') .should('have.length', albums.length) albums.forEach((album) => { cy.get('[data-test="title"]') .contains(album.title) .parent('[data-test="album-list-item"]') .should('exist') .find('[data-test="artists"]') .should('contain', album.artists.join(' - ')) }) })
it('albums can be viewed on home page', function () {
cy.get('[data-test="album-list-item"]') .should('have.length', albums.length) albums.forEach((album) => { cy.get('[data-test="title"]') .contains(album.title) .parent('[data-test="album-list-item"]') .should('exist') .find('[data-test="artists"]') .should('contain', album.artists.join(' - ')) }) })
it('albums can be viewed on home page', function () {
cy.get('[data-test="album-list-item"]') .should('have.length', albums.length) albums.forEach((album) => { cy.get('[data-test="title"]') .contains(album.title) .parent('[data-test="album-list-item"]') .should('exist') .find('[data-test="artists"]') .should('contain', album.artists.join(' - ')) }) })
it('albums can be viewed on home page', function () {
cy.get('[data-test="album-list-item"]') .should('have.length', albums.length) albums.forEach((album) => { cy.get('[data-test="title"]') .contains(album.title) .parent('[data-test="album-list-item"]') .should('exist') .find('[data-test="artists"]') .should('contain', album.artists.join(' - ')) }) })
it('albums can be viewed on home page', function () {
cy.get('[data-test="album-list-item"]') .should('have.length', albums.length) albums.forEach((album) => { cy.contains('[data-test="album-list-item"]', album.title) .should('contain', album.artists.join(' - ')) }) })
<select value={sorter} onChange={(e) => setSorter(e.target.value)} data-test="sort-by" > <option value={Sorter.Id}>Default</option> <option
value={Sorter.Title}>Title</option> <option value={Sorter.Artist}>Artist</option> </select>
cy.get('[data-test="sort-by"]').select('Title') albumsSortedLike(albumsSortedByTitle) cy.get('[data-test="sort-by"]').select('Artist') albumsSortedLike(albumsSortedByArtists) cy.get('[data-test="sort-by"]').select('Default') albumsSortedLike(albumsSortedById)
it('searches for artists', function () { cy.get('[data-test="search-artists"]') .type('John Coltrane') cy.get('[data-test="album-list-item"]')
.should('have.length', 2) .and('contain', 'A Love Supreme') .and('contain', 'Blue Train') .and('contain', 'John Coltrane') })
it('searches for artists', function () { cy.get('[data-test="search-artists"]') .type('John Coltrane') cy.get('[data-test="album-list-item"]')
.should('have.length', 2) .and('contain', 'A Love Supreme') .and('contain', 'Blue Train') .and('contain', 'John Coltrane') })
it('searches for artists', function () { cy.get('[data-test="search-artists"]') .type('John Coltrane') cy.get('[data-test="album-list-item"]')
.should('have.length', 2) .and('contain', 'A Love Supreme') .and('contain', 'Blue Train') .and('contain', 'John Coltrane') })
it('albums can be viewed on home page', function () {
cy.get('[data-test="album-list-item"]') .should('have.length', albums.length) albums.forEach((album) => { cy.get('[data-test="title"]') .contains(album.title) .parent('[data-test="album-list-item"]') .should('exist') .find('[data-test="artists"]') .should('contain', album.artists.join(' - ')) }) })
it('albums can be viewed on home page', function () {
cy.get('[data-test="album-list-item"]') .should('have.length', albums.length) albums.forEach((album) => { cy.get('[data-test="title"]') .contains(album.title) .parent('[data-test="album-list-item"]') .should('exist') .find('[data-test="artists"]') .should('contain', album.artists.join(' - ')) }) })
Page Objects
// cypress/support/pages/home.js export const albums = () => cy.get('[data-test="album-list-item"]') export
const album = (title) => cy.contains('[data-test="album-list-item"]', title) export const sortBy = () => cy.get('[data-test="sort-by"]') export const searchArtists = () => cy.get('[data-test="search-artists"]')
// cypress/support/pages/home.js export const albums = () => cy.get('[data-test="album-list-item"]') export
const album = (title) => cy.contains('[data-test="album-list-item"]', title) export const sortBy = () => cy.get('[data-test="sort-by"]') export const searchArtists = () => cy.get('[data-test="search-artists"]')
// cypress/support/pages/home.js export const albums = () => cy.get('[data-test="album-list-item"]') export
const album = (title) => cy.contains('[data-test="album-list-item"]', title) export const sortBy = () => cy.get('[data-test="sort-by"]') export const searchArtists = () => cy.get('[data-test="search-artists"]')
import * as homePage from '../support/pages/home' it('albums can be viewed
on home page', function () { homePage .albums() .should('have.length', albums.length) albums.forEach((album) => { homePage .album(album.title) .should('exist') .and('contain', album.artists.join(' - ')) }) })
import * as homePage from '../support/pages/home' it('albums can be viewed
on home page', function () { homePage .albums() .should('have.length', albums.length) albums.forEach((album) => { homePage .album(album.title) .should('exist') .and('contain', album.artists.join(' - ')) }) })
import * as homePage from '../support/pages/home' it('albums can be viewed
on home page', function () { homePage .albums() .should('have.length', albums.length) albums.forEach((album) => { homePage .album(album.title) .should('exist') .and('contain', album.artists.join(' - ')) }) })
import * as homePage from '../support/pages/home' it('albums can be viewed
on home page', function () { homePage .albums() .should('have.length', albums.length) albums.forEach((album) => { homePage .album(album.title) .should('exist') .and('contain', album.artists.join(' - ')) }) })
// cypress/support/pages/home.js export const albums = () => cy.get('[data-test="album-list-item"]') export
const album = (title) => cy.contains('[data-test="album-list-item"]', title) export const sortBy = () => cy.get('[data-test="sort-by"]') export const searchArtists = () => cy.get('[data-test="search-artists"]')
// cypress/support/pages/home.js export const albums = () => cy.get('[data-test="album-list-item"]') export
const album = (title) => cy.contains('[data-test="album-list-item"]', title) export const sortBy = () => cy.get('[data-test="sort-by"]') export const searchArtists = () => cy.get('[data-test="search-artists"]')
// cypress/support/pages/home.js export const albums = () => cy.get('[data-test="album-list-item"]') export
const album = (title) => cy.contains('[data-test="album-list-item"]', title) export const sortBy = () => cy.get('[data-test="sort-by"]') export const searchArtists = () => cy.get('[data-test="search-artists"]')
it('sorts albums', function () { function albumsSortedLike(albumList) { albumList.forEach((album, index)
=> { homePage.albums().eq(index).should('contain', album.title) }) } homePage.sortBy().select('Title') albumsSortedLike(albumsSortedByTitle) homePage.sortBy().select('Artist') albumsSortedLike(albumsSortedByArtists) homePage.sortBy().select('Default') albumsSortedLike(albumsSortedById) })
it('sorts albums', function () { function albumsSortedLike(albumList) { albumList.forEach((album, index)
=> { homePage.albums().eq(index).should('contain', album.title) }) } homePage.sortBy().select('Title') albumsSortedLike(albumsSortedByTitle) homePage.sortBy().select('Artist') albumsSortedLike(albumsSortedByArtists) homePage.sortBy().select('Default') albumsSortedLike(albumsSortedById) })
it('sorts albums', function () { function albumsSortedLike(albumList) { albumList.forEach((album, index)
=> { homePage.albums().eq(index).should('contain', album.title) }) } homePage.sortBy().select('Title') albumsSortedLike(albumsSortedByTitle) homePage.sortBy().select('Artist') albumsSortedLike(albumsSortedByArtists) homePage.sortBy().select('Default') albumsSortedLike(albumsSortedById) })
it('searches for artists', function () { homePage.searchArtists().type('John Coltrane') homePage .albums()
.should('have.length', 2) .and('contain', 'A Love Supreme') .and('contain', 'Blue Train') .and('contain', 'John Coltrane') })
it('searches for artists', function () { homePage.searchArtists().type('John Coltrane') homePage .albums()
.should('have.length', 2) .and('contain', 'A Love Supreme') .and('contain', 'Blue Train') .and('contain', 'John Coltrane') })
it('searches for artists', function () { homePage.searchArtists().type('John Coltrane') homePage .albums()
.should('have.length', 2) .and('contain', 'A Love Supreme') .and('contain', 'Blue Train') .and('contain', 'John Coltrane') })
Repetition
export const albums = () => cy.get('[data-test="album-list-item"]') export const album
= (title) => cy.contains('[data-test="album-list-item"]', title) export const sortBy = () => cy.get('[data-test="sort-by"]') export const searchArtists = () => cy.get('[data-test="search-artists"]')
export const albums = () => cy.get('[data-test="album-list-item"]') export const album
= (title) => cy.contains('[data-test="album-list-ite"]', title) export const sortBy = () => cy.get('[data-test=sort-by"]') export const searchArtists = () => cy.get('[data-test="search-artists"')
export const albums = () => cy.get('[data-test="album-list-item"]') export const album
= (title) => cy.contains('[data-test="album-list-ite"]', title) export const sortBy = () => cy.get('[data-test=sort-by"]') export const searchArtists = () => cy.get('[data-test="search-artists"')
export const albums = () => cy.get('[data-test="album-list-item"]') export const album
= (title) => cy.contains('[data-test="album-list-ite"]', title) export const sortBy = () => cy.get('[data-test=sort-by"]') export const searchArtists = () => cy.get('[data-test="search-artists"')
Commands
// cypress/support/commands.js Cypress.Commands.add( 'getByDataTest', (key) => cy.get(`[data-test="${key}"]`) ) // cypress/support/index.js
import './commands'
// cypress/support/commands.js Cypress.Commands.add( 'getByDataTest', (key) => cy.get(`[data-test="${key}"]`) ) // cypress/support/index.js
import './commands'
// cypress/support/commands.js Cypress.Commands.add( 'getByDataTest', (key) => cy.get(`[data-test="${key}"]`) ) // cypress/support/index.js
import './commands'
// cypress/support/commands.js Cypress.Commands.add( 'getByDataTest', (key) => cy.get(`[data-test="${key}"]`) ) // cypress/support/index.js
import './commands'
// cypress/support/commands.js Cypress.Commands.add( 'getByDataTest', (key) => cy.get(`[data-test="${key}"]`) ) // cypress/support/index.js
import './commands'
// cypress/support/commands.js Cypress.Commands.add( 'getByDataTest', (key) => cy.get(`[data-test="${key}"]`) ) // cypress/support/index.js
import './commands'
export const albums = () => cy.get('[data-test="album-list-item"]') export const album
= (title) => cy.contains('[data-test="album-list-item"]', title) export const sortBy = () => cy.get('[data-test="sort-by"]') export const searchArtists = () => cy.get('[data-test="search-artists"]')
export const albums = () => cy.getByDataTest('album-list-item') export const album
= (title) => cy.contains('[data-test="album-list-item"]', title) export const sortBy = () => cy.getByDataTest('sort-by') export const searchArtists = () => cy.getByDataTest('search-artists')
export const albums = () => cy.getByDataTest('album-list-item') export const album
= (title) => cy.contains('[data-test="album-list-item"]', title) export const sortBy = () => cy.getByDataTest('sort-by') export const searchArtists = () => cy.getByDataTest('search-artists')
Cypress.Commands.add( 'containsByDataTest', (key, content) => cy.contains(`[data-test="${key}"]`, content) )
Cypress.Commands.add( 'containsByDataTest', (key, content) => cy.contains(`[data-test="${key}"]`, content) )
export const album = (title) => cy.containsByDataTest('album-list-item', title)
API Timeouts
describe('Album page', function () { // ... it('can be viewed',
function () { albumPage.title().should('contain', this.album.title) albumPage.artists().should('contain', this.album.artists) }) // ... })
describe('Album page', function () { // ... it('can be viewed',
function () { albumPage.title().should('contain', this.album.title) albumPage.artists().should('contain', this.album.artists) }) // ... })
Wait
it('can be viewed', function () { cy.wait(6000) albumPage.title().should('contain', this.album.title) albumPage.artists().should('contain',
this.album.artists) })
it('can be viewed', function () { cy.wait(6000) albumPage.title().should('contain', this.album.title) albumPage.artists().should('contain',
this.album.artists) })
it('can be viewed', function () { cy.wait(6000) albumPage.title().should('contain', this.album.title) albumPage.artists().should('contain',
this.album.artists) })
beforeEach(function () { cy.server() cy.route(buildApiUrl('/albums/*')).as('getAlbum') // ... }) Routes and
Aliases
beforeEach(function () { cy.server() cy.route(buildApiUrl('/albums/*')).as('getAlbum') // ... }) Routes and
Aliases
beforeEach(function () { cy.server() cy.route(buildApiUrl('/albums/*')).as('getAlbum') // ... }) Routes and
Aliases
beforeEach(function () { cy.server() cy.route(buildApiUrl('/albums/*')).as('getAlbum') // ... }) Routes and
Aliases
beforeEach(function () { cy.server() cy.route(buildApiUrl('/albums/*')).as('getAlbum') // ... }) Routes and
Aliases
it('can be viewed', function () { cy.wait('@getAlbum') albumPage.title().should('contain', this.album.title) albumPage.artists().should('contain',
this.album.artists) })
Api Testing
describe('REST API: albums', function () { it('can fetch all albums',
function () { cy.request({ url: buildApiUrl('/albums'), headers: { 'Content-Type': 'application/json', }, auth: { bearer: this.token }, }) .its('body') .should('have.length', this.albums.length) .each((album) => { expect(this.albumTitles).to.include(album.title) }) }) })
describe('REST API: albums', function () { it('can fetch all albums',
function () { cy.request({ url: buildApiUrl('/albums'), headers: { 'Content-Type': 'application/json', }, auth: { bearer: this.token }, }) .its('body') .should('have.length', this.albums.length) .each((album) => { expect(this.albumTitles).to.include(album.title) }) }) })
describe('REST API: albums', function () { it('can fetch all albums',
function () { cy.request({ url: buildApiUrl('/albums'), headers: { 'Content-Type': 'application/json', }, auth: { bearer: this.token }, }) .its('body') .should('have.length', this.albums.length) .each((album) => { expect(this.albumTitles).to.include(album.title) }) }) })
describe('REST API: albums', function () { it('can fetch all albums',
function () { cy.request({ url: buildApiUrl('/albums'), headers: { 'Content-Type': 'application/json', }, auth: { bearer: this.token }, }) .its('body') .should('have.length', this.albums.length) .each((album) => { expect(this.albumTitles).to.include(album.title) }) }) })
describe('REST API: albums', function () { it('can fetch all albums',
function () { cy.request({ url: buildApiUrl('/albums'), headers: { 'Content-Type': 'application/json', }, auth: { bearer: this.token }, }) .its('body') .should('have.length', this.albums.length) .each((album) => { expect(this.albumTitles).to.include(album.title) }) }) })
describe('REST API: albums', function () { it('can fetch all albums',
function () { cy.request({ url: buildApiUrl('/albums'), headers: { 'Content-Type': 'application/json', }, auth: { bearer: this.token }, }) .its('body') .should('have.length', this.albums.length) .each((album) => { expect(this.albumTitles).to.include(album.title) }) }) })
describe('REST API: albums', function () { it('can fetch all albums',
function () { cy.request({ url: buildApiUrl('/albums'), headers: { 'Content-Type': 'application/json', }, auth: { bearer: this.token }, }) .its('body') .should('have.length', this.albums.length) .each((album) => { expect(this.albumTitles).to.include(album.title) }) }) })
describe('REST API: albums', function () { it('can fetch all albums',
function () { cy.request({ url: buildApiUrl('/albums'), headers: { 'Content-Type': 'application/json', }, auth: { bearer: this.token }, }) .its('body') .should('have.length', this.albums.length) .each((album) => { expect(this.albumTitles).to.include(album.title) }) }) })
it('can fetch an album', function () { const album =
this.albums[0] cy.request({ url: buildApiUrl(`/albums/${album.id}`), headers: { 'Content-Type': 'application/json', }, auth: { bearer: this.token }, }) .its('body') .should('containSubset', { id: album.id, title: album.title, artists: album.artists, rating: 'NOT_RATED', userReviews: [], }) })
it('can fetch an album', function () { const album =
this.albums[0] cy.request({ url: buildApiUrl(`/albums/${album.id}`), headers: { 'Content-Type': 'application/json', }, auth: { bearer: this.token }, }) .its('body') .should('containSubset', { id: album.id, title: album.title, artists: album.artists, rating: 'NOT_RATED', userReviews: [], }) })
it('can fetch an album', function () { const album =
this.albums[0] cy.request({ url: buildApiUrl(`/albums/${album.id}`), headers: { 'Content-Type': 'application/json', }, auth: { bearer: this.token }, }) .its('body') .should('containSubset', { // chai-subset plugin id: album.id, title: album.title, artists: album.artists, rating: 'NOT_RATED', userReviews: [], }) })
it('can fetch an album', function () { const album =
this.albums[0] cy.request({ url: buildApiUrl(`/albums/${album.id}`), headers: { 'Content-Type': 'application/json', }, auth: { bearer: this.token }, }) .its('body') .should('containSubset', { id: album.id, title: album.title, artists: album.artists, rating: 'NOT_RATED', userReviews: [], }) })
it('can fetch an album', function () { const album =
this.albums[0] cy.request({ url: buildApiUrl(`/albums/${album.id}`), headers: { 'Content-Type': 'application/json', }, auth: { bearer: this.token }, }) .its('body') .should('containSubset', { id: album.id, title: album.title, artists: album.artists, rating: 'NOT_RATED', userReviews: [], }) })
Cypress.Commands.add('apiRequest', ({ body, url, ...options }) => { const token
= localStorage.getItem('jwt') return cy.request({ ...options, failOnStatusCode: false, url: buildApiUrl(url), headers: { 'Content-Type': 'application/json', }, auth: { bearer: token }, ...(body && { body: JSON.stringify(body) }), }) })
Cypress.Commands.add('apiRequest', ({ body, url, ...options }) => { const token
= localStorage.getItem('jwt') return cy.request({ ...options, failOnStatusCode: false, url: buildApiUrl(url), headers: { 'Content-Type': 'application/json', }, auth: { bearer: token }, ...(body && { body: JSON.stringify(body) }), }) })
Cypress.Commands.add('apiRequest', ({ body, url, ...options }) => { const token
= localStorage.getItem('jwt') return cy.request({ ...options, failOnStatusCode: false, url: buildApiUrl(url), headers: { 'Content-Type': 'application/json', }, auth: { bearer: token }, ...(body && { body: JSON.stringify(body) }), }) })
Cypress.Commands.add('apiRequest', ({ body, url, ...options }) => { const token
= localStorage.getItem('jwt') return cy.request({ ...options, failOnStatusCode: false, url: buildApiUrl(url), headers: { 'Content-Type': 'application/json', }, auth: { bearer: token }, ...(body && { body: JSON.stringify(body) }), }) })
Cypress.Commands.add('apiRequest', ({ body, url, ...options }) => { const token
= localStorage.getItem('jwt') return cy.request({ ...options, failOnStatusCode: false, url: buildApiUrl(url), headers: { 'Content-Type': 'application/json', }, auth: { bearer: token }, ...(body && { body: JSON.stringify(body) }), }) })
Cypress.Commands.add('apiRequest', ({ body, url, ...options }) => { const token
= localStorage.getItem('jwt') return cy.request({ ...options, failOnStatusCode: false, url: buildApiUrl(url), headers: { 'Content-Type': 'application/json', }, auth: { bearer: token }, ...(body && { body: JSON.stringify(body) }), }) })
Cypress.Commands.add('apiRequest', ({ body, url, ...options }) => { const token
= localStorage.getItem('jwt') return cy.request({ ...options, failOnStatusCode: false, url: buildApiUrl(url), headers: { 'Content-Type': 'application/json', }, auth: { bearer: token }, ...(body && { body: JSON.stringify(body) }), }) })
Cypress.Commands.add('apiRequest', ({ body, url, ...options }) => { const token
= localStorage.getItem('jwt') return cy.request({ ...options, failOnStatusCode: false, url: buildApiUrl(url), headers: { 'Content-Type': 'application/json', }, auth: { bearer: token }, ...(body && { body: JSON.stringify(body) }), }) })
Cypress.Commands.add('apiRequest', ({ body, url, ...options }) => { const token
= localStorage.getItem('jwt') return cy.request({ ...options, failOnStatusCode: false, url: buildApiUrl(url), headers: { 'Content-Type': 'application/json', }, auth: { bearer: token }, ...(body && { body: JSON.stringify(body) }), }) })
Cypress.Commands.add('apiRequest', ({ body, url, ...options }) => { const token
= localStorage.getItem('jwt') return cy.request({ ...options, failOnStatusCode: false, url: buildApiUrl(url), headers: { 'Content-Type': 'application/json', }, auth: { bearer: token }, ...(body && { body: JSON.stringify(body) }), }) })
Cypress.Commands.add('apiGet', (url) => cy.apiRequest({ url, method: 'GET' }) ) Cypress.Commands.add('apiPost',
(url, body = null) => cy.apiRequest({ body, url, method: 'POST' }) )
Cypress.Commands.add('apiGet', (url) => cy.apiRequest({ url, method: 'GET' }) ) Cypress.Commands.add('apiPost',
(url, body = null) => cy.apiRequest({ body, url, method: 'POST' }) )
Cypress.Commands.add('apiGet', (url) => cy.apiRequest({ url, method: 'GET' }) ) Cypress.Commands.add('apiPost',
(url, body = null) => cy.apiRequest({ body, url, method: 'POST' }) )
it('can fetch all albums', function () { cy.apiGet('/albums') .its('body') .should('have.length',
this.albums.length) .each((album) => { expect(this.albumTitles).to.include(album.title) }) })
it('can review an album and remove the review', function ()
{ const album = this.albums[0] const review = 'Great album!' const expectedReview = { review, user: { name: 'Emmett Brown', email: '
[email protected]
' }, } cy.apiPost(`/albums/${album.id}/review`, { review }) .its('body') .should('deep.equal', expectedReview) cy.apiGet(`/albums/${album.id}`) .its('body.userReviews') .should('deep.equal', [expectedReview]) cy.apiPost(`/albums/${album.id}/remove-review`) .its('body') .should('deep.equal', { status: 'Removed review' }) cy.apiGet(`/albums/${album.id}`) .its('body.userReviews') .should('deep.equal', []) })
it('can review an album and remove the review', function ()
{ const album = this.albums[0] const review = 'Great album!' const expectedReview = { review, user: { name: 'Emmett Brown', email: '
[email protected]
' }, } cy.apiPost(`/albums/${album.id}/review`, { review }) .its('body') .should('deep.equal', expectedReview) cy.apiGet(`/albums/${album.id}`) .its('body.userReviews') .should('deep.equal', [expectedReview]) cy.apiPost(`/albums/${album.id}/remove-review`) .its('body') .should('deep.equal', { status: 'Removed review' }) cy.apiGet(`/albums/${album.id}`) .its('body.userReviews') .should('deep.equal', []) })
it('can review an album and remove the review', function ()
{ const album = this.albums[0] const review = 'Great album!' const expectedReview = { review, user: { name: 'Emmett Brown', email: '
[email protected]
' }, } cy.apiPost(`/albums/${album.id}/review`, { review }) .its('body') .should('deep.equal', expectedReview) cy.apiGet(`/albums/${album.id}`) .its('body.userReviews') .should('deep.equal', [expectedReview]) cy.apiPost(`/albums/${album.id}/remove-review`) .its('body') .should('deep.equal', { status: 'Removed review' }) cy.apiGet(`/albums/${album.id}`) .its('body.userReviews') .should('deep.equal', []) })
it('can review an album and remove the review', function ()
{ const album = this.albums[0] const review = 'Great album!' const expectedReview = { review, user: { name: 'Emmett Brown', email: '
[email protected]
' }, } cy.apiPost(`/albums/${album.id}/review`, { review }) .its('body') .should('deep.equal', expectedReview) cy.apiGet(`/albums/${album.id}`) .its('body.userReviews') .should('deep.equal', [expectedReview]) cy.apiPost(`/albums/${album.id}/remove-review`) .its('body') .should('deep.equal', { status: 'Removed review' }) cy.apiGet(`/albums/${album.id}`) .its('body.userReviews') .should('deep.equal', []) })
it('can review an album and remove the review', function ()
{ const album = this.albums[0] const review = 'Great album!' const expectedReview = { review, user: { name: 'Emmett Brown', email: '
[email protected]
' }, } cy.apiPost(`/albums/${album.id}/review`, { review }) .its('body') .should('deep.equal', expectedReview) cy.apiGet(`/albums/${album.id}`) .its('body.userReviews') .should('deep.equal', [expectedReview]) cy.apiPost(`/albums/${album.id}/remove-review`) .its('body') .should('deep.equal', { status: 'Removed review' }) cy.apiGet(`/albums/${album.id}`) .its('body.userReviews') .should('deep.equal', []) })
GraphQL
Cypress.Commands.add('graphQL', (query, variables = {}) => cy.apiPost('/graphql', { query, variables
}) )
Cypress.Commands.add('graphQL', (query, variables = {}) => cy.apiPost('/graphql', { query, variables
}) )
Cypress.Commands.add('graphQL', (query, variables = {}) => cy.apiPost('/graphql', { query, variables
}) )
it('can fetch an album', function () { const album =
this.albums[0] const query = ` query Album($id: ID!) { album(id: $id) { title artists rating userReviews { review } } } ` cy.graphQL(query, { id: album.id }) .its('body.data.album') .should('deep.equal', { title: album.title, artists: album.artists, rating: 'NOT_RATED', userReviews: [], }) })
it('can fetch an album', function () { const album =
this.albums[0] const query = ` query Album($id: ID!) { album(id: $id) { title artists rating userReviews { review } } } ` cy.graphQL(query, { id: album.id }) .its('body.data.album') .should('deep.equal', { title: album.title, artists: album.artists, rating: 'NOT_RATED', userReviews: [], }) })
it('can fetch an album', function () { const album =
this.albums[0] const query = ` query Album($id: ID!) { album(id: $id) { title artists rating userReviews { review } } } ` cy.graphQL(query, { id: album.id }) .its('body.data.album') .should('deep.equal', { title: album.title, artists: album.artists, rating: 'NOT_RATED', userReviews: [], }) })
it('can fetch an album', function () { const album =
this.albums[0] const query = ` query Album($id: ID!) { album(id: $id) { title artists rating userReviews { review } } } ` cy.graphQL(query, { id: album.id }) .its('body.data.album') .should('deep.equal', { title: album.title, artists: album.artists, rating: 'NOT_RATED', userReviews: [], }) })
it('can fetch an album', function () { const album =
this.albums[0] const query = ` query Album($id: ID!) { album(id: $id) { title artists rating userReviews { review } } } ` cy.graphQL(query, { id: album.id }) .its('body.data.album') .should('deep.equal', { title: album.title, artists: album.artists, rating: 'NOT_RATED', userReviews: [], }) })
Authentication?
Cypress.Commands.add('login', (email, password) => { cy.visit('/login') loginPage.email().type(email) loginPage.password().type(password) loginPage.logIn().click() })
Cypress.Commands.add('login', (email, password) => { cy.visit('/login') loginPage.email().type(email) loginPage.password().type(password) loginPage.logIn().click() })
Cypress.Commands.add( 'login', ( email = Cypress.env('USER_EMAIL'), password = Cypress.env('USER_PASSWORD') )
=> cy .request('POST', buildApiUrl('/login'), { email, password }) .its('body.token') .then((token) => { localStorage.setItem('jwt', token) }) )
Cypress.Commands.add( 'login', ( email = Cypress.env('USER_EMAIL'), password = Cypress.env('USER_PASSWORD') )
=> cy .request('POST', buildApiUrl('/login'), { email, password }) .its('body.token') .then((token) => { localStorage.setItem('jwt', token) }) )
Cypress.Commands.add( 'login', ( email = Cypress.env('USER_EMAIL'), password = Cypress.env('USER_PASSWORD') )
=> cy .request('POST', buildApiUrl('/login'), { email, password }) .its('body.token') .then((token) => { localStorage.setItem('jwt', token) }) )
Cypress.Commands.add( 'login', ( email = Cypress.env('USER_EMAIL'), password = Cypress.env('USER_PASSWORD') )
=> cy .request('POST', buildApiUrl('/login'), { email, password }) .its('body.token') .then((token) => { localStorage.setItem('jwt', token) }) )
Cypress.Commands.add( 'login', ( email = Cypress.env('USER_EMAIL'), password = Cypress.env('USER_PASSWORD') )
=> cy .request('POST', buildApiUrl('/login'), { email, password }) .its('body.token') .then((token) => { localStorage.setItem('jwt', token) }) )
beforeEach(function () { cy.login() })
Selector Syndrome
Selector Syndrome data-test Attributes and Page Objects
Repetitive Code
Repetitive Code Custom Commands
Timeouts/Arbitrary Waiting
Timeouts/Arbitrary Waiting Routes and Aliases
Untested APIs
Untested APIs cy.request and Commands
Authenticating via UI
Authenticating via UI API, localStorage/cookies, commands, and beforeEach
Thanks! Thanks! Jeremy Fairbank @elpapapollo Slides: bit.ly/ct-act Repo: bit.ly/act-repo