├── .babelrc.js ├── .eslintrc.js ├── .gitignore ├── .npmrc ├── .prettierrc ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── cypress.json ├── cypress ├── .eslintrc.js ├── e2e │ ├── calculator.js │ ├── login.js │ └── register.js ├── fixtures │ └── example.json ├── plugins │ └── index.js └── support │ ├── commands.js │ ├── generate.js │ └── index.js ├── jest.config.js ├── jsconfig.json ├── package-lock.json ├── package.json ├── public └── index.html ├── server ├── .npmrc ├── README.md ├── index.js ├── jest.config.js ├── package-lock.json ├── package.json └── src │ ├── __tests__ │ └── index.js │ └── index.js ├── src ├── __tests__ │ └── calculator.js ├── app.js ├── calculator.js ├── calculator.module.css ├── fonts │ ├── KFOkCnqEu92Fr1MmgVxEIzIXKMnyrYk.woff2 │ ├── KFOkCnqEu92Fr1MmgVxFIzIXKMnyrYk.woff2 │ ├── KFOkCnqEu92Fr1MmgVxGIzIXKMnyrYk.woff2 │ ├── KFOkCnqEu92Fr1MmgVxHIzIXKMnyrYk.woff2 │ ├── KFOkCnqEu92Fr1MmgVxIIzIXKMny.woff2 │ ├── KFOkCnqEu92Fr1MmgVxLIzIXKMnyrYk.woff2 │ └── KFOkCnqEu92Fr1MmgVxMIzIXKMnyrYk.woff2 ├── global.css ├── index.js ├── login-form.js ├── shared │ ├── __server_tests__ │ │ └── auto-scaling-text.js │ ├── __tests__ │ │ ├── auto-scaling-text.js │ │ ├── calculator-display.js │ │ └── utils.js │ ├── auto-scaling-text.js │ ├── auto-scaling-text.module.css │ ├── calculator-display.js │ └── utils.js └── themes.js ├── test ├── calculator-test-utils.js ├── jest-common.js ├── jest.client.js ├── jest.lint.js ├── jest.server.js └── style-mock.js └── webpack.config.js /.babelrc.js: -------------------------------------------------------------------------------- 1 | const isTest = String(process.env.NODE_ENV) === 'test' 2 | const isProd = String(process.env.NODE_ENV) === 'production' 3 | 4 | module.exports = { 5 | presets: [ 6 | ['@babel/preset-env', {modules: isTest ? 'commonjs' : false}], 7 | '@babel/preset-react', 8 | [ 9 | '@emotion/babel-preset-css-prop', 10 | { 11 | hoist: isProd, 12 | sourceMap: !isProd, 13 | autoLabel: isProd ? 'never' : 'always', 14 | labelFormat: '[filename]--[local]', 15 | }, 16 | ], 17 | ], 18 | plugins: ['@babel/plugin-transform-runtime'], 19 | } 20 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | extends: [ 5 | 'kentcdodds', 6 | 'kentcdodds/import', 7 | 'kentcdodds/jest', 8 | 'kentcdodds/react', 9 | ], 10 | rules: { 11 | // https://github.com/benmosher/eslint-plugin-import/issues/1446 12 | 'import/named': 'off', 13 | }, 14 | settings: {'import/resolver': 'node'}, 15 | overrides: [ 16 | { 17 | files: ['**/src/**'], 18 | settings: {'import/resolver': 'webpack'}, 19 | }, 20 | { 21 | files: ['**/__tests__/**'], 22 | settings: { 23 | 'import/resolver': { 24 | jest: { 25 | jestConfigFile: path.join(__dirname, './jest.config.js'), 26 | }, 27 | }, 28 | }, 29 | }, 30 | ], 31 | } 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | coverage 4 | cypress/videos 5 | cypress/screenshots 6 | .eslintcache 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.com 2 | legacy-peer-deps=true 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": false, 4 | "htmlWhitespaceSensitivity": "css", 5 | "insertPragma": false, 6 | "jsxBracketSameLine": false, 7 | "jsxSingleQuote": false, 8 | "printWidth": 80, 9 | "proseWrap": "always", 10 | "quoteProps": "as-needed", 11 | "requirePragma": false, 12 | "semi": false, 13 | "singleQuote": true, 14 | "tabWidth": 2, 15 | "trailingComma": "all", 16 | "useTabs": false 17 | } 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | addons: 4 | apt: 5 | packages: 6 | - libgconf-2-4 7 | cache: 8 | directories: 9 | - ~/.npm 10 | - ~/.cache 11 | notifications: 12 | email: false 13 | node_js: '12' 14 | install: echo "install happens as part of setup" 15 | script: npm run setup 16 | after_script: npx codecov@3 17 | branches: 18 | only: master 19 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Please refer to [kentcdodds.com/conduct/](https://kentcdodds.com/conduct/) 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for being willing to contribute! 4 | 5 | **Working on your first Pull Request?** You can learn how from this _free_ 6 | series [How to Contribute to an Open Source Project on GitHub][egghead] 7 | 8 | ## Project setup 9 | 10 | 1. Fork and clone the repo 11 | 2. Run `npm run setup -s` to install dependencies and run validation 12 | 3. Create a branch for your PR with `git checkout -b pr/your-branch-name` 13 | 14 | > Tip: Keep your `main` branch pointing at the original repository and make pull 15 | > requests from branches on your fork. To do this, run: 16 | > 17 | > ``` 18 | > git remote add upstream https://github.com/kentcdodds/jest-cypress-react-babel-webpack.git 19 | > git fetch upstream 20 | > git branch --set-upstream-to=upstream/main main 21 | > ``` 22 | > 23 | > This will add the original repository as a "remote" called "upstream," Then 24 | > fetch the git information from that remote, then set your local `main` branch 25 | > to use the upstream main branch whenever you run `git pull`. Then you can make 26 | > all of your pull request branches based on this `main` branch. Whenever you 27 | > want to update your version of `main`, do a regular `git pull`. 28 | 29 | ## Help needed 30 | 31 | Please checkout the [the open issues][issues] 32 | 33 | Also, please watch the repo and respond to questions/bug reports/feature 34 | requests! Thanks! 35 | 36 | [egghead]: 37 | https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github 38 | [issues]: https://github.com/kentcdodds/jest-cypress-react-babel-webpack/issues 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This material is available for private, non-commercial use under the 2 | [GPL version 3](http://www.gnu.org/licenses/gpl-3.0-standalone.html). If you 3 | would like to use this material to conduct your own workshop, please contact me 4 | at me@kentcdodds.com 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Jest and Cypress with React, Babel, and Webpack 3 |

4 | 5 |
6 |

TestingJavaScript.com

7 | 8 | Learn the smart, efficient way to test any JavaScript application. 13 | 14 |
15 | 16 |
17 | 18 |

19 | See how to configure Jest and Cypress with React, Babel, and Webpack 20 |

21 | 22 |
23 | 24 | This is used for both 25 | ["Configure Jest for Testing JavaScript Applications"](https://testingjavascript.com/courses/configure-jest-for-testing-javascript-applications-b3674a) 26 | and 27 | ["Install, Configure, and Script Cypress for JavaScript Web Applications"](https://testingjavascript.com/courses/install-configure-and-script-cypress-for-javascript-web-applications-b5ee43) 28 | 29 | > Note: This project is intentionally over-engineered. The application itself is 30 | > very simple, but the tooling around it is pretty complicated. The goal is to 31 | > show what configuration would be like for a large real-world application 32 | > without having all the extra complexities of a real-world application. 33 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:8080", 3 | "integrationFolder": "cypress/e2e", 4 | "viewportHeight": 900, 5 | "viewportWidth": 400 6 | } 7 | -------------------------------------------------------------------------------- /cypress/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | plugins: ['eslint-plugin-cypress'], 4 | extends: ['kentcdodds', 'kentcdodds/import', 'plugin:cypress/recommended'], 5 | env: {'cypress/globals': true}, 6 | } 7 | -------------------------------------------------------------------------------- /cypress/e2e/calculator.js: -------------------------------------------------------------------------------- 1 | describe('anonymous calculator', () => { 2 | it('can make calculations', () => { 3 | cy.visit('/') 4 | cy.findByText(/^1$/).click() 5 | cy.findByText(/^\+$/).click() 6 | cy.findByText(/^2$/).click() 7 | cy.findByText(/^=$/).click() 8 | cy.findByTestId('total').should('have.text', '3') 9 | }) 10 | }) 11 | 12 | describe('authenticated calculator', () => { 13 | it('displays the username', () => { 14 | cy.loginAsNewUser().then(user => { 15 | cy.visit('/') 16 | cy.findByTestId('username-display').should('have.text', user.username) 17 | cy.findByText(/logout/i).click() 18 | cy.findByTestId('username-display', {timeout: 300}).should('not.exist') 19 | }) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /cypress/e2e/login.js: -------------------------------------------------------------------------------- 1 | describe('login', () => { 2 | it('should login an existing user', () => { 3 | cy.createUser().then(user => { 4 | cy.visit('/') 5 | cy.findByText(/login/i).click() 6 | cy.findByLabelText(/username/i).type(user.username) 7 | cy.findByLabelText(/password/i).type(user.password) 8 | cy.findByText(/submit/i).click() 9 | cy.assertHome().assertLoggedInAs(user) 10 | }) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /cypress/e2e/register.js: -------------------------------------------------------------------------------- 1 | import {userBuilder} from '../support/generate' 2 | 3 | describe('registration', () => { 4 | it('should register a new user', () => { 5 | const user = userBuilder() 6 | cy.visit('/') 7 | cy.findByText(/register/i).click() 8 | cy.findByLabelText(/username/i).type(user.username) 9 | cy.findByLabelText(/password/i).type(user.password) 10 | cy.findByText(/submit/i).click() 11 | cy.assertHome().assertLoggedInAs(user) 12 | }) 13 | 14 | it(`should show an error message if there's an error registering`, () => { 15 | cy.server() 16 | cy.route({ 17 | method: 'POST', 18 | url: 'http://localhost:3000/register', 19 | status: 500, 20 | response: {}, 21 | }) 22 | cy.visit('/register') 23 | cy.findByText(/submit/i).click() 24 | cy.findByText(/error.*try again/i) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | module.exports = () => {} 2 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | import {userBuilder} from './generate' 2 | 3 | Cypress.Commands.add('createUser', overrides => { 4 | const user = userBuilder(overrides) 5 | return cy 6 | .request({ 7 | url: 'http://localhost:3000/register', 8 | method: 'POST', 9 | body: user, 10 | }) 11 | .then(({body}) => body.user) 12 | }) 13 | 14 | Cypress.Commands.add('login', user => { 15 | return cy 16 | .request({ 17 | url: 'http://localhost:3000/login', 18 | method: 'POST', 19 | body: user, 20 | }) 21 | .then(({body}) => { 22 | window.localStorage.setItem('token', body.user.token) 23 | return body.user 24 | }) 25 | }) 26 | 27 | Cypress.Commands.add('loginAsNewUser', () => { 28 | cy.createUser().then(user => { 29 | cy.login(user) 30 | }) 31 | }) 32 | 33 | Cypress.Commands.add('assertHome', () => { 34 | cy.url().should('eq', `${Cypress.config().baseUrl}/`) 35 | }) 36 | 37 | Cypress.Commands.add('assertLoggedInAs', user => { 38 | cy.window().its('localStorage.token').should('be.a', 'string') 39 | cy.findByTestId('username-display').should('have.text', user.username) 40 | }) 41 | -------------------------------------------------------------------------------- /cypress/support/generate.js: -------------------------------------------------------------------------------- 1 | import {build, fake} from 'test-data-bot' 2 | 3 | const userBuilder = build('User').fields({ 4 | username: fake(f => f.internet.userName()), 5 | password: fake(f => f.internet.password()), 6 | }) 7 | 8 | export {userBuilder} 9 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/cypress/add-commands' 2 | import './commands' 3 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('./test/jest-common'), 3 | collectCoverageFrom: [ 4 | '**/src/**/*.js', 5 | '!**/__tests__/**', 6 | '!**/__server_tests__/**', 7 | '!**/node_modules/**', 8 | ], 9 | coverageThreshold: { 10 | global: { 11 | statements: 15, 12 | branches: 10, 13 | functions: 15, 14 | lines: 15, 15 | }, 16 | './src/shared/utils.js': { 17 | statements: 100, 18 | branches: 80, 19 | functions: 100, 20 | lines: 100, 21 | }, 22 | }, 23 | projects: [ 24 | './test/jest.lint.js', 25 | './test/jest.client.js', 26 | './test/jest.server.js', 27 | './server', 28 | ], 29 | } 30 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "*": ["src/*", "src/shared/*", "test/*"] 6 | } 7 | }, 8 | "include": ["src", "test/*"] 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "calculator", 3 | "version": "2.0.0", 4 | "description": "See how to configure Jest and Cypress with React, Babel, and Webpack", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "is-ci-cli \"test:coverage\" \"test:watch\"", 8 | "test:coverage": "jest --coverage", 9 | "test:watch": "jest --watch", 10 | "test:debug": "node --inspect-brk ./node_modules/jest/bin/jest.js --runInBand --watch", 11 | "cy:run": "cypress run", 12 | "cy:open": "cypress open", 13 | "test:e2e": "is-ci-cli \"test:e2e:run\" \"test:e2e:dev\"", 14 | "pretest:e2e:run": "npm run build", 15 | "test:e2e:run": "start-server-and-test start http://localhost:8080 cy:run", 16 | "test:e2e:dev": "start-server-and-test dev http://localhost:8080 cy:open", 17 | "dev": "npm-run-all --parallel dev:*", 18 | "dev:server": "node server", 19 | "dev:client": "webpack serve --mode=development", 20 | "build": "webpack --mode=production", 21 | "postbuild": "node -e \"require('fs').copyFileSync('./public/index.html', './dist/index.html')\"", 22 | "start": "npm-run-all --parallel start:*", 23 | "start:server": "node server", 24 | "start:client": "serve --no-clipboard --single --listen 8080 dist", 25 | "format": "prettier --ignore-path .gitignore --write \"**/*.+(js|json|css|html|md)\"", 26 | "lint": "jest --config test/jest.lint.js", 27 | "validate": "npm run test:coverage && npm run test:e2e:run", 28 | "postinstall": "cd server && npm install", 29 | "netlify": "npm-run-all --parallel test:coverage build && cp -r coverage/lcov-report dist/lcov-report", 30 | "setup": "npm install && npm run validate" 31 | }, 32 | "jest-runner-eslint": { 33 | "cliOptions": { 34 | "ignorePath": "./.gitignore" 35 | } 36 | }, 37 | "husky": { 38 | "hooks": { 39 | "pre-commit": "lint-staged && npm run test:e2e:run" 40 | } 41 | }, 42 | "lint-staged": { 43 | "**/*.+(js|json|css|html|md)": [ 44 | "prettier", 45 | "jest --findRelatedTests" 46 | ] 47 | }, 48 | "keywords": [], 49 | "author": "Kent C. Dodds (http://kentcdodds.com/)", 50 | "license": "GPL-3.0", 51 | "devDependencies": { 52 | "@babel/core": "^7.12.10", 53 | "@babel/plugin-transform-runtime": "^7.12.10", 54 | "@babel/preset-env": "^7.12.11", 55 | "@babel/preset-react": "^7.12.10", 56 | "@babel/runtime": "^7.12.5", 57 | "@emotion/babel-preset-css-prop": "^11.0.0", 58 | "@emotion/jest": "^11.1.0", 59 | "@testing-library/cypress": "^7.0.3", 60 | "@testing-library/jest-dom": "^5.11.8", 61 | "@testing-library/react": "^11.2.2", 62 | "@testing-library/user-event": "^12.6.0", 63 | "babel-loader": "^8.2.2", 64 | "css-loader": "^5.0.1", 65 | "cypress": "^6.2.0", 66 | "eslint": "^7.17.0", 67 | "eslint-config-kentcdodds": "^17.3.0", 68 | "eslint-import-resolver-jest": "^3.0.0", 69 | "eslint-plugin-cypress": "^2.11.2", 70 | "file-loader": "^6.2.0", 71 | "husky": "^4.3.6", 72 | "identity-obj-proxy": "^3.0.0", 73 | "is-ci-cli": "^2.1.2", 74 | "jest": "^26.6.3", 75 | "jest-runner-eslint": "^0.10.0", 76 | "jest-watch-select-projects": "^2.0.0", 77 | "jest-watch-typeahead": "^0.6.1", 78 | "lint-staged": "^10.5.3", 79 | "npm-run-all": "^4.1.5", 80 | "prettier": "^2.2.1", 81 | "prop-types": "^15.7.2", 82 | "serve": "^11.3.2", 83 | "start-server-and-test": "^1.11.7", 84 | "style-loader": "^2.0.0", 85 | "test-data-bot": "^0.8.0", 86 | "webpack": "^5.11.1", 87 | "webpack-cli": "^4.3.1", 88 | "webpack-dev-server": "^3.11.1" 89 | }, 90 | "dependencies": { 91 | "@emotion/core": "^11.0.0", 92 | "@emotion/react": "^11.1.4", 93 | "@emotion/styled": "^11.0.0", 94 | "@reach/router": "^1.3.4", 95 | "axios": "^0.21.1", 96 | "react": "^17.0.1", 97 | "react-dom": "^17.0.1" 98 | }, 99 | "repository": { 100 | "type": "git", 101 | "url": "git+https://github.com/kentcdodds/jest-cypress-react-babel-webpack.git" 102 | }, 103 | "bugs": { 104 | "url": "https://github.com/kentcdodds/jest-cypress-react-babel-webpack/issues" 105 | }, 106 | "homepage": "https://github.com/kentcdodds/jest-cypress-react-babel-webpack#readme" 107 | } 108 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Calculator | Michael Jackson | React Training 6 | 10 | 11 | 17 | 18 | 19 | 20 |
21 |
22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /server/.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.com -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # server 2 | 3 | The server in this project is quite contrived (as is the project itself). This 4 | is intentional. The goal of this project is to show you an example of how you 5 | might configure a typical client-side application for testing. The server aspect 6 | of a client-side application varies widely from app to app, so keeping it as 7 | simple as possible is an effort to avoid distracting you from implementation 8 | details of this specific app and help you focus on what really matters for 9 | configuring these tools. 10 | 11 | Suffice it to say: When E2E testing an app with Cypress, you'll likely need to 12 | start the backend server in addition to the frontend app. This project will 13 | demonstrate how you might accomplish that, but the specific scripts you run to 14 | do that could vary. 15 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const {start} = require('./src') 2 | 3 | start() 4 | -------------------------------------------------------------------------------- /server/jest.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | ...require('../test/jest-common'), 5 | displayName: 'backend', 6 | testEnvironment: 'jest-environment-node', 7 | testMatch: ['**/__tests__/**/*.js'], 8 | rootDir: path.join(__dirname, 'src'), 9 | moduleDirectories: ['node_modules', path.join(__dirname, 'src'), 'shared'], 10 | } 11 | -------------------------------------------------------------------------------- /server/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "server", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "body-parser": "^1.19.0", 13 | "cors": "^2.8.4", 14 | "debug": "^4.1.1", 15 | "detect-port": "^1.2.3", 16 | "express": "^4.17.1" 17 | }, 18 | "devDependencies": { 19 | "axios": "^0.20.0", 20 | "cross-env": "^7.0.2" 21 | } 22 | }, 23 | "node_modules/accepts": { 24 | "version": "1.3.7", 25 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", 26 | "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", 27 | "dependencies": { 28 | "mime-types": "~2.1.24", 29 | "negotiator": "0.6.2" 30 | }, 31 | "engines": { 32 | "node": ">= 0.6" 33 | } 34 | }, 35 | "node_modules/address": { 36 | "version": "1.1.2", 37 | "resolved": "https://registry.npmjs.org/address/-/address-1.1.2.tgz", 38 | "integrity": "sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA==", 39 | "engines": { 40 | "node": ">= 0.12.0" 41 | } 42 | }, 43 | "node_modules/array-flatten": { 44 | "version": "1.1.1", 45 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 46 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 47 | }, 48 | "node_modules/axios": { 49 | "version": "0.20.0", 50 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.20.0.tgz", 51 | "integrity": "sha512-ANA4rr2BDcmmAQLOKft2fufrtuvlqR+cXNNinUmvfeSNCOF98PZL+7M/v1zIdGo7OLjEA9J2gXJL+j4zGsl0bA==", 52 | "deprecated": "Critical security vulnerability fixed in v0.21.1. For more information, see https://github.com/axios/axios/pull/3410", 53 | "dev": true, 54 | "dependencies": { 55 | "follow-redirects": "^1.10.0" 56 | } 57 | }, 58 | "node_modules/body-parser": { 59 | "version": "1.19.0", 60 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", 61 | "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", 62 | "dependencies": { 63 | "bytes": "3.1.0", 64 | "content-type": "~1.0.4", 65 | "debug": "2.6.9", 66 | "depd": "~1.1.2", 67 | "http-errors": "1.7.2", 68 | "iconv-lite": "0.4.24", 69 | "on-finished": "~2.3.0", 70 | "qs": "6.7.0", 71 | "raw-body": "2.4.0", 72 | "type-is": "~1.6.17" 73 | }, 74 | "engines": { 75 | "node": ">= 0.8" 76 | } 77 | }, 78 | "node_modules/body-parser/node_modules/debug": { 79 | "version": "2.6.9", 80 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 81 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 82 | "dependencies": { 83 | "ms": "2.0.0" 84 | } 85 | }, 86 | "node_modules/bytes": { 87 | "version": "3.1.0", 88 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", 89 | "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", 90 | "engines": { 91 | "node": ">= 0.8" 92 | } 93 | }, 94 | "node_modules/content-disposition": { 95 | "version": "0.5.3", 96 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", 97 | "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", 98 | "dependencies": { 99 | "safe-buffer": "5.1.2" 100 | }, 101 | "engines": { 102 | "node": ">= 0.6" 103 | } 104 | }, 105 | "node_modules/content-type": { 106 | "version": "1.0.4", 107 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 108 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", 109 | "engines": { 110 | "node": ">= 0.6" 111 | } 112 | }, 113 | "node_modules/cookie": { 114 | "version": "0.4.0", 115 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", 116 | "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", 117 | "engines": { 118 | "node": ">= 0.6" 119 | } 120 | }, 121 | "node_modules/cookie-signature": { 122 | "version": "1.0.6", 123 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 124 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 125 | }, 126 | "node_modules/cors": { 127 | "version": "2.8.5", 128 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 129 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 130 | "dependencies": { 131 | "object-assign": "^4", 132 | "vary": "^1" 133 | }, 134 | "engines": { 135 | "node": ">= 0.10" 136 | } 137 | }, 138 | "node_modules/cross-env": { 139 | "version": "7.0.2", 140 | "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.2.tgz", 141 | "integrity": "sha512-KZP/bMEOJEDCkDQAyRhu3RL2ZO/SUVrxQVI0G3YEQ+OLbRA3c6zgixe8Mq8a/z7+HKlNEjo8oiLUs8iRijY2Rw==", 142 | "dev": true, 143 | "dependencies": { 144 | "cross-spawn": "^7.0.1" 145 | }, 146 | "bin": { 147 | "cross-env": "src/bin/cross-env.js", 148 | "cross-env-shell": "src/bin/cross-env-shell.js" 149 | }, 150 | "engines": { 151 | "node": ">=10.14", 152 | "npm": ">=6", 153 | "yarn": ">=1" 154 | } 155 | }, 156 | "node_modules/cross-spawn": { 157 | "version": "7.0.3", 158 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 159 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 160 | "dev": true, 161 | "dependencies": { 162 | "path-key": "^3.1.0", 163 | "shebang-command": "^2.0.0", 164 | "which": "^2.0.1" 165 | }, 166 | "engines": { 167 | "node": ">= 8" 168 | } 169 | }, 170 | "node_modules/debug": { 171 | "version": "4.2.0", 172 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", 173 | "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", 174 | "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", 175 | "dependencies": { 176 | "ms": "2.1.2" 177 | }, 178 | "engines": { 179 | "node": ">=6.0" 180 | }, 181 | "peerDependenciesMeta": { 182 | "supports-color": { 183 | "optional": true 184 | } 185 | } 186 | }, 187 | "node_modules/debug/node_modules/ms": { 188 | "version": "2.1.2", 189 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 190 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 191 | }, 192 | "node_modules/depd": { 193 | "version": "1.1.2", 194 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 195 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", 196 | "engines": { 197 | "node": ">= 0.6" 198 | } 199 | }, 200 | "node_modules/destroy": { 201 | "version": "1.0.4", 202 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 203 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 204 | }, 205 | "node_modules/detect-port": { 206 | "version": "1.3.0", 207 | "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.3.0.tgz", 208 | "integrity": "sha512-E+B1gzkl2gqxt1IhUzwjrxBKRqx1UzC3WLONHinn8S3T6lwV/agVCyitiFOsGJ/eYuEUBvD71MZHy3Pv1G9doQ==", 209 | "dependencies": { 210 | "address": "^1.0.1", 211 | "debug": "^2.6.0" 212 | }, 213 | "bin": { 214 | "detect": "bin/detect-port", 215 | "detect-port": "bin/detect-port" 216 | }, 217 | "engines": { 218 | "node": ">= 4.2.1" 219 | } 220 | }, 221 | "node_modules/detect-port/node_modules/debug": { 222 | "version": "2.6.9", 223 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 224 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 225 | "dependencies": { 226 | "ms": "2.0.0" 227 | } 228 | }, 229 | "node_modules/ee-first": { 230 | "version": "1.1.1", 231 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 232 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 233 | }, 234 | "node_modules/encodeurl": { 235 | "version": "1.0.2", 236 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 237 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", 238 | "engines": { 239 | "node": ">= 0.8" 240 | } 241 | }, 242 | "node_modules/escape-html": { 243 | "version": "1.0.3", 244 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 245 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 246 | }, 247 | "node_modules/etag": { 248 | "version": "1.8.1", 249 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 250 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", 251 | "engines": { 252 | "node": ">= 0.6" 253 | } 254 | }, 255 | "node_modules/express": { 256 | "version": "4.17.1", 257 | "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", 258 | "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", 259 | "dependencies": { 260 | "accepts": "~1.3.7", 261 | "array-flatten": "1.1.1", 262 | "body-parser": "1.19.0", 263 | "content-disposition": "0.5.3", 264 | "content-type": "~1.0.4", 265 | "cookie": "0.4.0", 266 | "cookie-signature": "1.0.6", 267 | "debug": "2.6.9", 268 | "depd": "~1.1.2", 269 | "encodeurl": "~1.0.2", 270 | "escape-html": "~1.0.3", 271 | "etag": "~1.8.1", 272 | "finalhandler": "~1.1.2", 273 | "fresh": "0.5.2", 274 | "merge-descriptors": "1.0.1", 275 | "methods": "~1.1.2", 276 | "on-finished": "~2.3.0", 277 | "parseurl": "~1.3.3", 278 | "path-to-regexp": "0.1.7", 279 | "proxy-addr": "~2.0.5", 280 | "qs": "6.7.0", 281 | "range-parser": "~1.2.1", 282 | "safe-buffer": "5.1.2", 283 | "send": "0.17.1", 284 | "serve-static": "1.14.1", 285 | "setprototypeof": "1.1.1", 286 | "statuses": "~1.5.0", 287 | "type-is": "~1.6.18", 288 | "utils-merge": "1.0.1", 289 | "vary": "~1.1.2" 290 | }, 291 | "engines": { 292 | "node": ">= 0.10.0" 293 | } 294 | }, 295 | "node_modules/express/node_modules/debug": { 296 | "version": "2.6.9", 297 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 298 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 299 | "dependencies": { 300 | "ms": "2.0.0" 301 | } 302 | }, 303 | "node_modules/finalhandler": { 304 | "version": "1.1.2", 305 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", 306 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", 307 | "dependencies": { 308 | "debug": "2.6.9", 309 | "encodeurl": "~1.0.2", 310 | "escape-html": "~1.0.3", 311 | "on-finished": "~2.3.0", 312 | "parseurl": "~1.3.3", 313 | "statuses": "~1.5.0", 314 | "unpipe": "~1.0.0" 315 | }, 316 | "engines": { 317 | "node": ">= 0.8" 318 | } 319 | }, 320 | "node_modules/finalhandler/node_modules/debug": { 321 | "version": "2.6.9", 322 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 323 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 324 | "dependencies": { 325 | "ms": "2.0.0" 326 | } 327 | }, 328 | "node_modules/follow-redirects": { 329 | "version": "1.13.0", 330 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz", 331 | "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==", 332 | "dev": true, 333 | "funding": [ 334 | { 335 | "type": "individual", 336 | "url": "https://github.com/sponsors/RubenVerborgh" 337 | } 338 | ], 339 | "engines": { 340 | "node": ">=4.0" 341 | } 342 | }, 343 | "node_modules/forwarded": { 344 | "version": "0.1.2", 345 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 346 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", 347 | "engines": { 348 | "node": ">= 0.6" 349 | } 350 | }, 351 | "node_modules/fresh": { 352 | "version": "0.5.2", 353 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 354 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", 355 | "engines": { 356 | "node": ">= 0.6" 357 | } 358 | }, 359 | "node_modules/http-errors": { 360 | "version": "1.7.2", 361 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", 362 | "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", 363 | "dependencies": { 364 | "depd": "~1.1.2", 365 | "inherits": "2.0.3", 366 | "setprototypeof": "1.1.1", 367 | "statuses": ">= 1.5.0 < 2", 368 | "toidentifier": "1.0.0" 369 | }, 370 | "engines": { 371 | "node": ">= 0.6" 372 | } 373 | }, 374 | "node_modules/iconv-lite": { 375 | "version": "0.4.24", 376 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 377 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 378 | "dependencies": { 379 | "safer-buffer": ">= 2.1.2 < 3" 380 | }, 381 | "engines": { 382 | "node": ">=0.10.0" 383 | } 384 | }, 385 | "node_modules/inherits": { 386 | "version": "2.0.3", 387 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 388 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 389 | }, 390 | "node_modules/ipaddr.js": { 391 | "version": "1.9.1", 392 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 393 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 394 | "engines": { 395 | "node": ">= 0.10" 396 | } 397 | }, 398 | "node_modules/isexe": { 399 | "version": "2.0.0", 400 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 401 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", 402 | "dev": true 403 | }, 404 | "node_modules/media-typer": { 405 | "version": "0.3.0", 406 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 407 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", 408 | "engines": { 409 | "node": ">= 0.6" 410 | } 411 | }, 412 | "node_modules/merge-descriptors": { 413 | "version": "1.0.1", 414 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 415 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 416 | }, 417 | "node_modules/methods": { 418 | "version": "1.1.2", 419 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 420 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", 421 | "engines": { 422 | "node": ">= 0.6" 423 | } 424 | }, 425 | "node_modules/mime": { 426 | "version": "1.6.0", 427 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 428 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 429 | "bin": { 430 | "mime": "cli.js" 431 | }, 432 | "engines": { 433 | "node": ">=4" 434 | } 435 | }, 436 | "node_modules/mime-db": { 437 | "version": "1.44.0", 438 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", 439 | "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", 440 | "engines": { 441 | "node": ">= 0.6" 442 | } 443 | }, 444 | "node_modules/mime-types": { 445 | "version": "2.1.27", 446 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", 447 | "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", 448 | "dependencies": { 449 | "mime-db": "1.44.0" 450 | }, 451 | "engines": { 452 | "node": ">= 0.6" 453 | } 454 | }, 455 | "node_modules/ms": { 456 | "version": "2.0.0", 457 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 458 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 459 | }, 460 | "node_modules/negotiator": { 461 | "version": "0.6.2", 462 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", 463 | "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", 464 | "engines": { 465 | "node": ">= 0.6" 466 | } 467 | }, 468 | "node_modules/object-assign": { 469 | "version": "4.1.1", 470 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 471 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", 472 | "engines": { 473 | "node": ">=0.10.0" 474 | } 475 | }, 476 | "node_modules/on-finished": { 477 | "version": "2.3.0", 478 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 479 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 480 | "dependencies": { 481 | "ee-first": "1.1.1" 482 | }, 483 | "engines": { 484 | "node": ">= 0.8" 485 | } 486 | }, 487 | "node_modules/parseurl": { 488 | "version": "1.3.3", 489 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 490 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 491 | "engines": { 492 | "node": ">= 0.8" 493 | } 494 | }, 495 | "node_modules/path-key": { 496 | "version": "3.1.1", 497 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 498 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 499 | "dev": true, 500 | "engines": { 501 | "node": ">=8" 502 | } 503 | }, 504 | "node_modules/path-to-regexp": { 505 | "version": "0.1.7", 506 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 507 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 508 | }, 509 | "node_modules/proxy-addr": { 510 | "version": "2.0.6", 511 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", 512 | "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", 513 | "dependencies": { 514 | "forwarded": "~0.1.2", 515 | "ipaddr.js": "1.9.1" 516 | }, 517 | "engines": { 518 | "node": ">= 0.10" 519 | } 520 | }, 521 | "node_modules/qs": { 522 | "version": "6.7.0", 523 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", 524 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", 525 | "engines": { 526 | "node": ">=0.6" 527 | } 528 | }, 529 | "node_modules/range-parser": { 530 | "version": "1.2.1", 531 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 532 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 533 | "engines": { 534 | "node": ">= 0.6" 535 | } 536 | }, 537 | "node_modules/raw-body": { 538 | "version": "2.4.0", 539 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", 540 | "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", 541 | "dependencies": { 542 | "bytes": "3.1.0", 543 | "http-errors": "1.7.2", 544 | "iconv-lite": "0.4.24", 545 | "unpipe": "1.0.0" 546 | }, 547 | "engines": { 548 | "node": ">= 0.8" 549 | } 550 | }, 551 | "node_modules/safe-buffer": { 552 | "version": "5.1.2", 553 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 554 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 555 | }, 556 | "node_modules/safer-buffer": { 557 | "version": "2.1.2", 558 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 559 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 560 | }, 561 | "node_modules/send": { 562 | "version": "0.17.1", 563 | "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", 564 | "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", 565 | "dependencies": { 566 | "debug": "2.6.9", 567 | "depd": "~1.1.2", 568 | "destroy": "~1.0.4", 569 | "encodeurl": "~1.0.2", 570 | "escape-html": "~1.0.3", 571 | "etag": "~1.8.1", 572 | "fresh": "0.5.2", 573 | "http-errors": "~1.7.2", 574 | "mime": "1.6.0", 575 | "ms": "2.1.1", 576 | "on-finished": "~2.3.0", 577 | "range-parser": "~1.2.1", 578 | "statuses": "~1.5.0" 579 | }, 580 | "engines": { 581 | "node": ">= 0.8.0" 582 | } 583 | }, 584 | "node_modules/send/node_modules/debug": { 585 | "version": "2.6.9", 586 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 587 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 588 | "dependencies": { 589 | "ms": "2.0.0" 590 | } 591 | }, 592 | "node_modules/send/node_modules/debug/node_modules/ms": { 593 | "version": "2.0.0", 594 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 595 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 596 | }, 597 | "node_modules/send/node_modules/ms": { 598 | "version": "2.1.1", 599 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 600 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 601 | }, 602 | "node_modules/serve-static": { 603 | "version": "1.14.1", 604 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", 605 | "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", 606 | "dependencies": { 607 | "encodeurl": "~1.0.2", 608 | "escape-html": "~1.0.3", 609 | "parseurl": "~1.3.3", 610 | "send": "0.17.1" 611 | }, 612 | "engines": { 613 | "node": ">= 0.8.0" 614 | } 615 | }, 616 | "node_modules/setprototypeof": { 617 | "version": "1.1.1", 618 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", 619 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" 620 | }, 621 | "node_modules/shebang-command": { 622 | "version": "2.0.0", 623 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 624 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 625 | "dev": true, 626 | "dependencies": { 627 | "shebang-regex": "^3.0.0" 628 | }, 629 | "engines": { 630 | "node": ">=8" 631 | } 632 | }, 633 | "node_modules/shebang-regex": { 634 | "version": "3.0.0", 635 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 636 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 637 | "dev": true, 638 | "engines": { 639 | "node": ">=8" 640 | } 641 | }, 642 | "node_modules/statuses": { 643 | "version": "1.5.0", 644 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 645 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", 646 | "engines": { 647 | "node": ">= 0.6" 648 | } 649 | }, 650 | "node_modules/toidentifier": { 651 | "version": "1.0.0", 652 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", 653 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", 654 | "engines": { 655 | "node": ">=0.6" 656 | } 657 | }, 658 | "node_modules/type-is": { 659 | "version": "1.6.18", 660 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 661 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 662 | "dependencies": { 663 | "media-typer": "0.3.0", 664 | "mime-types": "~2.1.24" 665 | }, 666 | "engines": { 667 | "node": ">= 0.6" 668 | } 669 | }, 670 | "node_modules/unpipe": { 671 | "version": "1.0.0", 672 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 673 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", 674 | "engines": { 675 | "node": ">= 0.8" 676 | } 677 | }, 678 | "node_modules/utils-merge": { 679 | "version": "1.0.1", 680 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 681 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", 682 | "engines": { 683 | "node": ">= 0.4.0" 684 | } 685 | }, 686 | "node_modules/vary": { 687 | "version": "1.1.2", 688 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 689 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", 690 | "engines": { 691 | "node": ">= 0.8" 692 | } 693 | }, 694 | "node_modules/which": { 695 | "version": "2.0.2", 696 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 697 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 698 | "dev": true, 699 | "dependencies": { 700 | "isexe": "^2.0.0" 701 | }, 702 | "bin": { 703 | "node-which": "bin/node-which" 704 | }, 705 | "engines": { 706 | "node": ">= 8" 707 | } 708 | } 709 | } 710 | } 711 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "cross-env DEBUG=app node .", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "Kent C. Dodds (http://kentcdodds.com/)", 12 | "license": "MIT", 13 | "dependencies": { 14 | "body-parser": "^1.19.0", 15 | "cors": "^2.8.4", 16 | "debug": "^4.1.1", 17 | "detect-port": "^1.2.3", 18 | "express": "^4.17.1" 19 | }, 20 | "devDependencies": { 21 | "axios": "^0.20.0", 22 | "cross-env": "^7.0.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /server/src/__tests__/index.js: -------------------------------------------------------------------------------- 1 | // this isn't necessarily an example of a well-written test 2 | // I just wanted to have something that I could use to write the server 3 | // faster and make sure I don't break things as I go. 4 | // the whole server is pretty contrived. Keep that in mind :) 5 | const axios = require('axios') 6 | const {start} = require('..') 7 | 8 | let baseURL, api, server 9 | 10 | beforeAll(async () => { 11 | server = await start({port: 8765}) 12 | baseURL = `http://localhost:${server.address().port}/` 13 | api = axios.create({baseURL}) 14 | }) 15 | 16 | afterAll(async () => { 17 | await server.close() 18 | }) 19 | 20 | test('the server works', async () => { 21 | const testUser = {name: 'test'} 22 | // can register 23 | const { 24 | data: {user: registeredUser}, 25 | } = await api.post('register', testUser) 26 | expect(registeredUser).toEqual({ 27 | ...testUser, 28 | token: expect.any(String), 29 | }) 30 | 31 | // can login 32 | const { 33 | data: {user}, 34 | } = await api.post('login', testUser) 35 | expect(user).toEqual({ 36 | ...testUser, 37 | token: expect.any(String), 38 | }) 39 | 40 | // can get /me 41 | const {data: meUser} = await api({ 42 | url: 'me', 43 | method: 'GET', 44 | headers: {Authorization: `Bearer ${user.token}`}, 45 | }) 46 | const {token: ignoredToken, ...userWithoutToken} = user 47 | expect(meUser).toEqual(userWithoutToken) 48 | 49 | // can set user data 50 | const updates = {anything: 'goes'} 51 | const {data: meUserUpdated} = await api({ 52 | url: 'me', 53 | method: 'POST', 54 | headers: {Authorization: `Bearer ${user.token}`}, 55 | data: updates, 56 | }) 57 | expect(meUserUpdated).toMatchObject({ 58 | ...userWithoutToken, 59 | ...updates, 60 | }) 61 | 62 | // can logout 63 | await api({ 64 | url: 'logout', 65 | method: 'GET', 66 | headers: {Authorization: `Bearer ${user.token}`}, 67 | }) 68 | 69 | const meError = await api.get('me').catch(e => e.response) 70 | expect(meError.status).toEqual(400) 71 | }) 72 | -------------------------------------------------------------------------------- /server/src/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const bodyParser = require('body-parser') 3 | const detectPort = require('detect-port') 4 | const getDebugger = require('debug') 5 | const cors = require('cors') 6 | 7 | const debug = getDebugger('app') 8 | const users = {} 9 | 10 | async function start({port} = {}) { 11 | port = port || process.env.PORT || (await detectPort(3000)) 12 | 13 | const app = express() 14 | app.use(cors()) 15 | app.use(bodyParser.json()) 16 | 17 | function authenticate(req, res, next) { 18 | const token = 19 | req.headers.authorization && 20 | req.headers.authorization.slice('Bearer '.length) 21 | if (!token) { 22 | return res.sendStatus(400) 23 | } 24 | const user = users[token] 25 | if (!user) { 26 | return res.sendStatus(401) 27 | } 28 | req.user = user 29 | req.token = token 30 | return next() 31 | } 32 | 33 | function handleLogin(req, res) { 34 | const token = Math.random().toString() 35 | const user = req.body 36 | users[token] = user 37 | return res.json({user: {token, ...user}}) 38 | } 39 | 40 | app.get('/me', authenticate, (req, res) => { 41 | return res.json(req.user) 42 | }) 43 | 44 | app.post('/me', authenticate, (req, res) => { 45 | Object.assign(req.user, req.body) 46 | return res.json(req.user) 47 | }) 48 | 49 | app.get('/logout', authenticate, (req, res) => { 50 | delete users[req.token] 51 | res.sendStatus(200) 52 | }) 53 | 54 | app.post('/register', handleLogin) 55 | app.post('/login', handleLogin) 56 | 57 | return new Promise(resolve => { 58 | const server = app.listen(port, () => { 59 | debug(`Listening on port ${server.address().port}`) 60 | const originalClose = server.close.bind(server) 61 | server.close = () => { 62 | return new Promise(resolveClose => { 63 | originalClose(resolveClose) 64 | }) 65 | } 66 | resolve(server) 67 | }) 68 | }) 69 | } 70 | 71 | module.exports = {start} 72 | -------------------------------------------------------------------------------- /src/__tests__/calculator.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {render, screen, userEvent} from 'calculator-test-utils' 3 | import Calculator from '../calculator' 4 | 5 | test('the clear button switches from AC to C when there is an entry', () => { 6 | render() 7 | const clearButton = screen.getByText('AC') 8 | 9 | userEvent.click(screen.getByText(/3/)) 10 | expect(clearButton).toHaveTextContent('C') 11 | 12 | userEvent.click(clearButton) 13 | expect(clearButton).toHaveTextContent('AC') 14 | }) 15 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import PropTypes from 'prop-types' 3 | import {ThemeProvider} from '@emotion/react' 4 | import {Link} from '@reach/router' 5 | import Calculator from './calculator' 6 | import * as themes from './themes' 7 | 8 | function App({user, logout}) { 9 | const [theme, setTheme] = React.useState('dark') 10 | const handleThemeChange = ({target: {value}}) => setTheme(value) 11 | return ( 12 | 13 | 14 |
15 |
16 | Theme 17 | 27 | 37 |
38 |
39 |
47 | {user ? ( 48 | <> 49 |
{user.username}
50 | 53 | 54 | ) : ( 55 | <> 56 | Register 57 | Login 58 | 59 | )} 60 |
61 |
62 | Calculator component{' '} 63 | created 64 | {' by '} 65 |
66 | Michael Jackson of{' '} 67 | React Training 68 |
69 |
70 | ) 71 | } 72 | 73 | App.propTypes = { 74 | user: PropTypes.any, 75 | logout: PropTypes.func, 76 | } 77 | 78 | export default App 79 | 80 | /* eslint import/namespace:0 */ 81 | -------------------------------------------------------------------------------- /src/calculator.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import PropTypes from 'prop-types' 3 | import CalculatorDisplay from 'calculator-display' 4 | import styles from './calculator.module.css' 5 | 6 | function CalculatorKey({className = '', ...props}) { 7 | return ( 8 | 82 | {error ?
There was an error. Please try again.
: null} 83 | 84 | ) 85 | } 86 | 87 | LoginForm.propTypes = { 88 | onSuccess: PropTypes.func.isRequired, 89 | endpoint: PropTypes.string.isRequired, 90 | } 91 | 92 | export default LoginForm 93 | -------------------------------------------------------------------------------- /src/shared/__server_tests__/auto-scaling-text.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import ReactDOMServer from 'react-dom/server' 3 | import AutoScalingText from '../auto-scaling-text' 4 | 5 | test('renders', () => { 6 | ReactDOMServer.renderToString() 7 | }) 8 | -------------------------------------------------------------------------------- /src/shared/__tests__/auto-scaling-text.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {render} from 'calculator-test-utils' 3 | import AutoScalingText from '../auto-scaling-text' 4 | 5 | test('renders', () => { 6 | render() 7 | }) 8 | -------------------------------------------------------------------------------- /src/shared/__tests__/calculator-display.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {render} from 'calculator-test-utils' 3 | import CalculatorDisplay from '../calculator-display' 4 | 5 | test('renders', () => { 6 | const {container} = render() 7 | expect(container.firstChild).toMatchInlineSnapshot(` 8 | .emotion-0 { 9 | color: white; 10 | background: #1c191c; 11 | line-height: 130px; 12 | font-size: 6em; 13 | -webkit-flex: 1; 14 | -ms-flex: 1; 15 | flex: 1; 16 | position: relative; 17 | } 18 | 19 |
22 |
27 | 0 28 |
29 |
30 | `) 31 | }) 32 | -------------------------------------------------------------------------------- /src/shared/__tests__/utils.js: -------------------------------------------------------------------------------- 1 | import {getFormattedValue} from '../utils' 2 | 3 | test('formats the value', () => { 4 | expect(getFormattedValue('1234.0')).toBe('1,234.0') 5 | }) 6 | -------------------------------------------------------------------------------- /src/shared/auto-scaling-text.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import PropTypes from 'prop-types' 3 | import styles from './auto-scaling-text.module.css' 4 | 5 | function getScale(node) { 6 | if (!node) { 7 | return 1 8 | } 9 | const parentNode = node.parentNode 10 | 11 | const availableWidth = parentNode.offsetWidth 12 | const actualWidth = node.offsetWidth 13 | const actualScale = availableWidth / actualWidth 14 | 15 | if (actualScale < 1) { 16 | return actualScale * 0.9 17 | } 18 | return 1 19 | } 20 | 21 | function AutoScalingText({children}) { 22 | const nodeRef = React.useRef() 23 | const scale = getScale(nodeRef.current) 24 | return ( 25 |
31 | {children} 32 |
33 | ) 34 | } 35 | AutoScalingText.propTypes = { 36 | children: PropTypes.node, 37 | } 38 | 39 | export default AutoScalingText 40 | -------------------------------------------------------------------------------- /src/shared/auto-scaling-text.module.css: -------------------------------------------------------------------------------- 1 | .auto-scaling-text { 2 | display: inline-block; 3 | padding: 0 30px; 4 | position: absolute; 5 | right: 0; 6 | transform-origin: right; 7 | } 8 | -------------------------------------------------------------------------------- /src/shared/calculator-display.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import PropTypes from 'prop-types' 3 | import styled from '@emotion/styled' 4 | import AutoScalingText from './auto-scaling-text' 5 | import {getFormattedValue} from './utils' 6 | 7 | const DisplayContainer = styled.div(({theme}) => ({ 8 | color: theme.displayTextColor, 9 | background: theme.displayBackgroundColor, 10 | lineHeight: '130px', 11 | fontSize: '6em', 12 | flex: '1', 13 | position: 'relative', 14 | })) 15 | 16 | function CalculatorDisplay({value, ...props}) { 17 | const formattedValue = getFormattedValue( 18 | value, 19 | typeof window === 'undefined' ? 'en-US' : window.navigator.language, 20 | ) 21 | 22 | return ( 23 | 24 | {formattedValue} 25 | 26 | ) 27 | } 28 | 29 | CalculatorDisplay.propTypes = { 30 | value: PropTypes.string.isRequired, 31 | } 32 | 33 | export default CalculatorDisplay 34 | -------------------------------------------------------------------------------- /src/shared/utils.js: -------------------------------------------------------------------------------- 1 | function getFormattedValue(value, language = 'en-US') { 2 | let formattedValue = parseFloat(value).toLocaleString(language, { 3 | useGrouping: true, 4 | maximumFractionDigits: 6, 5 | }) 6 | 7 | // Add back missing .0 in e.g. 12.0 8 | const match = value.match(/\.\d*?(0*)$/) 9 | 10 | if (match) { 11 | formattedValue += /[1-9]/.test(match[0]) ? match[1] : match[0] 12 | } 13 | return formattedValue 14 | } 15 | 16 | export {getFormattedValue} 17 | -------------------------------------------------------------------------------- /src/themes.js: -------------------------------------------------------------------------------- 1 | export const dark = { 2 | displayTextColor: 'white', 3 | displayBackgroundColor: '#1c191c', 4 | } 5 | 6 | export const light = { 7 | displayTextColor: '#1c191c', 8 | displayBackgroundColor: 'white', 9 | } 10 | -------------------------------------------------------------------------------- /test/calculator-test-utils.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import PropTypes from 'prop-types' 3 | import {render as rtlRender} from '@testing-library/react' 4 | import userEvent from '@testing-library/user-event' 5 | import {ThemeProvider} from '@emotion/react' 6 | import * as themes from '../src/themes' 7 | 8 | function render(ui, {theme = themes.dark, ...options} = {}) { 9 | function Wrapper({children}) { 10 | return {children} 11 | } 12 | Wrapper.propTypes = { 13 | children: PropTypes.node, 14 | } 15 | 16 | return rtlRender(ui, {wrapper: Wrapper, ...options}) 17 | } 18 | 19 | export * from '@testing-library/react' 20 | // override the built-in render with our own 21 | export {render, userEvent} 22 | -------------------------------------------------------------------------------- /test/jest-common.js: -------------------------------------------------------------------------------- 1 | // common project configuration used by the other configs 2 | 3 | const path = require('path') 4 | 5 | module.exports = { 6 | rootDir: path.join(__dirname, '..'), 7 | moduleDirectories: [ 8 | 'node_modules', 9 | path.join(__dirname, '../src'), 10 | 'shared', 11 | __dirname, 12 | ], 13 | testPathIgnorePatterns: ['/server/'], 14 | moduleNameMapper: { 15 | // module must come first 16 | '\\.module\\.css$': 'identity-obj-proxy', 17 | '\\.css$': require.resolve('./style-mock.js'), 18 | // can also map files that are loaded by webpack with the file-loader 19 | }, 20 | watchPlugins: [ 21 | 'jest-watch-typeahead/filename', 22 | 'jest-watch-typeahead/testname', 23 | 'jest-watch-select-projects', 24 | ], 25 | } 26 | -------------------------------------------------------------------------------- /test/jest.client.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('./jest-common'), 3 | displayName: 'client', 4 | testEnvironment: 'jest-environment-jsdom', 5 | setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'], 6 | snapshotSerializers: ['@emotion/jest/serializer'], 7 | } 8 | -------------------------------------------------------------------------------- /test/jest.lint.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | rootDir: path.join(__dirname, '..'), 5 | displayName: 'lint', 6 | runner: 'jest-runner-eslint', 7 | testMatch: ['/**/*.js'], 8 | } 9 | -------------------------------------------------------------------------------- /test/jest.server.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('./jest-common'), 3 | displayName: 'server', 4 | testEnvironment: 'jest-environment-node', 5 | testMatch: ['**/__server_tests__/**/*.js'], 6 | } 7 | -------------------------------------------------------------------------------- /test/style-mock.js: -------------------------------------------------------------------------------- 1 | module.exports = {} 2 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | entry: './src/index.js', 5 | output: { 6 | path: path.resolve('dist'), 7 | filename: 'bundle.js', 8 | }, 9 | resolve: { 10 | modules: ['node_modules', path.join(__dirname, 'src'), 'shared'], 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.css$/, 16 | exclude: /\.module\.css$/, 17 | use: [{loader: 'style-loader'}, {loader: 'css-loader'}], 18 | }, 19 | { 20 | test: /\.module\.css$/, 21 | use: [ 22 | {loader: 'style-loader'}, 23 | { 24 | loader: 'css-loader', 25 | options: { 26 | modules: {exportLocalsConvention: 'camelCaseOnly'}, 27 | }, 28 | }, 29 | ], 30 | }, 31 | { 32 | test: /\.js$/, 33 | exclude: /node_modules/, 34 | use: 'babel-loader', 35 | }, 36 | { 37 | test: /\.(eot|svg|ttf|woff|woff2)$/, 38 | use: 'file-loader', 39 | }, 40 | ], 41 | }, 42 | devServer: { 43 | contentBase: path.join(__dirname, './public'), 44 | historyApiFallback: true, 45 | }, 46 | } 47 | --------------------------------------------------------------------------------