Move redux-devtools-test-generator package (#438)

* Move from zalmoxisus/redux-devtools-test-generator

* Update package and links

* Fix CI
This commit is contained in:
Mihail Diordiev 2018-12-23 02:13:56 +02:00 committed by GitHub
parent 4f6ac0279a
commit 2f91b8765c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 2378 additions and 49 deletions

View File

@ -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

View File

@ -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": {

View File

@ -0,0 +1,3 @@
{
"presets": ["es2015-loose", "stage-0", "react"]
}

View File

@ -0,0 +1,3 @@
lib
demo
**/node_modules

View File

@ -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"
]
}

View File

@ -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.

View File

@ -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) => (
<TestGenerator
expect={mochaTemplate.expect} wrap={mochaTemplate.wrap} useCodemirror
{...props}
/>
);
export default createDevTools(
<Inspector
tabs: defaultTabs => [...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

View File

@ -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 <zalmoxisus@gmail.com> (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"
}
}

View File

@ -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 (
<textarea
style={{ padding: '10px', width: '100%', height: '100%' }}
defaultValue={code}
/>
);
}
return <Editor value={code} />;
}
}
TestGenerator.propTypes = {
name: PropTypes.string,
isVanilla: PropTypes.bool,
computedStates: PropTypes.array,
actions: PropTypes.object,
selectedActionId: PropTypes.number,
startActionId: PropTypes.number,
wrap: PropTypes.oneOfType([
PropTypes.func,
PropTypes.string
]),
dispatcher: PropTypes.oneOfType([
PropTypes.func,
PropTypes.string
]),
assertion: PropTypes.oneOfType([
PropTypes.func,
PropTypes.string
]),
useCodemirror: PropTypes.bool,
indentation: PropTypes.number,
header: PropTypes.element
};
TestGenerator.defaultProps = {
useCodemirror: true,
selectedActionId: null,
startActionId: null
};

View File

