Learn Cypress React component testing by playing Sudoku

Gleb Bahmutov

Sr Director of Engineering

ConFoo.Ca 2023

What Is MY Plan?

Climate Crisis Is Here.

survival is possible* but we need to act now

Speaker: Gleb Bahmutov PhD

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

Learn how to test the web applications

using my courses at

https://cypress.tips/courses

Gleb Bahmutov

Sr Director of Engineering

Let's test

the game

modes

npm start

npx cypress open

npm i -D cypress

const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
  },

  component: {
    ...
  },
})

cypress.config.js

it('changes the number of filled cells', () => {
  cy.visit('/')
  cy.get('select[name=status__difficulty-select]').should(
    'have.value',
    'Easy',
  )
  cy.get('.game__cell--filled').should('have.length', 45)

  cy.get('select[name=status__difficulty-select]').select(
    'Medium',
  )
  cy.get('.game__cell--filled').should('have.length', 40)

  cy.get('select[name=status__difficulty-select]').select(
    'Hard',
  )
  cy.get('.game__cell--filled').should('have.length', 30)
})

cypress/e2e/modes.cy.js

The game modes test

See

the game

like a player user

Can we

test

the game

play?

It is hard

to control

the random

game board

😢

We can

use the

Hint

button

it('fills each empty cell using Hint', () => {
  cy.visit('/')
  cy.get('.game__cell.game__cell--filled').should(
    'have.length',
    45,
  )
  cy.get('.game__cell')
    .not('.game__cell--filled')
    .each(($cell) => {
      cy.wrap($cell, { log: false }).click()
      cy.get('.status__action-hint').click()
    })

  cy.contains('.overlay__text', 'You solved it').should(
    'be.visible',
  )
})

cypress/e2e/hint.cy.js

it('fills each empty cell using Hint', () => {
  cy.visit('/')
  cy.get('.game__cell.game__cell--filled').should(
    'have.length',
    45,
  )
  cy.get('.game__cell')
    .not('.game__cell--filled')
    .each(($cell) => {
      cy.wrap($cell, { log: false }).click()
      cy.get('.status__action-hint').click()
    })

  cy.contains('.overlay__text', 'You solved it').should(
    'be.visible',
  )
})

cypress/e2e/hint.cy.js

Click on the cell

it('fills each empty cell using Hint', () => {
  cy.visit('/')
  cy.get('.game__cell.game__cell--filled').should(
    'have.length',
    45,
  )
  cy.get('.game__cell')
    .not('.game__cell--filled')
    .each(($cell) => {
      cy.wrap($cell, { log: false }).click()
      cy.get('.status__action-hint').click()
    })

  cy.contains('.overlay__text', 'You solved it').should(
    'be.visible',
  )
})

cypress/e2e/hint.cy.js

Click on the hint

it('fills each empty cell using Hint', () => {
  cy.visit('/')
  cy.get('.game__cell.game__cell--filled').should(
    'have.length',
    45,
  )
  cy.get('.game__cell')
    .not('.game__cell--filled')
    .each(($cell) => {
      cy.wrap($cell, { log: false }).click()
      cy.get('.status__action-hint').click()
    })

  cy.contains('.overlay__text', 'You solved it').should(
    'be.visible',
  )
})

cypress/e2e/hint.cy.js

The hint test

  • Random data

  • Server data

  • Timers and clocks

  • Conditional testing

  • Unclear test requirements

Making E2E hard

Let's

test the

Timer

it('Timer shows 10 seconds', () => {
  cy.visit('/')
  for (let k = 0; k < 10; k++) {
    cy.contains('.status__time', `00:0${k}`)
  }
})

cypress/e2e/timer-clock.cy.js

How does the Timer component show 15 minutes?

it('Timer shows 15 minutes', () => {
  cy.visit('/')
  cy.contains('.status__time', '15:00', {
    timeout: 900_000,
  })
})

Wait 15 minutes...

There Must Be A Better Way

How do we test smaller pieces of code?

