├── .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 |
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 |
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 |
38 |
39 |
47 | {user ? (
48 | <>
49 |
{user.username}
50 |
53 | >
54 | ) : (
55 | <>
56 |
Register
57 |
Login
58 | >
59 | )}
60 |
61 |
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 |
9 | )
10 | }
11 | CalculatorKey.propTypes = {
12 | className: PropTypes.string,
13 | }
14 |
15 | const CalculatorOperations = {
16 | '/': (prevValue, nextValue) => prevValue / nextValue,
17 | '*': (prevValue, nextValue) => prevValue * nextValue,
18 | '+': (prevValue, nextValue) => prevValue + nextValue,
19 | '-': (prevValue, nextValue) => prevValue - nextValue,
20 | '=': (prevValue, nextValue) => nextValue,
21 | }
22 |
23 | function calcReducer(currentState, newState) {
24 | return {...currentState, ...newState}
25 | }
26 |
27 | function Calculator() {
28 | const [state, setState] = React.useReducer(calcReducer, {
29 | value: null,
30 | displayValue: '0',
31 | operator: null,
32 | waitingForOperand: false,
33 | })
34 | const {value, displayValue, operator, waitingForOperand} = state
35 |
36 | function handleKeyDown(event) {
37 | let {key} = event
38 |
39 | if (key === 'Enter') key = '='
40 |
41 | if (/\d/.test(key)) {
42 | event.preventDefault()
43 | inputDigit(parseInt(key, 10))
44 | } else if (key in CalculatorOperations) {
45 | event.preventDefault()
46 | performOperation(key)
47 | } else if (key === '.') {
48 | event.preventDefault()
49 | inputDot()
50 | } else if (key === '%') {
51 | event.preventDefault()
52 | inputPercent()
53 | } else if (key === 'Backspace') {
54 | event.preventDefault()
55 | clearLastChar()
56 | } else if (key === 'Clear') {
57 | event.preventDefault()
58 |
59 | if (state.displayValue === '0') {
60 | clearAll()
61 | } else {
62 | clearDisplay()
63 | }
64 | }
65 | }
66 |
67 | React.useEffect(() => {
68 | document.addEventListener('keydown', handleKeyDown)
69 | return () => document.removeEventListener('keydown', handleKeyDown)
70 | })
71 |
72 | function clearAll() {
73 | setState({
74 | value: null,
75 | displayValue: '0',
76 | operator: null,
77 | waitingForOperand: false,
78 | })
79 | }
80 |
81 | function clearDisplay() {
82 | setState({
83 | displayValue: '0',
84 | })
85 | }
86 |
87 | function clearLastChar() {
88 | setState({
89 | displayValue: displayValue.substring(0, displayValue.length - 1) || '0',
90 | })
91 | }
92 |
93 | function toggleSign() {
94 | const newValue = parseFloat(displayValue) * -1
95 |
96 | setState({
97 | displayValue: String(newValue),
98 | })
99 | }
100 |
101 | function inputPercent() {
102 | const currentValue = parseFloat(displayValue)
103 |
104 | if (currentValue === 0) return
105 |
106 | const fixedDigits = displayValue.replace(/^-?\d*\.?/, '')
107 | const newValue = parseFloat(displayValue) / 100
108 |
109 | setState({
110 | displayValue: String(newValue.toFixed(fixedDigits.length + 2)),
111 | })
112 | }
113 |
114 | function inputDot() {
115 | if (!/\./.test(displayValue)) {
116 | setState({
117 | displayValue: `${displayValue}.`,
118 | waitingForOperand: false,
119 | })
120 | }
121 | }
122 |
123 | function inputDigit(digit) {
124 | if (waitingForOperand) {
125 | setState({
126 | displayValue: String(digit),
127 | waitingForOperand: false,
128 | })
129 | } else {
130 | setState({
131 | displayValue:
132 | displayValue === '0' ? String(digit) : displayValue + digit,
133 | })
134 | }
135 | }
136 |
137 | function performOperation(nextOperator) {
138 | const inputValue = parseFloat(displayValue)
139 |
140 | if (value == null) {
141 | setState({
142 | value: inputValue,
143 | })
144 | } else if (operator) {
145 | const currentValue = value || 0
146 | const newValue = CalculatorOperations[operator](currentValue, inputValue)
147 |
148 | setState({
149 | value: newValue,
150 | displayValue: String(newValue),
151 | })
152 | }
153 |
154 | setState({
155 | waitingForOperand: true,
156 | operator: nextOperator,
157 | })
158 | }
159 |
160 | const displayIsNonZero = displayValue !== '0'
161 | const clearText = displayIsNonZero ? 'C' : 'AC'
162 |
163 | return (
164 |
165 | Loading display...
}
167 | >
168 |
169 |
170 |
171 |
172 |
173 | (displayIsNonZero ? clearDisplay() : clearAll())}
176 | >
177 | {clearText}
178 |
179 | toggleSign()}
182 | >
183 | ±
184 |
185 | inputPercent()}
188 | >
189 | %
190 |
191 |
192 |
193 | inputDigit(0)}
196 | >
197 | 0
198 |
199 | inputDot()}>
200 | ●
201 |
202 | inputDigit(1)}
205 | >
206 | 1
207 |
208 | inputDigit(2)}
211 | >
212 | 2
213 |
214 | inputDigit(3)}
217 | >
218 | 3
219 |
220 | inputDigit(4)}
223 | >
224 | 4
225 |
226 | inputDigit(5)}
229 | >
230 | 5
231 |
232 | inputDigit(6)}
235 | >
236 | 6
237 |
238 | inputDigit(7)}
241 | >
242 | 7
243 |
244 | inputDigit(8)}
247 | >
248 | 8
249 |
250 | inputDigit(9)}
253 | >
254 | 9
255 |
256 |
257 |
258 |
259 | performOperation('/')}
262 | >
263 | ÷
264 |
265 | performOperation('*')}
268 | >
269 | ×
270 |
271 | performOperation('-')}
274 | >
275 | −
276 |
277 | performOperation('+')}
280 | >
281 | +
282 |
283 | performOperation('=')}
286 | >
287 | =
288 |
289 |
290 |
291 |
292 | )
293 | }
294 |
295 | export default Calculator
296 |
297 | /* eslint no-eq-null:0, eqeqeq:0, react/display-name:0, max-lines-per-function:0 */
298 |
--------------------------------------------------------------------------------
/src/calculator.module.css:
--------------------------------------------------------------------------------
1 | .calculator {
2 | width: 320px;
3 | height: 520px;
4 | background: black;
5 |
6 | display: flex;
7 | flex-direction: column;
8 | box-shadow: 0px 0px 20px 0px #aaa;
9 | }
10 |
11 | .calculator-keypad {
12 | height: 400px;
13 |
14 | display: flex;
15 | }
16 |
17 | .input-keys {
18 | width: 240px;
19 | }
20 |
21 | .function-keys {
22 | display: flex;
23 | }
24 |
25 | .digit-keys {
26 | background: #e0e0e7;
27 |
28 | display: flex;
29 | flex-direction: row;
30 | flex-wrap: wrap-reverse;
31 | }
32 |
33 | .calculator-key {
34 | width: 80px;
35 | height: 80px;
36 | border-top: 1px solid #777;
37 | border-right: 1px solid #666;
38 | text-align: center;
39 | line-height: 80px;
40 | }
41 | .function-keys .calculator-key {
42 | font-size: 2em;
43 | }
44 | .function-keys .key-multiply {
45 | line-height: 50px;
46 | }
47 | .digit-keys .calculator-key {
48 | font-size: 2.25em;
49 | }
50 | .digit-keys .key-0 {
51 | width: 160px;
52 | text-align: left;
53 | padding-left: 32px;
54 | }
55 | .digit-keys .key-dot {
56 | padding-top: 1em;
57 | font-size: 0.75em;
58 | }
59 | .operator-keys .calculator-key {
60 | color: white;
61 | border-right: 0;
62 | font-size: 3em;
63 | }
64 |
65 | .function-keys {
66 | background: linear-gradient(
67 | to bottom,
68 | rgba(202, 202, 204, 1) 0%,
69 | rgba(196, 194, 204, 1) 100%
70 | );
71 | }
72 | .operator-keys {
73 | background: linear-gradient(
74 | to bottom,
75 | rgba(252, 156, 23, 1) 0%,
76 | rgba(247, 126, 27, 1) 100%
77 | );
78 | }
79 |
--------------------------------------------------------------------------------
/src/fonts/KFOkCnqEu92Fr1MmgVxEIzIXKMnyrYk.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kentcdodds/jest-cypress-react-babel-webpack/7ee0e1a563b6e3cbd3094f7014667b402245743c/src/fonts/KFOkCnqEu92Fr1MmgVxEIzIXKMnyrYk.woff2
--------------------------------------------------------------------------------
/src/fonts/KFOkCnqEu92Fr1MmgVxFIzIXKMnyrYk.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kentcdodds/jest-cypress-react-babel-webpack/7ee0e1a563b6e3cbd3094f7014667b402245743c/src/fonts/KFOkCnqEu92Fr1MmgVxFIzIXKMnyrYk.woff2
--------------------------------------------------------------------------------
/src/fonts/KFOkCnqEu92Fr1MmgVxGIzIXKMnyrYk.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kentcdodds/jest-cypress-react-babel-webpack/7ee0e1a563b6e3cbd3094f7014667b402245743c/src/fonts/KFOkCnqEu92Fr1MmgVxGIzIXKMnyrYk.woff2
--------------------------------------------------------------------------------
/src/fonts/KFOkCnqEu92Fr1MmgVxHIzIXKMnyrYk.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kentcdodds/jest-cypress-react-babel-webpack/7ee0e1a563b6e3cbd3094f7014667b402245743c/src/fonts/KFOkCnqEu92Fr1MmgVxHIzIXKMnyrYk.woff2
--------------------------------------------------------------------------------
/src/fonts/KFOkCnqEu92Fr1MmgVxIIzIXKMny.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kentcdodds/jest-cypress-react-babel-webpack/7ee0e1a563b6e3cbd3094f7014667b402245743c/src/fonts/KFOkCnqEu92Fr1MmgVxIIzIXKMny.woff2
--------------------------------------------------------------------------------
/src/fonts/KFOkCnqEu92Fr1MmgVxLIzIXKMnyrYk.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kentcdodds/jest-cypress-react-babel-webpack/7ee0e1a563b6e3cbd3094f7014667b402245743c/src/fonts/KFOkCnqEu92Fr1MmgVxLIzIXKMnyrYk.woff2
--------------------------------------------------------------------------------
/src/fonts/KFOkCnqEu92Fr1MmgVxMIzIXKMnyrYk.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kentcdodds/jest-cypress-react-babel-webpack/7ee0e1a563b6e3cbd3094f7014667b402245743c/src/fonts/KFOkCnqEu92Fr1MmgVxMIzIXKMnyrYk.woff2
--------------------------------------------------------------------------------
/src/global.css:
--------------------------------------------------------------------------------
1 | /* https://fonts.googleapis.com/css?family=Roboto:100 */
2 | /* cyrillic-ext */
3 | @font-face {
4 | font-family: 'Roboto';
5 | font-style: normal;
6 | font-weight: 100;
7 | src: local('Roboto Thin'), local('Roboto-Thin'),
8 | url(fonts/KFOkCnqEu92Fr1MmgVxFIzIXKMnyrYk.woff2) format('woff2');
9 | unicode-range: U + 0460-052f, U + 1c80-1c88, U + 20b4, U + 2de0-2dff,
10 | U + A640-A69F, U + FE2E-FE2F;
11 | }
12 | /* cyrillic */
13 | @font-face {
14 | font-family: 'Roboto';
15 | font-style: normal;
16 | font-weight: 100;
17 | src: local('Roboto Thin'), local('Roboto-Thin'),
18 | url(fonts/KFOkCnqEu92Fr1MmgVxMIzIXKMnyrYk.woff2) format('woff2');
19 | unicode-range: U + 0400-045f, U + 0490-0491, U + 04b0-04b1, U + 2116;
20 | }
21 | /* greek-ext */
22 | @font-face {
23 | font-family: 'Roboto';
24 | font-style: normal;
25 | font-weight: 100;
26 | src: local('Roboto Thin'), local('Roboto-Thin'),
27 | url(fonts/KFOkCnqEu92Fr1MmgVxEIzIXKMnyrYk.woff2) format('woff2');
28 | unicode-range: U + 1f00-1fff;
29 | }
30 | /* greek */
31 | @font-face {
32 | font-family: 'Roboto';
33 | font-style: normal;
34 | font-weight: 100;
35 | src: local('Roboto Thin'), local('Roboto-Thin'),
36 | url(fonts/KFOkCnqEu92Fr1MmgVxLIzIXKMnyrYk.woff2) format('woff2');
37 | unicode-range: U + 0370-03ff;
38 | }
39 | /* vietnamese */
40 | @font-face {
41 | font-family: 'Roboto';
42 | font-style: normal;
43 | font-weight: 100;
44 | src: local('Roboto Thin'), local('Roboto-Thin'),
45 | url(fonts/KFOkCnqEu92Fr1MmgVxHIzIXKMnyrYk.woff2) format('woff2');
46 | unicode-range: U + 0102-0103, U + 0110-0111, U + 1ea0-1ef9, U + 20ab;
47 | }
48 | /* latin-ext */
49 | @font-face {
50 | font-family: 'Roboto';
51 | font-style: normal;
52 | font-weight: 100;
53 | src: local('Roboto Thin'), local('Roboto-Thin'),
54 | url(fonts/KFOkCnqEu92Fr1MmgVxGIzIXKMnyrYk.woff2) format('woff2');
55 | unicode-range: U + 0100-024f, U + 0259, U + 1-1eff, U + 2020, U + 20a0-20ab,
56 | U + 20ad-20cf, U + 2113, U + 2c60-2c7f, U + A720-A7FF;
57 | }
58 | /* latin */
59 | @font-face {
60 | font-family: 'Roboto';
61 | font-style: normal;
62 | font-weight: 100;
63 | src: local('Roboto Thin'), local('Roboto-Thin'),
64 | url(fonts/KFOkCnqEu92Fr1MmgVxIIzIXKMny.woff2) format('woff2');
65 | unicode-range: U + 0000-00ff, U + 0131, U + 0152-0153, U + 02bb-02bc, U + 02c6,
66 | U + 02da, U + 02dc, U + 2000-206f, U + 2074, U + 20ac, U + 2122, U + 2191,
67 | U + 2193, U + 2212, U + 2215, U + FEFF, U + FFFD;
68 | }
69 |
70 | /* */
71 | html {
72 | box-sizing: border-box;
73 | height: 100%;
74 | }
75 | *,
76 | *:before,
77 | *:after {
78 | box-sizing: inherit;
79 | }
80 |
81 | body {
82 | margin: 0;
83 | font: 100 14px 'Roboto';
84 | height: 100%;
85 | }
86 |
87 | button {
88 | display: block;
89 | background: none;
90 | border: none;
91 | padding: 0;
92 | font-family: inherit;
93 | user-select: none;
94 | cursor: pointer;
95 | outline: none;
96 |
97 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
98 | }
99 |
100 | button:active {
101 | box-shadow: inset 0px 0px 80px 0px rgba(0, 0, 0, 0.25);
102 | }
103 |
104 | #wrapper {
105 | height: 100%;
106 | }
107 |
108 | #app {
109 | height: 100%;
110 | position: relative;
111 | display: flex;
112 | flex-direction: column;
113 | justify-content: center;
114 | align-items: center;
115 | }
116 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import './global.css'
2 | import * as React from 'react'
3 | import ReactDOM from 'react-dom'
4 | import {Router} from '@reach/router'
5 | import axios from 'axios'
6 | import LoginForm from './login-form'
7 | import App from './app'
8 |
9 | if (module.hot) {
10 | module.hot.accept()
11 | }
12 |
13 | function FullApp() {
14 | const [user, setUser] = React.useState(null)
15 |
16 | const token = window.localStorage.getItem('token')
17 |
18 | React.useEffect(() => {
19 | if (user) {
20 | return
21 | }
22 | axios({
23 | method: 'GET',
24 | url: 'http://localhost:3000/me',
25 | headers: {
26 | Authorization: `Bearer ${token}`,
27 | },
28 | }).then(
29 | r => setUser(r.data),
30 | () => {
31 | window.localStorage.removeItem('token')
32 | setUser(null)
33 | },
34 | )
35 | }, [token, user])
36 |
37 | if (!user && token) {
38 | return '...loading user'
39 | }
40 |
41 | return (
42 |
43 | {
47 | window.localStorage.removeItem('token')
48 | setUser(null)
49 | }}
50 | />
51 |
52 |
53 |
54 | )
55 | }
56 |
57 | ReactDOM.render(, document.getElementById('app'))
58 |
--------------------------------------------------------------------------------
/src/login-form.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import PropTypes from 'prop-types'
3 | import axios from 'axios'
4 | import {navigate} from '@reach/router'
5 |
6 | function LoginForm({onSuccess, endpoint}) {
7 | const [error, setError] = React.useState(null)
8 | const [formValues, setFormValues] = React.useState(null)
9 |
10 | function handleSubmit(e) {
11 | e.preventDefault()
12 | const {
13 | username: {value: username},
14 | password: {value: password},
15 | } = e.target.elements
16 | setFormValues({username, password})
17 | }
18 |
19 | React.useEffect(() => {
20 | if (!formValues) {
21 | return
22 | }
23 | axios({
24 | method: 'POST',
25 | url: `http://localhost:3000/${endpoint}`,
26 | data: formValues,
27 | }).then(
28 | ({data: {user}}) => {
29 | window.localStorage.setItem('token', user.token)
30 | onSuccess(user)
31 | navigate('/')
32 | },
33 | err => setError(err),
34 | )
35 | }, [endpoint, formValues, onSuccess])
36 |
37 | return (
38 |
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 |
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 |
--------------------------------------------------------------------------------