@ -0,0 +1,167 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Toolbar, Container, Button, Select, Notification, Dialog } from 'devui';
import { formSchema, uiSchema, defaultFormData } from './templateForm';
import AddIcon from 'react-icons/lib/md/add';
import EditIcon from 'react-icons/lib/md/edit';
import TestGenerator from './TestGenerator';
import jestTemplate from './redux/jest/template';
import mochaTemplate from './redux/mocha/template';
import tapeTemplate from './redux/tape/template';
import avaTemplate from './redux/ava/template';
export const getDefaultTemplates = (/* lib */) => (
/*
if (lib === 'redux') {
return [mochaTemplate, tapeTemplate, avaTemplate];
}
return [mochaVTemplate, tapeVTemplate, avaVTemplate];
*/
[jestTemplate, mochaTemplate, tapeTemplate, avaTemplate]
);
export default class TestTab extends Component {
constructor(props) {
super(props);
this.state = { dialogStatus: null };
}
getPersistedState = () => (
this.props.monitorState.testGenerator || {}
);
handleSelectTemplate = selectedTemplate => {
const { templates = getDefaultTemplates() } = this.getPersistedState();
this.updateState({ selected: templates.indexOf(selectedTemplate) });
};
handleCloseTip = () => {
this.updateState({ hideTip: true });
};
handleCloseDialog = () => {
this.setState({ dialogStatus: null });
};
handleSubmit = ({ formData: template }) => {
const { templates = getDefaultTemplates(), selected = 0 } = this.getPersistedState();
if (this.state.dialogStatus === 'Add') {
this.updateState({
selected: templates.length,
templates: [...templates, template]
});
} else {
const editedTemplates = [...templates];
editedTemplates[selected] = template;
this.updateState({
templates: editedTemplates
});
}
this.handleCloseDialog();
};
handleRemove = () => {
const { templates = getDefaultTemplates(), selected = 0 } = this.getPersistedState();
this.updateState({
selected: 0,
templates: templates.length === 1 ? undefined : [
...templates.slice(0, selected),
...templates.slice(selected + 1)
]
});
this.handleCloseDialog();
};
addTemplate = () => {
this.setState({ dialogStatus: 'Add' });
};
editTemplate = () => {
this.setState({ dialogStatus: 'Edit' });
};
updateState = newState => {
this.props.updateMonitorState({
testGenerator: {
...this.props.monitorState.testGenerator,
...newState
}
});
};
render() {
const { monitorState, updateMonitorState, ...rest } = this.props; // eslint-disable-line no-unused-vars, max-len
const { dialogStatus } = this.state;
const persistedState = this.getPersistedState();
const { selected = 0, templates = getDefaultTemplates() } = persistedState;
const template = templates[selected];
const { name, assertion, dispatcher, wrap } = template;
return (
<Container>
<Toolbar>
<Select
options={templates}
valueKey="name"
labelKey="name"
value={name}
simpleValue={false}
onChange={this.handleSelectTemplate}
/>
<Button onClick={this.editTemplate}><EditIcon /></Button>
<Button onClick={this.addTemplate}><AddIcon /></Button>
</Toolbar>
{!assertion ?
<Notification>
No template for tests specified.
</Notification>
:
<TestGenerator
isVanilla={false}
assertion={assertion}
dispatcher={dispatcher}
wrap={wrap}
{...rest}
/>
}
{!persistedState.hideTip && assertion && rest.startActionId === null &&
<Notification onClose={this.handleCloseTip}>
Hold <b>SHIFT</b> key to select more actions.
</Notification>
}
{dialogStatus &&
<Dialog
open
title={`${dialogStatus} test template`}
onDismiss={this.handleCloseDialog}
onSubmit={this.handleSubmit}
actions={[
<Button key="cancel" onClick={this.handleCloseDialog}>Cancel</Button>,
<Button key="remove" onClick={this.handleRemove}>Remove</Button>
]}
submitText={dialogStatus}
schema={formSchema}
uiSchema={uiSchema}
formData={dialogStatus === 'Edit' ? template : defaultFormData}
/>
}
</Container>
);
}
}
TestTab.propTypes = {
monitorState: PropTypes.shape({
testGenerator: PropTypes.shape({
templates: PropTypes.array,
selected: PropTypes.number,
hideTip: PropTypes.bool
})
}).isRequired,
/*
options: PropTypes.shape({
lib: PropTypes.string
}).isRequired,
*/
updateMonitorState: PropTypes.func.isRequired
};

View File

@ -0,0 +1,21 @@
export const name = 'Ava template';
export const dispatcher = ({ action, prevState }) => (
`state = reducers(${prevState}, ${action});`
);
export const assertion = ({ curState }) => (
`t.deepEqual(state, ${curState});`
);
export const wrap = ({ assertions }) => (
`import test from 'ava';
import reducers from '../../reducers';
test('reducers', (t) => {
let state;
${assertions}
});
`);
export default { name, assertion, dispatcher, wrap };

View File

@ -0,0 +1,17 @@
export const name = 'Ava template';
export const dispatcher = 'state = reducers(${prevState}, ${action});';
export const assertion = 't.deepEqual(state, ${curState});';
export const wrap = (
`import test from 'ava';
import reducers from '../../reducers';
test('reducers', (t) => {
let state;
\${assertions}
});
`);
export default { name, assertion, dispatcher, wrap };

View File

@ -0,0 +1,20 @@
export const name = 'Jest template';
export const dispatcher = ({ action, prevState }) => (
`state = reducers(${prevState}, ${action});`
);
export const assertion = ({ curState }) => (
`expect(state).toEqual(${curState});`
);
export const wrap = ({ assertions }) => (
`import reducers from '../../reducers';
test('reducers', () => {
let state;
${assertions}
});
`);
export default { name, assertion, dispatcher, wrap };

View File

@ -0,0 +1,16 @@
export const name = 'Jest template';
export const dispatcher = 'state = reducers(${prevState}, ${action});';
export const assertion = 'expect(state).toEqual(${curState});';
export const wrap = (
`import reducers from '../../reducers';
test('reducers', () => {
let state;
\${assertions}
});
`);
export default { name, assertion, dispatcher, wrap };

View File

