├── .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 [](https://circleci.com/gh/cypress-io/cypress-example-todomvc-redux) [![renovate-app badge][renovate-badge]][renovate-app] [](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 | [](https://gitlab.com/cypress-io/cypress-example-todomvc-redux/commits/master) [](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 | 
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 | 
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 |