├── .babelrc ├── .circleci └── config.yml ├── .gitignore ├── .gitlab-ci.yml ├── .node-version ├── LICENSE.md ├── README.md ├── cypress.config.js ├── cypress ├── e2e │ ├── first.cy.js │ ├── full.cy.js │ ├── selectors.cy.js │ ├── smoke-spec.cy.js │ ├── smoke.js │ └── viewports.cy.js ├── fixtures │ ├── 3-todos.json │ └── example.json └── support │ ├── commands.js │ ├── e2e.js │ └── index.d.ts ├── images ├── 100.png └── circle-report.png ├── package.json ├── public └── index.html ├── renovate.json ├── src ├── actions │ ├── index.cy-spec.js │ ├── index.js │ └── index.spec.js ├── components │ ├── App.cy-spec.js │ ├── App.js │ ├── App.spec.js │ ├── Footer.cy-spec.js │ ├── Footer.js │ ├── Footer.spec.js │ ├── Header.cy-spec.js │ ├── Header.js │ ├── Header.spec.js │ ├── Link.js │ ├── Link.spec.js │ ├── MainSection.cy-spec.js │ ├── MainSection.js │ ├── MainSection.spec.js │ ├── TodoItem.cy-spec.js │ ├── TodoItem.js │ ├── TodoItem.spec.js │ ├── TodoList.cy-spec.js │ ├── TodoList.js │ ├── TodoList.spec.js │ ├── TodoTextInput.js │ └── TodoTextInput.spec.js ├── constants │ ├── ActionTypes.js │ └── TodoFilters.js ├── containers │ ├── FilterLink.js │ ├── Header.js │ ├── MainSection.js │ └── VisibleTodoList.js ├── index.js ├── reducers │ ├── index.js │ ├── todos.js │ ├── todos.spec.js │ └── visibilityFilter.js ├── selectors │ └── index.js └── store.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-react"], 3 | "env": { 4 | "test": { 5 | "plugins": ["istanbul"] 6 | } 7 | }, 8 | "plugins": ["transform-class-properties"] 9 | } 10 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | defaultCypressOrbConfig: &defaultCypressOrbConfig 2 | executor: 3 | name: cypress/default 4 | node-version: '18.17.1' 5 | 6 | # Use the latest 2.1 version of CircleCI pipeline process engine. 7 | # See: https://circleci.com/docs/configuration-reference 8 | version: 2.1 9 | orbs: 10 | # see https://github.com/cypress-io/circleci-orb 11 | cypress: cypress-io/cypress@3.1.4 12 | jobs: 13 | install-and-persist: 14 | <<: *defaultCypressOrbConfig 15 | steps: 16 | - cypress/install: 17 | install-command: yarn 18 | - persist_to_workspace: 19 | paths: 20 | - .cache/Cypress 21 | - project 22 | root: ~/ 23 | run-tests: 24 | <<: *defaultCypressOrbConfig 25 | steps: 26 | - attach_workspace: 27 | at: ~/ 28 | - cypress/run-tests: 29 | start-command: npm run start 30 | cypress-command: NODE_ENV=test npm run cypress:run 31 | - run: npm run report:coverage:summary 32 | - run: npm run report:coverage:text 33 | # send code coverage to coveralls.io 34 | # https://coveralls.io/github/cypress-io/cypress-example-todomvc-redux 35 | # our coveralls account is currently not enabled 36 | # - run: npm run coveralls 37 | 38 | # See: https://circleci.com/docs/configuration-reference/#workflows 39 | workflows: 40 | main: 41 | jobs: 42 | - install-and-persist 43 | - run-tests: 44 | requires: 45 | - install-and-persist 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist 3 | .nyc_output 4 | coverage 5 | cypress/videos 6 | instrumented 7 | .vscode 8 | cypress/screenshots 9 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # to cache both npm modules and Cypress binary we use environment variables 2 | # to point at the folders we can list as paths in "cache" job settings 3 | variables: 4 | YARN_CACHE_DIR: "$CI_PROJECT_DIR/cache/yarn" 5 | CYPRESS_CACHE_FOLDER: "$CI_PROJECT_DIR/cache/Cypress" 6 | 7 | # cache using branch name 8 | # https://gitlab.com/help/ci/caching/index.md 9 | cache: 10 | key: ${CI_COMMIT_REF_SLUG} 11 | paths: 12 | - cache/yarn 13 | - cache/Cypress 14 | 15 | # this job installs NPM dependencies and Cypress 16 | test: 17 | image: cypress/base:18.16.1 18 | script: 19 | - yarn install --frozen-lockfile 20 | # check Cypress binary path and cached versions 21 | # useful to make sure we are not carrying around old versions 22 | - npx cypress cache path 23 | - npx cypress cache list 24 | - npm run cypress:verify 25 | # start the server, wait for it to respond, then run Cypress tests 26 | - NODE_ENV=test npm test 27 | # print all files in "cypress" folder 28 | - ls -laR cypress 29 | # print coverage summary so that GitLab CI can parse the coverage number 30 | # from a string like "Statements : 100% ( 135/135 )" 31 | - npx nyc report --reporter=text-summary 32 | artifacts: 33 | when: always 34 | paths: 35 | # save both cypress artifacts and coverage results 36 | - coverage 37 | - cypress/videos/*.mp4 38 | - cypress/screenshots/*.png 39 | expire_in: 10 days 40 | 41 | # store and publish code coverage HTML report folder 42 | # https://about.gitlab.com/blog/2016/11/03/publish-code-coverage-report-with-gitlab-pages/ 43 | # the coverage report will be available both as a job artifact 44 | # and at https://cypress-io.gitlab.io/cypress-example-todomvc-redux/ 45 | pages: 46 | stage: deploy 47 | dependencies: 48 | - test 49 | script: 50 | # delete everything in the current public folder 51 | # and replace with code coverage HTML report 52 | - rm -rf public/* 53 | - cp -r coverage/lcov-report/* public/ 54 | artifacts: 55 | paths: 56 | - public 57 | expire_in: 30 days 58 | only: 59 | - master 60 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 18.17.1 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## MIT License 2 | 3 | Copyright (c) 2019 Cypress.io https://www.cypress.io 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cypress-example-todomvc-redux [![CircleCI](https://circleci.com/gh/cypress-io/cypress-example-todomvc-redux.svg?style=svg)](https://circleci.com/gh/cypress-io/cypress-example-todomvc-redux) [![renovate-app badge][renovate-badge]][renovate-app] [![Coverage Status](https://coveralls.io/repos/github/cypress-io/cypress-example-todomvc-redux/badge.svg?branch=master)](https://coveralls.io/github/cypress-io/cypress-example-todomvc-redux?branch=master) 2 | > TodoMVC example with full e2e test code coverage 3 | 4 | This example is a fork of the official [Redux TodoMVC example](https://github.com/reduxjs/redux/tree/master/examples/todomvc) with a set of [Cypress.io](https://www.cypress.io) end-to-end tests. The tests run instrumented application code and the code coverage is saved automatically using [cypress-istanbul](https://github.com/cypress-io/cypress-istanbul) plugin. 5 | 6 | ## GitLab CI mirror 7 | 8 | [![pipeline status](https://gitlab.com/cypress-io/cypress-example-todomvc-redux/badges/master/pipeline.svg)](https://gitlab.com/cypress-io/cypress-example-todomvc-redux/commits/master) [![coverage report](https://gitlab.com/cypress-io/cypress-example-todomvc-redux/badges/master/coverage.svg)](https://gitlab.com/cypress-io/cypress-example-todomvc-redux/commits/master) 9 | 10 | Deployed coverage report is at [https://cypress-io.gitlab.io/cypress-example-todomvc-redux/](https://cypress-io.gitlab.io/cypress-example-todomvc-redux/), generated following this [GitLab coverage guide](https://about.gitlab.com/blog/2016/11/03/publish-code-coverage-report-with-gitlab-pages/). 11 | 12 | ## Install and use 13 | 14 | Because this project uses [Parcel bundler](https://parceljs.org) to serve the web application, it requires Node v12+. 15 | 16 | ```shell 17 | yarn 18 | yarn test 19 | ``` 20 | 21 | The full code coverage HTML report will be saved in `coverage`. You can also see text summary by running 22 | 23 | ```shell 24 | yarn report:coverage:text 25 | ``` 26 | 27 | ## How it works 28 | 29 | Application is served by [Parcel bundler](https://parceljs.org) that uses [.babelrc](.babelrc) file to load [babel-plugin-istanbul](https://github.com/istanbuljs/babel-plugin-istanbul) plugin. This plugin instruments the application source code. During tests [@cypress/code-coverage](https://github.com/cypress-io/code-coverage) plugin merges and saves application code coverage information, rendering the full HTML report at the end. 30 | 31 | Unit tests like [cypress/integration/selectors-spec.js](cypress/integration/selectors-spec.js) that reach into hard to test code paths are also instrumented using the same [.babelrc](.babelrc) file, and this additional code coverage is automatically added to the application code coverage. 32 | 33 | ### .babelrc 34 | 35 | To always instrument the code using Babel and [babel-plugin-istanbul](https://github.com/istanbuljs/babel-plugin-istanbul) one can simply use the `istanbul` plugin 36 | 37 | ``` 38 | { 39 | "plugins": ["istanbul"] 40 | } 41 | ``` 42 | 43 | But this will have instrumented code in the production bundle. To only instrument the code during tests, add the plugin to the `test` environment and serve with `NODE_ENV=test` 44 | 45 | ``` 46 | { 47 | "env": { 48 | "test": { 49 | "plugins": ["istanbul"] 50 | } 51 | } 52 | } 53 | ``` 54 | 55 | Parceljs note: there are some issues with environment-specific plugins, see [PR #2840](https://github.com/parcel-bundler/parcel/pull/2840). 56 | 57 | ### More info 58 | 59 | - [Cypress code coverage guide](https://on.cypress.io/code-coverage) 60 | - watch [Code coverage webinar](https://youtu.be/C8g5X4vCZJA), [slides](https://cypress.slides.com/cypress-io/complete-code-coverage-with-cypress) 61 | 62 | There are also separate blog posts 63 | 64 | - [Code Coverage for End-to-end Tests](https://glebbahmutov.com/blog/code-coverage-for-e2e-tests/) 65 | - [Code Coverage by Parcel Bundler](https://glebbahmutov.com/blog/code-coverage-by-parcel/) 66 | - [Combined End-to-end and Unit Test Coverage](https://glebbahmutov.com/blog/combined-end-to-end-and-unit-test-coverage/) 67 | 68 | ## CircleCI 69 | 70 | Code coverage is saved on CircleCI as a test artifact. You can view the full report there by clicking on the "Artifacts" tab and then on "index.html" 71 | 72 | ![Code coverage artifact](images/circle-report.png) 73 | 74 | The report is a static site, you can drill into each folder to see individual source files. This project should be 100% covered by Cypress tests: 75 | 76 | ![100% code coverage](images/100.png) 77 | 78 | ## Warning 79 | 80 | Full code coverage is not the guarantee of exceptional quality. For example, the application might NOT work on mobile viewport, while working perfectly on desktop with 100% code coverage. See [cypress/integration/viewports-spec.js](cypress/integration/viewports-spec.js) for how to test main user stories across several viewports. 81 | 82 | ## Smoke tests 83 | 84 | As an example, there is a reusable smoke test [cypress/integration/smoke.js](cypress/integration/smoke.js) that goes through the most important parts of the app, covering 84% of the source code. This test can be reused from other tests, for example from [cypress/integration/smoke-spec.js](cypress/integration/smoke-spec.js), that can be executed after deploy for example by using [cypress.config.smoke.js](cypress.config.smoke.js) config file 85 | 86 | ```shell 87 | npx cypress run --config-file cypress.config.smoke.js 88 | ``` 89 | 90 | ## License 91 | 92 | This project is licensed under the terms of the [MIT license](/LICENSE.md). 93 | 94 | [renovate-badge]: https://img.shields.io/badge/renovate-app-blue.svg 95 | [renovate-app]: https://renovateapp.com/ 96 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require("cypress"); 2 | 3 | module.exports = defineConfig({ 4 | env: { 5 | "cypress-plugin-snapshots": {}, 6 | }, 7 | 8 | e2e: { 9 | // We've imported your old cypress plugins here. 10 | // You may want to clean this up later by importing these. 11 | setupNodeEvents(on, config) { 12 | require("@cypress/code-coverage/task")(on, config); 13 | 14 | on("file:preprocessor", require("@cypress/code-coverage/use-babelrc")); 15 | 16 | return config; 17 | }, 18 | baseUrl: "http://localhost:1234", 19 | excludeSpecPattern: ["**/*.snap", "**/__snapshot__/*", "**/smoke.js"], 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /cypress/e2e/first.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | it('adds todos', () => { 4 | cy.visit('/') 5 | cy.get('.new-todo') 6 | .type('write code{enter}') 7 | .type('write tests{enter}') 8 | .type('deploy{enter}') 9 | cy.get('.todo') // command 10 | .should('have.length', 3) // assertion 11 | }) 12 | -------------------------------------------------------------------------------- /cypress/e2e/full.cy.js: -------------------------------------------------------------------------------- 1 | // type definitions for Cypress object "cy" 2 | /// 3 | 4 | // type definitions for custom commands like "createDefaultTodos" 5 | /// 6 | 7 | // check this file using TypeScript if available 8 | // @ts-check 9 | 10 | // *********************************************** 11 | // All of these tests are written to implement 12 | // the official TodoMVC tests written for Selenium. 13 | // 14 | // The Cypress tests cover the exact same functionality, 15 | // and match the same test names as TodoMVC. 16 | // Please read our getting started guide 17 | // https://on.cypress.io/introduction-to-cypress 18 | // 19 | // You can find the original TodoMVC tests here: 20 | // https://github.com/tastejs/todomvc/blob/master/tests/test.js 21 | // *********************************************** 22 | 23 | // setup these constants to match what TodoMVC does 24 | const TODO_ITEM_ONE = 'buy some cheese' 25 | const TODO_ITEM_TWO = 'feed the cat' 26 | const TODO_ITEM_THREE = 'book a doctors appointment' 27 | // if you need 3 todos created instantly on load 28 | const initialState = [ 29 | { 30 | id: 0, 31 | text: TODO_ITEM_ONE, 32 | completed: false 33 | }, 34 | { 35 | id: 1, 36 | text: TODO_ITEM_TWO, 37 | completed: false 38 | }, 39 | { 40 | id: 2, 41 | text: TODO_ITEM_THREE, 42 | completed: false 43 | } 44 | ] 45 | 46 | const visitWithInitialTodos = () => { 47 | cy.visit('/', { 48 | onBeforeLoad (win) { 49 | // @ts-ignore 50 | win.initialState = initialState 51 | } 52 | }) 53 | cy.get('.todo').as('todos') 54 | } 55 | 56 | describe('TodoMVC - React', function () { 57 | beforeEach(function () { 58 | // By default Cypress will automatically 59 | // clear the Local Storage prior to each 60 | // test which ensures no todos carry over 61 | // between tests. 62 | // 63 | // Go out and visit our local web server 64 | // before each test, which serves us the 65 | // TodoMVC App we want to test against 66 | // 67 | // We've set our baseUrl to be http://localhost:8888 68 | // which is automatically prepended to cy.visit 69 | // 70 | // https://on.cypress.io/api/visit 71 | cy.visit('/') 72 | }) 73 | 74 | // a very simple example helpful during presentations 75 | it('adds 2 todos', function () { 76 | cy.get('.new-todo') 77 | .type('learn testing{enter}') 78 | .type('be cool{enter}') 79 | cy.get('.todo-list li').should('have.length', 2) 80 | }) 81 | 82 | context('When page is initially opened', function () { 83 | it('should focus on the todo input field', function () { 84 | // get the currently focused element and assert 85 | // that it has class='new-todo' 86 | // 87 | // http://on.cypress.io/focused 88 | cy.focused().should('have.class', 'new-todo') 89 | }) 90 | }) 91 | 92 | context('No Todos', function () { 93 | it('should hide #main and #footer', function () { 94 | // Unlike the TodoMVC tests, we don't need to create 95 | // a gazillion helper functions which are difficult to 96 | // parse through. Instead we'll opt to use real selectors 97 | // so as to make our testing intentions as clear as possible. 98 | // 99 | // http://on.cypress.io/get 100 | cy.get('.todo-list li').should('not.exist') 101 | cy.get('.footer').should('not.exist') 102 | }) 103 | }) 104 | 105 | context('New Todo', function () { 106 | // New commands used here: 107 | // https://on.cypress.io/type 108 | // https://on.cypress.io/eq 109 | // https://on.cypress.io/find 110 | // https://on.cypress.io/contains 111 | // https://on.cypress.io/should 112 | // https://on.cypress.io/as 113 | 114 | it('should allow me to add todo items', function () { 115 | // create 1st todo 116 | cy.get('.new-todo') 117 | .type(TODO_ITEM_ONE) 118 | .type('{enter}') 119 | 120 | // make sure the 1st label contains the 1st todo text 121 | cy.get('.todo-list li') 122 | .eq(0) 123 | .find('label') 124 | .should('contain', TODO_ITEM_ONE) 125 | 126 | // create 2nd todo 127 | cy.get('.new-todo') 128 | .type(TODO_ITEM_TWO) 129 | .type('{enter}') 130 | 131 | // make sure the 2nd label contains the 2nd todo text 132 | cy.get('.todo-list li') 133 | .eq(1) 134 | .find('label') 135 | .should('contain', TODO_ITEM_TWO) 136 | }) 137 | 138 | it('adds items', function () { 139 | // create several todos then check the number of items in the list 140 | cy.get('.new-todo') 141 | .type('todo A{enter}') 142 | .type('todo B{enter}') // we can continue working with same element 143 | .type('todo C{enter}') // and keep adding new items 144 | .type('todo D{enter}') 145 | cy.get('.todo-list li').should('have.length', 4) 146 | }) 147 | 148 | it('should clear text input field when an item is added', function () { 149 | cy.get('.new-todo') 150 | .type(TODO_ITEM_ONE) 151 | .type('{enter}') 152 | cy.get('.new-todo').should('have.text', '') 153 | }) 154 | 155 | it('should append new items to the bottom of the list', function () { 156 | // this is an example of a custom command 157 | // defined in cypress/support/commands.js 158 | cy.createDefaultTodos().as('todos') 159 | 160 | // even though the text content is split across 161 | // multiple and elements 162 | // `cy.contains` can verify this correctly 163 | cy.get('.todo-count').contains('3 items left') 164 | 165 | cy.get('@todos') 166 | .eq(0) 167 | .find('label') 168 | .should('contain', TODO_ITEM_ONE) 169 | cy.get('@todos') 170 | .eq(1) 171 | .find('label') 172 | .should('contain', TODO_ITEM_TWO) 173 | cy.get('@todos') 174 | .eq(2) 175 | .find('label') 176 | .should('contain', TODO_ITEM_THREE) 177 | }) 178 | 179 | it('should trim text input', function () { 180 | // this is an example of another custom command 181 | // since we repeat the todo creation over and over 182 | // again. It's up to you to decide when to abstract 183 | // repetitive behavior and roll that up into a custom 184 | // command vs explicitly writing the code. 185 | cy.createTodo(` ${TODO_ITEM_ONE} `) 186 | 187 | // we use as explicit assertion here about the text instead of 188 | // using 'contain' so we can specify the exact text of the element 189 | // does not have any whitespace around it 190 | cy.get('.todo-list li') 191 | .eq(0) 192 | .should('have.text', TODO_ITEM_ONE) 193 | }) 194 | 195 | it('should show #main and #footer when items added', function () { 196 | cy.createTodo(TODO_ITEM_ONE) 197 | cy.get('.main').should('be.visible') 198 | cy.get('.footer').should('be.visible') 199 | }) 200 | 201 | it('does nothing without entered text', () => { 202 | cy.get('.new-todo').type('{enter}') 203 | }) 204 | }) 205 | 206 | context('Item', function () { 207 | // New commands used here: 208 | // - cy.clear https://on.cypress.io/api/clear 209 | 210 | it('should allow me to mark items as complete', function () { 211 | // we are aliasing the return value of 212 | // our custom command 'createTodo' 213 | // 214 | // the return value is the
  • in the 215 | cy.createTodo(TODO_ITEM_ONE).as('firstTodo') 216 | cy.createTodo(TODO_ITEM_TWO).as('secondTodo') 217 | 218 | cy.get('@firstTodo') 219 | .find('.toggle') 220 | .check() 221 | cy.get('@firstTodo').should('have.class', 'completed') 222 | 223 | cy.get('@secondTodo').should('not.have.class', 'completed') 224 | cy.get('@secondTodo') 225 | .find('.toggle') 226 | .check() 227 | 228 | cy.get('@firstTodo').should('have.class', 'completed') 229 | cy.get('@secondTodo').should('have.class', 'completed') 230 | }) 231 | 232 | it('should allow me to un-mark items as complete', function () { 233 | cy.createTodo(TODO_ITEM_ONE).as('firstTodo') 234 | cy.createTodo(TODO_ITEM_TWO).as('secondTodo') 235 | 236 | cy.get('@firstTodo') 237 | .find('.toggle') 238 | .check() 239 | cy.get('@firstTodo').should('have.class', 'completed') 240 | cy.get('@secondTodo').should('not.have.class', 'completed') 241 | 242 | cy.get('@firstTodo') 243 | .find('.toggle') 244 | .uncheck() 245 | cy.get('@firstTodo').should('not.have.class', 'completed') 246 | cy.get('@secondTodo').should('not.have.class', 'completed') 247 | }) 248 | 249 | it('should allow me to edit an item', function () { 250 | cy.createDefaultTodos().as('todos') 251 | 252 | cy.get('@todos') 253 | .eq(1) 254 | .as('secondTodo') 255 | // TODO: fix this, dblclick should 256 | // have been issued to label 257 | .find('label') 258 | .dblclick() 259 | 260 | // clear out the inputs current value 261 | // and type a new value 262 | cy.get('@secondTodo') 263 | .find('.edit') 264 | .clear() 265 | .type('buy some sausages') 266 | .type('{enter}') 267 | 268 | // explicitly assert about the text value 269 | cy.get('@todos') 270 | .eq(0) 271 | .should('contain', TODO_ITEM_ONE) 272 | cy.get('@secondTodo').should('contain', 'buy some sausages') 273 | cy.get('@todos') 274 | .eq(2) 275 | .should('contain', TODO_ITEM_THREE) 276 | }) 277 | 278 | it('should delete item', () => { 279 | cy.createDefaultTodos().as('todos') 280 | // the destroy element only becomes visible on hover 281 | cy.get('@todos') 282 | .eq(1) 283 | .find('.destroy') 284 | .click({ force: true }) 285 | cy.get('@todos').should('have.length', 2) 286 | cy.get('@todos') 287 | .eq(0) 288 | .should('contain', TODO_ITEM_ONE) 289 | cy.get('@todos') 290 | .eq(1) 291 | .should('contain', TODO_ITEM_THREE) 292 | }) 293 | }) 294 | 295 | context('Counter', function () { 296 | it('should display the current number of todo items', function () { 297 | cy.createTodo(TODO_ITEM_ONE) 298 | cy.get('.todo-count').contains('1 item left') 299 | cy.createTodo(TODO_ITEM_TWO) 300 | cy.get('.todo-count').contains('2 items left') 301 | }) 302 | }) 303 | }) 304 | 305 | context('Mark all as completed', function () { 306 | // New commands used here: 307 | // - cy.check https://on.cypress.io/api/check 308 | // - cy.uncheck https://on.cypress.io/api/uncheck 309 | const completeAll = () => { 310 | // complete all todos 311 | cy.get('[data-cy-toggle-all]').click({ force: true }) 312 | } 313 | 314 | beforeEach(visitWithInitialTodos) 315 | 316 | it('should allow me to mark all items as completed', function () { 317 | completeAll() 318 | 319 | // get each todo li and ensure its class is 'completed' 320 | cy.get('@todos') 321 | .eq(0) 322 | .should('have.class', 'completed') 323 | cy.get('@todos') 324 | .eq(1) 325 | .should('have.class', 'completed') 326 | cy.get('@todos') 327 | .eq(2) 328 | .should('have.class', 'completed') 329 | }) 330 | 331 | it('should allow me to clear the complete state of all items', function () { 332 | // check and then immediately uncheck 333 | cy.get('.toggle-all') 334 | .check() 335 | .uncheck() 336 | 337 | cy.get('@todos') 338 | .eq(0) 339 | .should('not.have.class', 'completed') 340 | cy.get('@todos') 341 | .eq(1) 342 | .should('not.have.class', 'completed') 343 | cy.get('@todos') 344 | .eq(2) 345 | .should('not.have.class', 'completed') 346 | }) 347 | 348 | it('complete all checkbox should update state when items are completed / cleared', function () { 349 | completeAll() 350 | 351 | // alias the .toggle-all for reuse later 352 | cy.get('.toggle-all') 353 | .as('toggleAll') 354 | // this assertion is silly here IMO but 355 | // it is what TodoMVC does 356 | .should('be.checked') 357 | 358 | // alias the first todo and then click it 359 | cy.get('.todo-list li') 360 | .eq(0) 361 | .as('firstTodo') 362 | .find('.toggle') 363 | .uncheck() 364 | 365 | // reference the .toggle-all element again 366 | // and make sure its not checked 367 | cy.get('@toggleAll').should('not.be.checked') 368 | 369 | // reference the first todo again and now toggle it 370 | cy.get('@firstTodo') 371 | .find('.toggle') 372 | .check() 373 | 374 | // assert the toggle all is checked again 375 | cy.get('@toggleAll').should('be.checked') 376 | }) 377 | }) 378 | 379 | context('Editing', function () { 380 | // New commands used here: 381 | // - cy.blur https://on.cypress.io/api/blur 382 | 383 | beforeEach(visitWithInitialTodos) 384 | 385 | it('should hide other controls when editing', function () { 386 | cy.get('@todos') 387 | .eq(1) 388 | .as('secondTodo') 389 | .find('label') 390 | .dblclick() 391 | 392 | cy.get('@secondTodo') 393 | .find('.toggle') 394 | .should('not.exist') 395 | cy.get('@secondTodo') 396 | .find('label') 397 | .should('not.exist') 398 | }) 399 | 400 | it('should save edits on blur', function () { 401 | cy.get('@todos') 402 | .eq(1) 403 | .as('secondTodo') 404 | .find('label') 405 | .dblclick() 406 | 407 | cy.get('@secondTodo') 408 | .find('.edit') 409 | .clear() 410 | .type('buy some sausages') 411 | // we can just send the blur event directly 412 | // to the input instead of having to click 413 | // on another button on the page. though you 414 | // could do that its just more mental work 415 | .blur() 416 | 417 | cy.get('@todos') 418 | .eq(0) 419 | .should('contain', TODO_ITEM_ONE) 420 | cy.get('@secondTodo').should('contain', 'buy some sausages') 421 | cy.get('@todos') 422 | .eq(2) 423 | .should('contain', TODO_ITEM_THREE) 424 | }) 425 | 426 | it('should trim entered text', function () { 427 | cy.get('@todos') 428 | .eq(1) 429 | .as('secondTodo') 430 | .find('label') 431 | .dblclick() 432 | 433 | cy.get('@secondTodo') 434 | .find('.edit') 435 | .clear() 436 | .type(' buy some sausages ') 437 | .type('{enter}') 438 | 439 | cy.get('@todos') 440 | .eq(0) 441 | .should('contain', TODO_ITEM_ONE) 442 | cy.get('@secondTodo').should('contain', 'buy some sausages') 443 | cy.get('@todos') 444 | .eq(2) 445 | .should('contain', TODO_ITEM_THREE) 446 | }) 447 | 448 | it('should remove the item if an empty text string was entered', function () { 449 | cy.get('@todos') 450 | .eq(1) 451 | .as('secondTodo') 452 | .find('label') 453 | .dblclick() 454 | 455 | cy.get('@secondTodo') 456 | .find('.edit') 457 | .clear() 458 | .type('{enter}') 459 | 460 | cy.get('@todos').should('have.length', 2) 461 | }) 462 | }) 463 | 464 | context('Clear completed button', function () { 465 | beforeEach(visitWithInitialTodos) 466 | 467 | it('should display the correct text', function () { 468 | cy.get('@todos') 469 | .eq(0) 470 | .find('.toggle') 471 | .check() 472 | cy.get('.clear-completed').contains('Clear completed') 473 | }) 474 | 475 | it('should remove completed items when clicked', function () { 476 | cy.get('@todos') 477 | .eq(1) 478 | .find('.toggle') 479 | .check() 480 | cy.get('.clear-completed').click() 481 | cy.get('@todos').should('have.length', 2) 482 | cy.get('@todos') 483 | .eq(0) 484 | .should('contain', TODO_ITEM_ONE) 485 | cy.get('@todos') 486 | .eq(1) 487 | .should('contain', TODO_ITEM_THREE) 488 | }) 489 | 490 | it('should be hidden when there are no items that are completed', function () { 491 | cy.get('@todos') 492 | .eq(1) 493 | .find('.toggle') 494 | .check() 495 | cy.get('.clear-completed') 496 | .should('be.visible') 497 | .click() 498 | cy.get('.clear-completed').should('not.exist') 499 | }) 500 | }) 501 | 502 | context('Routing', function () { 503 | beforeEach(visitWithInitialTodos) 504 | 505 | it('should allow me to display active items', function () { 506 | cy.get('@todos') 507 | .eq(1) 508 | .find('.toggle') 509 | .check() 510 | cy.get('.filters') 511 | .contains('Active') 512 | .click() 513 | cy.get('@todos') 514 | .eq(0) 515 | .should('contain', TODO_ITEM_ONE) 516 | cy.get('@todos') 517 | .eq(1) 518 | .should('contain', TODO_ITEM_THREE) 519 | }) 520 | 521 | it.skip('should respect the back button', function () { 522 | cy.get('@todos') 523 | .eq(1) 524 | .find('.toggle') 525 | .check() 526 | cy.get('.filters') 527 | .contains('Active') 528 | .click() 529 | cy.get('.filters') 530 | .contains('Completed') 531 | .click() 532 | cy.get('@todos').should('have.length', 1) 533 | cy.go('back') 534 | cy.get('@todos').should('have.length', 2) 535 | cy.go('back') 536 | cy.get('@todos').should('have.length', 3) 537 | }) 538 | 539 | it('should allow me to display completed items', function () { 540 | cy.get('@todos') 541 | .eq(1) 542 | .find('.toggle') 543 | .check() 544 | cy.get('.filters') 545 | .contains('Completed') 546 | .click() 547 | cy.get('@todos').should('have.length', 1) 548 | }) 549 | 550 | it('should allow me to display all items', function () { 551 | cy.get('@todos') 552 | .eq(1) 553 | .find('.toggle') 554 | .check() 555 | cy.get('.filters') 556 | .contains('Active') 557 | .click() 558 | cy.get('.filters') 559 | .contains('Completed') 560 | .click() 561 | cy.get('.filters') 562 | .contains('All') 563 | .click() 564 | cy.get('@todos').should('have.length', 3) 565 | }) 566 | 567 | it('should highlight the currently applied filter', function () { 568 | // using a within here which will automatically scope 569 | // nested 'cy' queries to our parent element 570 | cy.get('.filters').within(function () { 571 | cy.contains('All').should('have.class', 'selected') 572 | cy.contains('Active') 573 | .click() 574 | .should('have.class', 'selected') 575 | cy.contains('Completed') 576 | .click() 577 | .should('have.class', 'selected') 578 | }) 579 | }) 580 | }) 581 | -------------------------------------------------------------------------------- /cypress/e2e/selectors.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import {getVisibleTodos} from '../../src/selectors' 4 | 5 | describe('getVisibleTodos', () => { 6 | it('throws an error for unknown visibility filter', () => { 7 | expect(() => { 8 | getVisibleTodos({ 9 | todos: [], 10 | visibilityFilter: 'unknown-filter' 11 | }) 12 | }).to.throw() 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /cypress/e2e/smoke-spec.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { smokeTest } from './smoke' 4 | 5 | it('does not smoke', () => { 6 | smokeTest() 7 | }) 8 | -------------------------------------------------------------------------------- /cypress/e2e/smoke.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /** 4 | * This test goes through a longer user story 5 | * trying to do almost everything a typical user would do. 6 | */ 7 | export const smokeTest = () => { 8 | cy.visit('/') 9 | cy.log('add 3 todos') 10 | cy.get('.new-todo') 11 | .type('write code{enter}') 12 | .type('write tests{enter}') 13 | .type('deploy{enter}') 14 | cy.get('.todo').should('have.length', 3) 15 | 16 | cy.log('1st todo has been done') 17 | cy.get('.todo').first().find('.toggle') 18 | .check() 19 | cy.get('.todo') 20 | .first() 21 | .should('have.class', 'completed') 22 | 23 | cy.log('by default "All" filter is active') 24 | cy.contains('.filters a.selected', 'All').should('be.visible') 25 | cy.contains('.filters a', 'Active').click() 26 | .should('have.class', 'selected').and('be.visible') 27 | cy.get('.todo').should('have.length', 2) 28 | 29 | cy.log('check "Completed" todos') 30 | cy.contains('.filters a', 'Completed').click() 31 | .should('have.class', 'selected').and('be.visible') 32 | cy.get('.todo').should('have.length', 1) 33 | 34 | cy.log('remove completed todos') 35 | cy.get('.clear-completed').click() 36 | cy.get('.todo').should('have.length', 0) 37 | cy.contains('.filters a', 'All') 38 | .click() 39 | .should('have.class', 'selected') 40 | .and('be.visible') 41 | cy.get('.todo').should('have.length', 2) 42 | } 43 | -------------------------------------------------------------------------------- /cypress/e2e/viewports.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { smokeTest } from './smoke' 4 | 5 | Cypress._.each(['macbook-15', 'iphone-6'], viewport => { 6 | it(`works on ${viewport}`, () => { 7 | cy.viewport(viewport) 8 | smokeTest() 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /cypress/fixtures/3-todos.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 0, 4 | "text": "first", 5 | "completed": true 6 | }, 7 | { 8 | "id": 1, 9 | "text": "second", 10 | "completed": false 11 | }, 12 | { 13 | "id": 2, 14 | "text": "third", 15 | "completed": true 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create the custom commands: 'createDefaultTodos' 4 | // and 'createTodo'. 5 | // 6 | // The commands.js file is a great place to 7 | // modify existing commands and create custom 8 | // commands for use throughout your tests. 9 | // 10 | // You can read more about custom commands here: 11 | // https://on.cypress.io/commands 12 | // *********************************************** 13 | 14 | Cypress.Commands.add('createDefaultTodos', function () { 15 | 16 | let TODO_ITEM_ONE = 'buy some cheese' 17 | let TODO_ITEM_TWO = 'feed the cat' 18 | let TODO_ITEM_THREE = 'book a doctors appointment' 19 | 20 | // begin the command here, which by will display 21 | // as a 'spinning blue state' in the UI to indicate 22 | // the command is running 23 | let cmd = Cypress.log({ 24 | name: 'create default todos', 25 | message: [], 26 | consoleProps () { 27 | // we're creating our own custom message here 28 | // which will print out to our browsers console 29 | // whenever we click on this command 30 | return { 31 | 'Inserted Todos': [TODO_ITEM_ONE, TODO_ITEM_TWO, TODO_ITEM_THREE], 32 | } 33 | }, 34 | }) 35 | 36 | // additionally we pass {log: false} to all of our 37 | // sub-commands so none of them will output to 38 | // our command log 39 | 40 | cy.get('.new-todo', { log: false }) 41 | .type(`${TODO_ITEM_ONE}{enter}`, { log: false }) 42 | .type(`${TODO_ITEM_TWO}{enter}`, { log: false }) 43 | .type(`${TODO_ITEM_THREE}{enter}`, { log: false }) 44 | 45 | cy.get('.todo-list li', { log: false }) 46 | .then(function ($listItems) { 47 | // once we're done inserting each of the todos 48 | // above we want to return the .todo-list li's 49 | // to allow for further chaining and then 50 | // we want to snapshot the state of the DOM 51 | // and end the command so it goes from that 52 | // 'spinning blue state' to the 'finished state' 53 | cmd.set({ $el: $listItems }).snapshot().end() 54 | }) 55 | 56 | // return a query for the todo items so that we can 57 | // alias the result of this command in our tests 58 | return cy.get('.todo-list li', { log: false }) 59 | }) 60 | 61 | Cypress.Commands.add('createTodo', function (todo) { 62 | 63 | let cmd = Cypress.log({ 64 | name: 'create todo', 65 | message: todo, 66 | consoleProps () { 67 | return { 68 | 'Inserted Todo': todo, 69 | } 70 | }, 71 | }) 72 | 73 | // create the todo 74 | cy.get('.new-todo', { log: false }).type(`${todo}{enter}`, { log: false }) 75 | 76 | // now go find the actual todo 77 | // in the todo list so we can 78 | // easily alias this in our tests 79 | // and set the $el so its highlighted 80 | cy.get('.todo-list', { log: false }) 81 | .contains('li', todo.trim(), { log: false }) 82 | .then(function ($li) { 83 | // set the $el for the command so 84 | // it highlights when we hover over 85 | // our command 86 | cmd.set({ $el: $li }).snapshot().end() 87 | }) 88 | }) 89 | -------------------------------------------------------------------------------- /cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | import '@cypress/code-coverage/support' 2 | import './commands' 3 | -------------------------------------------------------------------------------- /cypress/support/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace Cypress { 4 | interface Chainable { 5 | /** 6 | * Create several Todo items via UI 7 | * @example 8 | * cy.createDefaultTodos() 9 | */ 10 | createDefaultTodos(): Chainable 11 | /** 12 | * Creates one Todo using UI 13 | * @example 14 | * cy.createTodo('new item') 15 | */ 16 | createTodo(title: string): Chainable 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /images/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cypress-io/cypress-example-todomvc-redux/0572c7e1e97bf30f4c7ab0f49956ee855e5f3bf5/images/100.png -------------------------------------------------------------------------------- /images/circle-report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cypress-io/cypress-example-todomvc-redux/0572c7e1e97bf30f4c7ab0f49956ee855e5f3bf5/images/circle-report.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypress-example-todomvc-redux", 3 | "version": "1.0.0", 4 | "description": "Example TodoMVC with full e2e test code coverage", 5 | "main": "index.js", 6 | "private": true, 7 | "engines": { 8 | "node": ">=18" 9 | }, 10 | "scripts": { 11 | "test": "start-test 1234 cypress:run", 12 | "cypress:open": "cypress open", 13 | "cypress:run": "cypress run", 14 | "cypress:verify": "cypress verify", 15 | "start": "cross-env NODE_ENV=test parcel serve --no-cache public/index.html", 16 | "dev": "cross-env NODE_ENV=test start-test 1234 cypress:open", 17 | "report:coverage": "nyc report --reporter=html", 18 | "report:coverage:text": "nyc report --reporter=text", 19 | "report:coverage:summary": "nyc report --reporter=text-summary", 20 | "coveralls": "cat coverage/lcov.info | coveralls" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/cypress-io/cypress-example-todomvc-redux.git" 25 | }, 26 | "keywords": [ 27 | "cypress", 28 | "cypress-example", 29 | "code-coverage" 30 | ], 31 | "author": "Gleb Bahmutov ", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/cypress-io/cypress-example-todomvc-redux/issues" 35 | }, 36 | "homepage": "https://github.com/cypress-io/cypress-example-todomvc-redux#readme", 37 | "devDependencies": { 38 | "@babel/core": "7.22.17", 39 | "@babel/preset-react": "7.22.15", 40 | "@cypress/code-coverage": "^3.10.0", 41 | "babel-loader": "8.3.0", 42 | "babel-plugin-istanbul": "6.1.1", 43 | "babel-plugin-transform-class-properties": "6.24.1", 44 | "coveralls": "3.1.1", 45 | "cross-env": "7.0.3", 46 | "cypress": "13.1.0", 47 | "cypress-react-unit-test": "4.17.2", 48 | "istanbul-lib-coverage": "3.2.2", 49 | "parcel-bundler": "1.12.5", 50 | "start-server-and-test": "1.15.4", 51 | "webpack": "^5.75.0" 52 | }, 53 | "dependencies": { 54 | "classnames": "2.3.2", 55 | "prop-types": "15.8.1", 56 | "react": "17.0.2", 57 | "react-dom": "17.0.2", 58 | "react-redux": "8.1.3", 59 | "redux": "4.2.1", 60 | "reselect": "4.1.8", 61 | "todomvc-app-css": "2.4.3" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Redux TodoMVC Example 7 | 8 | 9 |
    10 | 11 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "automerge": true, 6 | "major": { 7 | "automerge": false 8 | }, 9 | "labels": [ 10 | "type: dependencies", 11 | "renovate" 12 | ], 13 | "masterIssue": true, 14 | "prConcurrentLimit": 3, 15 | "prHourlyLimit": 2, 16 | "timezone": "America/New_York", 17 | "schedule": [ 18 | "every weekend" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/actions/index.cy-spec.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes' 2 | import * as actions from './index' 3 | 4 | describe('todo actions', () => { 5 | it('addTodo should create ADD_TODO action', () => { 6 | expect(actions.addTodo('Use Redux')).to.deep.equal({ 7 | type: types.ADD_TODO, 8 | text: 'Use Redux' 9 | }) 10 | }) 11 | 12 | it('deleteTodo should create DELETE_TODO action', () => { 13 | expect(actions.deleteTodo(1)).to.deep.equal({ 14 | type: types.DELETE_TODO, 15 | id: 1 16 | }) 17 | }) 18 | 19 | it('editTodo should create EDIT_TODO action', () => { 20 | expect(actions.editTodo(1, 'Use Redux everywhere')).to.deep.equal({ 21 | type: types.EDIT_TODO, 22 | id: 1, 23 | text: 'Use Redux everywhere' 24 | }) 25 | }) 26 | 27 | it('completeTodo should create COMPLETE_TODO action', () => { 28 | expect(actions.completeTodo(1)).to.deep.equal({ 29 | type: types.COMPLETE_TODO, 30 | id: 1 31 | }) 32 | }) 33 | 34 | it('completeAll should create COMPLETE_ALL action', () => { 35 | expect(actions.completeAllTodos()).to.deep.equal({ 36 | type: types.COMPLETE_ALL_TODOS 37 | }) 38 | }) 39 | 40 | it('clearCompleted should create CLEAR_COMPLETED action', () => { 41 | expect(actions.clearCompleted()).to.deep.equal({ 42 | type: types.CLEAR_COMPLETED 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes' 2 | 3 | export const addTodo = text => ({ type: types.ADD_TODO, text }) 4 | export const deleteTodo = id => ({ type: types.DELETE_TODO, id }) 5 | export const editTodo = (id, text) => ({ type: types.EDIT_TODO, id, text }) 6 | export const completeTodo = id => ({ type: types.COMPLETE_TODO, id }) 7 | export const completeAllTodos = () => ({ type: types.COMPLETE_ALL_TODOS }) 8 | export const clearCompleted = () => ({ type: types.CLEAR_COMPLETED }) 9 | export const setVisibilityFilter = filter => ({ type: types.SET_VISIBILITY_FILTER, filter}) 10 | -------------------------------------------------------------------------------- /src/actions/index.spec.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes' 2 | import * as actions from './index' 3 | 4 | describe('todo actions', () => { 5 | it('addTodo should create ADD_TODO action', () => { 6 | expect(actions.addTodo('Use Redux')).toEqual({ 7 | type: types.ADD_TODO, 8 | text: 'Use Redux' 9 | }) 10 | }) 11 | 12 | it('deleteTodo should create DELETE_TODO action', () => { 13 | expect(actions.deleteTodo(1)).toEqual({ 14 | type: types.DELETE_TODO, 15 | id: 1 16 | }) 17 | }) 18 | 19 | it('editTodo should create EDIT_TODO action', () => { 20 | expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({ 21 | type: types.EDIT_TODO, 22 | id: 1, 23 | text: 'Use Redux everywhere' 24 | }) 25 | }) 26 | 27 | it('completeTodo should create COMPLETE_TODO action', () => { 28 | expect(actions.completeTodo(1)).toEqual({ 29 | type: types.COMPLETE_TODO, 30 | id: 1 31 | }) 32 | }) 33 | 34 | it('completeAll should create COMPLETE_ALL action', () => { 35 | expect(actions.completeAllTodos()).toEqual({ 36 | type: types.COMPLETE_ALL_TODOS 37 | }) 38 | }) 39 | 40 | it('clearCompleted should create CLEAR_COMPLETED action', () => { 41 | expect(actions.clearCompleted()).toEqual({ 42 | type: types.CLEAR_COMPLETED 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /src/components/App.cy-spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | // compare to App.spec.js 3 | import React from 'react' 4 | import App from './App' 5 | import {mount} from 'cypress-react-unit-test' 6 | // we are making mini application - thus we need a store! 7 | import { Provider } from 'react-redux' 8 | import { createStore } from 'redux' 9 | import reducer from '../reducers' 10 | import {addTodo, completeTodo} from '../actions' 11 | const store = createStore(reducer) 12 | 13 | describe('components', () => { 14 | const setup = () => { 15 | cy.viewport(600, 700) 16 | // our CSS styles assume the app is inside 17 | // a DIV element with class "todoapp" 18 | mount( 19 | 20 |
    21 | 22 |
    23 |
    , 24 | { cssFile: 'node_modules/todomvc-app-css/index.css' } 25 | ) 26 | } 27 | 28 | it('should render', () => { 29 | setup() 30 | cy.get('header').should('be.visible') 31 | // you can see that without any todos, the main section is empty 32 | // and thus invisible 33 | cy.get('.main').should('exist') 34 | }) 35 | 36 | it('should render a couple todos', () => { 37 | // use application code to interact with store 38 | store.dispatch(addTodo('write app code')) 39 | store.dispatch(addTodo('test components using Cypress')) 40 | store.dispatch(completeTodo(1)) 41 | setup() 42 | 43 | // make sure the list of items is correctly checked 44 | cy.get('.todo').should('have.length', 2) 45 | cy.contains('.todo', 'write app code').should('not.have.class', 'completed') 46 | cy.contains('.todo', 'test components using Cypress').should('have.class', 'completed') 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Header from '../containers/Header' 3 | import MainSection from '../containers/MainSection' 4 | 5 | const App = () => ( 6 |
    7 |
    8 | 9 |
    10 | ) 11 | 12 | export default App 13 | -------------------------------------------------------------------------------- /src/components/App.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRenderer } from 'react-test-renderer/shallow' 3 | import App from './App' 4 | import Header from '../containers/Header' 5 | import MainSection from '../containers/MainSection' 6 | 7 | 8 | const setup = propOverrides => { 9 | const renderer = createRenderer() 10 | renderer.render() 11 | const output = renderer.getRenderOutput() 12 | return output 13 | } 14 | 15 | describe('components', () => { 16 | describe('Header', () => { 17 | it('should render', () => { 18 | const output = setup() 19 | const [ header ] = output.props.children 20 | expect(header.type).toBe(Header) 21 | }) 22 | }) 23 | 24 | describe('Mainsection', () => { 25 | it('should render', () => { 26 | const output = setup() 27 | const [ , mainSection ] = output.props.children 28 | expect(mainSection.type).toBe(MainSection) 29 | }) 30 | }) 31 | }) -------------------------------------------------------------------------------- /src/components/Footer.cy-spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | // compare to tests in "Footer.spec.js" 3 | import React from 'react' 4 | import Footer from './Footer' 5 | import { mount } from 'cypress-react-unit-test' 6 | 7 | // we are making mini application - thus we need a store! 8 | import { Provider } from 'react-redux' 9 | import { createStore } from 'redux' 10 | import reducer from '../reducers' 11 | const store = createStore(reducer) 12 | 13 | const setup = propOverrides => { 14 | const props = Object.assign({ 15 | completedCount: 0, 16 | activeCount: 0, 17 | onClearCompleted: cy.stub().as('clear'), 18 | }, propOverrides) 19 | 20 | mount( 21 | 22 |