├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── undoList.json ├── manifest.json └── index.html ├── .eslintrc.js ├── babel.config.js ├── config ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── pnpTs.js ├── getHttpsConfig.js ├── paths.js ├── env.js ├── modules.js ├── webpackDevServer.config.js └── webpack.config.js ├── src ├── setupTests.ts ├── index.css ├── index.tsx ├── components │ ├── Header.tsx │ └── List.tsx ├── __tests__ │ ├── integration │ │ └── App.tsx │ └── unit │ │ ├── Header.tsx │ │ └── List.tsx ├── react-app-env.d.ts ├── App.css ├── App.tsx ├── logo.svg └── serviceWorker.ts ├── .gitignore ├── tsconfig.json ├── scripts ├── test.js ├── start.js └── build.js ├── jest.config.js ├── package.json └── README.md /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjq990112/learning-react-test/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjq990112/learning-react-test/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjq990112/learning-react-test/HEAD/public/logo512.png -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file ESlint 配置 3 | * @author 炽翎 4 | */ 5 | module.exports = { 6 | extends: 'react-app' 7 | }; 8 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Babel 配置 3 | * @author 炽翎 4 | */ 5 | module.exports = { 6 | presets: ['react-app'] 7 | }; 8 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | process() { 5 | return 'module.exports = {};'; 6 | }, 7 | getCacheKey() { 8 | // The output is always the same. 9 | return 'cssTransform'; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /public/undoList.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": 200, 3 | "data": [ 4 | { 5 | "status": "div", 6 | "value": "学习 Jest" 7 | }, 8 | { 9 | "status": "div", 10 | "value": "学习 Enzyme" 11 | }, 12 | { 13 | "status": "div", 14 | "value": "学习 Testing-Library" 15 | } 16 | ], 17 | "message": "success" 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want your app to work offline and load faster, you can change 15 | // unregister() to register() below. Note this comes with some pitfalls. 16 | // Learn more about service workers: https://bit.ly/CRA-PWA 17 | serviceWorker.unregister(); 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": false, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /config/pnpTs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { resolveModuleName } = require('ts-pnp'); 4 | 5 | exports.resolveModuleName = ( 6 | typescript, 7 | moduleName, 8 | containingFile, 9 | compilerOptions, 10 | resolutionHost 11 | ) => { 12 | return resolveModuleName( 13 | moduleName, 14 | containingFile, 15 | compilerOptions, 16 | resolutionHost, 17 | typescript.resolveModuleName 18 | ); 19 | }; 20 | 21 | exports.resolveTypeReferenceDirective = ( 22 | typescript, 23 | moduleName, 24 | containingFile, 25 | compilerOptions, 26 | resolutionHost 27 | ) => { 28 | return resolveModuleName( 29 | moduleName, 30 | containingFile, 31 | compilerOptions, 32 | resolutionHost, 33 | typescript.resolveTypeReferenceDirective 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | interface IHeader { 4 | addUndoItem: (value: string) => void; 5 | } 6 | 7 | const Header: React.FC = (props) => { 8 | const [value, setValue] = useState(''); 9 | const { addUndoItem } = props; 10 | 11 | const handleInputKeyUp = (e: React.KeyboardEvent) => { 12 | if (e.keyCode === 13 && value) { 13 | addUndoItem(value); 14 | setValue(''); 15 | } 16 | }; 17 | return ( 18 | 19 | 20 | 21 | TodoList 22 | setValue(e.target.value)} 28 | onKeyUp={handleInputKeyUp} 29 | /> 30 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | export default Header; 37 | -------------------------------------------------------------------------------- /src/__tests__/integration/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, fireEvent, act, RenderResult } from '@testing-library/react'; 3 | import App from '../../App'; 4 | import axios from 'axios'; 5 | 6 | jest.mock('axios'); 7 | 8 | let wrapper: RenderResult; 9 | 10 | // 运行每一个测试用例前先渲染组件 11 | beforeEach(async () => { 12 | axios.get.mockResolvedValue({ 13 | data: { 14 | code: 200, 15 | data: [ 16 | { 17 | status: 'div', 18 | value: '学习 Jest' 19 | }, 20 | { 21 | status: 'div', 22 | value: '学习 Enzyme' 23 | }, 24 | { 25 | status: 'div', 26 | value: '学习 Testing-Library' 27 | } 28 | ], 29 | message: 'success' 30 | } 31 | }); 32 | await act(async () => { 33 | wrapper = render(); 34 | }); 35 | }); 36 | 37 | // 运行后重置 38 | afterAll(() => { 39 | wrapper = null; 40 | }); 41 | 42 | describe('App 组件', () => { 43 | test('测试组件初始化后的状态', () => { 44 | const count = wrapper.queryByTestId('count'); 45 | // 计数器存在且数值为 3 46 | expect(count).not.toBeNull(); 47 | expect(count.textContent).toEqual('3'); 48 | 49 | const list = wrapper.queryAllByTestId('list-item'); 50 | // 列表项不为空且长度为 3 51 | expect(list).toHaveLength(3); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const camelcase = require('camelcase'); 5 | 6 | // This is a custom Jest transformer turning file imports into filenames. 7 | // http://facebook.github.io/jest/docs/en/webpack.html 8 | 9 | module.exports = { 10 | process(src, filename) { 11 | const assetFilename = JSON.stringify(path.basename(filename)); 12 | 13 | if (filename.match(/\.svg$/)) { 14 | // Based on how SVGR generates a component name: 15 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 16 | const pascalCaseFilename = camelcase(path.parse(filename).name, { 17 | pascalCase: true, 18 | }); 19 | const componentName = `Svg${pascalCaseFilename}`; 20 | return `const React = require('react'); 21 | module.exports = { 22 | __esModule: true, 23 | default: ${assetFilename}, 24 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) { 25 | return { 26 | $$typeof: Symbol.for('react.element'), 27 | type: 'svg', 28 | ref: ref, 29 | key: null, 30 | props: Object.assign({}, props, { 31 | children: ${assetFilename} 32 | }) 33 | }; 34 | }), 35 | };`; 36 | } 37 | 38 | return `module.exports = ${assetFilename};`; 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | declare namespace NodeJS { 6 | interface ProcessEnv { 7 | readonly NODE_ENV: 'development' | 'production' | 'test'; 8 | readonly PUBLIC_URL: string; 9 | } 10 | } 11 | 12 | declare module '*.bmp' { 13 | const src: string; 14 | export default src; 15 | } 16 | 17 | declare module '*.gif' { 18 | const src: string; 19 | export default src; 20 | } 21 | 22 | declare module '*.jpg' { 23 | const src: string; 24 | export default src; 25 | } 26 | 27 | declare module '*.jpeg' { 28 | const src: string; 29 | export default src; 30 | } 31 | 32 | declare module '*.png' { 33 | const src: string; 34 | export default src; 35 | } 36 | 37 | declare module '*.webp' { 38 | const src: string; 39 | export default src; 40 | } 41 | 42 | declare module '*.svg' { 43 | import * as React from 'react'; 44 | 45 | export const ReactComponent: React.FunctionComponent & { title?: string }>; 48 | 49 | const src: string; 50 | export default src; 51 | } 52 | 53 | declare module '*.module.css' { 54 | const classes: { readonly [key: string]: string }; 55 | export default classes; 56 | } 57 | 58 | declare module '*.module.scss' { 59 | const classes: { readonly [key: string]: string }; 60 | export default classes; 61 | } 62 | 63 | declare module '*.module.sass' { 64 | const classes: { readonly [key: string]: string }; 65 | export default classes; 66 | } 67 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | 19 | const jest = require('jest'); 20 | const execSync = require('child_process').execSync; 21 | let argv = process.argv.slice(2); 22 | 23 | function isInGitRepository() { 24 | try { 25 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); 26 | return true; 27 | } catch (e) { 28 | return false; 29 | } 30 | } 31 | 32 | function isInMercurialRepository() { 33 | try { 34 | execSync('hg --cwd . root', { stdio: 'ignore' }); 35 | return true; 36 | } catch (e) { 37 | return false; 38 | } 39 | } 40 | 41 | // Watch unless on CI or explicitly running all tests 42 | if ( 43 | !process.env.CI && 44 | argv.indexOf('--watchAll') === -1 && 45 | argv.indexOf('--watchAll=false') === -1 46 | ) { 47 | // https://github.com/facebook/create-react-app/issues/5210 48 | const hasSourceControl = isInGitRepository() || isInMercurialRepository(); 49 | argv.push(hasSourceControl ? '' : ''); 50 | } 51 | 52 | 53 | jest.run(argv); 54 | -------------------------------------------------------------------------------- /src/__tests__/unit/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, fireEvent, RenderResult } from '@testing-library/react'; 3 | import Header from '../../components/Header'; 4 | 5 | let wrapper: RenderResult; 6 | let input: HTMLInputElement; 7 | const addUndoItem = jest.fn(); 8 | 9 | beforeEach(() => { 10 | wrapper = render(); 11 | input = wrapper.getByTestId('header-input') as HTMLInputElement; 12 | }); 13 | 14 | afterEach(() => { 15 | wrapper = null; 16 | }); 17 | 18 | describe('Header 组件', () => { 19 | it('测试组件初始化后的状态', () => { 20 | // input 存在 21 | expect(input).not.toBeNull(); 22 | 23 | // 组件初始化 input value 为空 24 | expect(input.value).toEqual(''); 25 | }); 26 | 27 | it('测试是否能够正常输入', () => { 28 | const inputEvent = { 29 | target: { 30 | value: 'Learn Jest' 31 | } 32 | }; 33 | // 模拟输入 34 | // 输入后 input value 为输入值 35 | fireEvent.change(input, inputEvent); 36 | expect(input.value).toEqual(inputEvent.target.value); 37 | }); 38 | 39 | it('测试是否能够正常回车并置空', () => { 40 | const inputEvent = { 41 | target: { 42 | value: 'Learn Jest' 43 | } 44 | }; 45 | const keyboardEvent = { 46 | keyCode: 13 47 | }; 48 | // 模拟回车 49 | // 调用 addUndoItem props 调用时参数为 input value 50 | // input value 置空 51 | fireEvent.change(input, inputEvent); 52 | fireEvent.keyUp(input, keyboardEvent); 53 | expect(addUndoItem).toHaveBeenCalled(); 54 | expect(addUndoItem).toHaveBeenCalledWith(inputEvent.target.value); 55 | expect(input.value).toEqual(''); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | body { 6 | background: #cdcdcd; 7 | } 8 | .header { 9 | line-height: 60px; 10 | background: #333; 11 | } 12 | .header-content { 13 | width: 600px; 14 | margin: 0 auto; 15 | font-size: 24px; 16 | color: #fff; 17 | } 18 | .header-input { 19 | outline: none; 20 | width: 360px; 21 | margin-top: 15px; 22 | float: right; 23 | line-height: 24px; 24 | border-radius: 5px; 25 | padding: 0 10px; 26 | } 27 | .undo-list { 28 | margin: 0 auto; 29 | width: 600px; 30 | } 31 | .undo-list-title { 32 | margin: 10px 0; 33 | line-height: 30px; 34 | font-size: 24px; 35 | font-weight: bold; 36 | } 37 | .undo-list-count { 38 | float: right; 39 | width: 30px; 40 | height: 30px; 41 | line-height: 30px; 42 | font-size: 12px; 43 | text-align: center; 44 | background: #e6e6ea; 45 | border-radius: 50%; 46 | } 47 | .undo-list-content { 48 | list-style-type: none; 49 | } 50 | .undo-list-item { 51 | line-height: 32px; 52 | font-size: 16px; 53 | margin-bottom: 10px; 54 | background: #fff; 55 | border-left: 3px solid #629a9c; 56 | text-indent: 10px; 57 | border-radius: 3px; 58 | } 59 | .undo-list-delete { 60 | float: right; 61 | margin-top: 6px; 62 | margin-right: 6px; 63 | width: 20px; 64 | height: 20px; 65 | line-height: 20px; 66 | text-align: center; 67 | background: #e6e6ea; 68 | text-indent: 0; 69 | border-radius: 50%; 70 | } 71 | .undo-list-input { 72 | position: relative; 73 | top: -1px; 74 | width: 300px; 75 | line-height: 22px; 76 | font-size: 14px; 77 | text-indent: 10px; 78 | outline: none; 79 | } 80 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | You need to enable JavaScript to run this app. 31 | 32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/List.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type TListItem = { 4 | status: 'div' | 'input'; 5 | value: string; 6 | }; 7 | 8 | export interface IList { 9 | list: TListItem[]; 10 | deleteItem: (index: number) => void; 11 | changeStatus: (index: number) => void; 12 | handleBlur: (index: number) => void; 13 | valueChange: (index: number, value: string) => void; 14 | } 15 | 16 | const List: React.FC = (props) => { 17 | const { list, changeStatus, handleBlur, valueChange, deleteItem } = props; 18 | 19 | return ( 20 | 21 | 22 | 正在进行 23 | 24 | {list.length} 25 | 26 | 27 | 28 | {list.map((item, index) => { 29 | return ( 30 | changeStatus(index)} 35 | > 36 | {item.status === 'div' ? ( 37 | item.value 38 | ) : ( 39 | handleBlur(index)} 44 | onChange={(e) => valueChange(index, e.target.value)} 45 | /> 46 | )} 47 | { 51 | e.stopPropagation(); 52 | deleteItem(index); 53 | }} 54 | > 55 | - 56 | 57 | 58 | ); 59 | })} 60 | 61 | 62 | ); 63 | }; 64 | 65 | export default List; 66 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Jest 配置 3 | * @author 炽翎 4 | */ 5 | module.exports = { 6 | // 测试根目录 7 | roots: ['/src'], 8 | // 测试覆盖率收集范围: src 下所有的 js jsx ts tsx 9 | collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts'], 10 | // 运行测试前准备文件: 引入 polyfill 解决 jsdom 的兼容性问题 11 | setupFiles: ['react-app-polyfill/jsdom'], 12 | // 测试运行环境搭建完成后引入的额外处理文件 13 | setupFilesAfterEnv: [ 14 | '/src/setupTests.ts', 15 | '/node_modules/jest-enzyme/lib/index.js' 16 | ], 17 | // test 匹配项: 在 __tests__ 文件夹下的所有 js jsx ts tsx 和以 spec test 为中间名的 js jsx ts tsx 18 | testMatch: [ 19 | '/src/**/__tests__/**/*.{js,jsx,ts,tsx}', 20 | '/src/**/*.{spec,test}.{js,jsx,ts,tsx}' 21 | ], 22 | // 创建测试运行环境: 在 node 环境下模拟浏览器环境 23 | testEnvironment: 'jest-environment-jsdom-fourteen', 24 | // 指定转换器: js jsx ts tsx 使用 babel-jest 进行转换 css 使用 cssTransform.js 进行转换 其他文件使用 fileTransform.js 25 | transform: { 26 | '^.+\\.(js|jsx|ts|tsx)$': '/node_modules/babel-jest', 27 | '^.+\\.css$': '/config/jest/cssTransform.js', 28 | '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': 29 | '/config/jest/fileTransform.js' 30 | }, 31 | // 转化器忽略文件: node_modules 目录下的所有 js jsx ts tsx cssModule 中的所有 css sass scss 32 | transformIgnorePatterns: [ 33 | '[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$', 34 | '^.+\\.module\\.(css|sass|scss)$' 35 | ], 36 | // 引入模块的路径: 默认 node_modules 可配置额外路径 37 | modulePaths: [], 38 | // 指定模块映射处理模块 39 | moduleNameMapper: { 40 | '^react-native$': 'react-native-web', 41 | '^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy' 42 | }, 43 | // 自动查找后缀名 44 | moduleFileExtensions: [ 45 | 'web.js', 46 | 'js', 47 | 'web.ts', 48 | 'ts', 49 | 'web.tsx', 50 | 'tsx', 51 | 'json', 52 | 'web.jsx', 53 | 'jsx', 54 | 'node' 55 | ], 56 | // watch 模式的插件 57 | watchPlugins: [ 58 | 'jest-watch-typeahead/filename', 59 | 'jest-watch-typeahead/testname' 60 | ] 61 | }; 62 | -------------------------------------------------------------------------------- /config/getHttpsConfig.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const crypto = require('crypto'); 6 | const chalk = require('react-dev-utils/chalk'); 7 | const paths = require('./paths'); 8 | 9 | // Ensure the certificate and key provided are valid and if not 10 | // throw an easy to debug error 11 | function validateKeyAndCerts({ cert, key, keyFile, crtFile }) { 12 | let encrypted; 13 | try { 14 | // publicEncrypt will throw an error with an invalid cert 15 | encrypted = crypto.publicEncrypt(cert, Buffer.from('test')); 16 | } catch (err) { 17 | throw new Error( 18 | `The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}` 19 | ); 20 | } 21 | 22 | try { 23 | // privateDecrypt will throw an error with an invalid key 24 | crypto.privateDecrypt(key, encrypted); 25 | } catch (err) { 26 | throw new Error( 27 | `The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${ 28 | err.message 29 | }` 30 | ); 31 | } 32 | } 33 | 34 | // Read file and throw an error if it doesn't exist 35 | function readEnvFile(file, type) { 36 | if (!fs.existsSync(file)) { 37 | throw new Error( 38 | `You specified ${chalk.cyan( 39 | type 40 | )} in your env, but the file "${chalk.yellow(file)}" can't be found.` 41 | ); 42 | } 43 | return fs.readFileSync(file); 44 | } 45 | 46 | // Get the https config 47 | // Return cert files if provided in env, otherwise just true or false 48 | function getHttpsConfig() { 49 | const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env; 50 | const isHttps = HTTPS === 'true'; 51 | 52 | if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) { 53 | const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE); 54 | const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE); 55 | const config = { 56 | cert: readEnvFile(crtFile, 'SSL_CRT_FILE'), 57 | key: readEnvFile(keyFile, 'SSL_KEY_FILE'), 58 | }; 59 | 60 | validateKeyAndCerts({ ...config, keyFile, crtFile }); 61 | return config; 62 | } 63 | return isHttps; 64 | } 65 | 66 | module.exports = getHttpsConfig; 67 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import axios from 'axios'; 3 | import Header from './components/Header'; 4 | import UndoList from './components/List'; 5 | 6 | import './App.css'; 7 | 8 | function App() { 9 | const [undoList, setUndoList] = useState([]); 10 | 11 | useEffect(() => { 12 | axios 13 | .get('/undoList.json') 14 | .then((res) => { 15 | setUndoList(res.data.data); 16 | }) 17 | .catch((e) => {}); 18 | }, []); 19 | 20 | const valueChange = (index: number, value: string) => { 21 | const newList = undoList.map((item, listIndex) => { 22 | if (index === listIndex) { 23 | return { 24 | ...item, 25 | value 26 | }; 27 | } 28 | return item; 29 | }); 30 | setUndoList(newList); 31 | }; 32 | 33 | const handleBlur = (index: number) => { 34 | const newList = undoList.map((item, listIndex) => { 35 | if (index === listIndex) { 36 | return { 37 | ...item, 38 | status: 'div' 39 | }; 40 | } 41 | return item; 42 | }); 43 | setUndoList(newList); 44 | }; 45 | 46 | const changeStatus = (index: number) => { 47 | const newList = undoList.map((item, listIndex) => { 48 | if (index === listIndex) { 49 | return { 50 | ...item, 51 | status: 'input' 52 | }; 53 | } 54 | return { 55 | ...item, 56 | status: 'div' 57 | }; 58 | }); 59 | setUndoList(newList); 60 | }; 61 | 62 | const addUndoItem = (value: string) => { 63 | const newList = [ 64 | ...undoList, 65 | { 66 | status: 'div', 67 | value 68 | } 69 | ]; 70 | setUndoList(newList); 71 | }; 72 | 73 | const deleteItem = (index: number) => { 74 | const newList = [...undoList]; 75 | newList.splice(index, 1); 76 | setUndoList(newList); 77 | }; 78 | 79 | return ( 80 | 81 | 82 | 89 | 90 | ); 91 | } 92 | 93 | export default App; 94 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebook/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 13 | // "public path" at which the app is served. 14 | // webpack needs to know it to put the right