AGENDA

  • End-to-End Sudoku test
  • Why E2E is hard
  • Unit to Component frontend test
  • Component tests for framework X
  • Component test to E2E test
  • The End
export const formatTime = ({ hours, minutes, seconds }) => {
  if (typeof seconds === 'undefined') {
    return '00:00'
  }
  let stringTimer = ''

  stringTimer += hours ? '' + hours + ':' : ''
  stringTimer += minutes ? (minutes < 10 ? '0' : '') + minutes + ':' : '00:'
  stringTimer += seconds < 10 ? '0' + seconds : seconds

  return stringTimer
}

src/components/Timer.js

import { formatTime } from '../../src/components/Timer'

it('formats the time', () => {
  expect(formatTime({})).to.equal('00:00')
  expect(formatTime({ seconds: 3 })).to.equal('00:03')
  expect(formatTime({ minutes: 55, seconds: 3 })).to.equal(
    '55:03',
  )
  expect(
    formatTime({ hours: 110, minutes: 55, seconds: 3 }),
  ).to.equal('110:55:03')
})

cypress/e2e/format-time.cy.js

import { formatTime } from '../../src/components/Timer'

it('formats the time', () => {
  expect(formatTime({})).to.equal('00:00')
  expect(formatTime({ seconds: 3 })).to.equal('00:03')
  expect(formatTime({ minutes: 55, seconds: 3 })).to.equal(
    '55:03',
  )
  expect(
    formatTime({ hours: 110, minutes: 55, seconds: 3 }),
  ).to.equal('110:55:03')
})

cypress/e2e/format-time.cy.js

It is time

to test the

<Timer .../>

component

for real

I really don't want this

import { Timer } from '../../src/components/Timer'
import React from 'react';
import { mount } from 'enzyme';

test('shows the time', () => {
  const wrapper = mount(<Timer minutes={15} />)
  const p = wrapper.find('.status__time');
  expect(p.text()).toBe('15:00');
});
import { Timer } from '../../src/components/Timer'

it('shows the time', () => {
  <Timer minutes={15} />
  cy.contains('.status__time', '15:00')
})

I want this!

// imports framework-specific component
// needs to bundle all its dependencies
import { Timer } from '../../src/components/Timer'
// needs to "start" the framework
mount(<Timer ...>)

We are not in cy.visit('Kansas') anymore

npm start

npx cypress open

Configure Cypress component testing

Supported frameworks

const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
  },
  component: {
    devServer: {
      framework: 'create-react-app',
      bundler: 'webpack',
    },
  },
})

cypress.config.js

import React from 'react'
import { Timer } from './Timer'
import '../App.css'
import { SudokuContext } from '../context/SudokuContext'
import moment from 'moment'

describe('Timer', () => {
  it('sets the clock to the given value', () => {
    const timeGameStarted = moment().subtract(
      900,
      'seconds',
    )

    cy.mount(
      <SudokuContext.Provider value={{ timeGameStarted }}>
        <section className="status">
          <Timer />
        </section>
      </SudokuContext.Provider>,
    )
    cy.contains('15:00')
  })
})

src/components/Timer.cy.js

  • 58ms test
  • live app
  • DOM snapshots

See the component running

import React from 'react'
import { Timer } from './Timer'
import '../App.css'
import { SudokuContext } from '../context/SudokuContext'
import moment from 'moment'

describe('Timer', () => {
  it('sets the clock to the given value', () => {
    const timeGameStarted = moment().subtract(
      900,
      'seconds',
    )

    cy.mount(
      <SudokuContext.Provider value={{ timeGameStarted }}>
        <section className="status">
          <Timer />
        </section>
      </SudokuContext.Provider>,
    )
    cy.contains('15:00')
  })
})

src/components/Timer.cy.js

Cypress "understands"  how to bundle and mount your framework component

import React from 'react'
import { Timer } from './Timer'
import '../App.css'
import { SudokuContext } from '../context/SudokuContext'
import moment from 'moment'