@ -0,0 +1,23 @@
export const name = 'Mocha template';
export const dispatcher = ({ action, prevState }) => (
`state = reducers(${prevState}, ${action});`
);
export const assertion = ({ curState }) => (
`expect(state).toEqual(${curState});`
);
export const wrap = ({ assertions }) => (
`import expect from 'expect';
import reducers from '../../reducers';
describe('reducers', () => {
it('should handle actions', () => {
let state;
${assertions}
});
});
`);
export default { name, assertion, dispatcher, wrap };

View File

@ -0,0 +1,19 @@
export const name = 'Mocha template';
export const dispatcher = 'state = reducers(${prevState}, ${action});';
export const assertion = 'expect(state).toEqual(${curState});';
export const wrap = (
`import expect from 'expect';
import reducers from '../../reducers';
describe('reducers', () => {
it('should handle actions', () => {
let state;
\${assertions}
});
});
`);
export default { name, assertion, dispatcher, wrap };

View File

@ -0,0 +1,22 @@
export const name = 'Tape template';
export const dispatcher = ({ action, prevState }) => (
`state = reducers(${prevState}, ${action});`
);
export const assertion = ({ curState }) => (
`t.deepEqual(state, ${curState});`
);
export const wrap = ({ assertions }) => (
`import test from 'tape';
import reducers from '../../reducers';
test('reducers', (t) => {
let state;
${assertions}
t.end();
});
`);
export default { name, assertion, dispatcher, wrap };

View File

@ -0,0 +1,18 @@
export const name = 'Tape template';
export const dispatcher = 'state = reducers(${prevState}, ${action});';
export const assertion = 't.deepEqual(state, ${curState});';
export const wrap = (
`import test from 'tape';
import reducers from '../../reducers';
test('reducers', (t) => {
let state;
\${assertions}
t.end();
});
`);
export default { name, assertion, dispatcher, wrap };

View File

@ -0,0 +1,42 @@
export const formSchema = {
type: 'object',
required: ['name'],
properties: {
name: {
type: 'string',
title: 'Template name'
},
dispatcher: {
type: 'string',
title: 'Dispatcher: ({ action, prevState }) => (`<template>`)'
},
assertion: {
type: 'string',
title: 'Assertion: ({ curState }) => (`<template>`)'
},
wrap: {
type: 'string',
title: 'Wrap code: ({ name, initialState, assertions }) => (`<template>`)'
}
}
};
export const uiSchema = {
dispatcher: {
'ui:widget': 'textarea'
},
assertion: {
'ui:widget': 'textarea'
},
wrap: {
'ui:widget': 'textarea'
}
};
export const defaultFormData = {
dispatcher: 'state = reducers(${prevState}, ${action});',
assertion: 't.deepEqual(state, ${curState});',
wrap: `test('reducers', (t) => {
\${assertions}
});`
};

View File

@ -0,0 +1,21 @@
export const name = 'Ava template';
export const dispatcher = ({ action }) => (
`${action};`
);
export const assertion = ({ path, curState }) => (
`t.deepEqual(state${path}, ${curState});`
);
export const wrap = ({ name, initialState, assertions }) => (
`import test from 'ava';
import ${name} from '../../stores/${name}';
test('${name}', (t) => {
const store = new ${name}(${initialState});
${assertions}
});
`);
export default { name, assertion, dispatcher, wrap };

View File

@ -0,0 +1,17 @@
export const name = 'Ava template';
export const dispatcher = '${action};';
export const assertion = 't.deepEqual(state${path}, ${curState});';
export const wrap = (
`import test from 'ava';
import \${name} from '../../stores/\${name}';
test('\${name}', (t) => {
const store = new \${name}(\${initialState});
\${assertions}
});
`);
export default { name, assertion, dispatcher, wrap };

View File

@ -0,0 +1,21 @@
export const name = 'Mocha template';
export const dispatcher = ({ action }) => (
`${action};`
);
export const assertion = ({ path, curState }) => (
`expect(store${path}).toEqual(${curState});`
);
export const wrap = ({ name, initialState, assertions }) => (
`import expect from 'expect';
import ${name} from '../../stores/${name}';
test('${name}', (t) => {
const store = new ${name}(${initialState});
${assertions}
});
`);
export default { name, assertion, dispatcher, wrap };

