├── .gitignore ├── .storybook └── main.js ├── README.md ├── cypress.json ├── cypress ├── README.md ├── fixtures │ └── example.json ├── integration │ └── spec.js ├── plugins │ └── index.js └── support │ └── index.js ├── fake.test.js ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── application │ ├── About │ │ └── index.js │ ├── App.js │ └── Users │ │ └── index.js ├── design-system │ ├── Alert │ │ └── index.js │ ├── GlobalStyles │ │ └── index.js │ ├── Message │ │ ├── index.js │ │ └── stories.js │ └── Toggle │ │ ├── index.js │ │ └── stories.js ├── index.css ├── index.js ├── setupTests.js └── utilities │ ├── addOne.js │ ├── useToggle.js │ └── useToggle.test.js ├── storyshots └── index.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | /cypress/videos 25 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['../src/**/*.stories.js', '../src/**/stories.js'], 3 | addons: [ 4 | '@storybook/preset-create-react-app', 5 | '@storybook/addon-actions', 6 | '@storybook/addon-links', 7 | ], 8 | }; 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Combining Storybook, Cypress and Jest Code Coverage 2 | 3 | Please refer to [this blog post for more detailed information](https://dev.to/penx/combining-storybook-cypress-and-jest-code-coverage-4pa5). 4 | 5 | This project shows how to collect code coverage from Storybook (e.g. when using [Chromatic](https://www.chromaticqa.com) for Visual Regression tests) and how to create a combined code coverage report for 3 types of test: 6 | 7 | - Visual regression tests ([Storybook](https://storybook.js.org)/[Chromatic](https://www.chromaticqa.com)) 8 | - Unit tests ([Jest](http://jestjs.io)) 9 | - Integration tests ([Cypress](http://cypress.io)) 10 | 11 | You can view a report for each type of test, or view the coverage of all tests together. 12 | 13 | To try out this project this locally: 14 | 15 | ```sh 16 | git clone git@github.com:penx/storybook-code-coverage.git 17 | cd storybook-code-coverage 18 | yarn 19 | yarn coverage 20 | open coverage/merged/lcov-report/index.html 21 | ``` 22 | 23 | ## Combining coverage 24 | 25 | You can also [merge reports with codecov](https://docs.codecov.io/docs/merging-reports). 26 | 27 | ## Known issues 28 | 29 | - cypress-specific nyc settings have to go in to generic files (e.g. `package.json` or `.nycrc`) 30 | - There's a bug with create-react-app@3.4.1 that prevents this from working, so for now you have to roll back to 3.4.0 [facebook/create-react-app#8689](https://github.com/facebook/create-react-app/issues/8689). 31 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:3000" 3 | } 4 | -------------------------------------------------------------------------------- /cypress/README.md: -------------------------------------------------------------------------------- 1 | # Cypress.io end-to-end tests 2 | 3 | [Cypress.io](https://www.cypress.io) is an open source, MIT licensed end-to-end test runner 4 | 5 | ## Folder structure 6 | 7 | These folders hold end-to-end tests and supporting files for the Cypress Test Runner. 8 | 9 | - [fixtures](fixtures) holds optional JSON data for mocking, [read more](https://on.cypress.io/fixture) 10 | - [integration](integration) holds the actual test files, [read more](https://on.cypress.io/writing-and-organizing-tests) 11 | - [plugins](plugins) allow you to customize how tests are loaded, [read more](https://on.cypress.io/plugins) 12 | - [support](support) file runs before all tests and is a great place to write or load additional custom commands, [read more](https://on.cypress.io/writing-and-organizing-tests#Support-file) 13 | 14 | ## `cypress.json` file 15 | 16 | You can configure project options in the [../cypress.json](../cypress.json) file, see [Cypress configuration doc](https://on.cypress.io/configuration). 17 | 18 | ## More information 19 | 20 | - [https://github.com/cypress.io/cypress](https://github.com/cypress.io/cypress) 21 | - [https://docs.cypress.io/](https://docs.cypress.io/) 22 | - [Writing your first Cypress test](http://on.cypress.io/intro) 23 | -------------------------------------------------------------------------------- /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 | } 6 | -------------------------------------------------------------------------------- /cypress/integration/spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | beforeEach(() => { 4 | cy.visit('/') 5 | }) 6 | 7 | it("has a visible header", function () { 8 | cy.get(".header").should("be.visible") 9 | .and('have.text', 'Example Application'); 10 | }); 11 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | module.exports = (on, config) => { 15 | // `on` is used to hook into various events Cypress emits 16 | // `config` is the resolved Cypress config 17 | require('@cypress/code-coverage/task')(on, config); 18 | return config; 19 | } 20 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | import '@cypress/code-coverage/support' 16 | -------------------------------------------------------------------------------- /fake.test.js: -------------------------------------------------------------------------------- 1 | it("passes", () => {}); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storybook-code-coverage", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^10.0.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "react": "^16.13.1", 10 | "react-dom": "^16.13.1", 11 | "react-router-dom": "^5.1.2", 12 | "react-scripts": "3.4.0", 13 | "start-server-and-test": "^1.11.0", 14 | "styled-components": "^5.1.0" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts -r @cypress/instrument-cra start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject", 21 | "storybook": "start-storybook -p 9009 -s public", 22 | "build-storybook": "build-storybook -s public", 23 | "test:integration": "cypress run", 24 | "coverage": "yarn coverage:clean && yarn coverage:init && yarn coverage:integration && yarn coverage:unit && yarn coverage:visual-regression && yarn coverage:merge && yarn coverage:merge-report", 25 | "coverage:clean": "rm -rf .nyc_output && rm -rf coverage", 26 | "coverage:init": "react-scripts test --watchAll=false --coverage --coverageDirectory=.nyc_output --roots=\"\" --testMatch=\"/fake.test.js\" --coverageReporters=json && mv .nyc_output/coverage-final.json .nyc_output/out.json", 27 | "coverage:integration": "start-server-and-test 3000 test:integration", 28 | "coverage:unit": "react-scripts test --watchAll=false --coverage --coverageDirectory=coverage/unit", 29 | "coverage:visual-regression": "react-scripts test --watchAll=false --coverage --coverageDirectory=coverage/visual-regression --roots=\"\" --testMatch=\"/storyshots/index.js\"", 30 | "coverage:merge": "istanbul-merge --out coverage/merged/coverage-final.json ./coverage/unit/coverage-final.json ./coverage/visual-regression/coverage-final.json ./coverage/integration/coverage-final.json", 31 | "coverage:merge-report": "nyc report --reporter=lcov --reporter=text --temp-dir=./coverage/merged --report-dir=./coverage/merged" 32 | }, 33 | "jest": { 34 | "collectCoverageFrom": [ 35 | "src/**/*.js", 36 | "!**/*.test.js", 37 | "!**/test.js", 38 | "!**/*.stories.js", 39 | "!**/stories.js" 40 | ] 41 | }, 42 | "nyc": { 43 | "report-dir": "coverage/integration", 44 | "reporter": ["text", "json", "lcov"], 45 | "all": true, 46 | "include": [ 47 | "src/**/*.js" 48 | ], 49 | "exclude": [ 50 | "**/*.test.js", 51 | "**/test.js", 52 | "**/*.stories.js", 53 | "**/stories.js", 54 | "src/setupTests.js" 55 | ] 56 | }, 57 | "eslintConfig": { 58 | "extends": "react-app" 59 | }, 60 | "browserslist": { 61 | "production": [ 62 | ">0.2%", 63 | "not dead", 64 | "not op_mini all" 65 | ], 66 | "development": [ 67 | "last 1 chrome version", 68 | "last 1 firefox version", 69 | "last 1 safari version" 70 | ] 71 | }, 72 | "devDependencies": { 73 | "@cypress/code-coverage": "^3.5.1", 74 | "@cypress/instrument-cra": "^1.1.1", 75 | "@storybook/addon-actions": "^5.3.18", 76 | "@storybook/addon-links": "^5.3.18", 77 | "@storybook/addon-storyshots": "^5.3.18", 78 | "@storybook/addons": "^5.3.18", 79 | "@storybook/preset-create-react-app": "^2.1.1", 80 | "@storybook/react": "^5.3.18", 81 | "cypress": "^4.4.0", 82 | "istanbul-lib-coverage": "^3.0.0", 83 | "istanbul-merge": "^1.1.1", 84 | "nyc": "^15.0.1", 85 | "react-test-renderer": "^16.13.1" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penx/storybook-code-coverage/bd07752ffa617a601c78dbbd0114edd9055fb16e/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penx/storybook-code-coverage/bd07752ffa617a601c78dbbd0114edd9055fb16e/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penx/storybook-code-coverage/bd07752ffa617a601c78dbbd0114edd9055fb16e/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/application/About/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const About = () => ( 4 |
5 |

About

6 |

About page

7 |
8 | ); 9 | -------------------------------------------------------------------------------- /src/application/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useToggle } from "../utilities/useToggle"; 3 | import { Toggle } from "../design-system/Toggle"; 4 | import { GlobalStyles } from "../design-system/GlobalStyles"; 5 | import { Switch, Route, Link } from "react-router-dom"; 6 | import { About } from "./About"; 7 | 8 | const Users = React.lazy(() => import('./Users')); 9 | 10 | function App() { 11 | const { on: darkModeOn, toggle: toggleDarkMode } = useToggle(); 12 | return ( 13 | 14 |

Example Application

15 | Dark Mode: 16 |
17 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |

Home

44 |

Home page

45 |
46 | {darkModeOn && ( 47 | 48 | 49 | 50 | )} 51 |
52 |
53 |
54 | ); 55 | } 56 | 57 | const DarkModePage = () => ( 58 | <> 59 |

Hidden Page

60 |

This route is only available in dark mode

61 | 62 | ); 63 | 64 | export default App; 65 | -------------------------------------------------------------------------------- /src/application/Users/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const Users = () => ( 4 |
5 |

Users

6 |

Users page

7 |
8 | ); 9 | -------------------------------------------------------------------------------- /src/design-system/Alert/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Alert = () =>
4 | This component is not used by the application and is not in storybook. 5 |
-------------------------------------------------------------------------------- /src/design-system/GlobalStyles/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ThemeProvider, createGlobalStyle } from "styled-components"; 3 | 4 | const defaultTheme = { 5 | }; 6 | 7 | const darkTheme = { 8 | primary: "black", 9 | text: "white", 10 | }; 11 | 12 | const GlobalStyle = createGlobalStyle` 13 | body { 14 | color: ${(props) => (props.theme.text)}; 15 | background-color: ${(props) => (props.theme.primary)}; 16 | } 17 | `; 18 | 19 | export const GlobalStyles = ({ darkModeEnabled, children }) => { 20 | return ( 21 | 22 | 23 | {children} 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/design-system/Message/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Message = ({message}) =>
4 | This component is not used by the application but is in storybook. 5 | {message} 6 |
-------------------------------------------------------------------------------- /src/design-system/Message/stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Message } from "."; 3 | 4 | export default { 5 | title: "Message", 6 | component: Message, 7 | }; 8 | 9 | export const Default = () => ; 10 | -------------------------------------------------------------------------------- /src/design-system/Toggle/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const StyledButton = styled.button` 5 | background-color: ${(props) => props.theme.primary || "green"}; 6 | color: ${(props) => props.theme.text || "white"}; 7 | `; 8 | 9 | export const Toggle = ({ on, onToggle }) => ( 10 | 11 | {" "} 12 | {on ? "Turn Off" : "Turn On"}{" "} 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /src/design-system/Toggle/stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Toggle } from "."; 3 | 4 | export default { 5 | title: "Toggle", 6 | component: Toggle, 7 | }; 8 | 9 | export const Default = () => ; 10 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./application/App"; 5 | import { BrowserRouter } from "react-router-dom"; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | 11 | 12 | , 13 | document.getElementById("root") 14 | ); 15 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/utilities/addOne.js: -------------------------------------------------------------------------------- 1 | export const addOne = (x) => x + 1; 2 | -------------------------------------------------------------------------------- /src/utilities/useToggle.js: -------------------------------------------------------------------------------- 1 | // from https://kentcdodds.com/blog/the-state-reducer-pattern-with-react-hooks 2 | 3 | import React from "react"; 4 | 5 | export const useToggle = (initial = false) => { 6 | const [on, setOnState] = React.useState(initial); 7 | 8 | const toggle = () => setOnState((o) => !o); 9 | const setOn = () => setOnState(true); 10 | const setOff = () => setOnState(false); 11 | 12 | return { on, toggle, setOn, setOff }; 13 | }; 14 | -------------------------------------------------------------------------------- /src/utilities/useToggle.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, fireEvent } from "@testing-library/react"; 3 | 4 | import { useToggle } from "./useToggle"; 5 | 6 | it("is off by default", () => { 7 | const onMessage = "Is On"; 8 | const offMessage = "Is Off"; 9 | 10 | const TestComponent = () => { 11 | const { on } = useToggle(); 12 | 13 | return ( 14 |
15 |

{on ? onMessage : offMessage}

16 |
17 | ); 18 | }; 19 | 20 | const { getByText } = render(); 21 | expect(getByText(offMessage)).toBeInTheDocument(); 22 | }); 23 | 24 | it("toggle changes on state", () => { 25 | const onMessage = "Is On"; 26 | const offMessage = "Is Off"; 27 | const toggleMessage = "Toggle"; 28 | 29 | const TestComponent = () => { 30 | const { on, toggle } = useToggle(); 31 | 32 | return ( 33 |
34 |

{on ? onMessage : offMessage}

35 | 36 |
37 | ); 38 | }; 39 | 40 | const { getByText } = render(); 41 | fireEvent.click(getByText(toggleMessage)); 42 | expect(getByText(onMessage)).toBeInTheDocument(); 43 | }); 44 | 45 | it("setOn changes on state from off to on", () => { 46 | const onMessage = "Is On"; 47 | const offMessage = "Is Off"; 48 | const setOnMessage = "Set On"; 49 | 50 | const TestComponent = () => { 51 | const { on, setOn } = useToggle(); 52 | 53 | return ( 54 |
55 |

{on ? onMessage : offMessage}

56 | 57 |
58 | ); 59 | }; 60 | 61 | const { getByText } = render(); 62 | fireEvent.click(getByText(setOnMessage)); 63 | expect(getByText(onMessage)).toBeInTheDocument(); 64 | }); 65 | 66 | it("supports initial on state", () => { 67 | const onMessage = "Is On"; 68 | const offMessage = "Is Off"; 69 | 70 | const TestComponent = () => { 71 | const { on } = useToggle(true); 72 | 73 | return ( 74 |
75 |

{on ? onMessage : offMessage}

76 |
77 | ); 78 | }; 79 | 80 | const { getByText } = render(); 81 | expect(getByText(onMessage)).toBeInTheDocument(); 82 | }); 83 | 84 | it("setOff changes on state from on to off", () => { 85 | const onMessage = "Is On"; 86 | const offMessage = "Is Off"; 87 | const setOffMessage = "Set Off"; 88 | 89 | const TestComponent = () => { 90 | const { on, setOff } = useToggle(true); 91 | 92 | return ( 93 |
94 |

{on ? onMessage : offMessage}

95 | 96 |
97 | ); 98 | }; 99 | 100 | const { getByText } = render(); 101 | fireEvent.click(getByText(setOffMessage)); 102 | expect(getByText(offMessage)).toBeInTheDocument(); 103 | }); 104 | -------------------------------------------------------------------------------- /storyshots/index.js: -------------------------------------------------------------------------------- 1 | import initStoryshots, {renderOnly} from '@storybook/addon-storyshots'; 2 | 3 | initStoryshots({test: renderOnly}); --------------------------------------------------------------------------------