describe('Timer', () => {
  it('sets the clock to the given value', () => {
    const timeGameStarted = moment().subtract(
      900,
      'seconds',
    )

    cy.mount(
      <SudokuContext.Provider value={{ timeGameStarted }}>
        <section className="status">
          <Timer />
        </section>
      </SudokuContext.Provider>,
    )
    cy.contains('15:00')
  })
})

src/components/Timer.cy.js

Import app or component CSS and use the markup close to the app

without App.css or markup

import React from 'react'
import { Timer } from './Timer'
import '../App.css'
import { SudokuContext } from '../context/SudokuContext'
import moment from 'moment'

describe('Timer', () => {
  it('sets the clock to the given value', () => {
    const timeGameStarted = moment().subtract(
      900,
      'seconds',
    )

    cy.mount(
      <SudokuContext.Provider value={{ timeGameStarted }}>
        <section className="status">
          <Timer />
        </section>
      </SudokuContext.Provider>,
    )
    cy.contains('15:00')
  })
})

src/components/Timer.cy.js

Import app or component CSS and use the markup close to the app

with App.css and markup

import React from 'react'
import { Timer } from './Timer'
import '../App.css'
import { SudokuContext } from '../context/SudokuContext'
import moment from 'moment'

describe('Timer', () => {
  it('sets the clock to the given value', () => {
    const timeGameStarted = moment().subtract(
      900,
      'seconds',
    )

    cy.mount(
      <SudokuContext.Provider value={{ timeGameStarted }}>
        <section className="status">
          <Timer />
        </section>
      </SudokuContext.Provider>,
    )
    cy.contains('15:00')
  })
})

src/components/Timer.cy.js

Pass the component data using props or other mechanisms

Timer

Numbers

Difficulty

StatusSection

GameSection

Game

App

import React from 'react'
import { Numbers } from './Numbers'
import '../App.css'
import { SudokuContext } from '../context/SudokuContext'

it('shows the selected number', () => {
  cy.mount(
    <SudokuContext.Provider
      value={{ numberSelected: '8' }}
    >
      <div className="innercontainer">
        <section className="status">
          <Numbers
            onClickNumber={cy.stub().as('click')}
          />
        </section>
      </div>
    </SudokuContext.Provider>,
  )

  cy.contains('.status__number', '8').should(
    'have.class',
    'status__number--selected',
  )
  cy.contains('.status__number', '9').click()
  cy.get('@click').should(
    'have.been.calledOnceWithExactly',
    '9',
  )
})

Props + Provider are framework specific

src/components/Numbers.cy.js

import React from 'react'
import { Numbers } from './Numbers'
import '../App.css'
import { SudokuContext } from '../context/SudokuContext'

it('shows the selected number', () => {
  cy.mount(
    <SudokuContext.Provider
      value={{ numberSelected: '8' }}
    >
      <div className="innercontainer">
        <section className="status">
          <Numbers
            onClickNumber={cy.stub().as('click')}
          />
        </section>
      </div>
    </SudokuContext.Provider>,
  )

  cy.contains('.status__number', '8').should(
    'have.class',
    'status__number--selected',
  )
  cy.contains('.status__number', '9').click()
  cy.get('@click').should(
    'have.been.calledOnceWithExactly',
    '9',
  )
})

Props + Provider are framework specific

The rest of the test is Cypress API only

src/components/Numbers.cy.js

import React from 'react'
import { Numbers } from './Numbers'
import '../App.css'
import { SudokuContext } from '../context/SudokuContext'

it('shows the selected number', () => {
  cy.mount(
    <SudokuContext.Provider
      value={{ numberSelected: '8' }}
    >
      <div className="innercontainer">
        <section className="status">
          <Numbers
            onClickNumber={cy.stub().as('click')}
          />
        </section>
      </div>
    </SudokuContext.Provider>,
  )

  cy.contains('.status__number', '8').should(
    'have.class',
    'status__number--selected',
  )
  cy.contains('.status__number', '9').click()
  cy.get('@click').should(
    'have.been.calledOnceWithExactly',
    '9',
  )
})

Props + Provider are framework specific

The rest of the test is Cypress API only

src/components/Numbers.cy.js

Want more React component test examples?

Follow: Murat Ozcan

