diff --git a/.travis.yml b/.travis.yml index 781e0508..9179858b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,12 @@ sudo: false language: node_js node_js: - - "lts/*" - "stable" cache: yarn: true directories: - "node_modules" script: - - npm run build:all - - npm run lint - - npm test + - yarn build:all + - yarn lint + - yarn test diff --git a/package.json b/package.json index e475e1ee..289ba1c2 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "devDependencies": { "babel-eslint": "^10.0.0", "eslint-plugin-react": "7.4.0", + "eslint-plugin-flowtype": "3.2.0", "lerna": "3.4.2" }, "scripts": { diff --git a/packages/redux-devtools-test-generator/.babelrc b/packages/redux-devtools-test-generator/.babelrc new file mode 100644 index 00000000..65836a67 --- /dev/null +++ b/packages/redux-devtools-test-generator/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015-loose", "stage-0", "react"] +} diff --git a/packages/redux-devtools-test-generator/.eslintignore b/packages/redux-devtools-test-generator/.eslintignore new file mode 100644 index 00000000..6397f1cb --- /dev/null +++ b/packages/redux-devtools-test-generator/.eslintignore @@ -0,0 +1,3 @@ +lib +demo +**/node_modules diff --git a/packages/redux-devtools-test-generator/.eslintrc b/packages/redux-devtools-test-generator/.eslintrc new file mode 100644 index 00000000..327b3081 --- /dev/null +++ b/packages/redux-devtools-test-generator/.eslintrc @@ -0,0 +1,18 @@ +{ + "extends": "eslint-config-airbnb", + "env": { + "browser": true, + "jest": true, + "node": true + }, + "rules": { + "prefer-template": 0, + "no-shadow": 0, + "comma-dangle": 0, + "react/sort-comp": 0 + }, + "parser": "babel-eslint", + "plugins": [ + "react" + ] +} diff --git a/packages/redux-devtools-test-generator/LICENSE.md b/packages/redux-devtools-test-generator/LICENSE.md new file mode 100644 index 00000000..1a68f555 --- /dev/null +++ b/packages/redux-devtools-test-generator/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Mihail Diordiev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/redux-devtools-test-generator/README.md b/packages/redux-devtools-test-generator/README.md new file mode 100644 index 00000000..0bab0031 --- /dev/null +++ b/packages/redux-devtools-test-generator/README.md @@ -0,0 +1,54 @@ +Redux DevTools Test Generator +============================== + +### Installation + +``` +npm install --save-dev redux-devtools-test-generator +``` + +### Usage + +If you use [Redux DevTools Extension](https://github.com/zalmoxisus/redux-devtools-extension), [Remote Redux DevTools](https://github.com/zalmoxisus/remote-redux-devtools) or [RemoteDev](https://github.com/zalmoxisus/remotedev), it's already there, and no additional actions required. + +With [`redux-devtools`](https://github.com/reduxjs/redux-devtools) and [`redux-devtools-inspector`](https://github.com/reduxjs/redux-devtools/packages/redux-devtools-inspector): + +##### `containers/DevTools.js` + +```js +import React from 'react'; +import { createDevTools } from 'redux-devtools'; +import Inspector from 'redux-devtools-inspector'; +import TestGenerator from 'redux-devtools-test-generator'; +import mochaTemplate from 'redux-devtools-test-generator/lib/redux/mocha'; // If using default tests. + +const testComponent = (props) => ( + +); + +export default createDevTools( + [...defaultTabs, { name: 'Test', component: testComponent }] + /> +); +``` + +Instead of `mochaTemplate.expect` and `mochaTemplate.wrap` you can use your function templates. + +If `useCodemirror` specified, include `codemirror/lib/codemirror.css` style and optionally themes from `codemirror/theme/`. + +### Props + +Name | Description +------------- | ------------- +`assertion` | String template or function with an object argument containing `action`, `prevState`, `curState` keys, which returns a string representing the assertion (see the [function](https://github.com/zalmoxisus/redux-devtools-test-generator/blob/master/src/redux/mocha/index.js#L1-L3) or [template](https://github.com/zalmoxisus/redux-devtools-test-generator/blob/master/src/redux/mocha/template.js#L1)). +[`wrap`] | Optional string template or function which gets `assertions` argument and returns a string (see the example [function](https://github.com/zalmoxisus/redux-devtools-test-generator/blob/master/src/redux/mocha/index.js#L5-L14) or [template](https://github.com/zalmoxisus/redux-devtools-test-generator/blob/master/src/redux/mocha/template.js#L3-L12)). +[`useCodemirror`] | Boolean. If specified will use codemirror styles. +[`theme`] | String. Name of [the codemirror theme](https://codemirror.net/demo/theme.html). + +### License + +MIT diff --git a/packages/redux-devtools-test-generator/package.json b/packages/redux-devtools-test-generator/package.json new file mode 100644 index 00000000..770d2bb5 --- /dev/null +++ b/packages/redux-devtools-test-generator/package.json @@ -0,0 +1,91 @@ +{ + "name": "redux-devtools-test-generator", + "version": "0.5.1", + "description": "Generate tests for redux devtools.", + "main": "lib/index.js", + "files": [ + "lib" + ], + "scripts": { + "start": "webpack-dev-server", + "clean": "rimraf lib", + "build": "babel src --out-dir lib", + "lint": "eslint src test", + "test": "jest --no-cache", + "prepare": "npm run clean && npm run build", + "prepublishOnly": "npm run lint && npm run test && npm run clean && npm run build" + }, + "repository": { + "type": "git", + "url": "https://github.com/reduxjs/redux-devtools.git" + }, + "keywords": [ + "redux", + "devtools", + "test", + "flux", + "react", + "hot reloading", + "time travel", + "live edit" + ], + "author": "Mihail Diordiev (https://github.com/zalmoxisus)", + "license": "MIT", + "bugs": { + "url": "https://github.com/reduxjs/redux-devtools/issues" + }, + "homepage": "https://github.com/reduxjs/redux-devtools", + "devDependencies": { + "babel-cli": "^6.10.1", + "babel-core": "^6.10.4", + "babel-eslint": "^6.0.5", + "babel-loader": "^6.2.4", + "babel-preset-es2015": "^6.9.0", + "babel-preset-es2015-loose": "^7.0.0", + "babel-preset-react": "^6.5.0", + "babel-preset-stage-0": "^6.5.0", + "babel-register": "^6.11.6", + "clean-webpack-plugin": "^0.1.15", + "css-loader": "^0.26.1", + "enzyme": "^2.6.0", + "enzyme-to-json": "^1.3.0", + "eslint": "^2.13.1", + "eslint-config-airbnb": "^9.0.1", + "eslint-plugin-import": "^1.9.2", + "eslint-plugin-jsx-a11y": "^1.5.3", + "eslint-plugin-react": "^5.2.2", + "expect": "^1.20.1", + "export-files-webpack-plugin": "0.0.1", + "file-loader": "^0.10.0", + "html-webpack-plugin": "^2.28.0", + "jest": "^23.6.0", + "lodash.shuffle": "^4.2.0", + "nyan-progress-webpack-plugin": "^1.1.4", + "react-addons-test-utils": "^15.1.0", + "react-dom": "^15.1.0", + "react-redux": "^5.0.2", + "react-router": "^3.0.2", + "react-router-redux": "^4.0.8", + "redux": "^3.6.0", + "redux-devtools": "^3.3.2", + "redux-devtools-dock-monitor": "^1.1.1", + "redux-logger": "^2.8.1", + "remotedev-inspector-monitor": "^0.11.0", + "rimraf": "^2.5.2", + "style-loader": "^0.13.1", + "url-loader": "^0.5.7", + "webpack": "^2.2.1", + "webpack-dev-server": "^2.3.0" + }, + "dependencies": { + "devui": "^1.0.0-0", + "es6template": "^1.0.4", + "javascript-stringify": "^1.2.0", + "jsan": "^3.1.3", + "object-path": "^0.11.1", + "prop-types": "^15.5.10", + "react": "^15.1.0", + "react-icons": "^2.2.3", + "simple-diff": "^1.3.0" + } +} diff --git a/packages/redux-devtools-test-generator/src/TestGenerator.js b/packages/redux-devtools-test-generator/src/TestGenerator.js new file mode 100644 index 00000000..479a85a7 --- /dev/null +++ b/packages/redux-devtools-test-generator/src/TestGenerator.js @@ -0,0 +1,180 @@ +import React, { PureComponent, Component } from 'react'; +import PropTypes from 'prop-types'; +import stringify from 'javascript-stringify'; +import objectPath from 'object-path'; +import jsan from 'jsan'; +import diff from 'simple-diff'; +import es6template from 'es6template'; +import { Editor } from 'devui'; + +export const fromPath = (path) => ( + path + .map(a => ( + typeof a === 'string' ? `.${a}` : `[${a}]` + )) + .join('') +); + +function getState(s, defaultValue) { + if (!s) return defaultValue; + return JSON.parse(jsan.stringify(s.state)); +} + +export function compare(s1, s2, cb, defaultValue) { + const paths = []; // Already processed + function generate({ type, newPath, newValue, newIndex }) { + let curState; + let path = fromPath(newPath); + + if (type === 'remove-item' || type === 'move-item') { + if (paths.length && paths.indexOf(path) !== -1) return; + paths.push(path); + const v = objectPath.get(s2.state, newPath); + curState = v.length; + path += '.length'; + } else if (type === 'add-item') { + generate({ type: 'move-item', newPath }); + path += `[${newIndex}]`; + curState = stringify(newValue); + } else { + curState = stringify(newValue); + } + + // console.log(`expect(store${path}).toEqual(${curState});`); + cb({ path, curState }); + } + + diff(getState(s1, defaultValue), getState(s2, defaultValue)/* , { idProp: '*' } */) + .forEach(generate); +} + +export default class TestGenerator extends (PureComponent || Component) { + getMethod(action) { + let type = action.type; + if (type[0] === '┗') type = type.substr(1).trim(); + let args = action.arguments; + if (args) args = args.map(arg => stringify(arg)).join(','); + else args = ''; + return `${type}(${args})`; + } + + getAction(action) { + if (action.type === '@@INIT') return '{}'; + return stringify(action); + } + + generateTest() { + const { + computedStates, actions, selectedActionId, startActionId, isVanilla, name + } = this.props; + + if (!actions || !computedStates || computedStates.length < 1) return ''; + + let { wrap, assertion, dispatcher, indentation } = this.props; + if (typeof assertion === 'string') assertion = es6template.compile(assertion); + if (typeof wrap === 'string') { + const ident = wrap.match(/\n.+\$\{assertions}/); + if (ident) indentation = ident[0].length - 13; + wrap = es6template.compile(wrap); + } + if (typeof dispatcher === 'string') dispatcher = es6template.compile(dispatcher); + + let space = ''; + if (indentation) space = Array(indentation).join(' '); + + let r = ''; + let isFirst = true; + let i; + if (startActionId !== null) i = startActionId; + else if (selectedActionId !== null) i = selectedActionId; + else i = computedStates.length - 1; + const startIdx = i > 0 ? i : 1; + + const addAssertions = ({ path, curState }) => { + r += space + assertion({ path, curState }) + '\n'; + }; + + while (actions[i]) { + if (!isVanilla || /^┗?\s?[a-zA-Z0-9_@.\[\]-]+?$/.test(actions[i].action.type)) { + if (isFirst) isFirst = false; + else r += space; + if (!isVanilla || actions[i].action.type[0] !== '@') { + r += dispatcher({ + action: !isVanilla ? + this.getAction(actions[i].action) : + this.getMethod(actions[i].action), + prevState: i > 0 ? stringify(computedStates[i - 1].state) : undefined + }) + '\n'; + } + if (!isVanilla) { + addAssertions({ path: '', curState: stringify(computedStates[i].state) }); + } else { + compare(computedStates[i - 1], computedStates[i], addAssertions, isVanilla && {}); + } + } + i++; + if (i > selectedActionId) break; + } + + r = r.trim(); + if (wrap) { + if (!isVanilla) r = wrap({ name, assertions: r }); + else { + r = wrap({ + name: /^[a-zA-Z0-9_-]+?$/.test(name) ? name : 'Store', + actionName: (selectedActionId === null || selectedActionId > 0) && actions[startIdx] ? + actions[startIdx].action.type.replace(/[^a-zA-Z0-9_-]+/, '') : + 'should return the initial state', + initialState: stringify(computedStates[startIdx - 1].state), + assertions: r + }); + } + } + return r; + } + + render() { + const code = this.generateTest(); + + if (!this.props.useCodemirror) { + return ( + +`; + +exports[`TestGenerator component should generate test for vanilla js class 1`] = ` + +`; + +exports[`TestGenerator component should generate test for vanilla js class with string template 1`] = ` + +`; + +exports[`TestGenerator component should match function template's test for first action 1`] = ` + +`; + +exports[`TestGenerator component should match string template's test for first action 1`] = ` + +`; + +exports[`TestGenerator component should show warning message when no params provided 1`] = ` +