├── .babelrc
├── .github
└── workflows
│ └── valid.yml
├── .gitignore
├── LICENSE
├── README.md
├── cypress.json
├── cypress
├── fixtures
│ └── todos.json
├── integration
│ ├── app-init.spec.js
│ ├── footer.spec.js
│ ├── input-form.spec.js
│ ├── list-items.spec.js
│ └── smoke-tests.spec.js
├── plugins
│ └── index.js
└── support
│ ├── commands.js
│ └── index.js
├── db.json
├── json-server.json
├── package-lock.json
├── package.json
├── routes.json
├── src
├── components
│ ├── Footer.js
│ ├── TodoApp.js
│ ├── TodoForm.js
│ └── TodoList.js
├── index.html
├── index.js
├── lib
│ ├── service.js
│ └── utils.js
└── styles.css
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets":[
3 | "env", "react"
4 | ],
5 | "plugins": [
6 | "transform-object-rest-spread"
7 | ]
8 | }
--------------------------------------------------------------------------------
/.github/workflows/valid.yml:
--------------------------------------------------------------------------------
1 | name: Validate initial app state
2 |
3 | on: push
4 |
5 | jobs:
6 | single-run:
7 | runs-on: ubuntu-16.04
8 | steps:
9 | - name: Checkout
10 | uses: actions/checkout@v2
11 | - name: Cypress run
12 | uses: cypress-io/github-action@v1
13 | with:
14 | build: npm run build
15 | start: npm run serve
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | logs
4 | *.log
5 | npm-debug.log*
6 | yarn-debug.log*
7 | yarn-error.log*
8 |
9 | .env
10 |
11 | build
12 |
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 avanslaars
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Build and test an application using Cypress
2 |
3 | This repository is the starting point for an official Cypress tutorial. We encourage you to clone this repo and follow along.
4 |
--------------------------------------------------------------------------------
/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "baseUrl": "http://localhost:3030"
3 | }
4 |
--------------------------------------------------------------------------------
/cypress/fixtures/todos.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 1,
4 | "name": "Buy Milk",
5 | "isComplete": false
6 | },
7 | {
8 | "id": 2,
9 | "name": "Buy Eggs",
10 | "isComplete": true
11 | },
12 | {
13 | "id": 3,
14 | "name": "Buy Bread",
15 | "isComplete": false
16 | },
17 | {
18 | "id": 4,
19 | "name": "Make French Toast",
20 | "isComplete": false
21 | }
22 | ]
23 |
--------------------------------------------------------------------------------
/cypress/integration/app-init.spec.js:
--------------------------------------------------------------------------------
1 | describe('App initialization', () => {
2 | it('Loads todos on page load', () => {
3 | cy.seedAndVisit()
4 |
5 | cy.get('.todo-list li')
6 | .should('have.length', 4)
7 | })
8 |
9 | it('Displays an error on failure', () => {
10 | cy.server()
11 | cy.route({
12 | url: '/api/todos',
13 | method: 'GET',
14 | status: 500,
15 | response: {}
16 | })
17 | cy.visit('/')
18 |
19 | cy.get('.todo-list li')
20 | .should('not.exist')
21 |
22 | cy.get('.error')
23 | .should('be.visible')
24 | })
25 | })
26 |
--------------------------------------------------------------------------------
/cypress/integration/footer.spec.js:
--------------------------------------------------------------------------------
1 | describe('Footer', () => {
2 | context('with a single todo', () => {
3 | it('displays a singular todo in count', () => {
4 | cy.seedAndVisit([{id: 1, name: 'Buy milk', isComplete: false}])
5 | cy.get('.todo-count')
6 | .should('contain', '1 todo left')
7 | })
8 | })
9 |
10 | context('with multiple todos', () => {
11 | beforeEach(() => {
12 | cy.seedAndVisit()
13 | })
14 |
15 | it('displays plural todos in count', () => {
16 | cy.get('.todo-count')
17 | .should('contain', '3 todos left')
18 | })
19 |
20 | it('Handles filter links', () => {
21 | const filters = [
22 | {link: 'Active', expectedLength: 3},
23 | {link: 'Completed', expectedLength: 1},
24 | {link: 'All', expectedLength: 4}
25 | ]
26 | cy.wrap(filters)
27 | .each(filter => {
28 | cy.contains(filter.link)
29 | .click()
30 |
31 | cy.get('.todo-list li')
32 | .should('have.length', filter.expectedLength)
33 | })
34 | })
35 | })
36 | })
37 |
--------------------------------------------------------------------------------
/cypress/integration/input-form.spec.js:
--------------------------------------------------------------------------------
1 | describe('Input form', () => {
2 | beforeEach(() => {
3 | cy.seedAndVisit([])
4 | })
5 |
6 | it('focuses input on load', () => {
7 | cy.focused()
8 | .should('have.class', 'new-todo')
9 | })
10 |
11 | it('accepts input', () => {
12 | const typedText = 'Buy Milk'
13 |
14 | cy.get('.new-todo')
15 | .type(typedText)
16 | .should('have.value', typedText)
17 | })
18 |
19 | context('Form submission', () => {
20 | beforeEach(() => {
21 | cy.server()
22 | })
23 |
24 | it('Adds a new todo on submit', () => {
25 | const itemText = 'Buy eggs'
26 | cy.route('POST', '/api/todos', {
27 | name: itemText,
28 | id: 1,
29 | isComplete: false
30 | })
31 |
32 | cy.get('.new-todo')
33 | .type(itemText)
34 | .type('{enter}')
35 | .should('have.value', '')
36 |
37 | cy.get('.todo-list li')
38 | .should('have.length', 1)
39 | .and('contain', itemText)
40 | })
41 |
42 | it('Shows an error message on a failed submission', () => {
43 | cy.route({
44 | url: '/api/todos',
45 | method: 'POST',
46 | status: 500,
47 | response: {}
48 | })
49 |
50 | cy.get('.new-todo')
51 | .type('test{enter}')
52 |
53 | cy.get('.todo-list li')
54 | .should('not.exist')
55 |
56 | cy.get('.error')
57 | .should('be.visible')
58 | })
59 | })
60 | })
61 |
--------------------------------------------------------------------------------
/cypress/integration/list-items.spec.js:
--------------------------------------------------------------------------------
1 | describe('List items', () => {
2 | beforeEach(() => {
3 | cy.seedAndVisit()
4 | })
5 |
6 | it('properly displays completed items', () => {
7 | cy.get('.todo-list li')
8 | .filter('.completed')
9 | .should('have.length', 1)
10 | .and('contain', 'Eggs')
11 | .find('.toggle')
12 | .should('be.checked')
13 | })
14 |
15 | it('Shows remaining todos in the footer', () => {
16 | cy.get('.todo-count')
17 | .should('contain', 3)
18 | })
19 |
20 | it('Removes a todo', () => {
21 | cy.route({
22 | url: '/api/todos/1',
23 | method: 'DELETE',
24 | status: 200,
25 | response: {}
26 | })
27 |
28 | cy.get('.todo-list li')
29 | .as('list')
30 |
31 | cy.get('@list')
32 | .first()
33 | .find('.destroy')
34 | .invoke('show')
35 | .click()
36 |
37 | cy.get('@list')
38 | .should('have.length', 3)
39 | .and('not.contain', 'Milk')
40 | })
41 |
42 | it('Marks an incomplete item complete', () => {
43 | cy.fixture('todos')
44 | .then(todos => {
45 | const target = Cypress._.head(todos)
46 | cy.route(
47 | 'PUT',
48 | `/api/todos/${target.id}`,
49 | Cypress._.merge(target, {isComplete: true})
50 | )
51 | })
52 |
53 | cy.get('.todo-list li')
54 | .first()
55 | .as('first-todo')
56 |
57 | cy.get('@first-todo')
58 | .find('.toggle')
59 | .click()
60 | .should('be.checked')
61 |
62 | cy.get('@first-todo')
63 | .should('have.class', 'completed')
64 |
65 | cy.get('.todo-count')
66 | .should('contain', 2)
67 | })
68 | })
69 |
--------------------------------------------------------------------------------
/cypress/integration/smoke-tests.spec.js:
--------------------------------------------------------------------------------
1 | describe('Smoke tests', () => {
2 | beforeEach(() => {
3 | cy.request('GET', '/api/todos')
4 | .its('body')
5 | .each(todo => cy.request('DELETE', `/api/todos/${todo.id}`))
6 | })
7 |
8 | context('With no todos', () => {
9 | it('Saves new todos', () => {
10 | const items = [
11 | {text: 'Buy milk', expectedLength: 1},
12 | {text: 'Buy eggs', expectedLength: 2},
13 | {text: 'Buy bread', expectedLength: 3}
14 | ]
15 | cy.visit('/')
16 | cy.server()
17 | cy.route('POST', '/api/todos')
18 | .as('create')
19 |
20 | cy.wrap(items)
21 | .each(todo => {
22 | cy.focused()
23 | .type(todo.text)
24 | .type('{enter}')
25 |
26 | cy.wait('@create')
27 |
28 | cy.get('.todo-list li')
29 | .should('have.length', todo.expectedLength)
30 | })
31 | })
32 | })
33 |
34 | context('With active todos', () => {
35 | beforeEach(() => {
36 | cy.fixture('todos')
37 | .each(todo => {
38 | const newTodo = Cypress._.merge(todo, {isComplete: false})
39 | cy.request('POST', '/api/todos', newTodo)
40 | })
41 | cy.visit('/')
42 | })
43 |
44 | it('Loads existing data from the DB', () => {
45 | cy.get('.todo-list li')
46 | .should('have.length', 4)
47 | })
48 |
49 | it('Deletes todos', () => {
50 | cy.server()
51 | cy.route('DELETE', '/api/todos/*')
52 | .as('delete')
53 |
54 | cy.get('.todo-list li')
55 | .each($el => {
56 | cy.wrap($el)
57 | .find('.destroy')
58 | .invoke('show')
59 | .click()
60 |
61 | cy.wait('@delete')
62 | })
63 | .should('not.exist')
64 | })
65 |
66 | it('Toggles todos', () => {
67 | const clickAndWait = ($el) => {
68 | cy.wrap($el)
69 | .as('item')
70 | .find('.toggle')
71 | .click()
72 |
73 | cy.wait('@update')
74 | }
75 | cy.server()
76 | cy.route('PUT', '/api/todos/*')
77 | .as('update')
78 |
79 | cy.get('.todo-list li')
80 | .each($el => {
81 | clickAndWait($el)
82 | cy.get('@item')
83 | .should('have.class', 'completed')
84 | })
85 | .each($el => {
86 | clickAndWait($el)
87 | cy.get('@item')
88 | .should('not.have.class', 'completed')
89 | })
90 | })
91 | })
92 | })
93 |
--------------------------------------------------------------------------------
/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example plugins/index.js can be used to load plugins
3 | //
4 | // You can change the location of this file or turn off loading
5 | // the plugins file with the 'pluginsFile' configuration option.
6 | //
7 | // You can read more here:
8 | // https://on.cypress.io/plugins-guide
9 | // ***********************************************************
10 |
11 | // This function is called when a project is opened or re-opened (e.g. due to
12 | // the project's config changing)
13 |
14 | module.exports = (on, config) => {
15 | // `on` is used to hook into various events Cypress emits
16 | // `config` is the resolved Cypress config
17 | }
18 |
--------------------------------------------------------------------------------
/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | Cypress.Commands.add('seedAndVisit', (seedData = 'fixture:todos') => {
2 | cy.server()
3 | cy.route('GET', '/api/todos', seedData)
4 | cy.visit('/')
5 | })
6 |
--------------------------------------------------------------------------------
/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/db.json:
--------------------------------------------------------------------------------
1 | {
2 | "todos": [
3 | {
4 | "id": 1,
5 | "name": "Buy Milk",
6 | "isComplete": false
7 | },
8 | {
9 | "id": 2,
10 | "name": "Buy Eggs",
11 | "isComplete": false
12 | },
13 | {
14 | "id": 3,
15 | "name": "Buy Bread",
16 | "isComplete": false
17 | },
18 | {
19 | "id": 4,
20 | "name": "Make French Toast",
21 | "isComplete": false
22 | }
23 | ]
24 | }
--------------------------------------------------------------------------------
/json-server.json:
--------------------------------------------------------------------------------
1 | {
2 | "port": 3030,
3 | "watch": false,
4 | "static": "./build",
5 | "routes": "./routes.json"
6 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cypress-tutorial-build-todo-starter",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "build": "webpack",
8 | "watch": "webpack --watch",
9 | "serve": "json-server db.json",
10 | "dev": "concurrently \"npm run watch\" \"npm run serve\"",
11 | "cypress": "cypress open",
12 | "cypress:all": "cypress run"
13 | },
14 | "keywords": [],
15 | "author": "",
16 | "license": "ISC",
17 | "dependencies": {
18 | "axios": "0.19.2",
19 | "concurrently": "3.6.1",
20 | "json-server": "0.16.1",
21 | "react": "16.13.1",
22 | "react-dom": "16.13.1",
23 | "react-router-dom": "4.2.2"
24 | },
25 | "devDependencies": {
26 | "babel-core": "6.26.3",
27 | "babel-loader": "7.1.5",
28 | "babel-plugin-transform-object-rest-spread": "6.26.0",
29 | "babel-preset-env": "1.7.0",
30 | "babel-preset-react": "6.24.1",
31 | "css-loader": "0.28.11",
32 | "cypress": "3.5.0",
33 | "html-webpack-plugin": "2.30.1",
34 | "style-loader": "0.19.1",
35 | "webpack": "3.12.0"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/routes.json:
--------------------------------------------------------------------------------
1 | {
2 | "/api/*": "/$1"
3 | }
--------------------------------------------------------------------------------
/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {Link} from 'react-router-dom'
3 |
4 | export default props =>
5 |
18 |
--------------------------------------------------------------------------------
/src/components/TodoApp.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import {BrowserRouter as Router, Route} from 'react-router-dom'
3 | import TodoForm from './TodoForm'
4 | import TodoList from './TodoList'
5 | import Footer from './Footer'
6 | import {saveTodo, loadTodos, destroyTodo, updateTodo} from '../lib/service'
7 | import {filterTodos} from '../lib/utils'
8 |
9 |
10 | export default class TodoApp extends Component {
11 | constructor(props) {
12 | super(props)
13 |
14 | this.state = {
15 | currentTodo: '',
16 | todos: []
17 | }
18 | this.handleNewTodoChange = this.handleNewTodoChange.bind(this)
19 | this.handleTodoSubmit = this.handleTodoSubmit.bind(this)
20 | this.handleDelete = this.handleDelete.bind(this)
21 | this.handleToggle = this.handleToggle.bind(this)
22 | }
23 |
24 | componentDidMount () {
25 | loadTodos()
26 | .then(({data}) => this.setState({todos: data}))
27 | .catch(() => this.setState({error: true}))
28 | }
29 |
30 | handleNewTodoChange (evt) {
31 | this.setState({currentTodo: evt.target.value})
32 | }
33 |
34 | handleDelete (id) {
35 | destroyTodo(id)
36 | .then(() => this.setState({
37 | todos: this.state.todos.filter(t => t.id !== id)
38 | }))
39 | }
40 |
41 | handleToggle (id) {
42 | const targetTodo = this.state.todos.find(t => t.id === id)
43 | const updated = {
44 | ...targetTodo,
45 | isComplete: !targetTodo.isComplete
46 | }
47 | updateTodo(updated)
48 | .then(({data}) => {
49 | const todos = this.state.todos.map(
50 | t => t.id === data.id ? data : t
51 | )
52 | this.setState({todos: todos})
53 | })
54 | }
55 |
56 | handleTodoSubmit (evt) {
57 | evt.preventDefault()
58 | const newTodo = {name: this.state.currentTodo, isComplete: false}
59 | saveTodo(newTodo)
60 | .then(({data}) => this.setState({
61 | todos: this.state.todos.concat(data),
62 | currentTodo: ''
63 | }))
64 | .catch(() => this.setState({error: true}))
65 | }
66 |
67 | render () {
68 | const remaining = this.state.todos.filter(t => !t.isComplete).length
69 | return (
70 | todos
74 | {this.state.error ? Oh no! : null}
75 |