View File

@ -0,0 +1,17 @@
export const name = 'Mocha template';
export const dispatcher = '${action};';
export const assertion = 'expect(store${path}).toEqual(${curState});';
export const wrap = (
`import expect from 'expect';
import \${name} from '../../stores/\${name}';
test('\${name}', (t) => {
const store = new \${name}(\${initialState});
\${assertions}
});
`);
export default { name, assertion, dispatcher, wrap };

View File

@ -0,0 +1,23 @@
export const name = 'Mocha template';
export const dispatcher = ({ action }) => (
`${action};`
);
export const assertion = ({ path, curState }) => (
`expect(store${path}).toEqual(${curState});`
);
export const wrap = ({ name, actionName, initialState, assertions }) => (
`import expect from 'expect';
import ${name} from '../../stores/${name}';
describe('${name}', () => {
it('${actionName}', () => {
const store = new ${name}(${initialState});
${assertions}
});
});
`);
export default { name, assertion, dispatcher, wrap };

View File

@ -0,0 +1,19 @@
export const name = 'Mocha template';
export const dispatcher = '${action};';
export const assertion = 'expect(store${path}).toEqual(${curState});';
export const wrap = (
`import expect from 'expect';
import \${name} from '../../stores/\${name}';
describe('\${name}', () => {
it('\${actionName}', () => {
const store = new \${name}(\${initialState});
\${assertions}
});
});
`);
export default { name, assertion, dispatcher, wrap };

View File

@ -0,0 +1,22 @@
export const name = 'Tape template';
export const dispatcher = ({ action }) => (
`${action};`
);
export const assertion = ({ path, curState }) => (
`t.deepEqual(state${path}, ${curState});`
);
export const wrap = ({ name, initialState, assertions }) => (
`import test from 'tape';
import ${name} from '../../stores/${name}';
test('${name}', (t) => {
const store = new ${name}(${initialState});
${assertions}
t.end();
});
`);
export default { name, assertion, dispatcher, wrap };

View File

@ -0,0 +1,18 @@
export const name = 'Tape template';
export const dispatcher = '${action};';
export const assertion = 't.deepEqual(state${path}, ${curState});';
export const wrap = (
`import test from 'tape';
import \${name} from '../../stores/\${name}';
test('\${name}', (t) => {
const store = new \${name}(\${initialState});
\${assertions}
t.end();
});
`);
export default { name, assertion, dispatcher, wrap };

View File

