├── docs ├── static │ └── css │ │ └── main.5518879a.css.map ├── asset-manifest.json └── index.html ├── src ├── index.js ├── libs │ ├── util.js │ └── ajax.js ├── style.css ├── components │ ├── Factory.jsx │ ├── App.jsx │ ├── FormModal.jsx │ ├── Search.jsx │ └── Content.jsx ├── api │ └── sqlData.js └── store │ └── sqlConfig.js ├── .gitignore ├── config ├── jest │ ├── fileTransform.js │ └── cssTransform.js ├── polyfills.js ├── env.js ├── paths.js ├── webpack.config.dev.js └── webpack.config.prod.js ├── scripts ├── test.js ├── build.js └── start.js ├── public └── index.html ├── README.md ├── package.json └── README-CLI.md /docs/static/css/main.5518879a.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":[],"names":[],"mappings":"","file":"static/css/main.5518879a.css","sourceRoot":""} -------------------------------------------------------------------------------- /docs/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "main.css": "static/css/main.5518879a.css", 3 | "main.css.map": "static/css/main.5518879a.css.map", 4 | "main.js": "static/js/main.2a25180d.js", 5 | "main.js.map": "static/js/main.2a25180d.js.map" 6 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import 'antd/dist/antd.css'; 4 | import './style.css'; 5 | import App from './components/App'; 6 | 7 | ReactDOM.render(, document.getElementById('App')); 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | .idea/ 19 | 20 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | React CRUD | cnzsb
-------------------------------------------------------------------------------- /src/libs/util.js: -------------------------------------------------------------------------------- 1 | /* eslint import/prefer-default-export: 0 */ 2 | 3 | /** 4 | * 对含有对象的数组,通过 key 和 value 筛选 index 5 | * @param key {String} 6 | * @param val {String | Number | Boolean} 7 | * @param arr {Array} 8 | * @return {Number} 9 | */ 10 | export const findIndexByKey = (key, val, arr) => arr.findIndex(item => item[key] === val); 11 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | // This is a custom Jest transformer turning file imports into filenames. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process(src, filename) { 8 | return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';'; 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | // This is a custom Jest transformer turning style imports into empty objects. 2 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 3 | 4 | module.exports = { 5 | process() { 6 | return 'module.exports = {};'; 7 | }, 8 | getCacheKey(fileData, filename) { 9 | // The output is always the same. 10 | return 'cssTransform'; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #f3f3f4; 3 | font-size: 14px; 4 | color: #676a6c; 5 | } 6 | 7 | .body { 8 | width: 960px; 9 | margin: 0 auto; 10 | } 11 | 12 | /* Fix ButtonGroup's borderRightColor */ 13 | .ant-btn-group .ant-btn-primary:last-child:not(:first-child)[disabled], 14 | .ant-btn-group .ant-btn-primary + .ant-btn[disabled] { 15 | border-right-color: #d9d9d9; 16 | } 17 | -------------------------------------------------------------------------------- /src/libs/ajax.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { notification } from 'antd'; 3 | 4 | const $http = axios.create({}); 5 | 6 | // response 7 | $http.interceptors.response.use( 8 | ({ data: { data } }) => data, 9 | ({ response }) => { 10 | notification.error({ 11 | message: '错误', 12 | description: '请求错误,请稍后再试', 13 | }); 14 | return Promise.reject(response); 15 | }, 16 | ); 17 | 18 | export default $http; 19 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'test'; 2 | process.env.PUBLIC_URL = ''; 3 | 4 | // Load environment variables from .env file. Suppress warnings using silent 5 | // if this file is missing. dotenv will never modify any environment variables 6 | // that have already been set. 7 | // https://github.com/motdotla/dotenv 8 | require('dotenv').config({silent: true}); 9 | 10 | const jest = require('jest'); 11 | const argv = process.argv.slice(2); 12 | 13 | // Watch unless on CI or in coverage mode 14 | if (!process.env.CI && argv.indexOf('--coverage') < 0) { 15 | argv.push('--watch'); 16 | } 17 | 18 | 19 | jest.run(argv); 20 | -------------------------------------------------------------------------------- /config/polyfills.js: -------------------------------------------------------------------------------- 1 | if (typeof Promise === 'undefined') { 2 | // Rejection tracking prevents a common issue where React gets into an 3 | // inconsistent state due to an error, but it gets swallowed by a Promise, 4 | // and the user has no idea what causes React's erratic future behavior. 5 | require('promise/lib/rejection-tracking').enable(); 6 | window.Promise = require('promise/lib/es6-extensions.js'); 7 | } 8 | 9 | // fetch() polyfill for making API calls. 10 | require('whatwg-fetch'); 11 | 12 | // Object.assign() is commonly used with React. 13 | // It will use the native implementation if it's present and isn't buggy. 14 | Object.assign = require('object-assign'); 15 | -------------------------------------------------------------------------------- /src/components/Factory.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Input } from 'antd'; 3 | 4 | export default class Factory extends React.Component { 5 | render() { 6 | const { type, target, keyName, onChange } = this.props; 7 | switch (type) { 8 | case 'text': 9 | return onChange(e, keyName)} />; 10 | case 'display': 11 | return !target[keyName] ? : ( 12 | {target[keyName]} 13 | ); 14 | default: 15 | return null; 16 | } 17 | } 18 | } 19 | 20 | Factory.propTypes = { 21 | type: React.PropTypes.string, 22 | target: React.PropTypes.objectOf(React.PropTypes.shape), 23 | keyName: React.PropTypes.string, 24 | onChange: React.PropTypes.func, 25 | }; 26 | -------------------------------------------------------------------------------- /src/api/sqlData.js: -------------------------------------------------------------------------------- 1 | import $http from '../libs/ajax'; 2 | 3 | export const getTableUsers = params => { 4 | /* return $http.get('api/users', params) 5 | .then(data => Promise.resolve(data)) */ 6 | // 模拟数据 7 | const data = []; 8 | // Create 1000 users 9 | for (let i = 1; i < 1000; i++) { 10 | data.push({ 11 | id: i, 12 | name: 'user' + i, 13 | sex: Math.random().toString().substr(2, 1) % 2 === 0 ? 'male' : 'female', 14 | age: 2 + Math.random().toString().substr(2, 1), 15 | remark: 'I have got ' + Math.random().toString().substr(2, 4) + ' coins' 16 | }); 17 | } 18 | return Promise.resolve(data); 19 | }; 20 | 21 | // todo 表单 Users 的 ajax 22 | export const getTableNone = params => ( 23 | $http.get('api/pageWhichIsNo', params) 24 | .then(data => Promise.resolve(data)) 25 | ); 26 | 27 | export default { 28 | getTableUsers, 29 | getTableNone, 30 | }; 31 | 32 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | React CRUD | cnzsb 16 | 17 | 18 |
19 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Menu } from 'antd'; 3 | import Content from './Content'; 4 | import sqlData from '../store/sqlConfig'; 5 | 6 | const MenuItem = Menu.Item; 7 | 8 | export default class App extends React.Component { 9 | constructor() { 10 | super(); 11 | this.state = { 12 | tableName: 'Users', 13 | }; 14 | 15 | this.openTable = this.openTable.bind(this); 16 | } 17 | 18 | openTable({ key }) { 19 | this.setState({ 20 | tableName: key, 21 | }); 22 | } 23 | 24 | render() { 25 | return ( 26 |
27 |

React CRUD

28 | 34 | { 35 | sqlData.map(data => ( 36 | 37 | {data.name} 38 | 39 | )) 40 | } 41 | 42 | 43 |
44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 2 | // injected into the application via DefinePlugin in Webpack configuration. 3 | 4 | var REACT_APP = /^REACT_APP_/i; 5 | 6 | function getClientEnvironment(publicUrl) { 7 | var raw = Object 8 | .keys(process.env) 9 | .filter(key => REACT_APP.test(key)) 10 | .reduce((env, key) => { 11 | env[key] = process.env[key]; 12 | return env; 13 | }, { 14 | // Useful for determining whether we’re running in production mode. 15 | // Most importantly, it switches React into the correct mode. 16 | 'NODE_ENV': process.env.NODE_ENV || 'development', 17 | // Useful for resolving the correct path to static assets in `public`. 18 | // For example, . 19 | // This should only be used as an escape hatch. Normally you would put 20 | // images into the `src` and `import` them in code to get their paths. 21 | 'PUBLIC_URL': publicUrl 22 | }); 23 | // Stringify all values so we can feed into Webpack DefinePlugin 24 | var stringified = { 25 | 'process.env': Object 26 | .keys(raw) 27 | .reduce((env, key) => { 28 | env[key] = JSON.stringify(raw[key]); 29 | return env; 30 | }, {}) 31 | }; 32 | 33 | return { raw, stringified }; 34 | } 35 | 36 | module.exports = getClientEnvironment; 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## React-antd-CRUD 2 | 3 | 本项目是基于 react 和 antd 的 CRUD 应用。 4 | 5 | ### 开始 6 | 7 | 项目使用官方脚手架 [create-react-app](https://github.com/facebookincubator/create-react-app) 搭建,相关指令同理。 8 | 9 | ### 说明 10 | 11 | 本项目详细说明参看[博客](http://www.zhaoshibo.net/blog/2017/03/09/%E4%BB%8E%E4%B8%80%E4%B8%AA%20CRUD%20%E4%B8%8A%E6%89%8B%20React%20%E5%92%8C%20AntD/)。 12 | 13 | 主要目录结构: 14 | 15 | ```bash 16 | |-- api 17 | | |-- sqlData.js # 操作 sqlData 的 ajax 方法 18 | |-- components 19 | | |-- App.jsx # 页面 20 | | |-- Content.jsx # 页面主体内容区 21 | | |-- Factory.jsx # 表单组件工厂 22 | | |-- FormModal.jsx # 弹出的增加或编辑的表单组件 23 | | |-- Search.jsx # 搜索组件 24 | |-- libs 25 | | |-- ajax.js # ajax 实例及公共方法 26 | | |-- util.js # 工具方法 27 | |-- store 28 | | |-- sqlConfig.js # 数据库表单配置项 29 | |-- index.js # 入口文件 30 | |-- style.css # 样式文件 31 | ``` 32 | 33 | 主要组件结构: 34 | 35 | ```bash 36 | |-- App 37 | | |-- Menu # 导航菜单 38 | | |-- Content # 主要内容区 39 | | |-- Search # 搜索组件 40 | | |-- Factory # 表单工厂 41 | | |-- ButtonGroup # 操作按钮群 42 | | |-- Table # 表格组件 43 | | |-- Pagination # 分页组件 44 | | |-- FormModal # 弹出的编辑表单 45 | | |-- Factory # 表单工厂 46 | ``` 47 | 48 | 组件结构图解: 49 | 50 | ![react-antd-crud](http://7xlivs.com1.z0.glb.clouddn.com/2017/03/09/%E4%BB%8E%E4%B8%80%E4%B8%AA%20CRUD%20%E4%B8%8A%E6%89%8B%20React%20%E5%92%8C%20AntD/react-antd-crud.png) -------------------------------------------------------------------------------- /src/store/sqlConfig.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | tableName: 'Users', 4 | name: '用户信息', 5 | headers: [ 6 | { 7 | tableKey: 'id', 8 | name: 'ID', 9 | type: 'display', 10 | width: 50, 11 | }, 12 | { 13 | tableKey: 'name', 14 | name: '姓名', 15 | type: 'text', 16 | width: 80, 17 | validators: [ 18 | 'required', 19 | ], 20 | }, 21 | { 22 | tableKey: 'sex', 23 | name: '性别', 24 | type: 'text', 25 | width: 50, 26 | }, 27 | { 28 | tableKey: 'age', 29 | name: '年龄', 30 | type: 'text', 31 | width: 50, 32 | validators: [ 33 | 'required', 34 | ], 35 | }, 36 | { 37 | tableKey: 'remark', 38 | name: '备注', 39 | type: 'text', 40 | width: 150, 41 | validators: [ 42 | 'required', 43 | ], 44 | }, 45 | ], 46 | }, 47 | { 48 | tableName: 'None', 49 | name: '异常模拟', 50 | headers: [ 51 | { 52 | tableKey: 'id', 53 | name: 'id', 54 | type: 'display', 55 | width: 50, 56 | }, 57 | { 58 | tableKey: 'name', 59 | name: '姓名', 60 | type: 'text', 61 | width: 80, 62 | validators: [ 63 | 'required', 64 | ], 65 | }, 66 | { 67 | tableKey: 'comment', 68 | name: '评论', 69 | type: 'text', 70 | width: 200, 71 | validators: [ 72 | '', 73 | ], 74 | }, 75 | { 76 | tableKey: 'status', 77 | name: '状态', 78 | type: 'text', 79 | width: 50, 80 | validators: [ 81 | 'required', 82 | ], 83 | }, 84 | ], 85 | }]; 86 | -------------------------------------------------------------------------------- /src/components/FormModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Modal, Form } from 'antd'; 3 | import Factory from './Factory'; 4 | 5 | const FormItem = Form.Item; 6 | 7 | export default class FormModal extends React.Component { 8 | constructor() { 9 | super(); 10 | 11 | this.state = {}; 12 | } 13 | 14 | render() { 15 | const { 16 | formModalTitle, 17 | formConfigs, 18 | formValues, 19 | showFormModal, 20 | submitFormModal, 21 | cancelFormModal, 22 | confirmLoading, 23 | onChange, 24 | } = this.props; 25 | 26 | return ( 27 | 34 |
35 | {formConfigs.map(items => ( 36 | 42 | 47 | 48 | ))} 49 |
50 |
51 | ); 52 | } 53 | } 54 | 55 | FormModal.propTypes = { 56 | formModalTitle: React.PropTypes.string, 57 | formConfigs: React.PropTypes.arrayOf(React.PropTypes.shape), 58 | formValues: React.PropTypes.objectOf(React.PropTypes.shape), 59 | onChange: React.PropTypes.objectOf(React.PropTypes.shape), 60 | showFormModal: React.PropTypes.bool, 61 | submitFormModal: React.PropTypes.func, 62 | cancelFormModal: React.PropTypes.func, 63 | confirmLoading: React.PropTypes.bool, 64 | }; 65 | -------------------------------------------------------------------------------- /src/components/Search.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Row, Col, Form, Button, Icon } from 'antd'; 3 | import Factory from './Factory'; 4 | 5 | const FormItem = Form.Item; 6 | 7 | export default class Search extends React.Component { 8 | constructor() { 9 | super(); 10 | 11 | this.state = {}; 12 | } 13 | 14 | render() { 15 | const { configs, values, onChange, submitSearch, resetSearch } = this.props; 16 | 17 | return ( 18 |
19 |
20 | 21 | {configs.map(config => ( 22 | config.type === 'display' ? null : ( 23 | 24 | 30 | 35 | 36 | 37 | ) 38 | ))} 39 | 40 |
41 | 42 | 43 | 47 | 51 | 52 |
53 | ); 54 | } 55 | } 56 | 57 | Search.propTypes = { 58 | configs: React.PropTypes.arrayOf(React.PropTypes.shape), 59 | values: React.PropTypes.objectOf(React.PropTypes.shape), 60 | onChange: React.PropTypes.objectOf(React.PropTypes.shape), 61 | submitSearch: React.PropTypes.func, 62 | resetSearch: React.PropTypes.func, 63 | }; 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-antd-crud", 3 | "version": "0.1.0", 4 | "private": true, 5 | "devDependencies": { 6 | "autoprefixer": "6.7.2", 7 | "babel-core": "6.22.1", 8 | "babel-eslint": "7.1.1", 9 | "babel-jest": "18.0.0", 10 | "babel-loader": "6.2.10", 11 | "babel-plugin-import": "1.1.1", 12 | "babel-preset-react-app": "^2.1.1", 13 | "babel-runtime": "^6.20.0", 14 | "case-sensitive-paths-webpack-plugin": "1.1.4", 15 | "chalk": "1.1.3", 16 | "connect-history-api-fallback": "1.3.0", 17 | "cross-spawn": "4.0.2", 18 | "css-loader": "0.26.1", 19 | "detect-port": "1.0.1", 20 | "dotenv": "2.0.0", 21 | "eslint": "3.8.1", 22 | "eslint-config-react-app": "^0.5.2", 23 | "eslint-loader": "1.6.0", 24 | "eslint-plugin-flowtype": "2.21.0", 25 | "eslint-plugin-import": "2.0.1", 26 | "eslint-plugin-jsx-a11y": "2.2.3", 27 | "eslint-plugin-react": "6.4.1", 28 | "extract-text-webpack-plugin": "1.0.1", 29 | "file-loader": "0.10.0", 30 | "filesize": "3.3.0", 31 | "fs-extra": "0.30.0", 32 | "gzip-size": "3.0.0", 33 | "html-webpack-plugin": "2.24.0", 34 | "http-proxy-middleware": "0.17.3", 35 | "jest": "18.1.0", 36 | "json-loader": "0.5.4", 37 | "object-assign": "4.1.1", 38 | "postcss-loader": "1.2.2", 39 | "promise": "7.1.1", 40 | "react-dev-utils": "^0.5.1", 41 | "recursive-readdir": "2.1.1", 42 | "strip-ansi": "3.0.1", 43 | "style-loader": "0.13.1", 44 | "url-loader": "0.5.7", 45 | "webpack": "1.14.0", 46 | "webpack-dev-server": "1.16.2", 47 | "webpack-manifest-plugin": "1.1.0", 48 | "whatwg-fetch": "2.0.2" 49 | }, 50 | "dependencies": { 51 | "antd": "2.7.4", 52 | "axios": "0.15.3", 53 | "react": "15.4.2", 54 | "react-dom": "15.4.2" 55 | }, 56 | "scripts": { 57 | "start": "node scripts/start.js", 58 | "build": "node scripts/build.js", 59 | "test": "node scripts/test.js --env=jsdom" 60 | }, 61 | "jest": { 62 | "collectCoverageFrom": [ 63 | "src/**/*.{js,jsx}" 64 | ], 65 | "setupFiles": [ 66 | "/config/polyfills.js" 67 | ], 68 | "testPathIgnorePatterns": [ 69 | "[/\\\\](build|docs|node_modules|scripts)[/\\\\]" 70 | ], 71 | "testEnvironment": "node", 72 | "testURL": "http://localhost", 73 | "transform": { 74 | "^.+\\.(js|jsx)$": "/node_modules/babel-jest", 75 | "^.+\\.css$": "/config/jest/cssTransform.js", 76 | "^(?!.*\\.(js|jsx|css|json)$)": "/config/jest/fileTransform.js" 77 | }, 78 | "transformIgnorePatterns": [ 79 | "[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$" 80 | ], 81 | "moduleNameMapper": { 82 | "^react-native$": "react-native-web" 83 | } 84 | }, 85 | "babel": { 86 | "presets": [ 87 | "react-app" 88 | ] 89 | }, 90 | "eslintConfig": { 91 | "extends": "react-app" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | var url = require('url'); 4 | 5 | // Make sure any symlinks in the project folder are resolved: 6 | // https://github.com/facebookincubator/create-react-app/issues/637 7 | var appDirectory = fs.realpathSync(process.cwd()); 8 | function resolveApp(relativePath) { 9 | return path.resolve(appDirectory, relativePath); 10 | } 11 | 12 | // We support resolving modules according to `NODE_PATH`. 13 | // This lets you use absolute paths in imports inside large monorepos: 14 | // https://github.com/facebookincubator/create-react-app/issues/253. 15 | 16 | // It works similar to `NODE_PATH` in Node itself: 17 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 18 | 19 | // We will export `nodePaths` as an array of absolute paths. 20 | // It will then be used by Webpack configs. 21 | // Jest doesn’t need this because it already handles `NODE_PATH` out of the box. 22 | 23 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 24 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 25 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 26 | 27 | var nodePaths = (process.env.NODE_PATH || '') 28 | .split(process.platform === 'win32' ? ';' : ':') 29 | .filter(Boolean) 30 | .filter(folder => !path.isAbsolute(folder)) 31 | .map(resolveApp); 32 | 33 | var envPublicUrl = process.env.PUBLIC_URL; 34 | 35 | function ensureSlash(path, needsSlash) { 36 | var hasSlash = path.endsWith('/'); 37 | if (hasSlash && !needsSlash) { 38 | return path.substr(path, path.length - 1); 39 | } else if (!hasSlash && needsSlash) { 40 | return path + '/'; 41 | } else { 42 | return path; 43 | } 44 | } 45 | 46 | function getPublicUrl(appPackageJson) { 47 | return envPublicUrl || require(appPackageJson).homepage; 48 | } 49 | 50 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 51 | // "public path" at which the app is served. 52 | // Webpack needs to know it to put the right 886 | ``` 887 | 888 | Then, on the server, you can replace `__SERVER_DATA__` with a JSON of real data right before sending the response. The client code can then read `window.SERVER_DATA` to use it. **Make sure to [sanitize the JSON before sending it to the client](https://medium.com/node-security/the-most-common-xss-vulnerability-in-react-js-applications-2bdffbcc1fa0) as it makes your app vulnerable to XSS attacks.** 889 | 890 | ## Running Tests 891 | 892 | >Note: this feature is available with `react-scripts@0.3.0` and higher.
893 | >[Read the migration guide to learn how to enable it in older projects!](https://github.com/facebookincubator/create-react-app/blob/master/CHANGELOG.md#migrating-from-023-to-030) 894 | 895 | Create React App uses [Jest](https://facebook.github.io/jest/) as its test runner. To prepare for this integration, we did a [major revamp](https://facebook.github.io/jest/blog/2016/09/01/jest-15.html) of Jest so if you heard bad things about it years ago, give it another try. 896 | 897 | Jest is a Node-based runner. This means that the tests always run in a Node environment and not in a real browser. This lets us enable fast iteration speed and prevent flakiness. 898 | 899 | While Jest provides browser globals such as `window` thanks to [jsdom](https://github.com/tmpvar/jsdom), they are only approximations of the real browser behavior. Jest is intended to be used for unit tests of your logic and your components rather than the DOM quirks. 900 | 901 | We recommend that you use a separate tool for browser end-to-end tests if you need them. They are beyond the scope of Create React App. 902 | 903 | ### Filename Conventions 904 | 905 | Jest will look for test files with any of the following popular naming conventions: 906 | 907 | * Files with `.js` suffix in `__tests__` folders. 908 | * Files with `.test.js` suffix. 909 | * Files with `.spec.js` suffix. 910 | 911 | The `.test.js` / `.spec.js` files (or the `__tests__` folders) can be located at any depth under the `src` top level folder. 912 | 913 | We recommend to put the test files (or `__tests__` folders) next to the code they are testing so that relative imports appear shorter. For example, if `App.test.jsx` and `App.jsx` are in the same folder, the test just needs to `import App from './App'` instead of a long relative path. Colocation also helps find tests more quickly in larger projects. 914 | 915 | ### Command Line Interface 916 | 917 | When you run `npm test`, Jest will launch in the watch mode. Every time you save a file, it will re-run the tests, just like `npm start` recompiles the code. 918 | 919 | The watcher includes an interactive command-line interface with the ability to run all tests, or focus on a search pattern. It is designed this way so that you can keep it open and enjoy fast re-runs. You can learn the commands from the “Watch Usage” note that the watcher prints after every run: 920 | 921 | ![Jest watch mode](http://facebook.github.io/jest/img/blog/15-watch.gif) 922 | 923 | ### Version Control Integration 924 | 925 | By default, when you run `npm test`, Jest will only run the tests related to files changed since the last commit. This is an optimization designed to make your tests runs fast regardless of how many tests you have. However it assumes that you don’t often commit the code that doesn’t pass the tests. 926 | 927 | Jest will always explicitly mention that it only ran tests related to the files changed since the last commit. You can also press `a` in the watch mode to force Jest to run all tests. 928 | 929 | Jest will always run all tests on a [continuous integration](#continuous-integration) server or if the project is not inside a Git or Mercurial repository. 930 | 931 | ### Writing Tests 932 | 933 | To create tests, add `it()` (or `test()`) blocks with the name of the test and its code. You may optionally wrap them in `describe()` blocks for logical grouping but this is neither required nor recommended. 934 | 935 | Jest provides a built-in `expect()` global function for making assertions. A basic test could look like this: 936 | 937 | ```js 938 | import sum from './sum'; 939 | 940 | it('sums numbers', () => { 941 | expect(sum(1, 2)).toEqual(3); 942 | expect(sum(2, 2)).toEqual(4); 943 | }); 944 | ``` 945 | 946 | All `expect()` matchers supported by Jest are [extensively documented here](http://facebook.github.io/jest/docs/expect.html).
947 | You can also use [`jest.fn()` and `expect(fn).toBeCalled()`](http://facebook.github.io/jest/docs/expect.html#tohavebeencalled) to create “spies” or mock functions. 948 | 949 | ### Testing Components 950 | 951 | There is a broad spectrum of component testing techniques. They range from a “smoke test” verifying that a component renders without throwing, to shallow rendering and testing some of the output, to full rendering and testing component lifecycle and state changes. 952 | 953 | Different projects choose different testing tradeoffs based on how often components change, and how much logic they contain. If you haven’t decided on a testing strategy yet, we recommend that you start with creating simple smoke tests for your components: 954 | 955 | ```js 956 | import React from 'react'; 957 | import ReactDOM from 'react-dom'; 958 | import App from './App'; 959 | 960 | it('renders without crashing', () => { 961 | const div = document.createElement('div'); 962 | ReactDOM.render(, div); 963 | }); 964 | ``` 965 | 966 | This test mounts a component and makes sure that it didn’t throw during rendering. Tests like this provide a lot value with very little effort so they are great as a starting point, and this is the test you will find in `src/App.test.jsx`. 967 | 968 | When you encounter bugs caused by changing components, you will gain a deeper insight into which parts of them are worth testing in your application. This might be a good time to introduce more specific tests asserting specific expected output or behavior. 969 | 970 | If you’d like to test components in isolation from the child components they render, we recommend using [`shallow()` rendering API](http://airbnb.io/enzyme/docs/api/shallow.html) from [Enzyme](http://airbnb.io/enzyme/). You can write a smoke test with it too: 971 | 972 | ```sh 973 | npm install --save-dev enzyme react-addons-test-utils 974 | ``` 975 | 976 | ```js 977 | import React from 'react'; 978 | import { shallow } from 'enzyme'; 979 | import App from './App'; 980 | 981 | it('renders without crashing', () => { 982 | shallow(); 983 | }); 984 | ``` 985 | 986 | Unlike the previous smoke test using `ReactDOM.render()`, this test only renders `` and doesn’t go deeper. For example, even if `` itself renders a `