Gleb Bahmutov PRO
JavaScript ninja, image processing expert, software quality fanatic
Gleb Bahmutov
Sr Director of Engineering
C / C++ / C# / Java / CoffeeScript / JavaScript / Node / Angular / Vue / Cycle.js / functional programming / testing
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
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
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
it('Timer shows 15 minutes', () => {
cy.visit('/')
cy.contains('.status__time', '15:00', {
timeout: 900_000,
})
})
Wait 15 minutes...
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
<Timer .../>
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
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
Each React component has
A huge collection of various React testing examples with matching Cypress component specs
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
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
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)
})
})
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 { 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
expect(formatTime({ seconds: 3 }))
.to.equal('00:03')
import { Component } from './Component'
cy.mount(<Component props=... />)
cy.get(...).click()
cy.visit('/')
cy.get(...).click()
👏 Thank you 👏
By Gleb Bahmutov
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.
JavaScript ninja, image processing expert, software quality fanatic