@ -0,0 +1,91 @@
import React from 'react';
import { render } from 'enzyme';
import { renderToJson } from 'enzyme-to-json';
import TestGenerator from '../src/TestGenerator';
import fnTemplate from '../src/redux/mocha';
import strTemplate from '../src/redux/mocha/template';
import fnVanillaTemplate from '../src/vanilla/mocha';
import strVanillaTemplate from '../src/vanilla/mocha/template';
const actions = {
0: { type: 'PERFORM_ACTION', action: { type: '@@INIT' } },
1: { type: 'PERFORM_ACTION', action: { type: 'INCREMENT_COUNTER' } }
};
const computedStates = [
{ state: { counter: 0 } },
{ state: { counter: 1 } }
];
describe('TestGenerator component', () => {
it('should show warning message when no params provided', () => {
const component = render(<TestGenerator useCodemirror={false} />);
expect(renderToJson(component)).toMatchSnapshot();
});
it('should be empty when no actions provided', () => {
const component = render(
<TestGenerator
assertion={fnTemplate.assertion} dispatcher={fnTemplate.dispatcher} wrap={fnTemplate.wrap}
useCodemirror={false}
/>
);
expect(renderToJson(component)).toMatchSnapshot();
});
it('should match function template\'s test for first action', () => {
const component = render(
<TestGenerator
assertion={fnTemplate.assertion} dispatcher={fnTemplate.dispatcher} wrap={fnTemplate.wrap}
actions={actions} computedStates={computedStates} selectedActionId={1}
useCodemirror={false}
/>
);
expect(renderToJson(component)).toMatchSnapshot();
});
it('should match string template\'s test for first action', () => {
const component = render(
<TestGenerator
assertion={strTemplate.assertion} dispatcher={strTemplate.dispatcher}
wrap={strTemplate.wrap} useCodemirror={false}
actions={actions} computedStates={computedStates} selectedActionId={1}
/>
);
expect(renderToJson(component)).toMatchSnapshot();
});
it('should generate test for the last action when selectedActionId not specified', () => {
const component = render(
<TestGenerator
assertion={fnTemplate.assertion} dispatcher={fnTemplate.dispatcher} wrap={fnTemplate.wrap}
actions={actions} computedStates={computedStates} useCodemirror={false}
/>
);
expect(renderToJson(component)).toMatchSnapshot();
});
it('should generate test for vanilla js class', () => {
const component = render(
<TestGenerator
assertion={fnVanillaTemplate.assertion} dispatcher={fnVanillaTemplate.dispatcher}
wrap={fnVanillaTemplate.wrap}
actions={actions} computedStates={computedStates} selectedActionId={1}
isVanilla name="SomeStore" useCodemirror={false}
/>
);
expect(renderToJson(component)).toMatchSnapshot();
});
it('should generate test for vanilla js class with string template', () => {
const component = render(
<TestGenerator
assertion={strVanillaTemplate.assertion} dispatcher={strVanillaTemplate.dispatcher}
wrap={strVanillaTemplate.wrap}
actions={actions} computedStates={computedStates} selectedActionId={1}
isVanilla name="SomeStore" useCodemirror={false}
/>
);
expect(renderToJson(component)).toMatchSnapshot();
});
});

View File

