Pro Yearly is on sale from $80 to $50! »

Connect.Tech 2020: Advanced Cypress Testing

Connect.Tech 2020: Advanced Cypress Testing

94bd558238b69c45d3d3e15797ae94f7?s=128

Jeremy Fairbank

September 28, 2020
Tweet

Transcript

  1. Testing Advanced Cypress Testing Jeremy Fairbank @elpapapollo

  2. @testdouble helps improves how the world build software. testdouble.com

  3. Available in print or e-book programming-elm.com

  4. Testing anti-patterns

  5. Assumptions

  6. Assumptions

  7. Assumptions End-to-End

  8. Assumptions End-to-End Real API

  9. Assumptions End-to-End Real API Test Data

  10. The App bit.ly/act-repo

  11. Selector Syndrome

  12. 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(' - ')) }) }) })
  13. 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(' - ')) }) }) })
  14. 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(' - ')) }) }) })
  15. 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(' - ')) }) }) })
  16. 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(' - ')) }) }) })
  17. 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(' - ')) }) }) })
  18. 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(' - ')) }) }) })
  19. 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) })
  20. 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) })
  21. 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) })
  22. 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) })
  23. 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) })
  24. 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) })
  25. 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) })
  26. 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') })
  27. 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') })
  28. 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') })
  29. 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') })
  30. 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') })
  31. 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') })
  32. 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') })
  33. Markup or CSS changes?

  34. 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> )
  35. 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> )
  36. 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> )
  37. None
  38. 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> )
  39. 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> )
  40. 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> )
  41. 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(' - ')) }) })
  42. 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(' - ')) }) })
  43. 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(' - ')) }) })
  44. 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(' - ')) }) })
  45. 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(' - ')) }) })
  46. 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(' - ')) }) })
  47. <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>
  48. 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)

  49. 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') })
  50. 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') })
  51. 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') })
  52. 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(' - ')) }) })
  53. 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(' - ')) }) })
  54. Page Objects

  55. // 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"]')
  56. // 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"]')
  57. // 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"]')
  58. 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(' - ')) }) })
  59. 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(' - ')) }) })
  60. 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(' - ')) }) })
  61. 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(' - ')) }) })
  62. // 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"]')
  63. // 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"]')
  64. // 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"]')
  65. 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) })
  66. 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) })
  67. 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) })
  68. 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') })
  69. 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') })
  70. 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') })
  71. Repetition

  72. 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"]')
  73. 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"')
  74. 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"')
  75. 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"')
  76. Commands

  77. // cypress/support/commands.js Cypress.Commands.add( 'getByDataTest', (key) => cy.get(`[data-test="${key}"]`) ) // cypress/support/index.js

    import './commands'
  78. // cypress/support/commands.js Cypress.Commands.add( 'getByDataTest', (key) => cy.get(`[data-test="${key}"]`) ) // cypress/support/index.js

    import './commands'
  79. // cypress/support/commands.js Cypress.Commands.add( 'getByDataTest', (key) => cy.get(`[data-test="${key}"]`) ) // cypress/support/index.js

    import './commands'
  80. // cypress/support/commands.js Cypress.Commands.add( 'getByDataTest', (key) => cy.get(`[data-test="${key}"]`) ) // cypress/support/index.js

    import './commands'
  81. // cypress/support/commands.js Cypress.Commands.add( 'getByDataTest', (key) => cy.get(`[data-test="${key}"]`) ) // cypress/support/index.js

    import './commands'
  82. // cypress/support/commands.js Cypress.Commands.add( 'getByDataTest', (key) => cy.get(`[data-test="${key}"]`) ) // cypress/support/index.js

    import './commands'
  83. 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"]')
  84. 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')
  85. 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')
  86. Cypress.Commands.add( 'containsByDataTest', (key, content) => cy.contains(`[data-test="${key}"]`, content) )

  87. Cypress.Commands.add( 'containsByDataTest', (key, content) => cy.contains(`[data-test="${key}"]`, content) )

  88. export const album = (title) => cy.containsByDataTest('album-list-item', title)

  89. API Timeouts

  90. describe('Album page', function () { // ... it('can be viewed',

    function () { albumPage.title().should('contain', this.album.title) albumPage.artists().should('contain', this.album.artists) }) // ... })
  91. describe('Album page', function () { // ... it('can be viewed',

    function () { albumPage.title().should('contain', this.album.title) albumPage.artists().should('contain', this.album.artists) }) // ... })
  92. Wait

  93. it('can be viewed', function () { cy.wait(6000) albumPage.title().should('contain', this.album.title) albumPage.artists().should('contain',

    this.album.artists) })
  94. it('can be viewed', function () { cy.wait(6000) albumPage.title().should('contain', this.album.title) albumPage.artists().should('contain',

    this.album.artists) })
  95. it('can be viewed', function () { cy.wait(6000) albumPage.title().should('contain', this.album.title) albumPage.artists().should('contain',

    this.album.artists) })
  96. beforeEach(function () { cy.server() cy.route(buildApiUrl('/albums/*')).as('getAlbum') // ... }) Routes and

    Aliases
  97. beforeEach(function () { cy.server() cy.route(buildApiUrl('/albums/*')).as('getAlbum') // ... }) Routes and

    Aliases
  98. beforeEach(function () { cy.server() cy.route(buildApiUrl('/albums/*')).as('getAlbum') // ... }) Routes and

    Aliases
  99. beforeEach(function () { cy.server() cy.route(buildApiUrl('/albums/*')).as('getAlbum') // ... }) Routes and

    Aliases
  100. beforeEach(function () { cy.server() cy.route(buildApiUrl('/albums/*')).as('getAlbum') // ... }) Routes and

    Aliases
  101. it('can be viewed', function () { cy.wait('@getAlbum') albumPage.title().should('contain', this.album.title) albumPage.artists().should('contain',

    this.album.artists) })
  102. Api Testing

  103. 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) }) }) })
  104. 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) }) }) })
  105. 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) }) }) })
  106. 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) }) }) })
  107. 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) }) }) })
  108. 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) }) }) })
  109. 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) }) }) })
  110. 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) }) }) })
  111. 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: [], }) })
  112. 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: [], }) })
  113. 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: [], }) })
  114. 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: [], }) })
  115. 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: [], }) })
  116. 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) }), }) })
  117. 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) }), }) })
  118. 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) }), }) })
  119. 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) }), }) })
  120. 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) }), }) })
  121. 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) }), }) })
  122. 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) }), }) })
  123. 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) }), }) })
  124. 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) }), }) })
  125. 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) }), }) })
  126. Cypress.Commands.add('apiGet', (url) => cy.apiRequest({ url, method: 'GET' }) ) Cypress.Commands.add('apiPost',

    (url, body = null) => cy.apiRequest({ body, url, method: 'POST' }) )
  127. Cypress.Commands.add('apiGet', (url) => cy.apiRequest({ url, method: 'GET' }) ) Cypress.Commands.add('apiPost',

    (url, body = null) => cy.apiRequest({ body, url, method: 'POST' }) )
  128. Cypress.Commands.add('apiGet', (url) => cy.apiRequest({ url, method: 'GET' }) ) Cypress.Commands.add('apiPost',

    (url, body = null) => cy.apiRequest({ body, url, method: 'POST' }) )
  129. 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) }) })
  130. 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: 'ebrown@jigowatts.com' }, } 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', []) })
  131. 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: 'ebrown@jigowatts.com' }, } 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', []) })
  132. 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: 'ebrown@jigowatts.com' }, } 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', []) })
  133. 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: 'ebrown@jigowatts.com' }, } 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', []) })
  134. 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: 'ebrown@jigowatts.com' }, } 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', []) })
  135. GraphQL

  136. Cypress.Commands.add('graphQL', (query, variables = {}) => cy.apiPost('/graphql', { query, variables

    }) )
  137. Cypress.Commands.add('graphQL', (query, variables = {}) => cy.apiPost('/graphql', { query, variables

    }) )
  138. Cypress.Commands.add('graphQL', (query, variables = {}) => cy.apiPost('/graphql', { query, variables

    }) )
  139. 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: [], }) })
  140. 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: [], }) })
  141. 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: [], }) })
  142. 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: [], }) })
  143. 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: [], }) })
  144. Authentication?

  145. Cypress.Commands.add('login', (email, password) => { cy.visit('/login') loginPage.email().type(email) loginPage.password().type(password) loginPage.logIn().click() })

  146. Cypress.Commands.add('login', (email, password) => { cy.visit('/login') loginPage.email().type(email) loginPage.password().type(password) loginPage.logIn().click() })

  147. 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) }) )
  148. 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) }) )
  149. 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) }) )
  150. 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) }) )
  151. 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) }) )
  152. beforeEach(function () { cy.login() })

  153. Selector Syndrome

  154. Selector Syndrome data-test Attributes and Page Objects

  155. Repetitive Code

  156. Repetitive Code Custom Commands

  157. Timeouts/Arbitrary Waiting

  158. Timeouts/Arbitrary Waiting Routes and Aliases

  159. Untested APIs

  160. Untested APIs cy.request and Commands

  161. Authenticating via UI

  162. Authenticating via UI API, localStorage/cookies, commands, and beforeEach

  163. Thanks! Thanks! Jeremy Fairbank @elpapapollo Slides: bit.ly/ct-act Repo: bit.ly/act-repo