Each React component has

  • React Testing Library spec
  • Cypress component spec

A huge collection of various React testing examples with matching Cypress component specs

What About Framework / Test Library X?

React

Vue

Angular

Svelte

Lit

ReactNative

ArrowJs

Qwik

No more framework-specific syntax

import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";

import Toggle from "./toggle";

let container = null;
beforeEach(() => {
  // setup a DOM element as a render target
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  // cleanup on exiting
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

it("changes value when clicked", () => {
  const onChange = jest.fn();
  act(() => {
    render(<Toggle onChange={onChange} />, container);
  });

  // get a hold of the button element, and trigger some clicks on it
  const button = document.querySelector("[data-testid=toggle]");
  expect(button.innerHTML).toBe("Turn on");

  act(() => {
    button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
  });

  expect(onChange).toHaveBeenCalledTimes(1);
  expect(button.innerHTML).toBe("Turn off");

  act(() => {
    for (let i = 0; i < 5; i++) {
      button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
    }
  });

  expect(onChange).toHaveBeenCalledTimes(6);
  expect(button.innerHTML).toBe("Turn on");
});
import React from "react";
import Toggle from "./toggle";

it("changes value when clicked", () => {
  cy.mount(<Toggle onChange={cy.stub().as('change')} />);

  // get a hold of the button element, and trigger some clicks on it
  cy.contains("[data-testid=toggle]", "Turn on").click()
  cy.get('@change').should('have.been.calledOnce')
  cy.contains("[data-testid=toggle]", "Turn off")
    .click()
    .click()
    .click()
    .click()
    .click()

  cy.get('@change').its('callCount').should('eq', 6)
  cy.contains("[data-testid=toggle]", "Turn on")
});

equivalent Cypress component test

https://on.cypress.io/api

No more framework-specific syntax

import React from "react";
import Toggle from "./toggle";

it("changes value when clicked", () => {
  cy.mount(<Toggle onChange={cy.stub().as('change')} />);

  // get a hold of the button element, and trigger some clicks on it
  cy.contains("[data-testid=toggle]", "Turn on").click()
  cy.get('@change').should('have.been.calledOnce')
  cy.contains("[data-testid=toggle]", "Turn off")
    .click()
    .click()
    .click()
    .click()
    .click()

  cy.get('@change').its('callCount').should('eq', 6)
  cy.contains("[data-testid=toggle]", "Turn on")
});

equivalent Cypress component test https://on.cypress.io/api

import { NavComponent } from './nav.component'
import { SigninService } from '../signin.service'

describe('NavComponent', () => {
  it('should create and show the links', () => {
    const signinService = new SigninService()
    cy.spy(signinService, 'login').as('login')
    cy.spy(signinService, 'logout').as('logout')
    cy.mount(NavComponent, {
      componentProperties: {
        signinService,
      },
    })

    cy.contains('a', 'HOME')
    cy.contains('button', 'LOG IN').should('be.visible').wait(1000).click()
    cy.get('@login').should('have.been.called')

    cy.contains('a', 'HOME')
    cy.contains('a', 'PROFILE').should('be.visible')
    cy.contains('button', 'LOG OUT').should('be.visible').wait(1000).click()
    cy.get('@logout').should('have.been.called')

    cy.contains('a', 'HOME')
    cy.contains('button', 'LOG IN').should('be.visible')
  })
})

src/app/nav/nav.component.cy.ts

Mount is framework specific

Cypress API

NavComponent

import { NavComponent } from './nav.component'
import { SigninService } from '../signin.service'

describe('NavComponent', () => {
  it('should create and show the links', () => {
    const signinService = new SigninService()
    cy.spy(signinService, 'login').as('login')
    cy.spy(signinService, 'logout').as('logout')
    cy.mount(NavComponent, {
      componentProperties: {
        signinService,
      },
    })

    cy.contains('a', 'HOME')
    cy.contains('button', 'LOG IN').should('be.visible').wait(1000).click()
    cy.get('@login').should('have.been.called')

    cy.contains('a', 'HOME')
    cy.contains('a', 'PROFILE').should('be.visible')
    cy.contains('button', 'LOG OUT').should('be.visible').wait(1000).click()
    cy.get('@logout').should('have.been.called')

    cy.contains('a', 'HOME')
    cy.contains('button', 'LOG IN').should('be.visible')
  })
})

src/app/nav/nav.component.cy.ts

🎁

import { NavComponent } from './nav.component'
import { SigninService } from '../signin.service'

describe('NavComponent', () => {
  it('should create and show the links', () => {
    const signinService = new SigninService()
    cy.spy(signinService, 'login').as('login')
    cy.spy(signinService, 'logout').as('logout')
    cy.mount(NavComponent, {
      componentProperties: {
        signinService,
      },
    })

    cy.contains('a', 'HOME')
    cy.contains('button', 'LOG IN').should('be.visible').wait(1000).click()
    cy.get('@login').should('have.been.called')

    cy.contains('a', 'HOME')
    cy.contains('a', 'PROFILE').should('be.visible')
    cy.contains('button', 'LOG OUT').should('be.visible').wait(1000).click()
    cy.get('@logout').should('have.been.called')

    cy.contains('a', 'HOME')
    cy.contains('button', 'LOG IN').should('be.visible')
  })
})

src/app/nav/nav.component.cy.ts

Angular

Examples for several frameworks

A Testing

Pyramid of Component Tests

Timer

Numbers

Difficulty

StatusSection

GameSection

Game

App

Overlay

import { Game } from './Game'
import { SudokuProvider } from './context/SudokuContext'
import { WinProvider } from './context/WinContext'
import { starting, solved } from '../cypress/fixtures/sudoku.json'

it('plays the game', () => {
  cy.mount(
    <SudokuProvider>
      <WinProvider>
        <Game initArray={starting} solvedArray={solved} />
      </WinProvider>
    </SudokuProvider>,
  )
  cy.get('.game__cell:not(.game__cell--filled)').should(
    'have.length',
    3,
  )
  starting.forEach((cell, index) => {
    if (cell === '0') {
      cy.get('.game__cell').eq(index).click()
      cy.contains('.status__number', solved[index])
        .click()
        .wait(500, { log: false })
    }
  })

  cy.contains('.overlay__text', 'You solved it').should('be.visible')
})

src/Game.cy.js

import { Game } from './Game'
import { SudokuProvider } from './context/SudokuContext'
import { WinProvider } from './context/WinContext'
import { starting, solved } from '../cypress/fixtures/sudoku.json'

it('plays the game', () => {
  cy.mount(
    <SudokuProvider>
      <WinProvider>
        <Game initArray={starting} solvedArray={solved} />
      </WinProvider>
    </SudokuProvider>,
  )
  cy.get('.game__cell:not(.game__cell--filled)').should(
    'have.length',
    3,
  )
  starting.forEach((cell, index) => {
    if (cell === '0') {
      cy.get('.game__cell').eq(index).click()
      cy.contains('.status__number', solved[index])
        .click()
        .wait(500, { log: false })
    }
  })

  cy.contains('.overlay__text', 'You solved it').should('be.visible')
})

src/Game.cy.js

import { Game } from './Game'
import { SudokuProvider } from './context/SudokuContext'
import { WinProvider } from './context/WinContext'
import { starting, solved } from '../cypress/fixtures/sudoku.json'

it('plays the game', () => {
  cy.mount(
    <SudokuProvider>
      <WinProvider>
        <Game initArray={starting} solvedArray={solved} />
      </WinProvider>
    </SudokuProvider>,
  )
  cy.get('.game__cell:not(.game__cell--filled)').should(
    'have.length',
    3,
  )
  starting.forEach((cell, index) => {
    if (cell === '0') {
      cy.get('.game__cell').eq(index).click()
      cy.contains('.status__number', solved[index])
        .click()
        .wait(500, { log: false })
    }
  })

  cy.contains('.overlay__text', 'You solved it').should('be.visible')
})

src/Game.cy.js

src/Game.cy.js

import React, { useState, useEffect } from 'react'
import { formatTime } from './Timer'

const useFetch = (url) => {
  const [data, setData] = useState([])
  const [loading, setLoading] = useState(url ? true : false)

  async function fetchData() {
    if (url) {
      const response = await fetch(url)
      const json = await response.json()
      setData(json)
      setLoading(false)
    }
  }

  useEffect(() => {
    if (!url) {
      return
    }
    fetchData()
  }, [url])

  return { loading, data }
}

export const Overlay = (props) => {
  const { loading, data } = useFetch(
    props.overlay && props.time ? '/times/' + props.time : null,
  )
  const className = props.overlay
    ? 'overlay overlay--visible'
    : 'overlay'
  return (
    <div className={className} onClick={props.onClickOverlay}>
      <h2 className="overlay__text">
        <div className="overlay__greeting">
          You <span className="overlay__textspan1">solved</span>{' '}
          <span className="overlay__textspan2">it!</span>
        </div>
        {loading && (
          <div className="overlay__loading">Loading...</div>
        )}

        {data.length > 0 && (
          <ul className="overlay__times">
            {data.map((item, index) => {
              return (
                <li
                  key={index}
                  className={item.current ? 'overlay__current' : ''}
                >
                  {formatTime(item)}
                </li>
              )
            })}
          </ul>
        )}
      </h2>
    </div>
  )
}

src/components/Overlay.js

import React, { useState, useEffect } from 'react'
import { formatTime } from './Timer'

const useFetch = (url) => {
  const [data, setData] = useState([])
  const [loading, setLoading] = useState(url ? true : false)

  async function fetchData() {
    if (url) {
      const response = await fetch(url)
      const json = await response.json()
      setData(json)
      setLoading(false)
    }
  }

  useEffect(() => {
    if (!url) {
      return
    }
    fetchData()
  }, [url])

  return { loading, data }
}

export const Overlay = (props) => {
  const { loading, data } = useFetch(
    props.overlay && props.time ? '/times/' + props.time : null,
  )
  const className = props.overlay
    ? 'overlay overlay--visible'
    : 'overlay'
  return (
    <div className={className} onClick={props.onClickOverlay}>
      <h2 className="overlay__text">
        <div className="overlay__greeting">
          You <span className="overlay__textspan1">solved</span>{' '}
          <span className="overlay__textspan2">it!</span>
        </div>
        {loading && (
          <div className="overlay__loading">Loading...</div>
        )}

        {data.length > 0 && (
          <ul className="overlay__times">
            {data.map((item, index) => {
              return (
                <li
                  key={index}
                  className={item.current ? 'overlay__current' : ''}
                >
                  {formatTime(item)}
                </li>
              )
            })}
          </ul>
        )}
      </h2>
    </div>
  )
}

src/components/Overlay.js

it('shows the loading element', () => {
  cy.intercept('GET', '/times/90', {
    delay: 1000,
    statusCode: 404,
    body: [],
  }).as('times')
  cy.mount(<Overlay overlay={true} time={90} />)
  cy.contains('.overlay__loading', 'Loading').should('be.visible')
  cy.wait('@times')
  cy.get('.overlay__loading').should('not.exist')
})
it('shows the loading element', () => {
  cy.intercept('GET', '/times/90', {
    delay: 1000,
    statusCode: 404,
    body: [],
  }).as('times')
  cy.mount(<Overlay overlay={true} time={90} />)
  cy.contains('.overlay__loading', 'Loading').should('be.visible')
  cy.wait('@times')
  cy.get('.overlay__loading').should('not.exist')
})
it('shows the top times', () => {
  cy.intercept('GET', '/times/90', {
    fixture: 'times.json',
  }).as('scores')
  cy.mount(<Overlay overlay={true} time={90} />)
  cy.wait('@scores')
  cy.get('.overlay__times li').should('have.length', 4)
  cy.contains('.overlay__times li', '01:30').should(
    'have.class',
    'overlay__current',
  )
})

My advice: Mock / spy on the public APIs (network, storage, cookies, DOM) from your component tests rather than the inner code.

 

It is ok to pass spies and stubs as props.

import { starting, solved } from '../fixtures/sudoku.json'

describe('Sudoku', () => {
  it('plays the same game', () => {
    // to play the same game, we will pass
    // the starting and the solved arrays
    // to the application via the "window" object
    cy.visit('/', {
      onBeforeLoad(window) {
        window.starting = starting
        window.solved = solved
      },
    })

    cy.intercept('GET', '/times/*', {
      fixture: 'times.json',
    }).as('scores')

    // our initial array only has 3 cells to fill
    cy.get('.game__cell:contains(0)').should('have.length', 3)
    starting.forEach((cell, index) => {
      if (cell === '0') {
        cy.get('.game__cell').eq(index).click()
        cy.contains('.status__number', solved[index])
          .click()
      }
    })

    cy.contains('.overlay__text', 'You solved it').should('be.visible')
    cy.wait('@scores')
    cy.fixture('times.json')
      .its('length')
      .then((n) => {
        cy.get('.overlay__times li').should('have.length', n)
      })

    cy.get('.overlay__times li.overlay__current').should('have.length', 1)
  })
})

Back to the Future End-to-End Tests

import { starting, solved } from '../fixtures/sudoku.json'

describe('Sudoku', () => {
  it('plays the same game', () => {
    // to play the same game, we will pass
    // the starting and the solved arrays
    // to the application via the "window" object
    cy.visit('/', {
      onBeforeLoad(window) {
        window.starting = starting
        window.solved = solved
      },
    })

    cy.intercept('GET', '/times/*', {
      fixture: 'times.json',
    }).as('scores')

    // our initial array only has 3 cells to fill
    cy.get('.game__cell:contains(0)').should('have.length', 3)
    starting.forEach((cell, index) => {
      if (cell === '0') {
        cy.get('.game__cell').eq(index).click()
        cy.contains('.status__number', solved[index])
          .click()
      }
    })

    cy.contains('.overlay__text', 'You solved it').should('be.visible')
    cy.wait('@scores')
    cy.fixture('times.json')
      .its('length')
      .then((n) => {
        cy.get('.overlay__times li').should('have.length', n)
      })

    cy.get('.overlay__times li.overlay__current').should('have.length', 1)
  })
})

Back to the Future End-to-End Tests

import { starting, solved } from '../fixtures/sudoku.json'

describe('Sudoku', () => {
  it('plays the same game', () => {
    // to play the same game, we will pass
    // the starting and the solved arrays
    // to the application via the "window" object
    cy.visit('/', {
      onBeforeLoad(window) {
        window.starting = starting
        window.solved = solved
      },
    })

    cy.intercept('GET', '/times/*', {
      fixture: 'times.json',
    }).as('scores')

    // our initial array only has 3 cells to fill
    cy.get('.game__cell:contains(0)').should('have.length', 3)
    starting.forEach((cell, index) => {
      if (cell === '0') {
        cy.get('.game__cell').eq(index).click()
        cy.contains('.status__number', solved[index])
          .click()
      }
    })

    cy.contains('.overlay__text', 'You solved it').should('be.visible')
    cy.wait('@scores')
    cy.fixture('times.json')
      .its('length')
      .then((n) => {
        cy.get('.overlay__times li').should('have.length', n)
      })

    cy.get('.overlay__times li.overlay__current').should('have.length', 1)
  })
})
import { Game } from './Game'
import { SudokuProvider } from './context/SudokuContext'
import { WinProvider } from './context/WinContext'
import { starting, solved } from '../cypress/fixtures/sudoku.json'

it('plays the game', () => {
  cy.mount(
    <SudokuProvider>
      <WinProvider>
        <Game initArray={starting} solvedArray={solved} />
      </WinProvider>
    </SudokuProvider>,
  )
  
  cy.intercept('GET', '/times/*', {
      fixture: 'times.json',
    }).as('scores')

  // our initial array only has 3 cells to fill
  cy.get('.game__cell:contains(0)').should('have.length', 3)
  starting.forEach((cell, index) => {
    if (cell === '0') {
      cy.get('.game__cell').eq(index).click()
      cy.contains('.status__number', solved[index])
        .click()
    }
  })

  cy.contains('.overlay__text', 'You solved it').should('be.visible')
  cy.wait('@scores')
    cy.fixture('times.json')
      .its('length')
      .then((n) => {
        cy.get('.overlay__times li').should('have.length', n)
      })

    cy.get('.overlay__times li.overlay__current').should('have.length', 1)
})

End-to-End test

Component test

import { starting, solved } from '../fixtures/sudoku.json'

describe('Sudoku', () => {
  it('plays the same game', () => {
    // to play the same game, we will pass
    // the starting and the solved arrays
    // to the application via the "window" object
    cy.visit('/', {
      onBeforeLoad(window) {
        window.starting = starting
        window.solved = solved
      },
    })

    cy.intercept('GET', '/times/*', {
      fixture: 'times.json',
    }).as('scores')

    // our initial array only has 3 cells to fill
    cy.get('.game__cell:contains(0)').should('have.length', 3)
    starting.forEach((cell, index) => {
      if (cell === '0') {
        cy.get('.game__cell').eq(index).click()
        cy.contains('.status__number', solved[index])
          .click()
      }
    })

    cy.contains('.overlay__text', 'You solved it').should('be.visible')
    cy.wait('@scores')
    cy.fixture('times.json')
      .its('length')
      .then((n) => {
        cy.get('.overlay__times li').should('have.length', n)
      })

    cy.get('.overlay__times li.overlay__current').should('have.length', 1)
  })
})
import { Game } from './Game'
import { SudokuProvider } from './context/SudokuContext'
import { WinProvider } from './context/WinContext'
import { starting, solved } from '../cypress/fixtures/sudoku.json'

it('plays the game', () => {
  cy.mount(
    <SudokuProvider>
      <WinProvider>
        <Game initArray={starting} solvedArray={solved} />
      </WinProvider>
    </SudokuProvider>,
  )
  
  cy.intercept('GET', '/times/*', {
      fixture: 'times.json',
    }).as('scores')

  // our initial array only has 3 cells to fill
  cy.get('.game__cell:contains(0)').should('have.length', 3)
  starting.forEach((cell, index) => {
    if (cell === '0') {
      cy.get('.game__cell').eq(index).click()
      cy.contains('.status__number', solved[index])
        .click()
    }
  })

  cy.contains('.overlay__text', 'You solved it').should('be.visible')
  cy.wait('@scores')
    cy.fixture('times.json')
      .its('length')
      .then((n) => {
        cy.get('.overlay__times li').should('have.length', n)
      })

    cy.get('.overlay__times li.overlay__current').should('have.length', 1)
})

End-to-End test

Component test

The exact same code

🤯 🎁 🎉

expect(formatTime({ seconds: 3 }))
  .to.equal('00:03')

Unit test

import { Component } from './Component'
cy.mount(<Component props=... />)
cy.get(...).click()

Component test

cy.visit('/')
cy.get(...).click()

End-to-End test

  • Small chunks of code like functions and classes
  • Front-end React / Angular / Vue / X components
  • Easy to test edge conditions
  • Web application
  • Easy to test the entire user flow

Web Application Testability

Benefits of Cypress Component Testing

Benefits of Cypress Component Testing

Use The Same Cy API for all tests

Benefits of Cypress Component Testing

Runs In The Browser

Downsides of Cypress Component Testing

None

Gleb Bahmutov

Sr Director of Engineering

gleb.dev

👏 Thank you 👏

Learn Cypress React component testing by playing Sudoku

Learn Cypress React component testing by playing Sudoku

By Gleb Bahmutov

Learn Cypress React component testing by playing Sudoku

When developers see Cypress component testing, their eyes light up. You see the component running right in your browser, you have all your debugging tools, and you can gain full confidence in how the component is working. In this talk, I will show how component tests can verify that a React Sudoku application is implemented correctly. The same testing approach can also work for Angular, Vue, and Svelte component testing.

  • 1,073