@ -0,0 +1,103 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TestGenerator component should be empty when no actions provided 1`] = `
<textarea
style="padding:10px;width:100%;height:100%;"
/>
`;
exports[`TestGenerator component should generate test for the last action when selectedActionId not specified 1`] = `
<textarea
style="padding:10px;width:100%;height:100%;"
>
import expect from 'expect';
import reducers from '../../reducers';
describe('reducers', () =&gt; {
it('should handle actions', () =&gt; {
let state;
state = reducers({counter:0}, {type:'INCREMENT_COUNTER'});
expect(state).toEqual({counter:1});
});
});
</textarea>
`;
exports[`TestGenerator component should generate test for vanilla js class 1`] = `
<textarea
style="padding:10px;width:100%;height:100%;"
>
import expect from 'expect';
import SomeStore from '../../stores/SomeStore';
describe('SomeStore', () =&gt; {
it('INCREMENT_COUNTER', () =&gt; {
const store = new SomeStore({counter:0});
INCREMENT_COUNTER();
expect(store.counter).toEqual(1);
});
});
</textarea>
`;
exports[`TestGenerator component should generate test for vanilla js class with string template 1`] = `
<textarea
style="padding:10px;width:100%;height:100%;"
>
import expect from 'expect';
import SomeStore from '../../stores/SomeStore';
describe('SomeStore', () =&gt; {
it('INCREMENT_COUNTER', () =&gt; {
const store = new SomeStore({counter:0});
INCREMENT_COUNTER();
expect(store.counter).toEqual(1);
});
});
</textarea>
`;
exports[`TestGenerator component should match function template's test for first action 1`] = `
<textarea
style="padding:10px;width:100%;height:100%;"
>
import expect from 'expect';
import reducers from '../../reducers';
describe('reducers', () =&gt; {
it('should handle actions', () =&gt; {
let state;
state = reducers({counter:0}, {type:'INCREMENT_COUNTER'});
expect(state).toEqual({counter:1});
});
});
</textarea>
`;
exports[`TestGenerator component should match string template's test for first action 1`] = `
<textarea
style="padding:10px;width:100%;height:100%;"
>
import expect from 'expect';
import reducers from '../../reducers';
describe('reducers', () =&gt; {
it('should handle actions', () =&gt; {
let state;
state = reducers({counter:0}, {type:'INCREMENT_COUNTER'});
expect(state).toEqual({counter:1});
});
});
</textarea>
`;
exports[`TestGenerator component should show warning message when no params provided 1`] = `
<textarea
style="padding:10px;width:100%;height:100%;"
/>
`;

View File

@ -0,0 +1,85 @@
import { assertion } from '../src/vanilla/mocha';
import { compare } from '../src/TestGenerator';
const computedStates = [
{ state: { o1: 0 } },
{ state: { o1: 0, o2: 1 } },
{ state: { o1: 0, o2: 'a' } },
{ state: { o1: [{ t: 1 }], o3: { t: 2 } } },
{ state: { o1: [{ t: 3 }], o3: { t: 2 } } },
{ state: [0, 1, 2, 3, 4] },
{ state: [0, 3] },
{ state: [0, 2, 3, 4] }
];
const test = (s1, s2) => compare(s1, s2,
({ path, curState }) => (
expect(`expect(store${path}).toEqual(${curState});`)
.toBe(assertion({ path, curState }))
)
);
describe('Assertions', () => {
it('should return initial state', () => {
test(
undefined,
computedStates[0]
);
});
it('should add element', () => {
test(
computedStates[0],
computedStates[1]
);
});
it('should remove element', () => {
test(
computedStates[1],
computedStates[0]
);
});
it('should change element', () => {
test(
computedStates[1],
computedStates[2]
);
});
it('should add, change and remove elements', () => {
test(
computedStates[2],
computedStates[3]
);
});
it('should change in array', () => {
test(
computedStates[3],
computedStates[4]
);
});
it('should remove elements in array', () => {
test(
computedStates[5],
computedStates[6]
);
});
it('should add elements in array', () => {
test(
computedStates[6],
computedStates[5]
);
});
it('should add and change elements in array', () => {
test(
computedStates[5],
computedStates[7]
);
});
});

View File

@ -0,0 +1,84 @@
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const ExportFilesWebpackPlugin = require('export-files-webpack-plugin');
const NyanProgressWebpackPlugin = require('nyan-progress-webpack-plugin');
const pkg = require('./package.json');
const isProduction = process.env.NODE_ENV === 'production';
module.exports = {
devtool: 'eval',
entry: isProduction ?
['./demo/src/js/index'] :
[
'webpack-dev-server/client?http://localhost:3000',
'webpack/hot/only-dev-server',
'./demo/src/js/index'
],
output: {
path: path.join(__dirname, 'demo/dist'),
filename: 'js/bundle.js'
},
plugins: [
new CleanWebpackPlugin(isProduction ? ['demo/dist'] : []),
new HtmlWebpackPlugin({
inject: true,
template: 'demo/src/index.html',
filename: 'index.html',
package: pkg
}),
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV)
},
}),
new NyanProgressWebpackPlugin()
].concat(isProduction ? [
new webpack.optimize.UglifyJsPlugin({
compress: { warnings: false },
output: { comments: false }
})
] : [
new ExportFilesWebpackPlugin('demo/dist/index.html'),
new webpack.HotModuleReplacementPlugin()
]),
resolve: {
extensions: ['.js', '.jsx']
},
module: {
rules: [{
test: /\.jsx?$/,
loader: 'babel-loader',
include: [
path.join(__dirname, 'src'),
path.join(__dirname, 'demo/src/js')
]
},
{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' }
]
},
{
test: /\.(ttf|eot|svg|woff|woff2)$/,
loader: 'file-loader',
options: { outputPath: 'fonts/', publicPath: 'fonts/' }
}
]
},
devServer: isProduction ? null : {
quiet: false,
port: 3001,
hot: true,
stats: {
chunkModules: false,
colors: true
},
historyApiFallback: true
}
};

View File

@ -20,7 +20,7 @@
"lint:fix": "eslint --fix src test",
"test": "jest --no-cache",
"prepare": "npm run clean && npm run build",
"prepublishOnly": "npm run lint && npm run test"
"prepublishOnly": "npm run lint && npm run test && npm run clean && npm run build"
},
"devDependencies": {
"babel-cli": "^6.10.1",

1161
yarn.lock

File diff suppressed because it is too large Load Diff