diff --git a/README.md b/README.md index b93ea512..f71bc3aa 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ > A little helper to unit test React components in the open source [Cypress.io](https://www.cypress.io/) E2E test runner **v4.5.0+** +**Jump to:** [Comparison](#comparison), [Install](#install), Examples: [basic](#basic-examples), [advanced](#advanced-examples), [full](#full-examples), [external](#external-examples), [Options](#options), [Code coverage](#code-coverage), [Visual testing](#visual-testing), [Commoon problems](#common-problems) + ## TLDR - What is this? This package allows you to use [Cypress](https://www.cypress.io/) test runner to unit test your React components with zero effort. Here is a typical component testing, notice there is not external URL shown, since it is mounting the component directly. @@ -64,8 +66,6 @@ module.exports = (on, config) => { See [Recipes](./docs/recipes.md) for more examples. -**⚠️ Note:** when using `react-scripts` you must place component specs in the `src` folder too, otherwise they won't be transpiled correctly. - 3. ⚠️ Turn the experimental component support on in your `cypress.json`. You can also specify where component spec files are located. For example, to have them located in `src` folder use: ```json @@ -145,7 +145,18 @@ Spec | Description [react-bootstrap](cypress/component/advanced/react-bootstrap) | Confirms [react-bootstrap](https://react-bootstrap.github.io/) components are working -### Large examples +### Full examples + +We have several subfolders in [examples](examples) folder that have complete projects with just their dependencies installed in the root folder. + + +Folder Name | Description +--- | --- +[react-scripts](examples/react-scripts) | A project using `react-scripts` with component tests in `src` folder +[react-scripts-folder](examples/react-scripts-folder) | A project using `react-scripts` with component tests in `cypress/component` + + +### External examples This way of component testing has been verified in a number of forked 3rd party projects. diff --git a/circle.yml b/circle.yml index 1ed04169..71ae11c6 100644 --- a/circle.yml +++ b/circle.yml @@ -33,6 +33,20 @@ workflows: command: npm test store_artifacts: true + - cypress/run: + # react-scripts example with component tests not in "src" folder + # but in "cypress/component" folder + name: Example Component Folder + requires: + - Install + # each example installs "cypress-react-unit-test" as a local dependency (symlink) + install-command: npm install + verify-command: echo 'Already verified' + no-workspace: true + working_directory: examples/react-scripts-folder + command: npm test + store_artifacts: true + - cypress/run: name: Test parallelism: 4 @@ -63,6 +77,7 @@ workflows: - Install - Test - Example React Scripts + - Example Component Folder install-command: echo 'Already installed' verify-command: echo 'Already verified' no-workspace: true diff --git a/examples/react-scripts-folder/.npmrc b/examples/react-scripts-folder/.npmrc new file mode 100644 index 00000000..43c97e71 --- /dev/null +++ b/examples/react-scripts-folder/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/examples/react-scripts-folder/README.md b/examples/react-scripts-folder/README.md new file mode 100644 index 00000000..811959e7 --- /dev/null +++ b/examples/react-scripts-folder/README.md @@ -0,0 +1,13 @@ +# example: react-scripts-folder + +A typical project using `react-scripts` with components in the [src](src) folder and component tests inside [cypress/component](cypress/component) folder. Cypress automatically finds Webpack settings used by `react-scripts` and inserts the component folder name allowing to transpile the component specs the same way the `src` code is transpiled. + +Note: run `npm install` in this folder to symlink `cypress-react-unit-test` dependency. + +```shell +npm run cy:open +# or just run headless tests +npm test +``` + +![App test](images/app-test.png) diff --git a/examples/react-scripts-folder/cypress.json b/examples/react-scripts-folder/cypress.json new file mode 100644 index 00000000..7307c0f7 --- /dev/null +++ b/examples/react-scripts-folder/cypress.json @@ -0,0 +1,8 @@ +{ + "fixturesFolder": false, + "testFiles": "**/*cy-spec.js", + "viewportWidth": 500, + "viewportHeight": 500, + "experimentalComponentTesting": true, + "componentFolder": "cypress/component" +} diff --git a/examples/react-scripts-folder/cypress/component/App.cy-spec.js b/examples/react-scripts-folder/cypress/component/App.cy-spec.js new file mode 100644 index 00000000..64bdfd4c --- /dev/null +++ b/examples/react-scripts-folder/cypress/component/App.cy-spec.js @@ -0,0 +1,18 @@ +/// +// compare to App.test.js +import React from 'react' +import App from '../../src/App' +import { mount } from 'cypress-react-unit-test' + +describe('App', () => { + it('renders learn react link', () => { + expect(1).to.equal(1) + mount() + cy.contains(/Learn React/) + }) + + it('renders inline component', () => { + mount(
JSX
) + cy.contains('JSX') + }) +}) diff --git a/examples/react-scripts/cypress/integration/spec.js b/examples/react-scripts-folder/cypress/integration/cy-spec.js similarity index 100% rename from examples/react-scripts/cypress/integration/spec.js rename to examples/react-scripts-folder/cypress/integration/cy-spec.js diff --git a/examples/react-scripts-folder/cypress/plugins/index.js b/examples/react-scripts-folder/cypress/plugins/index.js new file mode 100644 index 00000000..7a27c8cd --- /dev/null +++ b/examples/react-scripts-folder/cypress/plugins/index.js @@ -0,0 +1,7 @@ +const preprocessor = require('../../../../plugins/react-scripts') +module.exports = (on, config) => { + preprocessor(on, config) + // IMPORTANT to return the config object + // with the any changed environment variables + return config +} diff --git a/examples/react-scripts-folder/cypress/support/index.js b/examples/react-scripts-folder/cypress/support/index.js new file mode 100644 index 00000000..6d8199e9 --- /dev/null +++ b/examples/react-scripts-folder/cypress/support/index.js @@ -0,0 +1,2 @@ +require('cypress-react-unit-test/support') +require('@cypress/code-coverage/support') diff --git a/examples/react-scripts-folder/images/app-test.png b/examples/react-scripts-folder/images/app-test.png new file mode 100644 index 00000000..49837f1b Binary files /dev/null and b/examples/react-scripts-folder/images/app-test.png differ diff --git a/examples/react-scripts-folder/package.json b/examples/react-scripts-folder/package.json new file mode 100644 index 00000000..d8e5eb84 --- /dev/null +++ b/examples/react-scripts-folder/package.json @@ -0,0 +1,12 @@ +{ + "name": "example-react-scripts-folder", + "description": "Handles component tests from cypress folder", + "private": true, + "scripts": { + "test": "../../node_modules/.bin/cypress run", + "cy:open": "../../node_modules/.bin/cypress open" + }, + "devDependencies": { + "cypress-react-unit-test": "file:../.." + } +} diff --git a/examples/react-scripts-folder/public/favicon.ico b/examples/react-scripts-folder/public/favicon.ico new file mode 100644 index 00000000..bcd5dfd6 Binary files /dev/null and b/examples/react-scripts-folder/public/favicon.ico differ diff --git a/examples/react-scripts-folder/public/index.html b/examples/react-scripts-folder/public/index.html new file mode 100644 index 00000000..aa069f27 --- /dev/null +++ b/examples/react-scripts-folder/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/examples/react-scripts-folder/public/logo192.png b/examples/react-scripts-folder/public/logo192.png new file mode 100644 index 00000000..fc44b0a3 Binary files /dev/null and b/examples/react-scripts-folder/public/logo192.png differ diff --git a/examples/react-scripts-folder/public/logo512.png b/examples/react-scripts-folder/public/logo512.png new file mode 100644 index 00000000..a4e47a65 Binary files /dev/null and b/examples/react-scripts-folder/public/logo512.png differ diff --git a/examples/react-scripts-folder/public/manifest.json b/examples/react-scripts-folder/public/manifest.json new file mode 100644 index 00000000..080d6c77 --- /dev/null +++ b/examples/react-scripts-folder/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/examples/react-scripts-folder/public/robots.txt b/examples/react-scripts-folder/public/robots.txt new file mode 100644 index 00000000..e9e57dc4 --- /dev/null +++ b/examples/react-scripts-folder/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/examples/react-scripts-folder/src/App.css b/examples/react-scripts-folder/src/App.css new file mode 100644 index 00000000..74b5e053 --- /dev/null +++ b/examples/react-scripts-folder/src/App.css @@ -0,0 +1,38 @@ +.App { + text-align: center; +} + +.App-logo { + height: 40vmin; + pointer-events: none; +} + +@media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: App-logo-spin infinite 20s linear; + } +} + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.App-link { + color: #61dafb; +} + +@keyframes App-logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/examples/react-scripts-folder/src/App.js b/examples/react-scripts-folder/src/App.js new file mode 100644 index 00000000..b0511af9 --- /dev/null +++ b/examples/react-scripts-folder/src/App.js @@ -0,0 +1,38 @@ +import React from 'react' +// this CSS will be inlined ✅ +import './App.css' +import logo from './logo.svg' // => "/__root/src/logo.svg" +import cypressLogo from './cypress-logo-dark.png' // => "/__root/src/cypress-logo-dark.png" + +// large image will be transformed into +// a different url static/media/vans.25e5784d.jpg +// import giantImage from './vans.jpg' + +// we cannot load the image from direct url +// import giantImage from '/__root/src/vans.jpg' +// cypress-logo + +function App() { + return ( +
+ + +
+ logo +

+ Edit src/App.js and save to reload. +

+ + Learn React + +
+
+ ) +} + +export default App diff --git a/examples/react-scripts-folder/src/App.test.js b/examples/react-scripts-folder/src/App.test.js new file mode 100644 index 00000000..4a863755 --- /dev/null +++ b/examples/react-scripts-folder/src/App.test.js @@ -0,0 +1,9 @@ +import React from 'react' +import { render } from '@testing-library/react' +import App from './App' + +test('renders learn react link', () => { + const { getByText } = render() + const linkElement = getByText(/learn react/i) + expect(linkElement).toBeInTheDocument() +}) diff --git a/examples/react-scripts-folder/src/Logo.cy-spec.js b/examples/react-scripts-folder/src/Logo.cy-spec.js new file mode 100644 index 00000000..37b26b08 --- /dev/null +++ b/examples/react-scripts-folder/src/Logo.cy-spec.js @@ -0,0 +1,12 @@ +/// +import React from 'react' +import { mount } from 'cypress-react-unit-test' +// import SVG as ReactComponent +import { ReactComponent as Logo } from './logo.svg' + +describe('Logo', () => { + it('imports SVG', () => { + mount() + cy.get('path').should('have.attr', 'd') + }) +}) diff --git a/examples/react-scripts-folder/src/cypress-logo-dark.png b/examples/react-scripts-folder/src/cypress-logo-dark.png new file mode 100644 index 00000000..6553e4a0 Binary files /dev/null and b/examples/react-scripts-folder/src/cypress-logo-dark.png differ diff --git a/examples/react-scripts-folder/src/index.css b/examples/react-scripts-folder/src/index.css new file mode 100644 index 00000000..ec2585e8 --- /dev/null +++ b/examples/react-scripts-folder/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/examples/react-scripts-folder/src/index.js b/examples/react-scripts-folder/src/index.js new file mode 100644 index 00000000..0a2df99c --- /dev/null +++ b/examples/react-scripts-folder/src/index.js @@ -0,0 +1,17 @@ +import React from 'react' +import ReactDOM from 'react-dom' +import './index.css' +import App from './App' +import * as serviceWorker from './serviceWorker' + +ReactDOM.render( + + + , + document.getElementById('root'), +) + +// If you want your app to work offline and load faster, you can change +// unregister() to register() below. Note this comes with some pitfalls. +// Learn more about service workers: https://bit.ly/CRA-PWA +serviceWorker.unregister() diff --git a/examples/react-scripts-folder/src/logo.svg b/examples/react-scripts-folder/src/logo.svg new file mode 100644 index 00000000..6b60c104 --- /dev/null +++ b/examples/react-scripts-folder/src/logo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/examples/react-scripts-folder/src/serviceWorker.js b/examples/react-scripts-folder/src/serviceWorker.js new file mode 100644 index 00000000..d482dfa0 --- /dev/null +++ b/examples/react-scripts-folder/src/serviceWorker.js @@ -0,0 +1,141 @@ +// This optional code is used to register a service worker. +// register() is not called by default. + +// This lets the app load faster on subsequent visits in production, and gives +// it offline capabilities. However, it also means that developers (and users) +// will only see deployed updates on subsequent visits to a page, after all the +// existing tabs open on the page have been closed, since previously cached +// resources are updated in the background. + +// To learn more about the benefits of this model and instructions on how to +// opt-in, read https://bit.ly/CRA-PWA + +const isLocalhost = Boolean( + window.location.hostname === 'localhost' || + // [::1] is the IPv6 localhost address. + window.location.hostname === '[::1]' || + // 127.0.0.0/8 are considered localhost for IPv4. + window.location.hostname.match( + /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/, + ), +) + +export function register(config) { + if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { + // The URL constructor is available in all browsers that support SW. + const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href) + if (publicUrl.origin !== window.location.origin) { + // Our service worker won't work if PUBLIC_URL is on a different origin + // from what our page is served on. This might happen if a CDN is used to + // serve assets; see https://github.com/facebook/create-react-app/issues/2374 + return + } + + window.addEventListener('load', () => { + const swUrl = `${process.env.PUBLIC_URL}/service-worker.js` + + if (isLocalhost) { + // This is running on localhost. Let's check if a service worker still exists or not. + checkValidServiceWorker(swUrl, config) + + // Add some additional logging to localhost, pointing developers to the + // service worker/PWA documentation. + navigator.serviceWorker.ready.then(() => { + console.log( + 'This web app is being served cache-first by a service ' + + 'worker. To learn more, visit https://bit.ly/CRA-PWA', + ) + }) + } else { + // Is not localhost. Just register service worker + registerValidSW(swUrl, config) + } + }) + } +} + +function registerValidSW(swUrl, config) { + navigator.serviceWorker + .register(swUrl) + .then(registration => { + registration.onupdatefound = () => { + const installingWorker = registration.installing + if (installingWorker == null) { + return + } + installingWorker.onstatechange = () => { + if (installingWorker.state === 'installed') { + if (navigator.serviceWorker.controller) { + // At this point, the updated precached content has been fetched, + // but the previous service worker will still serve the older + // content until all client tabs are closed. + console.log( + 'New content is available and will be used when all ' + + 'tabs for this page are closed. See https://bit.ly/CRA-PWA.', + ) + + // Execute callback + if (config && config.onUpdate) { + config.onUpdate(registration) + } + } else { + // At this point, everything has been precached. + // It's the perfect time to display a + // "Content is cached for offline use." message. + console.log('Content is cached for offline use.') + + // Execute callback + if (config && config.onSuccess) { + config.onSuccess(registration) + } + } + } + } + } + }) + .catch(error => { + console.error('Error during service worker registration:', error) + }) +} + +function checkValidServiceWorker(swUrl, config) { + // Check if the service worker can be found. If it can't reload the page. + fetch(swUrl, { + headers: { 'Service-Worker': 'script' }, + }) + .then(response => { + // Ensure service worker exists, and that we really are getting a JS file. + const contentType = response.headers.get('content-type') + if ( + response.status === 404 || + (contentType != null && contentType.indexOf('javascript') === -1) + ) { + // No service worker found. Probably a different app. Reload the page. + navigator.serviceWorker.ready.then(registration => { + registration.unregister().then(() => { + window.location.reload() + }) + }) + } else { + // Service worker found. Proceed as normal. + registerValidSW(swUrl, config) + } + }) + .catch(() => { + console.log( + 'No internet connection found. App is running in offline mode.', + ) + }) +} + +export function unregister() { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.ready + .then(registration => { + registration.unregister() + }) + .catch(error => { + console.error(error.message) + }) + } +} diff --git a/examples/react-scripts-folder/src/setupTests.js b/examples/react-scripts-folder/src/setupTests.js new file mode 100644 index 00000000..2eb59b05 --- /dev/null +++ b/examples/react-scripts-folder/src/setupTests.js @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom/extend-expect' diff --git a/examples/react-scripts/README.md b/examples/react-scripts/README.md new file mode 100644 index 00000000..f6766d6b --- /dev/null +++ b/examples/react-scripts/README.md @@ -0,0 +1,13 @@ +# example: react-scripts + +A typical project using `react-scripts` with components and matching component tests residing in the [src](src) folder. + +Note: run `npm install` in this folder to symlink `cypress-react-unit-test` dependency. + +```shell +npm run cy:open +# or just run headless tests +npm test +``` + +![App test](images/app-test.png) diff --git a/examples/react-scripts/cypress.json b/examples/react-scripts/cypress.json index 1db519ea..d94c98fd 100644 --- a/examples/react-scripts/cypress.json +++ b/examples/react-scripts/cypress.json @@ -1,6 +1,6 @@ { "fixturesFolder": false, - "testFiles": "**/*.cy-spec.js", + "testFiles": "**/*cy-spec.js", "viewportWidth": 500, "viewportHeight": 500, "experimentalComponentTesting": true, diff --git a/examples/react-scripts/cypress/integration/cy-spec.js b/examples/react-scripts/cypress/integration/cy-spec.js new file mode 100644 index 00000000..d815fd54 --- /dev/null +++ b/examples/react-scripts/cypress/integration/cy-spec.js @@ -0,0 +1,6 @@ +/// +describe('integration spec', () => { + it('works', () => { + expect(1).to.equal(1) + }) +}) diff --git a/examples/react-scripts/images/app-test.png b/examples/react-scripts/images/app-test.png new file mode 100644 index 00000000..e06cfeb5 Binary files /dev/null and b/examples/react-scripts/images/app-test.png differ diff --git a/package-lock.json b/package-lock.json index 412fd2d8..c73ce459 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12106,9 +12106,9 @@ } }, "find-webpack": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/find-webpack/-/find-webpack-1.7.2.tgz", - "integrity": "sha512-hLbO6NSaq+8P1rCr+Wqrd0d3UVB3rHyzz69Zx8GkX3tYWoV3MGYsqTTPqPcRDHqgznqNCWiuWWg5CnbtLU8xiQ==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/find-webpack/-/find-webpack-1.7.3.tgz", + "integrity": "sha512-+fTV+WEZc9RyB0nmiHTAp0+ZoFDQWqCL288uvdMp5BJ8WGWLlmRMMtSoqrhXL8aMIWAXnDfbWTXJd2IHNuAo/Q==", "requires": { "debug": "4.1.1", "find-yarn-workspace-root": "1.2.1", diff --git a/package.json b/package.json index a7640935..ec9b36a0 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "@cypress/webpack-preprocessor": "5.2.0", "babel-plugin-istanbul": "6.0.0", "debug": "4.1.1", - "find-webpack": "1.7.2", + "find-webpack": "1.7.3", "mime-types": "2.1.26" }, "release": { diff --git a/plugins/cra-v3/file-preprocessor.js b/plugins/cra-v3/file-preprocessor.js index 585e5e9a..7b4a2741 100644 --- a/plugins/cra-v3/file-preprocessor.js +++ b/plugins/cra-v3/file-preprocessor.js @@ -35,11 +35,15 @@ module.exports = config => { const coverageIsDisabled = config && config.env && config.env.coverage === false debug('coverage is disabled? %o', { coverageIsDisabled }) + debug('component test folder: %s', config.componentFolder) const opts = { reactScripts: true, + addFolderToTranspile: config.componentFolder, coverage: !coverageIsDisabled, } const options = getWebpackOptions(opts) + debug('final webpack options %o', options.webpackOptions) + return webpackPreprocessor(options) }