Fix eslint errors

This commit is contained in:
Dmitriy Kovalenko
2020-10-01 17:06:51 +03:00
parent 6d3454d972
commit 37fe4e434c
291 changed files with 8024 additions and 2980 deletions

View File

@@ -39,3 +39,5 @@ npm/webpack-preprocessor/examples/use-babelrc/cypress/integration/spec.js
**/.history
**/.cy
**/.git
/npm/react/bin/*

1
.gitignore vendored
View File

@@ -323,4 +323,5 @@ $RECYCLE.BIN/
# Windows shortcuts
*.lnk
/npm/react/bin/*
# End of https://www.gitignore.io/api/osx,git,node,windows,intellij,linux

43
npm/react/.eslintrc Normal file
View File

@@ -0,0 +1,43 @@
{
"plugins": [
"cypress",
"@cypress/dev"
],
"extends": [
"plugin:@cypress/dev/general",
"plugin:@cypress/dev/tests",
"plugin:@cypress/dev/react"
],
"parser": "@typescript-eslint/parser",
"env": {
"cypress/globals": true
},
"globals": {
"jest": "readonly"
},
"rules": {
"no-console": "off",
"mocha/no-global-tests": "off",
"@typescript-eslint/no-unused-vars": "off",
"react/jsx-filename-extension": [
"warn",
{
"extensions": [
".js",
".jsx",
".tsx"
]
}
]
},
"overrides": [
{
"files": [
"lib/*"
],
"rules": {
"no-console": 1
}
}
]
}

View File

@@ -13,7 +13,10 @@
"root": "#cypress-root"
}
},
"ignoreTestFiles": ["**/__snapshots__/*", "**/__image_snapshots__/*"],
"ignoreTestFiles": [
"**/__snapshots__/*",
"**/__image_snapshots__/*"
],
"experimentalComponentTesting": true,
"experimentalFetchPolyfill": true
}

View File

@@ -21,18 +21,19 @@ describe('Component and API tests', () => {
// same or similar URL to the one the component is using
url: 'https://jsonplaceholder.cypress.io/users?_limit=3',
})
.its('body')
.should('have.length', 3)
.its('body')
.should('have.length', 3)
})
// another component test
it('shows stubbed users', () => {
cy.stub(window, 'fetch').resolves({
json: cy
.stub()
.resolves([{ id: 101, name: 'Test User' }])
.as('users'),
.stub()
.resolves([{ id: 101, name: 'Test User' }])
.as('users'),
})
// no mocking, just real request to the backend REST endpoint
mount(<Users />)
cy.get('li').should('have.length', 1)
@@ -43,10 +44,10 @@ describe('Component and API tests', () => {
cy.api({
url: 'https://jsonplaceholder.cypress.io/users/1',
})
.its('body')
.should('include', {
id: 1,
name: 'Leanne Graham',
})
.its('body')
.should('include', {
id: 1,
name: 'Leanne Graham',
})
})
})

View File

@@ -1,29 +1,29 @@
import React from 'react'
export class Users extends React.Component {
constructor(props) {
constructor (props) {
super(props)
this.state = {
users: [],
}
}
componentDidMount() {
componentDidMount () {
fetch('https://jsonplaceholder.cypress.io/users?_limit=3')
.then(response => {
return response.json()
})
.then(list => {
this.setState({
users: list,
})
.then((response) => {
return response.json()
})
.then((list) => {
this.setState({
users: list,
})
})
}
render() {
render () {
return (
<div>
{this.state.users.map(user => (
{this.state.users.map((user) => (
<li key={user.id}>
<strong>{user.id}</strong> - {user.name}
</li>

View File

@@ -8,10 +8,10 @@ describe('Counter with access', () => {
it('works', () => {
mount(<Counter />)
cy.contains('count: 0')
.click()
.contains('count: 1')
.click()
.contains('count: 2')
.click()
.contains('count: 1')
.click()
.contains('count: 2')
})
it('allows access via reference', () => {
@@ -19,15 +19,17 @@ describe('Counter with access', () => {
// the window.counter was set from the Counter's constructor
cy.window()
.should('have.property', 'counter')
.its('state')
.should('deep.equal', { count: 0 })
.should('have.property', 'counter')
.its('state')
.should('deep.equal', { count: 0 })
// let's change the state of the component
cy.window()
.its('counter')
.invoke('setState', {
count: 101,
})
.its('counter')
.invoke('setState', {
count: 101,
})
// the UI should update to reflect the new count
cy.contains('count: 101').should('be.visible')
})

View File

@@ -1,7 +1,7 @@
import React from 'react'
export class Counter extends React.Component {
constructor(props) {
constructor (props) {
super(props)
this.state = {
count: 0,
@@ -15,6 +15,7 @@ export class Counter extends React.Component {
'set window.counter to this component in window',
window.location.pathname,
)
window.counter = this
} else {
console.log('running outside Cypress')
@@ -27,7 +28,7 @@ export class Counter extends React.Component {
})
}
render() {
render () {
return <p onClick={this.click}>count: {this.state.count}</p>
}
}

View File

@@ -3,7 +3,7 @@ import { ThemeContext } from './context'
import { Toolbar } from './Toolbar.jsx'
export default class App extends React.Component {
render() {
render () {
// Use a Provider to pass the current theme to the tree below.
// Any component can read it, no matter how deep it is.
// In this example, we're passing "dark" as the current value.

View File

@@ -10,6 +10,7 @@ describe('Mocking context', () => {
<Toolbar />
</ThemeContext.Provider>,
)
// the label "mocked" was passed through React context
cy.contains('button', 'mocked').should('be.visible')
})

View File

@@ -3,7 +3,7 @@ import { ThemeContext } from './context'
// A component in the middle doesn't have to
// pass the theme down explicitly anymore.
export function Toolbar(props) {
export function Toolbar (props) {
return (
<div>
<ThemedButton />
@@ -11,7 +11,7 @@ export function Toolbar(props) {
)
}
function Button(props) {
function Button (props) {
return <button>{props.theme}</button>
}
@@ -20,7 +20,7 @@ class ThemedButton extends React.Component {
// React will find the closest theme Provider above and use its value.
// In this example, the current theme is "dark".
static contextType = ThemeContext
render() {
render () {
return <Button theme={this.context} />
}
}

View File

@@ -10,6 +10,7 @@ Cypress.Commands.add('myMount', () => {
Cypress.Commands.add('myMount2', () => {
const toMount = React.createElement('div', null, ['mount 2'])
return mount(toMount)
})

View File

@@ -6,13 +6,13 @@ import { mount } from 'cypress-react-unit-test'
import Button from './forward-ref.jsx'
/* eslint-env mocha */
describe('Button component', function() {
it('works', function() {
describe('Button component', function () {
it('works', function () {
mount(<Button>Hello, World</Button>)
cy.contains('Hello, World')
})
it('forwards refs as expected', function() {
it('forwards refs as expected', function () {
const ref = React.createRef()
mount(
@@ -20,6 +20,7 @@ describe('Button component', function() {
Hello, World
</Button>,
)
expect(ref).to.have.property('current')
// expect(ref.current).not.be.null;
})

View File

@@ -6,23 +6,23 @@ describe('framer-motion', () => {
it('Renders component and retries the animation', () => {
mount(<Motion />)
cy.get("[data-testid='motion']").should('have.css', 'border-radius', '50%')
cy.get("[data-testid='motion']").should('have.css', 'border-radius', '20%')
cy.get('[data-testid=\'motion\']').should('have.css', 'border-radius', '50%')
cy.get('[data-testid=\'motion\']').should('have.css', 'border-radius', '20%')
})
// looks like cy.tick issue. Refer to the https://github.com/bahmutov/cypress-react-unit-test/issues/420
// NOTE: looks like cy.tick issue. Refer to the https://github.com/bahmutov/cypress-react-unit-test/issues/420
it.skip('Mocks setTimeout and requestAnimationFrame', () => {
cy.clock()
mount(<Motion />)
// CI is slow, so check only the approximate values
cy.tick(800)
cy.get("[data-testid='motion']").within(element => {
cy.get('[data-testid=\'motion\']').within((element) => {
expect(parseInt(element.css('borderRadius'))).to.equal(43)
})
cy.tick(100)
cy.get("[data-testid='motion']").within(element => {
cy.get('[data-testid=\'motion\']').within((element) => {
expect(parseInt(element.css('borderRadius'))).to.equal(48)
})
})

View File

@@ -1,6 +1,6 @@
import React from 'react'
export default function CounterWithHooks({ initialCount = 0 }) {
export default function CounterWithHooks ({ initialCount = 0 }) {
const [count, setCount] = React.useState(initialCount)
const handleCountIncrement = React.useCallback(() => {

View File

@@ -6,8 +6,8 @@ import ReactDom from 'react-dom'
import { mount } from 'cypress-react-unit-test'
import CounterWithHooks from './counter-with-hooks.jsx'
describe('CounterWithHooks component', function() {
it('works', function() {
describe('CounterWithHooks component', function () {
it('works', function () {
mount(<CounterWithHooks initialCount={3} />, { React, ReactDom })
cy.contains('3')
})

View File

@@ -1,7 +1,7 @@
// example from https://reactjs.org/docs/hooks-overview.html
import React, { useState, useEffect } from 'react'
export default function Counter2WithHooks() {
export default function Counter2WithHooks () {
const [count, setCount] = useState(0)
useEffect(() => {

View File

@@ -5,16 +5,17 @@ import ReactDom from 'react-dom'
import { mount } from 'cypress-react-unit-test'
import Counter2WithHooks from './counter2-with-hooks.jsx'
describe('Counter2WithHooks', function() {
it('changes document title', function() {
describe('Counter2WithHooks', function () {
it('changes document title', function () {
mount(<Counter2WithHooks />, { React, ReactDom })
cy.contains('0')
cy.document().should('have.property', 'title', 'You clicked 0 times')
cy.log('Clicking changes document title')
cy.get('#increment')
.click()
.click()
.click()
.click()
cy.document().should('have.property', 'title', 'You clicked 2 times')
})
})

View File

@@ -1,20 +1,20 @@
// @ts-check
/// <reference types="cypress" />
import React, { useState, useCallback } from 'react'
// @ts-ignore
import { useState, useCallback } from 'react'
import { mountHook } from 'cypress-react-unit-test'
// testing example hook function from
// https://dev.to/jooforja/12-recipes-for-testing-react-applications-using-testing-library-1bh2#hooks
function useCounter() {
function useCounter () {
const [count, setCount] = useState(0)
const increment = useCallback(() => setCount(x => x + 1), [])
const increment = useCallback(() => setCount((x) => x + 1), [])
return { count, increment }
}
describe('useCounter hook', function() {
it('increments the count', function() {
mountHook(() => useCounter()).then(result => {
describe('useCounter hook', function () {
it('increments the count', function () {
mountHook(() => useCounter()).then((result) => {
expect(result.current.count).to.equal(0)
result.current.increment()
expect(result.current.count).to.equal(1)

View File

@@ -3,7 +3,7 @@ import { I18nextProvider } from 'react-i18next'
import i18n from './i18n'
import { LocalizedComponent } from './LocalizedComponent'
export function App() {
export function App () {
return (
<I18nextProvider i18n={i18n}>
<LocalizedComponent count={15} name="SomeUserName" />

View File

@@ -1,15 +1,13 @@
import * as React from 'react'
import { useTranslation, Trans } from 'react-i18next'
import { Trans } from 'react-i18next'
interface LocalizedComponentProps {
name: string
count: number
}
export function LocalizedComponent({ name, count }: LocalizedComponentProps) {
// See ./App.tsx for localization setup
const { t } = useTranslation()
// See ./App.tsx for localization setup
export function LocalizedComponent ({ name, count }: LocalizedComponentProps) {
return (
<Trans
i18nKey={count === 1 ? 'userMessagesUnread' : 'userMessagesUnread_plural'}

View File

@@ -2,32 +2,32 @@ import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
i18n
.use(initReactI18next) // passes i18n down to react-i18next
.init({
resources: {
en: {
translation: {
userMessagesUnread:
.use(initReactI18next) // passes i18n down to react-i18next
.init({
resources: {
en: {
translation: {
userMessagesUnread:
'Hello <1>{{name}}</1>, you have {{count}} unread message.',
userMessagesUnread_plural:
userMessagesUnread_plural:
'Hello <1>{{name}}</1>, you have {{count}} unread messages.',
},
},
ru: {
translation: {
userMessagesUnread:
},
ru: {
translation: {
userMessagesUnread:
'Привет, <1>{{name}}</1>, y тебя {{count}} непрочитанное сообщение.',
userMessagesUnread_plural:
userMessagesUnread_plural:
'Привет, <1>{{name}}</1>, y тебя {{count}} непрочитанных сообщений.',
},
},
},
lng: 'en',
fallbackLng: 'en',
},
lng: 'en',
fallbackLng: 'en',
interpolation: {
escapeValue: false,
},
})
interpolation: {
escapeValue: false,
},
})
export default i18n

View File

@@ -4,7 +4,7 @@ import SamoyedImage from './samoyed.jpg'
interface DogProps {}
export const Dog: React.FC<DogProps> = ({}) => {
export const Dog: React.FC<DogProps> = () => {
return (
<div>
<h1> Your dog is Samoyed: </h1>

View File

@@ -1,9 +1,10 @@
import * as React from 'react'
const LazyDog = React.lazy(() => import(/* webpackChunkName: "Dog" */ './Dog'))
interface LazyComponentProps {}
export const LazyComponent: React.FC<LazyComponentProps> = ({}) => {
export const LazyComponent: React.FC<LazyComponentProps> = () => {
return (
<div>
Loading a dog:

View File

@@ -2,6 +2,7 @@ import * as React from 'react'
import { LazyComponent } from './LazyComponent'
import { mount } from 'cypress-react-unit-test'
// NOTE: It doesn't work because of chunk splitting issue with webpack
describe.skip('React.lazy component with <Suspense />', () => {
it('renders and retries till component is loaded', () => {
mount(<LazyComponent />)

View File

@@ -2,8 +2,10 @@ import React from 'react'
const OtherComponent = React.lazy(() => import('./OtherComponent'))
export default App = () => (
<div className="app">
<OtherComponent />
</div>
)
export default function App () {
return (
<div className="app">
<OtherComponent />
</div>
)
}

View File

@@ -1,8 +1,8 @@
import React from 'react'
import { mount } from 'cypress-react-unit-test'
// import App from './App'
import App from './App'
// https://github.com/bahmutov/cypress-react-unit-test/issues/136
// NOTE: https://github.com/bahmutov/cypress-react-unit-test/issues/136
describe.skip('App loads', () => {
it('renders lazy component', () => {
mount(<App />)

View File

@@ -1,4 +1,4 @@
// https://github.com/bahmutov/cypress-react-unit-test/issues/136
// NOTE: https://github.com/bahmutov/cypress-react-unit-test/issues/136
// dynamic imports like this work in example projects, but not inside this repo
// probably due to webpack plugins not set up correctly ☹️
describe.skip('dynamic import', () => {

View File

@@ -9,16 +9,18 @@ import TextField from '@material-ui/core/TextField'
import Autocomplete from '@material-ui/lab/Autocomplete'
import { top100Films } from './top-100-movies'
export default function ComboBox() {
export default function ComboBox () {
return (
<Autocomplete
id="combo-box-demo"
options={top100Films}
getOptionLabel={option => option.title}
getOptionLabel={(option) => option.title}
style={{ width: 300 }}
renderInput={params => (
<TextField {...params} label="Combo box" variant="outlined" fullWidth />
)}
renderInput={(params) => {
return (
<TextField {...params} label="Combo box" variant="outlined" fullWidth />
)
}}
/>
)
}
@@ -29,11 +31,13 @@ it('finds my favorite movie', () => {
<Autocomplete
id="combo-box-demo"
options={top100Films}
getOptionLabel={option => option.title}
getOptionLabel={(option) => option.title}
style={{ width: 300 }}
renderInput={params => (
<TextField {...params} label="Combo box" variant="outlined" fullWidth />
)}
renderInput={(params) => {
return (
<TextField {...params} label="Combo box" variant="outlined" fullWidth />
)
}}
/>,
{
stylesheets: [
@@ -46,8 +50,9 @@ it('finds my favorite movie', () => {
cy.get('#combo-box-demo').click()
cy.focused().type('god')
cy.contains('The Godfather')
.should('be.visible')
.and('have.class', 'MuiAutocomplete-option')
.click()
.should('be.visible')
.and('have.class', 'MuiAutocomplete-option')
.click()
cy.get('#combo-box-demo').should('have.value', 'The Godfather')
})

View File

@@ -17,9 +17,9 @@ const GreenCheckbox = withStyles({
},
},
checked: {},
})(props => <Checkbox color="default" {...props} />)
})((props) => <Checkbox color="default" {...props} />)
export default function CheckboxLabels() {
export default function CheckboxLabels () {
const [state, setState] = React.useState({
checkedA: true,
checkedB: true,
@@ -27,8 +27,10 @@ export default function CheckboxLabels() {
checkedG: true,
})
const handleChange = name => event => {
setState({ ...state, [name]: event.target.checked })
const handleChange = (name) => {
return (event) => {
setState({ ...state, [name]: event.target.checked })
}
}
return (

View File

@@ -8,19 +8,21 @@ import Divider from '@material-ui/core/Divider'
import InboxIcon from '@material-ui/icons/Inbox'
import DraftsIcon from '@material-ui/icons/Drafts'
const useStyles = makeStyles(theme => ({
root: {
width: '100%',
maxWidth: 360,
backgroundColor: theme.palette.background.paper,
},
}))
const useStyles = makeStyles((theme) => {
return {
root: {
width: '100%',
maxWidth: 360,
backgroundColor: theme.palette.background.paper,
},
}
})
function ListItemLink(props) {
function ListItemLink (props) {
return <ListItem button component="a" {...props} />
}
export default function SimpleList() {
export default function SimpleList () {
const classes = useStyles()
return (

View File

@@ -11,6 +11,7 @@ it('renders a list item', () => {
<ListItemText primary={'my example list item'} />
</ListItem>,
)
cy.contains('my example list item')
})
@@ -19,9 +20,9 @@ it('renders full list', () => {
cy.viewport(500, 800)
mount(<SimpleList />)
cy.contains('Drafts')
.click()
.wait(1000)
.click()
.wait(1000)
.click()
.click()
.wait(1000)
.click()
.wait(1000)
.click()
})

View File

@@ -9,27 +9,30 @@ import FormHelperText from '@material-ui/core/FormHelperText'
import FormControl from '@material-ui/core/FormControl'
import Select from '@material-ui/core/Select'
const useStyles = makeStyles(theme => ({
formControl: {
margin: theme.spacing(1),
minWidth: 120,
},
selectEmpty: {
marginTop: theme.spacing(2),
},
}))
const useStyles = makeStyles((theme) => {
return {
formControl: {
margin: theme.spacing(1),
minWidth: 120,
},
selectEmpty: {
marginTop: theme.spacing(2),
},
}
})
export default function SimpleSelect() {
export default function SimpleSelect () {
const classes = useStyles()
const [age, setAge] = React.useState('')
const inputLabel = React.useRef(null)
const [labelWidth, setLabelWidth] = React.useState(0)
React.useEffect(() => {
setLabelWidth(inputLabel.current.offsetWidth)
}, [])
const handleChange = event => {
const handleChange = (event) => {
setAge(event.target.value)
}
@@ -126,7 +129,7 @@ export default function SimpleSelect() {
id="demo-simple-select-error"
value={age}
onChange={handleChange}
renderValue={value => `⚠️ - ${value}`}
renderValue={(value) => `⚠️ - ${value}`}
>
<MenuItem value="">
<em>None</em>
@@ -255,10 +258,12 @@ it('renders selects', () => {
'https://fonts.googleapis.com/icon?family=Material+Icons',
],
})
cy.get('#demo-simple-select').click()
cy.contains('[role=option]', 'Twenty')
.should('be.visible')
.click()
.should('be.visible')
.click()
// check that other select has changed
cy.contains('#demo-simple-select-outlined', 'Twenty').should('be.visible')
})

View File

@@ -7,9 +7,8 @@ import React from 'react'
import Rating from '@material-ui/lab/Rating'
import Typography from '@material-ui/core/Typography'
import Box from '@material-ui/core/Box'
import { ExpansionPanelActions } from '@material-ui/core'
export default function SimpleRating({ onSetRating }) {
export default function SimpleRating ({ onSetRating }) {
const [value, setValue] = React.useState(2)
return (
@@ -47,15 +46,17 @@ export default function SimpleRating({ onSetRating }) {
it('renders simple rating', () => {
cy.viewport(300, 400)
const onSetRating = cy.stub()
mount(<SimpleRating onSetRating={onSetRating} />, {
stylesheets: [
'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap',
'https://fonts.googleapis.com/icon?family=Material+Icons',
],
})
cy.get('label[for=simple-controlled-4]')
.click()
.then(() => {
expect(onSetRating).to.have.been.calledWith(4)
})
.click()
.then(() => {
expect(onSetRating).to.have.been.calledWith(4)
})
})

View File

@@ -5,7 +5,7 @@ export const top100Films = [
{ title: 'The Godfather: Part II', year: 1974 },
{ title: 'The Dark Knight', year: 2008 },
{ title: '12 Angry Men', year: 1957 },
{ title: "Schindler's List", year: 1993 },
{ title: 'Schindler\'s List', year: 1993 },
{ title: 'Pulp Fiction', year: 1994 },
{ title: 'The Lord of the Rings: The Return of the King', year: 2003 },
{ title: 'The Good, the Bad and the Ugly', year: 1966 },
@@ -15,7 +15,7 @@ export const top100Films = [
{ title: 'Forrest Gump', year: 1994 },
{ title: 'Inception', year: 2010 },
{ title: 'The Lord of the Rings: The Two Towers', year: 2002 },
{ title: "One Flew Over the Cuckoo's Nest", year: 1975 },
{ title: 'One Flew Over the Cuckoo\'s Nest', year: 1975 },
{ title: 'Goodfellas', year: 1990 },
{ title: 'The Matrix', year: 1999 },
{ title: 'Seven Samurai', year: 1954 },
@@ -23,7 +23,7 @@ export const top100Films = [
{ title: 'City of God', year: 2002 },
{ title: 'Se7en', year: 1995 },
{ title: 'The Silence of the Lambs', year: 1991 },
{ title: "It's a Wonderful Life", year: 1946 },
{ title: 'It\'s a Wonderful Life', year: 1946 },
{ title: 'Life Is Beautiful', year: 1997 },
{ title: 'The Usual Suspects', year: 1995 },
{ title: 'Léon: The Professional', year: 1994 },
@@ -96,7 +96,7 @@ export const top100Films = [
{ title: 'Dangal', year: 2016 },
{ title: 'The Sting', year: 1973 },
{ title: '2001: A Space Odyssey', year: 1968 },
{ title: "Singin' in the Rain", year: 1952 },
{ title: 'Singin\' in the Rain', year: 1952 },
{ title: 'Toy Story', year: 1995 },
{ title: 'Bicycle Thieves', year: 1948 },
{ title: 'The Kid', year: 1921 },

View File

@@ -3,11 +3,11 @@ import { makeAutoObservable } from 'mobx'
export class Timer {
secondsPassed = 0
constructor() {
constructor () {
makeAutoObservable(this)
}
increaseTimer() {
increaseTimer () {
this.secondsPassed += 1
}
}

View File

@@ -7,11 +7,13 @@ describe('MobX v6', { viewportWidth: 200, viewportHeight: 100 }, () => {
context('TimerView', () => {
it('increments every second', () => {
const myTimer = new Timer()
mount(<TimerView timer={myTimer} />)
cy.contains('Seconds passed: 0').then(() => {
// we can increment the timer from outside
myTimer.increaseTimer()
})
cy.contains('Seconds passed: 1')
// by wrapping the timer and giving it an alias
@@ -30,8 +32,8 @@ describe('MobX v6', { viewportWidth: 200, viewportHeight: 100 }, () => {
// we can also ask the timer for the current value
cy.get('@timer').invoke('increaseTimer')
cy.get('@timer')
.its('secondsPassed')
.should('equal', 5)
.its('secondsPassed')
.should('equal', 5)
})
})
})

View File

@@ -10,14 +10,14 @@ it('renders user data', () => {
}
// window.fetch
cy.window().then(win => {
cy.window().then((win) => {
// Cypress cleans up stubs automatically after each test
// https://on.cypress.io/stub
cy.stub(win, 'fetch')
.withArgs('/123')
.resolves({
json: () => Promise.resolve(fakeUser),
})
.withArgs('/123')
.resolves({
json: () => Promise.resolve(fakeUser),
})
})
mount(<User id="123" />)

View File

@@ -1,13 +1,13 @@
// example from https://reactjs.org/docs/testing-recipes.html#data-fetching
import React, { useState, useEffect } from 'react'
export default function User(props) {
export default function User (props) {
const [user, setUser] = useState(null)
function fetchUserData(id) {
fetch('/' + id)
.then(response => response.json())
.then(setUser)
function fetchUserData (id) {
fetch(`/${id}`)
.then((response) => response.json())
.then(setUser)
}
useEffect(() => {

View File

@@ -4,28 +4,28 @@ import React from 'react'
import axios from 'axios'
export class Users extends React.Component {
constructor(props) {
constructor (props) {
super(props)
this.state = {
users: [],
}
}
componentDidMount() {
componentDidMount () {
axios
.get('https://jsonplaceholder.cypress.io/users?_limit=3')
.then(response => {
// JSON responses are automatically parsed.
this.setState({
users: response.data,
})
.get('https://jsonplaceholder.cypress.io/users?_limit=3')
.then((response) => {
// JSON responses are automatically parsed.
this.setState({
users: response.data,
})
})
}
render() {
render () {
return (
<div>
{this.state.users.map(user => (
{this.state.users.map((user) => (
<li key={user.id}>
<strong>{user.id}</strong> - {user.name}
</li>

View File

@@ -14,15 +14,15 @@ describe('Mocking Axios', () => {
// https://github.com/bahmutov/cypress-react-unit-test/issues/338
it('mocks axios.get', () => {
cy.stub(Axios, 'get')
.resolves({
data: [
{
id: 101,
name: 'Test User',
},
],
})
.as('get')
.resolves({
data: [
{
id: 101,
name: 'Test User',
},
],
})
.as('get')
mount(<Users />)
// only the test user should be shown

View File

@@ -3,15 +3,15 @@ import React from 'react'
import { get } from 'axios'
export class Users extends React.Component {
constructor(props) {
constructor (props) {
super(props)
this.state = {
users: [],
}
}
componentDidMount() {
get('https://jsonplaceholder.cypress.io/users?_limit=3').then(response => {
componentDidMount () {
get('https://jsonplaceholder.cypress.io/users?_limit=3').then((response) => {
// JSON responses are automatically parsed.
this.setState({
users: response.data,
@@ -19,10 +19,10 @@ export class Users extends React.Component {
})
}
render() {
render () {
return (
<div>
{this.state.users.map(user => (
{this.state.users.map((user) => (
<li key={user.id}>
<strong>{user.id}</strong> - {user.name}
</li>

View File

@@ -14,15 +14,15 @@ describe('Mocking Axios named import get', () => {
it('mocks get', () => {
console.log('Axios', Axios)
cy.stub(Axios, 'get')
.resolves({
data: [
{
id: 101,
name: 'Test User',
},
],
})
.as('get')
.resolves({
data: [
{
id: 101,
name: 'Test User',
},
],
})
.as('get')
mount(<Users />)
// only the test user should be shown

View File

@@ -3,15 +3,15 @@ import React from 'react'
import { get } from './axios-api'
export class Users extends React.Component {
constructor(props) {
constructor (props) {
super(props)
this.state = {
users: [],
}
}
componentDidMount() {
get('https://jsonplaceholder.cypress.io/users?_limit=3').then(response => {
componentDidMount () {
get('https://jsonplaceholder.cypress.io/users?_limit=3').then((response) => {
// JSON responses are automatically parsed.
this.setState({
users: response.data,
@@ -19,10 +19,10 @@ export class Users extends React.Component {
})
}
render() {
render () {
return (
<div>
{this.state.users.map(user => (
{this.state.users.map((user) => (
<li key={user.id}>
<strong>{user.id}</strong> - {user.name}
</li>

View File

@@ -13,15 +13,15 @@ describe('Mocking wrapped Axios', () => {
it('mocks get', () => {
console.log('Axios', Axios)
cy.stub(Axios, 'get')
.resolves({
data: [
{
id: 101,
name: 'Test User',
},
],
})
.as('get')
.resolves({
data: [
{
id: 101,
name: 'Test User',
},
],
})
.as('get')
mount(<Users />)
// only the test user should be shown

View File

@@ -2,12 +2,12 @@
import React from 'react'
import Map from './map'
export default function Contact(props) {
export default function Contact (props) {
return (
<div>
<address>
Contact {props.name} via{' '}
<a data-testid="email" href={'mailto:' + props.email}>
<a data-testid="email" href={`mailto:${props.email}`}>
email
</a>{' '}
or on their{' '}

View File

@@ -3,10 +3,10 @@ import React from 'react'
import { GoogleMap, withGoogleMap, withScriptjs } from 'react-google-maps'
const GMap = withScriptjs(
withGoogleMap(props => <GoogleMap id="example-map" center={props.center} />),
withGoogleMap((props) => <GoogleMap id="example-map" center={props.center} />),
)
export default function Map(props) {
export default function Map (props) {
return (
<GMap
googleMapURL="https://maps.googleapis.com/maps/api/js?key=AIzaSyC4R6AN7SmujjPUIGKdyao2Kqitzr1kiRg&v=3.exp&libraries=geometry,drawing,places"

View File

@@ -2,29 +2,31 @@ import React from 'react'
import { mount } from 'cypress-react-unit-test'
// Component "Contact" has child component "Map" that is expensive to render
import Contact from './contact'
import Map from './map'
import * as MapModule from './map'
describe('Mock imported component', () => {
// mock Map component used by Contact component
// whenever React tries to instantiate using Map constructor
// call DummyMap constructor
const DummyMap = props => (
<div data-testid="map">
const DummyMap = (props) => {
return (
<div data-testid="map">
DummyMap {props.center.lat}:{props.center.long}
</div>
)
</div>
)
}
context('by stubbing React.createElement method', () => {
// stubbing like this works, but is less than ideal
it('should render contact information', () => {
cy.stub(React, 'createElement')
.callThrough()
.withArgs(Map)
.callsFake((constructor, props) => React.createElement(DummyMap, props))
.callThrough()
.withArgs(Map)
.callsFake((constructor, props) => React.createElement(DummyMap, props))
cy.viewport(500, 500)
const center = { lat: 0, long: 0 }
mount(
<Contact
name="Joni Baez"
@@ -47,6 +49,7 @@ describe('Mock imported component', () => {
cy.viewport(500, 500)
const center = { lat: 0, long: 0 }
mount(
<Contact
name="Joni Baez"

View File

@@ -1,11 +1,11 @@
import React from 'react'
import { fetchIngredients as defaultFetchIngredients } from './services'
export default function PizzaProps({ fetchIngredients }) {
export default function PizzaProps ({ fetchIngredients }) {
const [ingredients, setIngredients] = React.useState([])
const handleCook = () => {
fetchIngredients().then(response => {
fetchIngredients().then((response) => {
setIngredients(response.args.ingredients)
})
}
@@ -16,9 +16,11 @@ export default function PizzaProps({ fetchIngredients }) {
<button onClick={handleCook}>Cook</button>
{ingredients.length > 0 && (
<ul>
{ingredients.map(ingredient => (
<li key={ingredient}>{ingredient}</li>
))}
{ingredients.map((ingredient) => {
return (
<li key={ingredient}>{ingredient}</li>
)
})}
</ul>
)}
</>

View File

@@ -7,8 +7,9 @@ const ingredients = ['bacon', 'tomato', 'mozzarella', 'pineapples']
describe('PizzaProps', () => {
it('mocks method in the default props', () => {
cy.stub(PizzaProps.defaultProps, 'fetchIngredients')
.resolves({ args: { ingredients } })
.as('fetchMock')
.resolves({ args: { ingredients } })
.as('fetchMock')
mount(<PizzaProps />)
cy.contains('button', /cook/i).click()

View File

@@ -1,11 +1,11 @@
import React from 'react'
import { fetchIngredients as defaultFetchIngredients } from './services'
export default function RemotePizza() {
export default function RemotePizza () {
const [ingredients, setIngredients] = React.useState([])
const handleCook = () => {
defaultFetchIngredients().then(response => {
defaultFetchIngredients().then((response) => {
setIngredients(response.args.ingredients)
})
}
@@ -16,9 +16,11 @@ export default function RemotePizza() {
<button onClick={handleCook}>Cook</button>
{ingredients.length > 0 && (
<ul>
{ingredients.map(ingredient => (
<li key={ingredient}>{ingredient}</li>
))}
{ingredients.map((ingredient) => {
return (
<li key={ingredient}>{ingredient}</li>
)
})}
</ul>
)}
</>

View File

@@ -9,8 +9,9 @@ const ingredients = ['bacon', 'tomato', 'mozzarella', 'pineapples']
describe('RemotePizza', () => {
it('mocks named import from services', () => {
cy.stub(services, 'fetchIngredients')
.resolves({ args: { ingredients } })
.as('fetchMock')
.resolves({ args: { ingredients } })
.as('fetchMock')
mount(<RemotePizza />)
cy.contains('button', /cook/i).click()

View File

@@ -1,4 +1,5 @@
export const fetchIngredients = () =>
fetch(
export const fetchIngredients = () => {
return fetch(
'https://httpbin.org/anything?ingredients=bacon&ingredients=mozzarella&ingredients=pineapples',
).then(r => r.json())
).then((r) => r.json())
}

View File

@@ -10,10 +10,12 @@ describe('reactive-state Counter', () => {
<Counter />
</div>,
)
cy.contains('.count', '0')
.click()
.click()
.click()
.click()
.click()
.click()
cy.contains('.count', '3')
})
})

View File

@@ -8,7 +8,7 @@ const Counters = () => {
sum: 0,
})
const increment = i => {
const increment = (i) => {
state.counts[i]++
state.sum++
}

View File

@@ -10,10 +10,12 @@ describe('reactive-state Counters', () => {
<Counters />
</div>,
)
cy.contains('.count', '0')
.click()
.click()
.click()
.click()
.click()
.click()
// increments the counter itself
cy.contains('.count', '3')
// increments the sum
@@ -21,17 +23,20 @@ describe('reactive-state Counters', () => {
// add two more counters
cy.contains('Add Counter')
.click()
.click()
.click()
.click()
cy.get('.counts .count').should('have.length', 3)
// clicking the new counters increments the sum
cy.get('.count')
.eq(1)
.click()
.eq(1)
.click()
cy.contains('.sum', '4')
cy.get('.count')
.eq(2)
.click()
.eq(2)
.click()
cy.contains('.sum', '5')
})
})

View File

@@ -7,11 +7,13 @@ const AddTodo = ({ onAdd }) => {
})
// handle events
const handleEnter = e => e.key === 'Enter' && handleAdd()
const handleEnter = (e) => e.key === 'Enter' && handleAdd()
const handleAdd = () => {
if (!state.input) return // ignore empty input
const todo = { title: state.input, completed: false } // make todo
onAdd(todo) // add it
state.input = '' // clear input
}

View File

@@ -8,8 +8,8 @@ const Todos = () => {
todos: [],
})
const removeTodo = i => state.todos.splice(i, 1)
const addTodo = todo => state.todos.push(todo)
const removeTodo = (i) => state.todos.splice(i, 1)
const addTodo = (todo) => state.todos.push(todo)
return (
<div className="container">
@@ -18,9 +18,11 @@ const Todos = () => {
{!state.todos.length && <div className="no-todos"> No Todos added </div>}
<div className="todos">
{state.todos.map((todo, i) => (
<Todo todo={todo} key={i} onRemove={() => removeTodo(i)} />
))}
{state.todos.map((todo, i) => {
return (
<Todo todo={todo} key={i} onRemove={() => removeTodo(i)} />
)
})}
</div>
</div>
)

View File

@@ -10,23 +10,26 @@ describe('reactive-state Todos', () => {
<Todos />
</div>,
)
cy.get('.add-todo input').type('code{enter}')
cy.get('.add-todo input').type('test')
cy.get('.add-todo')
.contains('Add')
.click()
.contains('Add')
.click()
// now check things
cy.get('.todos .todo').should('have.length', 2)
// remove the first one
cy.get('.todos .todo')
.first()
.should('contain', 'code')
.find('.todo_remove')
.click()
.first()
.should('contain', 'code')
.find('.todo_remove')
.click()
// single todo left
cy.get('.todos .todo')
.should('have.length', 1)
.first()
.should('contain', 'test')
.should('have.length', 1)
.first()
.should('contain', 'test')
})
})

View File

@@ -5,7 +5,7 @@ import Todos from './Todos'
import Select from './Select'
class App extends Component {
render() {
render () {
return (
<div className="App">
<header className="App-header">

View File

@@ -6,10 +6,11 @@ class Note extends React.Component {
saved: '',
}
onChange = evt => {
onChange = (evt) => {
this.setState({
content: evt.target.value,
})
console.log('updating content')
}
@@ -20,7 +21,8 @@ class Note extends React.Component {
}
load = () => {
var me = this
let me = this
setTimeout(() => {
me.setState({
data: [{ title: 'test' }, { title: 'test2' }],
@@ -28,7 +30,7 @@ class Note extends React.Component {
}, 3000)
}
render() {
render () {
return (
<React.Fragment>
<label htmlFor="change">Change text</label>
@@ -36,11 +38,13 @@ class Note extends React.Component {
<div data-testid="saved">{this.state.saved}</div>
{this.state.data && (
<div data-testid="data">
{this.state.data.map(item => (
<div data-testid="item" className="item">
{item.title}
</div>
))}
{this.state.data.map((item) => {
return (
<div data-testid="item" className="item">
{item.title}
</div>
)
})}
</div>
)}
<div>

View File

@@ -17,9 +17,9 @@ describe('Note', () => {
// there is a built-in delay in loading the data
// but we don't worry about it - we just check if the text eventually appears
cy.get('[data-testid=item]')
.should('have.length', 2)
.and('be.visible')
.first()
.should('have.text', 'test')
.should('have.length', 2)
.and('be.visible')
.first()
.should('have.text', 'test')
})
})

View File

@@ -1,37 +1,41 @@
import React from 'react'
import './Todos.css'
const Todos = ({ todos, select, selected }) => (
<React.Fragment>
{todos.map(todo => (
<React.Fragment key={todo.title}>
<h3
data-testid="item"
className={
selected && selected.title === todo.title ? 'selected' : ''
}
>
{todo.title}
</h3>
<div>{todo.description}</div>
<button onClick={() => select(todo)}>Select</button>
</React.Fragment>
))}
</React.Fragment>
)
const Todos = ({ todos, select, selected }) => {
return (
<React.Fragment>
{todos.map((todo) => {
return (
<React.Fragment key={todo.title}>
<h3
data-testid="item"
className={
selected && selected.title === todo.title ? 'selected' : ''
}
>
{todo.title}
</h3>
<div>{todo.description}</div>
<button onClick={() => select(todo)}>Select</button>
</React.Fragment>
)
})}
</React.Fragment>
)
}
class TodosContainer extends React.Component {
state = {
todo: void 0,
}
select = todo => {
select = (todo) => {
this.setState({
todo,
})
}
render() {
render () {
return (
<Todos {...this.props} select={this.select} selected={this.state.todo} />
)

View File

@@ -15,6 +15,7 @@ it('Todo - should create snapshot', () => {
]}
/>,
)
cy.get('[data-testid=item]').should('have.length', 2)
// disabled snapshot commands for now
// to speed up bundling
@@ -23,25 +24,26 @@ it('Todo - should create snapshot', () => {
// entire test area
cy.get('#cypress-root')
.invoke('html')
.then(pretty)
.should(
'equal',
stripIndent`
.invoke('html')
.then(pretty)
.should(
'equal',
stripIndent`
<h3 data-testid="item" class="">item1</h3>
<div>an item</div><button>Select</button>
<h3 data-testid="item" class="">item2</h3>
<div>another item</div><button>Select</button>
`,
)
)
cy.contains('[data-testid=item]', 'item1').should('be.visible')
// selecting works
cy.contains('[data-testid=item]', 'item2')
.next()
.should('have.text', 'another item')
.next()
.should('have.text', 'Select')
.click()
.next()
.should('have.text', 'another item')
.next()
.should('have.text', 'Select')
.click()
cy.contains('[data-testid=item]', 'item2').should('have.class', 'selected')
})

View File

@@ -1,7 +1,8 @@
function add(a, b) {
function add (a, b) {
if (a > 0 && b > 0) {
return a + b
}
throw new Error('parameters must be larger than zero')
}

View File

@@ -3,6 +3,7 @@ import add from './add'
describe('add', () => {
it('testing addition', () => {
const actual = add(1, 2)
expect(actual).to.equal(3)
})

View File

@@ -3,6 +3,7 @@ import add from './add'
describe('add', () => {
it('testing addition2', () => {
const actual = add(2, 2)
expect(actual).to.equal(4)
})
})

View File

@@ -2,7 +2,7 @@ import React from 'react'
import { getProducts } from '../products'
class AProduct extends React.Component {
constructor(props) {
constructor (props) {
super(props)
this.state = {
myName: props.name,
@@ -10,13 +10,13 @@ class AProduct extends React.Component {
}
}
order() {
order () {
this.setState({
orderCount: this.state.orderCount + 1,
})
}
render() {
render () {
return (
<div className="product">
<span className="name">{this.state.myName}</span>
@@ -31,7 +31,7 @@ class AProduct extends React.Component {
const Products = ({ products }) => (
<React.Fragment>
{products.map(product => (
{products.map((product) => (
<AProduct key={product.id} name={product.name} />
))}
</React.Fragment>
@@ -42,16 +42,16 @@ class ProductsContainer extends React.Component {
products: [],
}
componentDidMount() {
componentDidMount () {
// for now use promises
return getProducts().then(products => {
return getProducts().then((products) => {
this.setState({
products,
})
})
}
render() {
render () {
return (
<div className="product-container">
<Products products={this.state.products} />

View File

@@ -9,15 +9,16 @@ describe('Selecting by React props and state', () => {
context('without delay', () => {
beforeEach(() => {
cy.stub(window, 'fetch')
.withArgs('http://myapi.com/products')
.resolves({
json: cy.stub().resolves({
products: [
{ id: 1, name: 'First item' },
{ id: 2, name: 'Second item' },
],
}),
})
.withArgs('http://myapi.com/products')
.resolves({
json: cy.stub().resolves({
products: [
{ id: 1, name: 'First item' },
{ id: 2, name: 'Second item' },
],
}),
})
mount(<ProductsList />)
// to find DOM elements by React component constructor name, props, or state
@@ -35,32 +36,35 @@ describe('Selecting by React props and state', () => {
// find the top level <ProductsContainer> that we have mounted
// under imported name "ProductsList"
cy.react('ProductsContainer')
.first()
.should('have.class', 'product-container')
.first()
.should('have.class', 'product-container')
// find all instances of <AProduct> component
cy.react('AProduct').should('have.length', 2)
// find a single instance with prop
// <AProduct name={'Second item'} />
cy.react('AProduct', { props: { name: 'Second item' } })
.first()
.find('.name')
.and('have.text', 'Second item')
.first()
.find('.name')
.and('have.text', 'Second item')
})
it('find React components', () => {
cy.log('**cy.getReact**')
// returns React component wrapper with props
cy.getReact('AProduct', { props: { name: 'Second item' } })
.getProps()
.should('deep.equal', { name: 'Second item' })
.getProps()
.should('deep.equal', { name: 'Second item' })
cy.getReact('AProduct', { props: { name: 'First item' } })
// get single prop
.getProps('name')
.should('eq', 'First item')
// get single prop
.getProps('name')
.should('eq', 'First item')
cy.log('**.getCurrentState**')
cy.getReact('AProduct', { props: { name: 'Second item' } })
.getCurrentState()
.should('include', { myName: 'Second item' })
.getCurrentState()
.should('include', { myName: 'Second item' })
// find component using state
cy.getReact('AProduct', { state: { myName: 'Second item' } }).should(
@@ -71,47 +75,47 @@ describe('Selecting by React props and state', () => {
it('chains getReact', () => {
// note that by itself, the component is found
cy.getReact('AProduct', { props: { name: 'First item' } })
.getProps('name')
.should('eq', 'First item')
.getProps('name')
.should('eq', 'First item')
// chaining getReact
cy.getReact('ProductsContainer')
.getReact('AProduct', { props: { name: 'First item' } })
.getProps('name')
.should('eq', 'First item')
.getReact('AProduct', { props: { name: 'First item' } })
.getProps('name')
.should('eq', 'First item')
})
it('finds components by props and state', () => {
// by clicking on the Order button we change the
// internal state of that component
cy.contains('.product', 'First item')
.find('button.order')
.click()
.wait(1000)
.find('button.order')
.click()
.wait(1000)
// the component is there for sure, since the DOM has updated
cy.contains('.product', '1')
.find('.name')
.should('have.text', 'First item')
.find('.name')
.should('have.text', 'First item')
// now find that component using the state value
cy.react('AProduct', { state: { orderCount: 1 } })
.find('.name')
.should('have.text', 'First item')
.find('.name')
.should('have.text', 'First item')
})
it('finds components by props and state (click twice)', () => {
// by clicking on the Order button we change the
// internal state of that component
cy.contains('.product', 'First item')
.find('button.order')
.click()
.click()
.find('button.order')
.click()
.click()
// now find that component using the state value
cy.react('AProduct', { state: { orderCount: 2 } })
.find('.name')
.should('have.text', 'First item')
.find('.name')
.should('have.text', 'First item')
})
})
@@ -129,9 +133,10 @@ describe('Selecting by React props and state', () => {
}
cy.stub(window, 'fetch')
.withArgs('http://myapi.com/products')
// simulate slow load by delaying the response
.resolves(Cypress.Promise.resolve(response).delay(1000))
.withArgs('http://myapi.com/products')
// simulate slow load by delaying the response
.resolves(Cypress.Promise.resolve(response).delay(1000))
mount(<ProductsList />)
// to find DOM elements by React component constructor name, props, or state

View File

@@ -1,9 +1,11 @@
export const getProducts = () => {
console.log('fetch products')
return fetch('http://myapi.com/products')
.then(r => r.json())
.then(json => {
console.log('products', json.products)
return json.products
})
.then((r) => r.json())
.then((json) => {
console.log('products', json.products)
return json.products
})
}

View File

@@ -3,15 +3,15 @@ import { getProducts } from './products'
describe('products', () => {
it('should return a list', () => {
const fetchStub = cy
.stub(window, 'fetch')
.withArgs('http://myapi.com/products')
.resolves({
json: cy.stub().resolves({
products: [{ id: 1, name: 'test' }],
}),
})
.stub(window, 'fetch')
.withArgs('http://myapi.com/products')
.resolves({
json: cy.stub().resolves({
products: [{ id: 1, name: 'test' }],
}),
})
return getProducts().then(list => {
return getProducts().then((list) => {
expect(list).to.have.length(1)
expect(fetchStub).to.have.been.calledOnce.and.have.been.calledWith(
'http://myapi.com/products',

View File

@@ -3,12 +3,12 @@ import { Dropdown } from 'react-bootstrap'
import { mount } from 'cypress-react-unit-test'
class UserControls extends React.Component {
constructor(props) {
constructor (props) {
super(props)
this.state = {}
}
render() {
render () {
return (
<Dropdown>
<Dropdown.Toggle>Top Toggle</Dropdown.Toggle>
@@ -30,6 +30,7 @@ describe('react-bootstrap Dropdown', () => {
mount(<UserControls />, {
cssFile: 'node_modules/bootstrap/dist/css/bootstrap.min.css',
})
cy.contains('Top Toggle').click()
cy.contains('li', 'First').should('be.visible')
cy.get('li').should('have.length', 3)

View File

@@ -3,7 +3,7 @@ import { Button, Modal } from 'react-bootstrap'
import { mount } from 'cypress-react-unit-test'
export class Example extends React.Component {
constructor(props, context) {
constructor (props, context) {
super(props, context)
this.handleShow = this.handleShow.bind(this)
@@ -14,15 +14,15 @@ export class Example extends React.Component {
}
}
handleClose() {
handleClose () {
this.setState({ show: false })
}
handleShow() {
handleShow () {
this.setState({ show: true })
}
render() {
render () {
return (
<div>
This text is all that renders. And the modal is not rendered, regardless
@@ -53,6 +53,7 @@ describe('react-bootstrap Modal', () => {
mount(<Example />, {
cssFile: 'node_modules/bootstrap/dist/css/bootstrap.min.css',
})
// confirm modal is visible
cy.contains('h4', 'Text in a modal').should('be.visible')
cy.contains('button', 'Close').click()

View File

@@ -1,7 +1,7 @@
import React from 'react'
import { Routes, Route, Link } from 'react-router-dom'
function Home() {
function Home () {
return (
<div>
<h2>Home</h2>
@@ -10,7 +10,7 @@ function Home() {
)
}
function About() {
function About () {
return <h2>About</h2>
}

View File

@@ -15,13 +15,14 @@ describe('React Router', () => {
cy.get('nav').should('be.visible')
cy.contains('Home')
.click()
.location('pathname')
.should('equal', '/') // Home route
.click()
.location('pathname')
.should('equal', '/') // Home route
cy.contains('h2', 'Home')
cy.contains('About')
.click()
.location('pathname')
.should('equal', '/about') // About route
.click()
.location('pathname')
.should('equal', '/about') // About route
})
})

View File

@@ -3,44 +3,47 @@ import React from 'react'
import PropTypes from 'prop-types'
export default class MouseMovement extends React.Component {
constructor(props) {
constructor (props) {
console.log('MouseMovement constructor')
super(props)
this.state = {
timer: undefined,
}
this.timeout = this.timeout.bind(this)
this.onMouseMove = this.onMouseMove.bind(this)
}
componentWillMount() {
componentWillMount () {
console.log('MouseMovement componentWillMount')
document.addEventListener('mousemove', this.onMouseMove)
const timer = setTimeout(this.timeout, 4000)
this.setState({ timer })
}
componentWillUnmount() {
componentWillUnmount () {
console.log('MouseMovement componentWillUnmount')
document.removeEventListener('mousemove', this.onMouseMove)
clearTimeout(this.state.timer)
this.setState({ timer: undefined })
}
onMouseMove() {
onMouseMove () {
console.log('MouseMovement onMouseMove')
clearTimeout(this.state.timer)
const timer = setTimeout(this.timeout, 4000)
this.setState({ timer })
this.props.onMoved(true)
}
timeout() {
timeout () {
console.log('timeout')
clearTimeout(this.state.timer)
this.props.onMoved(false)
}
render() {
render () {
return null
}
}

View File

@@ -8,32 +8,35 @@ describe('Renderless component', () => {
// let's also spy on "console.log" calls
// to make sure the entire sequence of calls happens
cy.window()
.its('console')
.then(console => {
cy.spy(console, 'log').as('log')
})
.its('console')
.then((console) => {
cy.spy(console, 'log').as('log')
})
const onMoved = cy.stub()
mount(<MouseMovement onMoved={onMoved} />)
cy.get('#cypress-root').should('be.empty')
cy.document()
.trigger('mousemove')
.then(() => {
expect(onMoved).to.have.been.calledWith(true)
})
.trigger('mousemove')
.then(() => {
expect(onMoved).to.have.been.calledWith(true)
})
unmount()
cy.get('@log')
.its('callCount')
.should('equal', 4)
.its('callCount')
.should('equal', 4)
cy.get('@log')
.invoke('getCalls')
.then(calls => calls.map(call => call.args[0]))
.should('deep.equal', [
'MouseMovement constructor',
'MouseMovement componentWillMount',
'MouseMovement onMouseMove',
'MouseMovement componentWillUnmount',
])
.invoke('getCalls')
.then((calls) => calls.map((call) => call.args[0]))
.should('deep.equal', [
'MouseMovement constructor',
'MouseMovement componentWillMount',
'MouseMovement onMouseMove',
'MouseMovement componentWillUnmount',
])
})
})

View File

@@ -8,15 +8,15 @@ class App extends Component {
isLoading: true,
}
componentDidMount() {
componentDidMount () {
this._timer = setTimeout(() => this.setState({ isLoading: false }), 2000)
}
componentWillUnmount() {
componentWillUnmount () {
clearTimeout(this._timer)
}
render() {
render () {
return (
<div className="App">
<header className="App-header">

View File

@@ -10,7 +10,7 @@ export default class LoadingIndicator extends Component {
isPastDelay: false,
}
componentDidMount() {
componentDidMount () {
console.log('component did mount')
this._delayTimer = setTimeout(() => {
console.log('2000ms passed')
@@ -18,18 +18,20 @@ export default class LoadingIndicator extends Component {
}, 2000)
}
componentWillUnmount() {
componentWillUnmount () {
console.log('componentWillUnmount')
clearTimeout(this._delayTimer)
}
render() {
render () {
if (this.props.isLoading) {
if (!this.state.isPastDelay) {
return null
}
return <div>loading...</div>
}
return this.props.children
}
}

View File

@@ -14,6 +14,7 @@ describe('LoadingIndicator', () => {
<div>ahoy!</div>
</LoadingIndicator>,
)
cy.contains('div', 'ahoy!')
})
})
@@ -26,6 +27,7 @@ describe('LoadingIndicator', () => {
<div>ahoy!</div>
</LoadingIndicator>,
)
cy.contains('loading...').should('be.visible')
cy.contains('ahoy!').should('not.exist')
})
@@ -38,6 +40,7 @@ describe('LoadingIndicator', () => {
<div>ahoy!</div>
</LoadingIndicator>,
)
// this test runs for 2 seconds, since it just waits for the
// loading indicator to show up after "setTimeout"
cy.contains('loading...', { timeout: 2100 }).should('be.visible')
@@ -50,6 +53,7 @@ describe('LoadingIndicator', () => {
<div>ahoy!</div>
</LoadingIndicator>,
)
// force 2 seconds to pass instantly
// and component's setTimeout to fire
cy.tick(2010)
@@ -61,23 +65,25 @@ describe('LoadingIndicator', () => {
describe('on unmount', () => {
it('should clear timeout', () => {
cy.clock()
cy.window().then(win => cy.spy(win, 'clearTimeout').as('clearTimeout'))
cy.window().then((win) => cy.spy(win, 'clearTimeout').as('clearTimeout'))
mount(
<LoadingIndicator isLoading={true}>
<div>ahoy!</div>
</LoadingIndicator>,
)
cy.tick(2010)
cy.get('#cypress-root').then($el => {
cy.get('#cypress-root').then(($el) => {
unmountComponentAtNode($el[0])
})
cy.get('@clearTimeout').should('have.been.calledOnce')
})
})
afterEach(() => {
cy.get('#cypress-root').then($el => {
cy.get('#cypress-root').then(($el) => {
unmountComponentAtNode($el[0])
})
})

View File

@@ -5,6 +5,7 @@ import { mount } from 'cypress-react-unit-test'
// https://github.com/cypress-io/cypress/pull/3968
// you can skip the tests if there is no retries feature
const describeOrSkip = Cypress.getTestRetries ? describe : describe.skip
describeOrSkip('Test', () => {
const Hello = () => {
// this is how you can get the current retry number
@@ -15,6 +16,7 @@ describeOrSkip('Test', () => {
const n = cy.state('test').currentRetry
? cy.state('test').currentRetry()
: 0
return <div>retry {n}</div>
}

View File

@@ -1,14 +1,15 @@
import React, { useState } from 'react'
import axios from 'axios'
export default function Fetcher({ url }) {
export default function Fetcher ({ url }) {
const [greeting, setGreeting] = useState('')
const [buttonClicked, setButtonClicked] = useState(false)
const fetchGreeting = () => {
axios.get(url).then(response => {
axios.get(url).then((response) => {
const data = response.data
const { greeting } = data
setGreeting(greeting)
setButtonClicked(true)
})

View File

@@ -7,12 +7,13 @@ it('loads and displays greeting', () => {
cy.route('/greeting', { greeting: 'Hello there' }).as('greet')
const url = '/greeting'
mount(<Fetcher url={url} />)
cy.contains('Load Greeting').click()
cy.get('[role=heading]').should('have.text', 'Hello there')
cy.get('[role=button]').should('be.disabled')
cy.get('@greet')
.its('url')
.should('match', /\/greeting$/)
.its('url')
.should('match', /\/greeting$/)
})

View File

@@ -10,14 +10,16 @@ it('loads and displays greeting (testing-lib)', () => {
cy.route('/greeting', { greeting: 'Hello there' }).as('greet')
const url = '/greeting'
mount(<Fetcher url={url} />)
cy.findByText('Load Greeting')
.wait(1000)
.click()
.wait(1000)
.click()
cy.findByRole('heading').should('have.text', 'Hello there')
cy.findByRole('button').should('be.disabled')
cy.get('@greet')
.its('url')
.should('match', /\/greeting$/)
.its('url')
.should('match', /\/greeting$/)
})

View File

@@ -7,6 +7,7 @@ import { mount } from 'cypress-react-unit-test'
// https://github.com/bahmutov/cypress-react-unit-test/issues/200
it('should select null after timing out (fast)', () => {
const onSelect = cy.stub()
// https://on.cypress.io/clock
cy.clock()
@@ -18,22 +19,27 @@ it('should select null after timing out (fast)', () => {
// not yet
expect(onSelect).to.not.have.been.called
})
cy.tick(1000).then(() => {
// not yet
expect(onSelect).to.not.have.been.called
})
cy.tick(1000).then(() => {
// not yet
expect(onSelect).to.not.have.been.called
})
cy.tick(1000).then(() => {
// not yet
expect(onSelect).to.not.have.been.called
})
cy.tick(1000).then(() => {
// not yet
expect(onSelect).to.not.have.been.called
})
cy.log('5 seconds passed')
cy.tick(1000).then(() => {
// NOW
@@ -44,16 +50,18 @@ it('should select null after timing out (fast)', () => {
it('should select null after timing out (slow)', () => {
// without synthetic clock we must wait for the real delay
const onSelect = cy.stub().as('selected')
mount(<Card onSelect={onSelect} />)
cy.get('@selected', { timeout: 5100 }).should('have.been.calledWith', null)
})
it('should accept selections', () => {
const onSelect = cy.stub()
mount(<Card onSelect={onSelect} />)
cy.get("[data-testid='2']")
.click()
.then(() => {
expect(onSelect).to.have.been.calledWith(2)
})
cy.get('[data-testid=\'2\']')
.click()
.then(() => {
expect(onSelect).to.have.been.calledWith(2)
})
})

View File

@@ -6,6 +6,7 @@ import { unmountComponentAtNode } from 'react-dom'
it('should select null after timing out', () => {
const onSelect = cy.stub()
// https://on.cypress.io/clock
cy.clock()
mount(<Card onSelect={onSelect} />)
@@ -13,6 +14,7 @@ it('should select null after timing out', () => {
cy.tick(100).then(() => {
expect(onSelect).to.not.have.been.called
})
cy.tick(5000).then(() => {
expect(onSelect).to.have.been.calledWith(null)
})
@@ -20,13 +22,14 @@ it('should select null after timing out', () => {
it('should cleanup on being removed', () => {
const onSelect = cy.stub()
cy.clock()
mount(<Card onSelect={onSelect} />)
cy.tick(100).then(() => {
expect(onSelect).to.not.have.been.called
})
cy.get('#cypress-root').then($el => {
cy.get('#cypress-root').then(($el) => {
unmountComponentAtNode($el[0])
})
@@ -37,6 +40,7 @@ it('should cleanup on being removed', () => {
it('should cleanup on being removed (using unmount)', () => {
const onSelect = cy.stub()
cy.clock()
mount(<Card onSelect={onSelect} />)
cy.tick(100).then(() => {
@@ -52,10 +56,11 @@ it('should cleanup on being removed (using unmount)', () => {
it('should accept selections', () => {
const onSelect = cy.stub()
mount(<Card onSelect={onSelect} />)
cy.get("[data-testid='2']")
.click()
.then(() => {
expect(onSelect).to.have.been.calledWith(2)
})
cy.get('[data-testid=\'2\']')
.click()
.then(() => {
expect(onSelect).to.have.been.calledWith(2)
})
})

View File

@@ -1,18 +1,18 @@
import React, { Component } from 'react'
export default class Card extends Component {
componentDidMount() {
componentDidMount () {
this._timeoutID = setTimeout(() => {
this.props.onSelect(null)
}, 5000)
}
componentWillUnmount() {
componentWillUnmount () {
clearTimeout(this._timeoutID)
}
render() {
return [1, 2, 3, 4].map(choice => (
render () {
return [1, 2, 3, 4].map((choice) => (
<button
key={choice}
data-testid={choice}

View File

@@ -1,6 +1,6 @@
import React, { useEffect } from 'react'
export default function Card(props) {
export default function Card (props) {
useEffect(() => {
const timeoutID = setTimeout(() => {
console.log('after timeout')
@@ -14,7 +14,8 @@ export default function Card(props) {
}, [props.onSelect])
console.log('inside Card')
return [1, 2, 3, 4].map(choice => (
return [1, 2, 3, 4].map((choice) => (
<button
key={choice}
data-testid={choice}

View File

@@ -7,6 +7,7 @@ describe('Shopping list', () => {
beforeEach(() => {
cy.viewport(600, 600)
})
it('shows FB list', () => {
mount(<ShoppingList name="Facebook" />)
cy.get('li').should('have.length', 3)

View File

@@ -1,7 +1,7 @@
import React from 'react'
export default class ShoppingList extends React.Component {
render() {
render () {
return (
<div className="shopping-list">
<h1>Shopping List for {this.props.name}</h1>

View File

@@ -4,14 +4,14 @@ import { mount } from 'cypress-react-unit-test'
// let's put React component right in the spec file
class Square extends React.Component {
constructor(props) {
constructor (props) {
super(props)
this.state = {
value: null,
}
}
render() {
render () {
return (
<button
className="square"
@@ -26,19 +26,21 @@ class Square extends React.Component {
describe('Square', () => {
it('changes value on click', () => {
const selector = 'button.square'
mount(<Square value="X" />)
// initially button is blank
cy.get(selector).should('have.text', '')
// but it changes text on click
cy.get(selector)
.click()
.should('have.text', 'X')
.click()
.should('have.text', 'X')
})
it('looks good', () => {
mount(<Square />, {
cssFile: 'cypress/component/advanced/tutorial/tic-tac-toe.css',
})
// pause to show it
cy.wait(1000)
cy.get('.square').click()
@@ -46,7 +48,7 @@ describe('Square', () => {
// check if style was applied
cy.get('.square')
.should('have.css', 'background-color', 'rgb(255, 255, 255)')
.and('have.css', 'border', '1px solid rgb(153, 153, 153)')
.should('have.css', 'background-color', 'rgb(255, 255, 255)')
.and('have.css', 'border', '1px solid rgb(153, 153, 153)')
})
})

View File

@@ -16,10 +16,12 @@ describe('Tic Tac Toe', () => {
beforeEach(() => {
cy.viewport(200, 200)
})
it('starts and lets X win', () => {
mount(<Game />, {
cssFile: 'cypress/component/advanced/tutorial/tic-tac-toe.css',
})
cy.contains('.status', 'Next player: X')
clickSquare(0, 0).click()
cy.contains('.status', 'Next player: O')

View File

@@ -2,7 +2,7 @@
// the code taken from https://codepen.io/gaearon/pen/LyyXgK
import React from 'react'
export function calculateWinner(squares) {
export function calculateWinner (squares) {
const lines = [
[0, 1, 2],
[3, 4, 5],
@@ -13,16 +13,19 @@ export function calculateWinner(squares) {
[0, 4, 8],
[2, 4, 6],
]
for (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i]
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return squares[a]
}
}
return null
}
function Square(props) {
function Square (props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
@@ -31,7 +34,7 @@ function Square(props) {
}
export class Board extends React.Component {
constructor(props) {
constructor (props) {
super(props)
this.state = {
squares: Array(9).fill(null),
@@ -39,19 +42,21 @@ export class Board extends React.Component {
}
}
handleClick(i) {
handleClick (i) {
const squares = this.state.squares.slice()
if (calculateWinner(squares) || squares[i]) {
return
}
squares[i] = this.state.xIsNext ? 'X' : 'O'
this.setState({
squares: squares,
squares,
xIsNext: !this.state.xIsNext,
})
}
renderSquare(i) {
renderSquare (i) {
return (
<Square
value={this.state.squares[i]}
@@ -60,13 +65,14 @@ export class Board extends React.Component {
)
}
render() {
render () {
const winner = calculateWinner(this.state.squares)
let status
if (winner) {
status = 'Winner: ' + winner
status = `Winner: ${winner}`
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O')
status = `Next player: ${this.state.xIsNext ? 'X' : 'O'}`
}
return (
@@ -93,7 +99,7 @@ export class Board extends React.Component {
}
export class Game extends React.Component {
render() {
render () {
return (
<div className="game">
<div className="game-board">

View File

@@ -1,12 +1,13 @@
import React, { useState, useEffect } from 'react'
import * as React from 'react'
export default function App() {
const [cart, setCart] = useState(
export default function App () {
const [cart, setCart] = React.useState(
JSON.parse(localStorage.getItem('cart')) || ['kiwi 🥝'],
)
const addJuice = () => {
const updatedCart = cart.concat('juice 🧃')
setCart(updatedCart)
localStorage.setItem('cart', JSON.stringify(updatedCart))
}

View File

@@ -11,6 +11,7 @@ describe('App', () => {
it('uses cart from localStorage', () => {
const items = ['apples 🍎', 'oranges 🍊', 'grapes 🍇']
localStorage.setItem('cart', JSON.stringify(items))
mount(<App />)
cy.get('.item').should('have.length', 3)
@@ -27,7 +28,7 @@ describe('App', () => {
// is updated after a delay, the assertion waits for it
// https://on.cypress.io/retry-ability
cy.wrap(localStorage)
.invoke('getItem', 'cart')
.should('equal', JSON.stringify(['kiwi 🥝', 'juice 🧃']))
.invoke('getItem', 'cart')
.should('equal', JSON.stringify(['kiwi 🥝', 'juice 🧃']))
})
})

View File

@@ -7,6 +7,7 @@ import { mount } from 'cypress-react-unit-test'
describe('Stateless alert', () => {
beforeEach(() => {
const spy = cy.spy().as('alert')
cy.on('window:alert', spy)
mount(<HelloWorld name="React" />, { ReactDom })
})
@@ -18,7 +19,7 @@ describe('Stateless alert', () => {
it('alerts with name', () => {
cy.contains('Say Hi').click()
cy.get('@alert')
.should('have.been.calledOnce')
.and('have.been.be.calledWithExactly', 'Hi React')
.should('have.been.calledOnce')
.and('have.been.be.calledWithExactly', 'Hi React')
})
})

View File

@@ -5,24 +5,27 @@ import { mount } from 'cypress-react-unit-test'
describe('Alias', () => {
it('returns component by its name', () => {
const Greeting = () => <div>Hello!</div>
mount(<Greeting />)
// get the component instance by name "Greeting"
cy.get('@Greeting')
.its('props')
.should('be.empty')
.its('props')
.should('be.empty')
// the component was constructed from the function Greeting
cy.get('@Greeting')
.its('type')
.should('equal', Greeting)
.its('type')
.should('equal', Greeting)
})
it('returns component by given display name', () => {
const GreetingCard = props => <div>Hello {props.name}!</div>
const GreetingCard = (props) => <div>Hello {props.name}!</div>
mount(<GreetingCard name="World" />, { alias: 'Hello' })
cy.get('@Hello')
.its('props')
.should('deep.equal', {
name: 'World',
})
.its('props')
.should('deep.equal', {
name: 'World',
})
})
})

View File

@@ -9,19 +9,19 @@ describe('Counter', () => {
it('counts clicks', () => {
mount(<Counter />)
cy.contains('count: 0')
.click()
.contains('count: 1')
.click()
.contains('count: 2')
.click()
.contains('count: 1')
.click()
.contains('count: 2')
})
it('counts clicks 2', () => {
mount(<Counter />)
cy.contains('count: 0')
.click()
.contains('count: 1')
.click()
.contains('count: 2')
.click()
.contains('count: 1')
.click()
.contains('count: 2')
})
})
@@ -32,9 +32,9 @@ describe('Counter mounted before each test', () => {
it('goes to 3', () => {
cy.contains('count: 0')
.click()
.click()
.click()
.contains('count: 3')
.click()
.click()
.click()
.contains('count: 3')
})
})

View File

@@ -1,7 +1,7 @@
import React from 'react'
export class Counter extends React.Component {
constructor(props) {
constructor (props) {
super(props)
this.state = {
count: 0,
@@ -14,7 +14,7 @@ export class Counter extends React.Component {
})
}
render() {
render () {
return <p onClick={this.click}>count: {this.state.count}</p>
}
}

View File

@@ -7,9 +7,10 @@ describe('Counter using hooks', () => {
mount(<Counter />)
cy.contains('You clicked 0 times')
cy.contains('Click me')
.click()
.click()
.click()
.click()
.click()
.click()
cy.contains('You clicked 3 times')
})
})

Some files were not shown because too many files have changed in this diff Show More