JSON server
API e2e
CT
UI integration
UI e2e
15
5 :
1 :
Cypress Component Test Example
// src/components/HeaderBarBrand.cy.tsx
import HeaderBarBrand from './HeaderBarBrand'
import '../styles.scss'
describe('HeaderBarBrand', () => {
it('should', () => {
cy.mount(<HeaderBarBrand />)
})
})
// src/components/HeaderBarBrand.tsx
export default function HeaderBarBrand() {
return <div>hello</div>
}
// src/components/HeaderBarBrand.cy.tsx
import HeaderBarBrand from './HeaderBarBrand'
import '../styles.scss'
describe('HeaderBarBrand', () => {
it('should', () => {
cy.mount(<HeaderBarBrand />)
cy.get('a').should('have.attr', 'href', 'https://reactjs.org/')
})
})
TDD ensures fault-finding tests
for improvements that matter.
// src/components/HeaderBarBrand.tsx
export default function HeaderBarBrand() {
return (
<div>
<a href="https://reactjs.org/" />
</div>
)
}
Start with something failing,
do the minimum to get it to work,
and then make it better.
// src/components/HeaderBarBrand.cy.tsx
import HeaderBarBrand from './HeaderBarBrand'
import '../styles.scss'
describe('HeaderBarBrand', () => {
it('should', () => {
cy.mount(<HeaderBarBrand />)
cy.get('a')
.should('have.attr', 'href', 'https://reactjs.org/')
.should('be.visible')
})
})
Visual feedback inspires testing.
// src/components/HeaderBarBrand.tsx
import {FaReact} from 'react-icons/fa'
export default function HeaderBarBrand() {
return (
<div>
<a href="https://reactjs.org/">
<FaReact />
</a>
</div>
)
}
The "Red" can be a visual feedback that's not up to our expectation.
// src/components/HeaderBarBrand.tsx
import HeaderBarBrand from './HeaderBarBrand'
import '../styles.scss'
describe('HeaderBarBrand', () => {
it('should', () => {
cy.mount(<HeaderBarBrand />)
cy.get('a')
.should('have.attr', 'href', 'https://reactjs.org/')
.and('have.attr', 'target', '_blank')
.and('have.attr', 'rel', 'noopener noreferrer')
})
})
// src/components/HeaderBarBrand.tsx
import {FaReact} from 'react-icons/fa'
export default function HeaderBarBrand() {
return (
<div className="navbar-brand">
<a
href="https://reactjs.org/"
target="_blank"
rel="noopener noreferrer"
className="navbar-item"
>
</a>
</div>
)
}
Obvious, but hard; write very small incremental tests at a time.
// src/components/HeaderBarBrand.tsx
import HeaderBarBrand from './HeaderBarBrand'
import '../styles.scss'
describe('HeaderBarBrand', () => {
it('should', () => {
cy.mount(<HeaderBarBrand />)
cy.get('a')
.should('have.attr', 'href', 'https://reactjs.org/')
.and('have.attr', 'target', '_blank')
.and('have.attr', 'rel', 'noopener noreferrer')
cy.getByCy('header-bar-brand')
.within(() => cy.get('svg'))
})
})
// src/components/HeaderBarBrand.tsx
import {FaReact} from 'react-icons/fa'
export default function HeaderBarBrand() {
return (
<div data-cy="header-bar-brand" className="navbar-brand">
<a
href="https://reactjs.org/"
target="_blank"
rel="noopener noreferrer"
className="navbar-item"
>
<FaReact />
</a>
</div>
)
}
Build the component iteratively with tests.
A Component Test is a small scale App, so replicate the app wrappers
// src/components/HeaderBarBrand.cy.tsx
import HeaderBarBrand from './HeaderBarBrand'
import {BrowserRouter} from 'react-router-dom'
import '../styles.scss'
describe('HeaderBarBrand', () => {
it('should', () => {
cy.mount(
<BrowserRouter>
<HeaderBarBrand />
</BrowserRouter>,
)
cy.get('a')
.should('have.attr', 'href', 'https://reactjs.org/')
.and('have.attr', 'target', '_blank')
.and('have.attr', 'rel', 'noopener noreferrer')
cy.getByCy('header-bar-brand')
.within(() => cy.get('svg'))
})
})
// src/components/HeaderBarBrand.tsx
import {FaReact} from 'react-icons/fa'
import {NavLink} from 'react-router-dom'
export default function HeaderBarBrand() {
return (
<div data-cy="header-bar-brand" className="navbar-brand">
<a
href="https://reactjs.org/"
target="_blank"
rel="noopener noreferrer"
className="navbar-item"
>
<FaReact />
</a>
<NavLink to="/" />
</div>
)
}
// src/components/HeaderBarBrand.cy.tsx
import HeaderBarBrand from './HeaderBarBrand'
import {BrowserRouter} from 'react-router-dom'
import '../styles.scss'
describe('HeaderBarBrand', () => {
it('should verify external link attributes', () => {
cy.mount(
<BrowserRouter>
<HeaderBarBrand />
</BrowserRouter>,
)
cy.get('a')
.should('have.attr', 'href', 'https://reactjs.org/')
.and('have.attr', 'target', '_blank')
.and('have.attr', 'rel', 'noopener noreferrer')
cy.getByCy('header-bar-brand').within(() => cy.get('svg'))
cy.getByCy('navLink').within(() =>
['TOUR', 'OF', 'HEROES'].forEach((part: string) =>
cy.contains('span', part),
),
)
})
})
// src/components/HeaderBarBrand.tsx
import {FaReact} from 'react-icons/fa'
import {NavLink} from 'react-router-dom'
export default function HeaderBarBrand() {
return (
<div data-cy="header-bar-brand" className="navbar-brand">
<a
href="https://reactjs.org/"
target="_blank"
rel="noopener noreferrer"
className="navbar-item"
data-cy="header-bar-brand-link"
>
<div data-cy="react-icon-svg">
<FaReact />
</div>
</a>
<NavLink data-cy="navLink" to="/" className="navbar-item navbar-home">
<span className="tour">TOUR</span>
<span className="of">OF</span>
<span className="heroes">HEROES</span>
</NavLink>
</div>
)
}
describe('HeaderBarBrand', () => {
it('should verify external link attributes', () => {
cy.mount(
<BrowserRouter>
<HeaderBarBrand />
</BrowserRouter>,
)
cy.get('a')
.should('have.attr', 'href', 'https://reactjs.org/')
.and('have.attr', 'target', '_blank')
.and('have.attr', 'rel', 'noopener noreferrer')
cy.getByCy('header-bar-brand').within(() => cy.get('svg'))
cy.getByCy('navLink').within(() =>
['TOUR', 'OF', 'HEROES'].forEach((part: string) =>
cy.contains('span', part),
),
)
cy.getByCy('navLink').click()
cy.url().should('contain', '/')
})
})
describe('HeaderBarBrand', () => {
beforeEach(() => {
cy.mount(
<BrowserRouter>
<HeaderBarBrand />
</BrowserRouter>,
)
})
it('should verify external link attributes', () => {
cy.get('a')
.should('have.attr', 'href', 'https://reactjs.org/')
.and('have.attr', 'target', '_blank')
.and('have.attr', 'rel', 'noopener noreferrer')
cy.getByCy('header-bar-brand').within(() => cy.get('svg'))
})
it('should verify internal link spans and navigation', () => {
cy.getByCy('navLink').within(() =>
['TOUR', 'OF', 'HEROES'].forEach((part: string) =>
cy.contains('span', part),
),
)
cy.getByCy('navLink').click()
cy.url().should('contain', '/')
})
})
What matters is the beginning state of a test;
if reaching that state is common, it is an opportunity for a test enhancement. So long as tests do not duplicate, they are order-independent and stateless, either style is fine.
No need to keep the tests short to have a smaller blast radius.
Cypress runner makes diagnosis easy.
it('should render Add Hero button', ()_ => {
addHero().should('be.visible').contains('Add Hero')
})
it('should open Hero form on click', () => {
addHero.click()
cy.getByCy('add-hero-form').should('be.visible')
})
it('should remove Hero form on Cancel', () => {
addHero().click()
cy.getByCy('add-hero-form').should('be.visible')
cy.getByCy('cancel-button').click()
cy.getByCy('add-hero-form').should('not.exist')
})
it('should toggle Add Hero form', () => {
addHero().should('be.visible').contains('Add Hero').click()
cy.getByCy('add-hero-form').should('be.visible')
cy.getByCy('cancel-button').click()
cy.getByCy('add-hero-form').should('not.exist')
})
Same test, different APIs.
props, wrappers, parent vs child
// src/components/InputDetail.cy.tsx
import InputDetail from './InputDetail'
describe('InputDetail', () => {
it('should', () => {
cy.mount(<InputDetail />)
cy.findByPlaceholderText('Aslaug')
})
})
// src/components/InputDetail.tsx
export default function InputDetail() {
return (
<div>
<input placeholder="Aslaug"></input>
</div>
)
}
...or console.log
make the test pass
// src/components/InputDetail.cy.tsx
import InputDetail from "./InputDetail";
describe("InputDetail", () => {
it("should", () => {
const placeholder = "Aslaug";
cy.mount(<InputDetail placeholder={placeholder} />);
cy.findByPlaceholderText(placeholder);
});
});
// src/components/InputDetail.tsx
type InputDetailProps = {
placeholder: string
}
export default function InputDetail({placeholder}: InputDetailProps) {
return (
<div>
<input placeholder={placeholder}></input>
</div>
)
}
We either manipulate our components via props or what wraps them.
passing data via prop
cy.wrappedMount(
<VillainsContext.Provider value={villains}>
<VillainList />
</VillainsContext.Provider>
)
cy.wrappedMount(
<VillainList />
)
cy.wrappedMount(
<HeroList heroes={heroes} />
)
cy.wrappedMount(
<HeroList heroes={[]} />
)
vs passing data using Context API
// src/components/InputDetail.cy.tsx
import InputDetail from './InputDetail'
describe('InputDetail', () => {
it('should', () => {
const placeholder = 'Aslaug'
const name = 'name'
cy.mount(<InputDetail name={name} placeholder={placeholder} />)
cy.contains(name)
cy.findByPlaceholderText(placeholder)
})
})
// src/components/InputDetail.tsx
type InputDetailProps = {
name: string
placeholder: string
}
export default function InputDetail({name, placeholder}: InputDetailProps) {
return (
<div>
<label>{name}</label>
<input placeholder={placeholder}></input>
</div>
)
}
Add the prop to the component types.
Add it to the arguments or the component.
Use the prop in the component.
// src/components/InputDetail.cy.tsx
import InputDetail from './InputDetail'
import '../styles.scss'
describe('InputDetail', () => {
it('should', () => {
const placeholder = 'Aslaug'
const name = 'name'
const value = 'some value'
cy.mount(
<InputDetail name={name} value={value} placeholder={placeholder} />,
)
cy.contains(name)
cy.get('input').should('have.value', value)
cy.findByPlaceholderText(placeholder)
})
})
// src/components/InputDetail.tsx
type InputDetailProps = {
name: string
value: string
placeholder: string
}
export default function InputDetail({
name,
value,
placeholder,
}: InputDetailProps) {
return (
<div className="field">
<label className="label" htmlFor={name}>
{name}
</label>
<input
name={name}
defaultValue={value}
placeholder={placeholder}
className="input"
type="text"
></input>
</div>
)
}
keep adding props...
describe('InputDetail', () => {
it('should allow the input field to be modified', () => {
const placeholder = 'Aslaug'
const name = 'name'
const value = 'some value'
const newValue = '42'
cy.mount(
<InputDetail name={name} value={value} placeholder={placeholder} />,
)
cy.contains(name)
cy.findByPlaceholderText(placeholder).clear().type(newValue)
cy.get('input').should('have.value', newValue)
})
it('should not allow the input field to be modified when readonly', () => {
const placeholder = 'Aslaug'
const name = 'name'
const value = 'some value'
cy.mount(
<InputDetail
name={name}
value={value}
placeholder={placeholder}
readOnly={true}
/>,
)
cy.contains(name)
cy.findByPlaceholderText(placeholder).should('have.attr', 'readOnly')
})
})
type InputDetailProps = {
name: string
value: string
placeholder?: string
readOnly?: boolean
}
export default function InputDetail({
name,
value,
placeholder,
readOnly,
}: InputDetailProps) {
return (
<div className="field">
<label className="label" htmlFor={name}>
{name}
</label>
<input
name={name}
defaultValue={value}
placeholder={placeholder}
readOnly={readOnly}
className="input"
type="text"
></input>
</div>
)
}
import {ChangeEvent} from 'react'
type InputDetailProps = {
name: string
value: string
placeholder?: string
onChange?: (e: ChangeEvent<HTMLInputElement>) => void
readOnly?: boolean
}
export default function InputDetail({
name,
value,
placeholder,
onChange,
readOnly,
}: InputDetailProps) {
return (
<div className="field">
<label className="label" htmlFor={name}>
{name}
</label>
<input
name={name}
defaultValue={value}
placeholder={placeholder}
onChange={onChange}
readOnly={readOnly}
className="input"
type="text"
></input>
</div>
)
}
describe('InputDetail', () => {
it('should allow the input field to be modified', () => {
const placeholder = 'Aslaug'
const name = 'name'
const value = 'some value'
const newValue = '42'
cy.mount(
<InputDetail
name={name}
value={value}
placeholder={placeholder}
onChange={cy.stub().as('onChange')}
/>,
)
cy.contains(name)
cy.findByPlaceholderText(placeholder).clear().type(newValue)
cy.get('input').should('have.value', newValue)
cy.get('@onChange').should('have.been.calledTwice')
}) ...
Seek failures through tests:
when we have green tests, we want to prefer adding more tests or refactoring
versus adding additional source code.
it('should allow the input field to be modified', () => {
cy.mount(
<InputDetail
name={name}
value={value}
placeholder={placeholder}
onChange={cy.stub().as('onChange')}
/>,
)
cy.contains('label', name)
cy.findByPlaceholderText(placeholder).clear().type(newValue)
cy.findByDisplayValue(newValue).should('be.visible')
cy.get('@onChange').its('callCount').should('eq', newValue.length)
})
it('should handle name change', () => {
const newHeroName = 'abc'
cy.getByCy('input-detail-name').type(newHeroName)
cy.findByDisplayValue(newHeroName).should('be.visible')
})
it('should handle description change', () => {
const newHeroDescription = '123'
cy.getByCy('input-detail-description').type(newHeroDescription)
cy.findByDisplayValue(newHeroDescription).should('be.visible')
})
Use data-cy in the top tag of the component
Are we duplicating the test effort covered elsewhere?
Find opportunities to cover different functionalities.
Great for E2e + TDD:
Aim to test at lowest level, we move up when we cannot test confidently
// cypress/e2e/routes-nav.cy.ts
describe('e2e sanity', () => {
it('should display header bar', () => {
cy.visit('/')
cy.getByCy('header-bar').should('be.visible')
})
})
// src/App.tsx
import HeaderBar from "components/HeaderBar";
import { BrowserRouter } from "react-router-dom";
import "./styles.scss";
function App() {
return (
<BrowserRouter>
<HeaderBar />
</BrowserRouter>
);
}
export default App;
Take hints from child components.
// src/components/HeaderBar.cy.tsx
import HeaderBar from './HeaderBar'
import '../styles.scss'
import {BrowserRouter} from 'react-router-dom'
describe('HeaderBar', () => {
it('should render', () => {
cy.mount(
<BrowserRouter>
<HeaderBar />
</BrowserRouter>,
)
cy.getByCy('header-bar-brand')
})
})
// cypress/e2e/routes-nav.cy.ts
describe('e2e sanity', () => {
it('should render header bar and nav bar', () => {
cy.visit('/')
cy.getByCy('header-bar').should('be.visible')
cy.getByCy('nav-bar').should('be.visible')
})
})
// src/App.tsx
import HeaderBar from 'components/HeaderBar'
import NavBar from 'components/NavBar'
import {BrowserRouter} from 'react-router-dom'
import './styles.scss'
function App() {
return (
<BrowserRouter>
<HeaderBar />
<NavBar />
</BrowserRouter>
)
}
export default App
Start with something failing,
do the minimum to get it to work,
and then make it better.
// cypress/e2e/routes-nav.cy.ts
describe('e2e sanity', () => {
it('should render header bar and nav bar', () => {
cy.visit('/')
cy.getByCy('header-bar').should('be.visible')
cy.getByCy('nav-bar').should('be.visible')
})
it('should land on not found for non-existing', () => {
cy.visit('/route48')
cy.getByCy('not-found').should('be.visible')
})
it('should direct-navigate to about', () => {
cy.visit('/about')
cy.getByCy('about').contains('CCTDD')
})
})
// src/App.tsx
import About from 'About'
import HeaderBar from 'components/HeaderBar'
import NavBar from 'components/NavBar'
import NotFound from 'components/NotFound'
import {BrowserRouter, Routes, Route} from 'react-router-dom'
import './styles.scss'
export default function App() {
return (
<BrowserRouter>
<HeaderBar />
<div className="section columns">
<NavBar />
<main className="column">
<Routes>
<Route path="/about" element={<About />} />
<Route path="*" element={<NotFound />} />
</Routes>
</main>
</div>
</BrowserRouter>
)
}
Incremental steps...
Find out about the application through testing.
// cypress/e2e/routes-nav.cy.ts
describe('e2e sanity', () => {
it('shoul v, () => {
cy.visit('/')
cy.getByCy('header-bar').should('be.visible')
cy.getByCy('nav-bar').should('be.visible')
cy.location('pathname').should('eq', route)
})
it('should land on not found', () => {
const route = '/route48'
cy.visit(route)
cy.location('pathname').should('eq', route)
cy.getByCy('not-found').should('be.visible')
})
it('should direct-navigate to about', () => {
const route = '/about'
cy.visit(route)
cy.location('pathname').should('eq', route)
cy.getByCy('about').contains('CCTDD')
})
})
If we have passing tests,
add tests or refactor before adding more source code.
describe('e2e sanity', () => {
it('should land on baseUrl, redirect to /heroes', () => {
cy.visit('/')
cy.getByCy('header-bar').should('be.visible')
cy.getByCy('nav-bar').should('be.visible')
cy.location('pathname').should('eq', route)
cy.getByCy('heroes').should('be.visible')
})
it('should direct-navigate to /heroes', () => {
const route = '/heroes'
cy.visit(route)
cy.location('pathname').should('eq', route)
cy.getByCy('heroes').should('be.visible')
})
it('should land on not found ', () => {
const route = '/route48'
cy.visit(route)
cy.location('pathname').should('eq', route)
cy.getByCy('not-found').should('be.visible')
})
// ...
})
})
Seek failures: keep adding tests until there is a failure,
and then add the new feature to pass the test.
// src/App.tsx
// ...
export default function App() {
return (
<BrowserRouter>
<HeaderBar />
<div className="section columns">
<NavBar />
<main className="column">
<Routes>
<Route path="/" element={<Navigate replace to="/heroes" />} />
<Route path="/heroes" element={<Heroes />} />
<Route path="/about" element={<About />} />
<Route path="*" element={<NotFound />} />
</Routes>
</main>
</div>
</BrowserRouter>
)
}
Use an API test client, vs late UI e2e.
We don't get to do naive UI e2e
before there is confidence that the backend works.
Be careful with effort duplication, use modest UI e2e & gap-fill.
Creation
Update
Delete
Update covers it all
Preferably where the backend code lives.
it('should CRUD a new hero entity', () => {
const newHero = {
id: faker.datatype.uuid(),
name: faker.internet.userName(),
description: `description ${faker.internet.userName()}`,
}
cy.crud('POST', 'heroes', {body: newHero}).its('status').should('eq', 201)
cy.crud('GET', 'heroes')
.its('body')
.then(body => {
expect(body.at(-1)).to.deep.eq(newHero)
})
const editedHero = {...newHero, name: 'Murat'}
cy.crud('PUT', `heroes/${editedHero.id}`, {body: editedHero})
.its('status')
.should('eq', 200)
cy.crud('GET', `heroes/${editedHero.id}`)
.its('body')
.should('deep.eq', editedHero)
cy.crud('DELETE', `heroes/${editedHero.id}`).its('status').should('eq', 200)
cy.crud('GET', `heroes/${editedHero.id}`, {allowedToFail: true})
.its('status')
.should('eq', 404)
})
Creation
Update
Delete
Creation
Update
Delete
// cypress/e2e/create-hero.cy.ts
it('should go through the add hero flow (ui-e2e)', () => {
cy.intercept("GET", `${Cypress.env("API_URL")}/heroes`)
.as("getHeroes")
cy.visit("/");
cy.wait("@getHeroes")
navToAddHero()
const newHero = {
name: faker.internet.userName(),
description: `description ${faker.internet.userName()}`,
}
cy.getByCy('input-detail-name').type(newHero.name)
cy.getByCy('input-detail-description').type(newHero.description)
cy.getByCy('save-button').click()
cy.location('pathname').should('eq', '/heroes')
cy.getByCy('heroes').should('be.visible')
cy.getByCyLike('hero-list-item').should('have.length.gt', 0)
cy.getByCy('hero-list')
.should('contain', newHero.name)
.and('contain', newHero.description)
cy.getEntityByProperty('hero', newHero.name).then(myHero =>
cy.crud('DELETE', `heroes/${myHero.id}`),
)
})
UI create
API delete
Assertions
Use even smaller steps with
TDD + e2e.
Always evaluate if you need the backend to gain confidence in your app's functionality.
You should only use true e2e tests when you need this confidence, and you should not have to repeat the same costly e2e tests everywhere.
If your backend is tested by its own e2e tests, your UI e2e needs at the front end are even less; be careful not to duplicate too much of the backend effort.
it('should go through the refresh flow', () => {
cy.intercept('GET', `${Cypress.env('API_URL')}/heroes`)
.as('getHeroes')
cy.visit('/')
cy.wait('@getHeroes')
navToAddHero()
cy.getByCy('refresh-button').click()
cy.location('pathname').should('eq', '/heroes')
cy.getByCy('hero-list').should('be.visible')
})
it('should go through the refresh flow (ui-integration)', () => {
cy.intercept('GET', `${Cypress.env('API_URL')}/heroes`, {
fixture: 'heroes',
}).as('stubbedGetHeroes')
cy.visit('/')
cy.wait('@stubbedGetHeroes')
navToAddHero()
cy.getByCy('refresh-button').click()
cy.location('pathname').should('eq', '/heroes')
cy.getByCy('hero-list').should('be.visible')
})
it('should go through the cancel flow', () => {
cy.intercept('GET', `${Cypress.env('API_URL')}/heroes`)
.as('getHeroes')
cy.visit('/heroes/add-hero')
cy.wait('@getHeroes')
cy.getByCy('cancel-button').click()
cy.location('pathname').should('eq', '/heroes')
cy.getByCy('hero-list').should('be.visible')
})
it('should go through the cancel flow (ui-integration)', () => {
cy.intercept('GET', `${Cypress.env('API_URL')}/heroes`, {
fixture: 'heroes',
}).as('stubbedGetHeroes')
cy.visit('/heroes/add-hero')
cy.wait('@stubbedGetHeroes')
cy.getByCy('cancel-button').click()
cy.location('pathname').should('eq', '/heroes')
cy.getByCy('hero-list').should('be.visible')
})
Any test covering the positive flows with cy.intercept()
is a good branching spot.
Start at the component level,
move up to ui-integration if further negative tests are not possible.
it('should handle Save', () => {
cy.intercept('POST', '*', {statusCode: 200})
.as('postHero')
cy.getByCy('save-button').click()
cy.wait('@postHero')
})
it('should handle non-200 Save', () => {
cy.intercept('POST', '*', {statusCode: 400, delay: 100})
.as('postHero')
cy.getByCy('save-button').click()
cy.getByCy('spinner')
cy.wait('@postHero')
cy.getByCy('error')
})
it('should handle Save (implementaiton details)', () => {
cy.spy(React, 'useState').as('useState')
cy.spy(reactQuery, 'useQueryClient').as('useMutation')
cy.spy(postHook, 'usePostEntity').as('usePostEntity')
cy.getByCy('save-button').click()
cy.get('@useState').should('have.been.called')
cy.get('@useMutation').should('have.been.calledOnce')
cy.get('@usePostEntity').should('have.been.calledOnce')
})
it('should handle Save', () => {
cy.intercept('POST', '*', {statusCode: 200})
.as('postHero')
cy.getByCy('save-button').click()
cy.wait('@postHero')
})
In component tests, lean more towards black box
to avoid testing implementation details.
Same idea in RTL with Mock Service Worker (msw), comparable to cy.intercept.
// src/components/NavBar.cy.tsx
import NavBar from './NavBar'
import {BrowserRouter} from 'react-router-dom'
import '../styles.scss'
describe('NavBar', () => {
it('should navigate to the correct routes', () => {
cy.mount(
<BrowserRouter>
<NavBar />
</BrowserRouter>,
)
cy.getByCy('nav-bar').within(() => {
cy.contains('p', 'Menu')
cy.getByCy('menu-list').children().should('have.length', 3)
['heroes', 'villains', 'about'].each((route: string) => {
cy.get(`[href="/${route}"]`).contains(route, {matchCase: false}).click()
cy.url().should('contain', route)
})
})
})
})
The quality of the feedback improves TDD and early defect detection.
The quality of the feedback with Cypress component testing improves TDD, component engineering and early defect detection.
Once we move up to routing & state management, TDD with e2e is more relevant.
With TDD the key idea is to start with something failing, do the minimum to get it to work, and then make it better.
the book
the app