feature(redux-devtools-test-generator): convert to TypeScript (#634)

* stash

* stash

* compiles

* Tests

* finish

* fix packages

* somemore

* update snapshot
This commit is contained in:
Nathan Bierema 2020-09-13 00:02:24 -04:00 committed by GitHub
parent d1c222d847
commit d49535da03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 998 additions and 602 deletions

View File

@ -18,12 +18,14 @@
"babel-loader": "^8.1.0",
"clean-webpack-plugin": "^3.0.0",
"cross-env": "^7.0.2",
"css-loader": "^4.2.1",
"eslint": "^7.6.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-babel": "^5.3.1",
"eslint-plugin-jest": "^23.20.0",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.20.5",
"file-loader": "^6.0.0",
"fork-ts-checker-webpack-plugin": "^5.1.0",
"html-webpack-plugin": "^4.3.0",
"jest": "^26.2.2",

View File

@ -36,6 +36,14 @@
"prepublishOnly": "npm run clean && npm run build"
},
"dependencies": {
"@types/base16": "^1.0.2",
"@types/codemirror": "^0.0.97",
"@types/prop-types": "^15.7.3",
"@types/react-jsonschema-form": "^1.7.4",
"@types/react-select": "^3.0.19",
"@types/redux-devtools-themes": "^1.0.0",
"@types/simple-element-resize-detector": "^1.3.0",
"@types/styled-components": "^5.1.2",
"base16": "^1.0.0",
"codemirror": "^5.56.0",
"color": "^3.1.2",
@ -51,11 +59,8 @@
"devDependencies": {
"@storybook/addon-essentials": "^6.0.21",
"@storybook/react": "^6.0.21",
"@types/codemirror": "^0.0.97",
"@types/enzyme": "^3.10.5",
"@types/enzyme-adapter-react-16": "^1.0.6",
"@types/react-jsonschema-form": "^1.7.4",
"@types/react-select": "^3.0.19",
"csstype": "^3.0.2",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.3",

View File

@ -44,13 +44,13 @@ function isForm<P>(rest?: FormProps<P>): rest is FormProps<P> {
}
export default class Dialog<P> extends (PureComponent || Component)<
DialogProps | (DialogProps & FormProps<P>)
DialogProps | (Omit<DialogProps, 'onSubmit'> & FormProps<P>)
> {
submitButton?: HTMLInputElement | null;
onSubmit = () => {
if (this.submitButton) this.submitButton.click();
else this.props.onSubmit();
else (this.props.onSubmit as () => void)();
};
getFormButtonRef: React.RefCallback<HTMLInputElement> = (node) => {

View File

@ -23,7 +23,7 @@ export interface EditorProps {
theme?: Base16Theme;
foldGutter: boolean;
autofocus: boolean;
onChange: (value: string, change: CodeMirror.EditorChangeLinkedList) => void;
onChange?: (value: string, change: CodeMirror.EditorChangeLinkedList) => void;
}
/**
@ -47,7 +47,7 @@ export default class Editor extends Component<EditorProps> {
if (this.props.onChange) {
this.cm.on('change', (doc, change) => {
this.props.onChange(doc.getValue(), change);
this.props.onChange!(doc.getValue(), change);
});
}
}

View File

@ -1,6 +0,0 @@
declare module 'simple-element-resize-detector' {
export default function (
element: HTMLElement,
handler: () => void
): HTMLIFrameElement;
}

View File

@ -25,14 +25,7 @@ describe('Editor', function () {
return range;
};
const wrapper = mount(
<Editor
value="var a = 1;"
onChange={() => {
//noop
}}
/>
);
const wrapper = mount(<Editor value="var a = 1;" />);
it('renders correctly', () => {
expect(mountToJson(wrapper)).toMatchSnapshot();

View File

@ -7,7 +7,6 @@ exports[`Editor renders correctly 1`] = `
lineNumbers={true}
lineWrapping={false}
mode="javascript"
onChange={[Function]}
readOnly={false}
value="var a = 1;"
>

View File

@ -2,10 +2,10 @@ import React from 'react';
import { connect } from 'react-redux';
import { createDevTools } from 'redux-devtools';
import DockMonitor from 'redux-devtools-dock-monitor';
import { Location } from 'history';
import DevtoolsInspector from '../../../src/DevtoolsInspector';
import getOptions from './getOptions';
import { base16Themes } from '../../../src/utils/createStylingFromTheme';
import { Location } from 'history';
import { DemoAppState } from './reducers';
const CustomComponent = () => (

View File

@ -194,7 +194,7 @@ const createRootReducer = (
): Reducer<DemoAppState, DemoAppAction> =>
combineReducers<DemoAppState, DemoAppAction>({
router: connectRouter(history) as Reducer<RouterState, DemoAppAction>,
timeoutUpdateEnabled: (state = false, action: DemoAppAction) =>
timeoutUpdateEnabled: (state = false, action) =>
action.type === 'TOGGLE_TIMEOUT_UPDATE'
? action.timeoutUpdateEnabled
: state,
@ -207,7 +207,7 @@ const createRootReducer = (
// noop
}
) => state,
array: (state = [], action: DemoAppAction) =>
array: (state = [], action) =>
action.type === 'PUSH'
? [...state, Math.random()]
: action.type === 'POP'
@ -215,13 +215,13 @@ const createRootReducer = (
: action.type === 'REPLACE'
? [Math.random(), ...state.slice(1)]
: state,
hugeArrays: (state = [], action: DemoAppAction) =>
hugeArrays: (state = [], action) =>
action.type === 'PUSH_HUGE_ARRAY' ? [...state, ...HUGE_ARRAY] : state,
hugeObjects: (state = [], action: DemoAppAction) =>
hugeObjects: (state = [], action) =>
action.type === 'ADD_HUGE_OBJECT' ? [...state, HUGE_OBJECT] : state,
iterators: (state = [], action: DemoAppAction) =>
iterators: (state = [], action) =>
action.type === 'ADD_ITERATOR' ? [...state, createIterator()] : state,
nested: (state = NESTED, action: DemoAppAction) =>
nested: (state = NESTED, action) =>
action.type === 'CHANGE_NESTED'
? {
...state,
@ -238,29 +238,26 @@ const createRootReducer = (
},
}
: state,
recursive: (state: { obj?: unknown }[] = [], action: DemoAppAction) =>
recursive: (state: { obj?: unknown }[] = [], action) =>
action.type === 'ADD_RECURSIVE' ? [...state, { ...RECURSIVE }] : state,
immutables: (
state: Immutable.Map<string, unknown>[] = [],
action: DemoAppAction
) =>
immutables: (state: Immutable.Map<string, unknown>[] = [], action) =>
action.type === 'ADD_IMMUTABLE_MAP' ? [...state, IMMUTABLE_MAP] : state,
maps: (state: Map<string, MapValue>[] = [], action: DemoAppAction) =>
maps: (state: Map<string, MapValue>[] = [], action) =>
action.type === 'ADD_NATIVE_MAP' ? [...state, NATIVE_MAP] : state,
immutableNested: (state = IMMUTABLE_NESTED, action: DemoAppAction) =>
immutableNested: (state = IMMUTABLE_NESTED, action) =>
action.type === 'CHANGE_IMMUTABLE_NESTED'
? state.updateIn(
['long', 'nested', 0, 'path', 'to', 'a'],
(str: string) => str + '!'
)
: state,
addFunction: (state = null, action: DemoAppAction) =>
addFunction: (state = null, action) =>
action.type === 'ADD_FUNCTION' ? { f: FUNC } : state,
addSymbol: (state = null, action: DemoAppAction) =>
addSymbol: (state = null, action) =>
action.type === 'ADD_SYMBOL'
? { s: window.Symbol('symbol'), error: new Error('TEST') }
: state,
shuffleArray: (state = DEFAULT_SHUFFLE_ARRAY, action: DemoAppAction) =>
shuffleArray: (state = DEFAULT_SHUFFLE_ARRAY, action) =>
action.type === 'SHUFFLE_ARRAY' ? shuffle(state) : state,
});

View File

@ -53,11 +53,9 @@
"redux-devtools-themes": "^1.0.0"
},
"devDependencies": {
"@babel/runtime": "^7.11.2",
"@types/dateformat": "^3.0.1",
"@types/hex-rgba": "^1.0.0",
"@types/history": "^4.7.7",
"@types/lodash.debounce": "^4.0.6",
"@types/lodash.shuffle": "^4.2.6",
"@types/react": "^16.9.46",
"@types/react-dragula": "^1.1.0",
@ -67,7 +65,6 @@
"connected-react-router": "^6.8.0",
"history": "^4.10.1",
"immutable": "^4.0.0-rc.12",
"lodash.isequalwith": "^4.4.0",
"lodash.shuffle": "^4.2.0",
"react": "^16.13.1",
"react-bootstrap": "^1.3.0",

View File

@ -1,2 +1,5 @@
import DevtoolsInspector from './DevtoolsInspector';
export default DevtoolsInspector;
export { Tab, TabComponentProps } from './ActionPreview';
export { DevtoolsInspectorState } from './redux';
export { base16Themes } from './utils/createStylingFromTheme';

View File

@ -1,7 +1,11 @@
{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"presets": [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript"
],
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-do-expressions"
"@babel/plugin-transform-runtime"
]
}

View File

@ -0,0 +1 @@
lib

View File

@ -0,0 +1,37 @@
module.exports = {
extends: '../../.eslintrc',
overrides: [
{
files: ['*.ts', '*.tsx'],
extends: '../../eslintrc.ts.react.base.json',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
},
{
files: ['demo/**/*.ts', 'demo/**/*.tsx'],
extends: '../../eslintrc.ts.react.base.json',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./demo/tsconfig.json'],
},
},
{
files: ['demo/config/webpack.config.ts'],
extends: '../../eslintrc.ts.base.json',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./demo/config/tsconfig.json'],
},
},
{
files: ['test/*.ts', 'test/*.tsx'],
extends: '../../eslintrc.ts.react.jest.base.json',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./test/tsconfig.json'],
},
},
],
};

View File

@ -29,7 +29,7 @@ const testComponent = (props) => (
);
export default createDevTools(
<Inspector
<InspectorMonitor
tabs: defaultTabs => [...defaultTabs, { name: 'Test', component: testComponent }]
/>
);

View File

@ -0,0 +1,7 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"resolveJsonModule": true
},
"include": ["webpack.config.ts"]
}

View File

@ -1,14 +1,14 @@
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const pkg = require('./package.json');
import * as path from 'path';
import * as webpack from 'webpack';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import pkg from '../../package.json';
const isProduction = process.env.NODE_ENV === 'production';
module.exports = {
devtool: 'eval-source-map',
mode: process.env.NODE_ENV || 'development',
entry: isProduction
? ['./demo/src/js/index']
: [
@ -20,39 +20,14 @@ module.exports = {
path: path.join(__dirname, 'demo/dist'),
filename: 'js/bundle.js',
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
inject: true,
template: 'demo/src/index.html',
package: pkg,
}),
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV),
},
}),
].concat(
isProduction
? [
new webpack.optimize.UglifyJsPlugin({
compress: { warnings: false },
output: { comments: false },
}),
]
: [new webpack.HotModuleReplacementPlugin()]
),
resolve: {
extensions: ['.js', '.jsx'],
},
module: {
rules: [
{
test: /\.jsx?$/,
test: /\.(js|ts)x?$/,
loader: 'babel-loader',
include: [
path.join(__dirname, 'src'),
path.join(__dirname, 'demo/src/js'),
path.join(__dirname, '../../src'),
path.join(__dirname, '../src/js'),
],
},
{
@ -66,6 +41,22 @@ module.exports = {
},
],
},
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
inject: true,
template: 'demo/src/index.html',
package: pkg,
}),
new ForkTsCheckerWebpackPlugin({
typescript: {
configFile: 'demo/tsconfig.json',
},
}),
].concat(isProduction ? [] : [new webpack.HotModuleReplacementPlugin()]),
devServer: isProduction
? null
: {
@ -78,4 +69,5 @@ module.exports = {
},
historyApiFallback: true,
},
devtool: 'eval-source-map',
};

View File

@ -1,11 +1,35 @@
import React from 'react';
import React, { CSSProperties } from 'react';
import { connect } from 'react-redux';
import pkg from '../../../package.json';
import { Button, Toolbar, Spacer } from 'devui';
import getOptions from './getOptions';
import { push as pushRoute } from 'connected-react-router';
import pkg from '../../../package.json';
import getOptions from './getOptions';
import { DemoAppState } from './reducers';
import {
AddFunctionAction,
AddHugeObjectAction,
AddImmutableMapAction,
AddIteratorAction,
AddRecursiveAction,
AddSymbolAction,
ChangeImmutableNestedAction,
ChangeNestedAction,
HugePayloadAction,
IncrementAction,
PopAction,
PushAction,
PushHugeArrayAction,
ReplaceAction,
ShuffleArrayAction,
TimeoutUpdateAction,
ToggleTimeoutUpdateAction,
} from './reducers';
const styles = {
const styles: {
wrapper: CSSProperties;
muted: CSSProperties;
link: CSSProperties;
} = {
wrapper: {
height: '100vh',
width: '450px',
@ -24,7 +48,30 @@ const styles = {
const ROOT = '/'; // process.env.NODE_ENV === 'production' ? '/' : '/';
class DemoApp extends React.Component {
interface Props
extends Omit<DemoAppState, 'addFunction' | 'addSymbol' | 'shuffleArray'> {
toggleTimeoutUpdate: (timeoutUpdateEnabled: boolean) => void;
timeoutUpdate: () => void;
increment: () => void;
push: () => void;
pop: () => void;
replace: () => void;
changeNested: () => void;
pushHugeArray: () => void;
addIterator: () => void;
addHugeObject: () => void;
addRecursive: () => void;
addImmutableMap: () => void;
changeImmutableNested: () => void;
hugePayload: () => void;
addFunction: () => void;
addSymbol: () => void;
shuffleArray: () => void;
}
class DemoApp extends React.Component<Props> {
timeout?: number;
render() {
const options = getOptions(this.props.router.location);
@ -48,7 +95,7 @@ class DemoApp extends React.Component {
<Toolbar>
<Spacer />
<Button onClick={this.props.pushHugeArray}>Push Huge Array</Button>
<Button onClick={this.props.addHugeObect}>Add Huge Object</Button>
<Button onClick={this.props.addHugeObject}>Add Huge Object</Button>
<Button onClick={this.props.hugePayload}>Huge Payload</Button>
<Spacer />
</Toolbar>
@ -98,36 +145,40 @@ class DemoApp extends React.Component {
this.props.toggleTimeoutUpdate(enabled);
if (enabled) {
this.timeout = setInterval(this.props.timeoutUpdate, 1000);
this.timeout = window.setInterval(this.props.timeoutUpdate, 1000);
} else {
clearTimeout(this.timeout);
}
};
}
export default connect((state) => state, {
toggleTimeoutUpdate: (timeoutUpdateEnabled) => ({
export default connect((state: DemoAppState) => state, {
toggleTimeoutUpdate: (
timeoutUpdateEnabled: boolean
): ToggleTimeoutUpdateAction => ({
type: 'TOGGLE_TIMEOUT_UPDATE',
timeoutUpdateEnabled,
}),
timeoutUpdate: () => ({ type: 'TIMEOUT_UPDATE' }),
increment: () => ({ type: 'INCREMENT' }),
push: () => ({ type: 'PUSH' }),
pop: () => ({ type: 'POP' }),
replace: () => ({ type: 'REPLACE' }),
changeNested: () => ({ type: 'CHANGE_NESTED' }),
pushHugeArray: () => ({ type: 'PUSH_HUGE_ARRAY' }),
addIterator: () => ({ type: 'ADD_ITERATOR' }),
addHugeObect: () => ({ type: 'ADD_HUGE_OBJECT' }),
addRecursive: () => ({ type: 'ADD_RECURSIVE' }),
addImmutableMap: () => ({ type: 'ADD_IMMUTABLE_MAP' }),
changeImmutableNested: () => ({ type: 'CHANGE_IMMUTABLE_NESTED' }),
hugePayload: () => ({
timeoutUpdate: (): TimeoutUpdateAction => ({ type: 'TIMEOUT_UPDATE' }),
increment: (): IncrementAction => ({ type: 'INCREMENT' }),
push: (): PushAction => ({ type: 'PUSH' }),
pop: (): PopAction => ({ type: 'POP' }),
replace: (): ReplaceAction => ({ type: 'REPLACE' }),
changeNested: (): ChangeNestedAction => ({ type: 'CHANGE_NESTED' }),
pushHugeArray: (): PushHugeArrayAction => ({ type: 'PUSH_HUGE_ARRAY' }),
addIterator: (): AddIteratorAction => ({ type: 'ADD_ITERATOR' }),
addHugeObject: (): AddHugeObjectAction => ({ type: 'ADD_HUGE_OBJECT' }),
addRecursive: (): AddRecursiveAction => ({ type: 'ADD_RECURSIVE' }),
addImmutableMap: (): AddImmutableMapAction => ({ type: 'ADD_IMMUTABLE_MAP' }),
changeImmutableNested: (): ChangeImmutableNestedAction => ({
type: 'CHANGE_IMMUTABLE_NESTED',
}),
hugePayload: (): HugePayloadAction => ({
type: 'HUGE_PAYLOAD',
payload: Array.from({ length: 10000 }).map((_, i) => i),
}),
addFunction: () => ({ type: 'ADD_FUNCTION' }),
addSymbol: () => ({ type: 'ADD_SYMBOL' }),
shuffleArray: () => ({ type: 'SHUFFLE_ARRAY' }),
addFunction: (): AddFunctionAction => ({ type: 'ADD_FUNCTION' }),
addSymbol: (): AddSymbolAction => ({ type: 'ADD_SYMBOL' }),
shuffleArray: (): ShuffleArrayAction => ({ type: 'SHUFFLE_ARRAY' }),
pushRoute,
})(DemoApp);

View File

@ -1,12 +1,18 @@
import React from 'react';
import { connect } from 'react-redux';
import { createDevTools } from 'redux-devtools';
import InspectorMonitor from 'redux-devtools-inspector-monitor';
import InspectorMonitor, {
base16Themes,
Tab,
} from 'redux-devtools-inspector-monitor';
import DockMonitor from 'redux-devtools-dock-monitor';
import { Location } from 'history';
import getOptions from './getOptions';
import TestGenerator from '../../../src';
import { DemoAppState } from './reducers';
import { Action } from 'redux';
export const getDevTools = (location) =>
export const getDevTools = (location: { search: string }) =>
createDevTools(
<DockMonitor
defaultIsVisible
@ -15,27 +21,28 @@ export const getDevTools = (location) =>
changeMonitorKey="ctrl-m"
>
<InspectorMonitor
theme={getOptions(location).theme}
shouldPersistState
theme={getOptions(location).theme as keyof typeof base16Themes}
invertTheme={!getOptions(location).dark}
supportImmutable={getOptions(location).supportImmutable}
tabs={(defaultTabs) => [
tabs={(defaultTabs) =>
[
{
name: 'Test',
component: TestGenerator,
},
...defaultTabs,
]}
] as Tab<unknown, Action<unknown>>[]
}
/>
</DockMonitor>
);
const UnconnectedDevTools = ({ location }) => {
const UnconnectedDevTools = ({ location }: { location: Location }) => {
const DevTools = getDevTools(location);
return <DevTools />;
};
const mapStateToProps = (state) => ({
const mapStateToProps = (state: DemoAppState) => ({
location: state.router.location,
});

View File

@ -1,11 +0,0 @@
export default function getOptions(location) {
return {
useExtension: location.search.indexOf('ext') !== -1,
supportImmutable: location.search.indexOf('immutable') !== -1,
theme: do {
const match = location.search.match(/theme=([^&]+)/);
match ? match[1] : 'inspector';
},
dark: location.search.indexOf('dark') !== -1,
};
}

View File

@ -0,0 +1,20 @@
export interface Options {
useExtension: boolean;
supportImmutable: boolean;
theme: string;
dark: boolean;
}
export default function getOptions(location: { search: string }) {
return {
useExtension: location.search.indexOf('ext') !== -1,
supportImmutable: location.search.indexOf('immutable') !== -1,
theme: getTheme(),
dark: location.search.indexOf('dark') !== -1,
};
}
function getTheme() {
const match = /theme=([^&]+)/.exec(location.search);
return match ? match[1] : 'inspector';
}

View File

@ -1,22 +1,27 @@
import '@babel/polyfill';
import 'devui/lib/presets';
import React from 'react';
import { render } from 'react-dom';
import { Container } from 'devui';
import DemoApp from './DemoApp';
import { Provider } from 'react-redux';
import createRootReducer from './reducers';
import { createStore, applyMiddleware, compose } from 'redux';
import {
createStore,
applyMiddleware,
compose,
StoreEnhancer,
StoreEnhancerStoreCreator,
} from 'redux';
import logger from 'redux-logger';
import { Route } from 'react-router';
import { createBrowserHistory } from 'history';
import { ConnectedRouter, routerMiddleware } from 'connected-react-router';
import { persistState } from 'redux-devtools';
import DemoApp from './DemoApp';
import createRootReducer from './reducers';
import getOptions from './getOptions';
import { ConnectedDevTools, getDevTools } from './DevTools';
function getDebugSessionKey() {
const matches = window.location.href.match(/[?&]debug_session=([^&#]+)\b/);
const matches = /[?&]debug_session=([^&#]+)\b/.exec(window.location.href);
return matches && matches.length > 0 ? matches[1] : null;
}
@ -30,21 +35,23 @@ const DevTools = getDevTools(window.location);
const history = createBrowserHistory();
const useDevtoolsExtension =
!!window.__REDUX_DEVTOOLS_EXTENSION__ &&
!!((window as unknown) as { __REDUX_DEVTOOLS_EXTENSION__: unknown }) &&
getOptions(window.location).useExtension;
const enhancer = compose(
applyMiddleware(logger, routerMiddleware(history)),
(...args) => {
(next: StoreEnhancerStoreCreator) => {
const instrument = useDevtoolsExtension
? window.__REDUX_DEVTOOLS_EXTENSION__()
? ((window as unknown) as {
__REDUX_DEVTOOLS_EXTENSION__(): StoreEnhancer;
}).__REDUX_DEVTOOLS_EXTENSION__()
: DevTools.instrument();
return instrument(...args);
return instrument(next);
},
persistState(getDebugSessionKey())
);
const store = createStore(createRootReducer(history), {}, enhancer);
const store = createStore(createRootReducer(history), enhancer);
render(
<Provider store={store}>

View File

@ -1,125 +0,0 @@
import Immutable from 'immutable';
import shuffle from 'lodash.shuffle';
import { combineReducers } from 'redux';
import { connectRouter } from 'connected-react-router';
const NESTED = {
long: {
nested: [
{
path: {
to: {
a: 'key',
},
},
},
],
},
};
const IMMUTABLE_NESTED = Immutable.fromJS(NESTED);
/* eslint-disable babel/new-cap */
const IMMUTABLE_MAP = Immutable.Map({
map: Immutable.Map({ a: 1, b: 2, c: 3 }),
list: Immutable.List(['a', 'b', 'c']),
set: Immutable.Set(['a', 'b', 'c']),
stack: Immutable.Stack(['a', 'b', 'c']),
seq: Immutable.Seq([1, 2, 3, 4, 5, 6, 7, 8]),
});
/* eslint-enable babel/new-cap */
const HUGE_ARRAY = Array.from({ length: 5000 }).map((_, key) => ({
str: 'key ' + key,
}));
const HUGE_OBJECT = Array.from({ length: 5000 }).reduce(
(o, _, key) => ((o['key ' + key] = 'item ' + key), o),
{}
);
const FUNC = function (a, b, c) {
return a + b + c;
};
const RECURSIVE = {};
RECURSIVE.obj = RECURSIVE;
function createIterator() {
const iterable = {};
iterable[window.Symbol.iterator] = function* iterator() {
for (var i = 0; i < 333; i++) {
yield 'item ' + i;
}
};
return iterable;
}
const DEFAULT_SHUFFLE_ARRAY = [0, 1, null, { id: 1 }, { id: 2 }, 'string'];
const createRootReducer = (history) =>
combineReducers({
router: connectRouter(history),
timeoutUpdateEnabled: (state = false, action) =>
action.type === 'TOGGLE_TIMEOUT_UPDATE'
? action.timeoutUpdateEnabled
: state,
store: (state = 0, action) =>
action.type === 'INCREMENT' ? state + 1 : state,
undefined: (state = { val: undefined }) => state,
null: (state = null) => state,
func: (state = () => {}) => state,
array: (state = [], action) =>
action.type === 'PUSH'
? [...state, Math.random()]
: action.type === 'POP'
? state.slice(0, state.length - 1)
: action.type === 'REPLACE'
? [Math.random(), ...state.slice(1)]
: state,
hugeArrays: (state = [], action) =>
action.type === 'PUSH_HUGE_ARRAY' ? [...state, ...HUGE_ARRAY] : state,
hugeObjects: (state = [], action) =>
action.type === 'ADD_HUGE_OBJECT' ? [...state, HUGE_OBJECT] : state,
iterators: (state = [], action) =>
action.type === 'ADD_ITERATOR' ? [...state, createIterator()] : state,
nested: (state = NESTED, action) =>
action.type === 'CHANGE_NESTED'
? {
...state,
long: {
nested: [
{
path: {
to: {
a: state.long.nested[0].path.to.a + '!',
},
},
},
],
},
}
: state,
recursive: (state = [], action) =>
action.type === 'ADD_RECURSIVE' ? [...state, { ...RECURSIVE }] : state,
immutables: (state = [], action) =>
action.type === 'ADD_IMMUTABLE_MAP' ? [...state, IMMUTABLE_MAP] : state,
immutableNested: (state = IMMUTABLE_NESTED, action) =>
action.type === 'CHANGE_IMMUTABLE_NESTED'
? state.updateIn(
['long', 'nested', 0, 'path', 'to', 'a'],
(str) => str + '!'
)
: state,
addFunction: (state = null, action) =>
action.type === 'ADD_FUNCTION' ? { f: FUNC } : state,
addSymbol: (state = null, action) =>
action.type === 'ADD_SYMBOL' ? { s: window.Symbol('symbol') } : state,
shuffleArray: (state = DEFAULT_SHUFFLE_ARRAY, action) =>
action.type === 'SHUFFLE_ARRAY' ? shuffle(state) : state,
});
export default createRootReducer;

View File

@ -0,0 +1,232 @@
import Immutable from 'immutable';
import shuffle from 'lodash.shuffle';
import { combineReducers, Reducer } from 'redux';
import {
connectRouter,
LocationChangeAction,
RouterState,
} from 'connected-react-router';
import { History } from 'history';
type Nested = { long: { nested: { path: { to: { a: string } } }[] } };
const NESTED = {
long: {
nested: [
{
path: {
to: {
a: 'key',
},
},
},
],
},
};
const IMMUTABLE_NESTED = Immutable.fromJS(NESTED);
const IMMUTABLE_MAP = Immutable.Map({
map: Immutable.Map({ a: 1, b: 2, c: 3 }),
list: Immutable.List(['a', 'b', 'c']),
set: Immutable.Set(['a', 'b', 'c']),
stack: Immutable.Stack(['a', 'b', 'c']),
seq: Immutable.Seq([1, 2, 3, 4, 5, 6, 7, 8]),
});
const HUGE_ARRAY = Array.from({ length: 5000 }).map((_, key) => ({
str: `key ${key}`,
}));
const HUGE_OBJECT = Array.from({ length: 5000 }).reduce(
(o: { [key: string]: string }, _, key) => (
(o[`key ${key}`] = `item ${key}`), o
),
{}
);
const FUNC = function (a: number, b: number, c: number) {
return a + b + c;
};
const RECURSIVE: { obj?: unknown } = {};
RECURSIVE.obj = RECURSIVE;
function createIterator() {
const iterable: { [Symbol.iterator](): IterableIterator<string> } = {
[Symbol.iterator]: function* iterator() {
for (let i = 0; i < 333; i++) {
yield `item ${i}`;
}
},
};
return iterable;
}
const DEFAULT_SHUFFLE_ARRAY = [0, 1, null, { id: 1 }, { id: 2 }, 'string'];
export interface ToggleTimeoutUpdateAction {
type: 'TOGGLE_TIMEOUT_UPDATE';
timeoutUpdateEnabled: boolean;
}
export interface TimeoutUpdateAction {
type: 'TIMEOUT_UPDATE';
}
export interface IncrementAction {
type: 'INCREMENT';
}
export interface PushAction {
type: 'PUSH';
}
export interface PopAction {
type: 'POP';
}
export interface ReplaceAction {
type: 'REPLACE';
}
export interface ChangeNestedAction {
type: 'CHANGE_NESTED';
}
export interface PushHugeArrayAction {
type: 'PUSH_HUGE_ARRAY';
}
export interface AddIteratorAction {
type: 'ADD_ITERATOR';
}
export interface AddHugeObjectAction {
type: 'ADD_HUGE_OBJECT';
}
export interface AddRecursiveAction {
type: 'ADD_RECURSIVE';
}
export interface AddImmutableMapAction {
type: 'ADD_IMMUTABLE_MAP';
}
export interface ChangeImmutableNestedAction {
type: 'CHANGE_IMMUTABLE_NESTED';
}
export interface HugePayloadAction {
type: 'HUGE_PAYLOAD';
payload: number[];
}
export interface AddFunctionAction {
type: 'ADD_FUNCTION';
}
export interface AddSymbolAction {
type: 'ADD_SYMBOL';
}
export interface ShuffleArrayAction {
type: 'SHUFFLE_ARRAY';
}
type DemoAppAction =
| ToggleTimeoutUpdateAction
| TimeoutUpdateAction
| IncrementAction
| PushAction
| PopAction
| ReplaceAction
| ChangeNestedAction
| PushHugeArrayAction
| AddIteratorAction
| AddHugeObjectAction
| AddRecursiveAction
| AddImmutableMapAction
| ChangeImmutableNestedAction
| HugePayloadAction
| AddFunctionAction
| AddSymbolAction
| ShuffleArrayAction
| LocationChangeAction;
export interface DemoAppState {
router: RouterState;
timeoutUpdateEnabled: boolean;
store: number;
undefined: { val: undefined };
null: null;
func: () => void;
array: number[];
hugeArrays: { str: string }[];
hugeObjects: { [key: string]: string }[];
iterators: { [Symbol.iterator](): IterableIterator<string> }[];
nested: Nested;
recursive: { obj?: unknown }[];
immutables: Immutable.Map<string, unknown>[];
immutableNested: Immutable.Map<unknown, unknown>;
addFunction: { f: (a: number, b: number, c: number) => number } | null;
addSymbol: { s: symbol; error: Error } | null;
shuffleArray: unknown[];
}
const createRootReducer = (
history: History
): Reducer<DemoAppState, DemoAppAction> =>
combineReducers<DemoAppState, DemoAppAction>({
router: connectRouter(history) as Reducer<RouterState, DemoAppAction>,
timeoutUpdateEnabled: (state = false, action) =>
action.type === 'TOGGLE_TIMEOUT_UPDATE'
? action.timeoutUpdateEnabled
: state,
store: (state = 0, action) =>
action.type === 'INCREMENT' ? state + 1 : state,
undefined: (state = { val: undefined }) => state,
null: (state = null) => state,
func: (
state = () => {
// noop
}
) => state,
array: (state = [], action) =>
action.type === 'PUSH'
? [...state, Math.random()]
: action.type === 'POP'
? state.slice(0, state.length - 1)
: action.type === 'REPLACE'
? [Math.random(), ...state.slice(1)]
: state,
hugeArrays: (state = [], action) =>
action.type === 'PUSH_HUGE_ARRAY' ? [...state, ...HUGE_ARRAY] : state,
hugeObjects: (state = [], action) =>
action.type === 'ADD_HUGE_OBJECT' ? [...state, HUGE_OBJECT] : state,
iterators: (state = [], action) =>
action.type === 'ADD_ITERATOR' ? [...state, createIterator()] : state,
nested: (state = NESTED, action) =>
action.type === 'CHANGE_NESTED'
? {
...state,
long: {
nested: [
{
path: {
to: {
a: state.long.nested[0].path.to.a + '!',
},
},
},
],
},
}
: state,
recursive: (state = [], action) =>
action.type === 'ADD_RECURSIVE' ? [...state, { ...RECURSIVE }] : state,
immutables: (state = [], action) =>
action.type === 'ADD_IMMUTABLE_MAP' ? [...state, IMMUTABLE_MAP] : state,
immutableNested: (state = IMMUTABLE_NESTED, action) =>
action.type === 'CHANGE_IMMUTABLE_NESTED'
? state.updateIn(
['long', 'nested', 0, 'path', 'to', 'a'],
(str: string) => str + '!'
)
: state,
addFunction: (state = null, action) =>
action.type === 'ADD_FUNCTION' ? { f: FUNC } : state,
addSymbol: (state = null, action) =>
action.type === 'ADD_SYMBOL'
? { s: window.Symbol('symbol'), error: new Error('TEST') }
: state,
shuffleArray: (state = DEFAULT_SHUFFLE_ARRAY, action) =>
action.type === 'SHUFFLE_ARRAY' ? shuffle(state) : state,
});
export default createRootReducer;

View File

@ -0,0 +1,7 @@
{
"extends": "../../../tsconfig.react.base.json",
"compilerOptions": {
"resolveJsonModule": true
},
"include": ["../src", "src"]
}

View File

@ -0,0 +1,4 @@
module.exports = {
preset: 'ts-jest',
setupFilesAfterEnv: ['<rootDir>/test/setup.ts'],
};

View File

@ -2,22 +2,6 @@
"name": "redux-devtools-test-generator",
"version": "0.6.2",
"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",
"test": "jest --no-cache",
"prepare": "npm run clean && npm run build",
"prepublishOnly": "npm run test && npm run clean && npm run build"
},
"repository": {
"type": "git",
"url": "https://github.com/reduxjs/redux-devtools.git"
},
"keywords": [
"redux",
"devtools",
@ -28,35 +12,64 @@
"time travel",
"live edit"
],
"author": "Mihail Diordiev <zalmoxisus@gmail.com> (https://github.com/zalmoxisus)",
"license": "MIT",
"homepage": "https://github.com/reduxjs/redux-devtools/tree/master/packages/redux-devtools-test-generator",
"bugs": {
"url": "https://github.com/reduxjs/redux-devtools/issues"
},
"homepage": "https://github.com/reduxjs/redux-devtools",
"license": "MIT",
"author": "Mihail Diordiev <zalmoxisus@gmail.com> (https://github.com/zalmoxisus)",
"files": [
"lib",
"src"
],
"main": "lib/index.js",
"types": "lib/index.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/reduxjs/redux-devtools.git"
},
"scripts": {
"start": "webpack-dev-server --config demo/config/webpack.config.ts",
"build": "npm run build:types && npm run build:js",
"build:types": "tsc --emitDeclarationOnly",
"build:js": "babel src --out-dir lib --extensions \".ts,.tsx\" --source-maps inline",
"clean": "rimraf lib",
"test": "jest",
"lint": "eslint . --ext .ts,.tsx",
"lint:fix": "eslint . --ext .ts,.tsx --fix",
"type-check": "tsc --noEmit",
"type-check:watch": "npm run type-check -- --watch",
"preversion": "npm run type-check && npm run lint && npm run test",
"prepublishOnly": "npm run clean && npm run build"
},
"dependencies": {
"@types/prop-types": "^15.7.3",
"devui": "^1.0.0-6",
"es6template": "^1.0.5",
"javascript-stringify": "^2.0.1",
"jsan": "^3.1.13",
"object-path": "^0.11.4",
"prop-types": "^15.7.2",
"react-icons": "^3.10.0",
"simple-diff": "^1.6.0"
},
"devDependencies": {
"@babel/cli": "^7.10.5",
"@babel/core": "^7.11.1",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/plugin-proposal-do-expressions": "^7.10.4",
"@babel/polyfill": "^7.10.4",
"@babel/preset-env": "^7.11.0",
"@babel/preset-react": "^7.10.4",
"babel-loader": "^8.1.0",
"clean-webpack-plugin": "^3.0.0",
"@types/history": "^4.7.7",
"@types/jsan": "^3.1.0",
"@types/lodash.shuffle": "^4.2.6",
"@types/object-path": "^0.11.0",
"@types/react": "^16.9.46",
"@types/react-router": "^5.1.8",
"@types/redux-logger": "^3.0.8",
"connected-react-router": "^6.8.0",
"css-loader": "^4.2.1",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.3",
"enzyme-to-json": "^3.5.0",
"expect": "^26.2.0",
"file-loader": "^6.0.0",
"history": "^4.10.1",
"html-webpack-plugin": "^4.3.0",
"immutable": "^4.0.0-rc.12",
"jest": "^26.2.2",
"lodash.isequalwith": "^4.4.0",
"lodash.shuffle": "^4.2.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-redux": "^7.2.1",
"react-router": "^5.2.0",
@ -64,27 +77,11 @@
"redux-devtools": "^3.7.0",
"redux-devtools-dock-monitor": "^1.2.0",
"redux-devtools-inspector-monitor": "^0.14.0",
"redux-logger": "^3.0.6",
"rimraf": "^3.0.2",
"seamless-immutable": "^7.1.4",
"style-loader": "^1.2.1",
"webpack": "^4.44.1",
"webpack-dev-server": "^3.11.0"
"redux-logger": "^3.0.6"
},
"dependencies": {
"devui": "^1.0.0-6",
"es6template": "^1.0.5",
"javascript-stringify": "^2.0.1",
"jsan": "^3.1.13",
"object-path": "^0.11.4",
"prop-types": "^15.7.2",
"react": "^16.13.1",
"react-icons": "^3.10.0",
"simple-diff": "^1.6.0"
},
"jest": {
"setupFilesAfterEnv": [
"<rootDir>/test/setup.js"
]
"peerDependencies": {
"@types/react": "^16.3.18",
"react": "^16.3.0",
"redux-devtools-inspector-monitor": "^0.14.0"
}
}

View File

@ -1,38 +1,54 @@
import React, { PureComponent, Component } from 'react';
import React, { PureComponent, Component, ReactNode } 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 diff, { Event } from 'simple-diff';
import es6template from 'es6template';
import { Editor } from 'devui';
import { TabComponentProps } from 'redux-devtools-inspector-monitor';
import { Action } from 'redux';
import { AssertionLocals, DispatcherLocals, WrapLocals } from './types';
export const fromPath = (path) =>
export const fromPath = (path: (string | number)[]) =>
path.map((a) => (typeof a === 'string' ? `.${a}` : `[${a}]`)).join('');
function getState(s, defaultValue) {
function getState<S>(
s: { state: S; error?: string } | undefined,
// eslint-disable-next-line @typescript-eslint/ban-types
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);
export function compare<S>(
s1: { state: S; error?: string } | undefined,
s2: { state: S; error?: string },
cb: (value: { path: string; curState: number | string | undefined }) => void,
// eslint-disable-next-line @typescript-eslint/ban-types
defaultValue?: {}
) {
const paths: string[] = []; // Already processed
function generate(
event: Event | { type: 'move-item'; newPath: (string | number)[] }
) {
let curState: number | string | undefined;
let path = fromPath(event.newPath);
if (type === 'remove-item' || type === 'move-item') {
if (event.type === 'remove-item' || event.type === 'move-item') {
if (paths.length && paths.indexOf(path) !== -1) return;
paths.push(path);
const v = objectPath.get(s2.state, newPath);
// eslint-disable-next-line @typescript-eslint/ban-types
const v = objectPath.get((s2.state as unknown) as object, event.newPath);
curState = v.length;
path += '.length';
} else if (type === 'add-item') {
generate({ type: 'move-item', newPath });
path += `[${newIndex}]`;
curState = stringify(newValue);
} else if (event.type === 'add-item') {
generate({ type: 'move-item', newPath: event.newPath });
path += `[${event.newIndex}]`;
curState = stringify(event.newValue);
} else {
curState = stringify(newValue);
curState = stringify(event.newValue);
}
// console.log(`expect(store${path}).toEqual(${curState});`);
@ -45,17 +61,34 @@ export function compare(s1, s2, cb, defaultValue) {
).forEach(generate);
}
export default class TestGenerator extends (PureComponent || Component) {
getMethod(action) {
let type = action.type;
interface Props<S, A extends Action<unknown>>
extends Omit<TabComponentProps<S, A>, 'monitorState' | 'updateMonitorState'> {
name?: string;
isVanilla?: boolean;
wrap?: string | ((locals: WrapLocals) => string);
dispatcher?: string | ((locals: DispatcherLocals) => string);
assertion?: string | ((locals: AssertionLocals) => string);
useCodemirror: boolean;
indentation?: number;
header?: ReactNode;
}
export default class TestGenerator<
S,
A extends Action<unknown>
> extends (PureComponent || Component)<Props<S, A>> {
getMethod(action: A) {
let type: string = action.type as string;
if (type[0] === '┗') type = type.substr(1).trim();
let args = action.arguments;
if (args) args = args.map((arg) => stringify(arg)).join(',');
else args = '';
const args = ((action as unknown) as { arguments: unknown[] }).arguments
? ((action as unknown) as { arguments: unknown[] }).arguments
.map((arg) => stringify(arg))
.join(',')
: '';
return `${type}(${args})`;
}
getAction(action) {
getAction(action: A) {
if (action.type === '@@INIT') return '{}';
return stringify(action);
}
@ -76,7 +109,7 @@ export default class TestGenerator extends (PureComponent || Component) {
if (typeof assertion === 'string')
assertion = es6template.compile(assertion);
if (typeof wrap === 'string') {
const ident = wrap.match(/\n.+\$\{assertions}/);
const ident = /\n.+\$\{assertions}/.exec(wrap);
if (ident) indentation = ident[0].length - 13;
wrap = es6template.compile(wrap);
}
@ -94,21 +127,30 @@ export default class TestGenerator extends (PureComponent || Component) {
else i = computedStates.length - 1;
const startIdx = i > 0 ? i : 1;
const addAssertions = ({ path, curState }) => {
r += space + assertion({ path, curState }) + '\n';
const addAssertions = ({
path,
curState,
}: {
path: string;
curState: number | string | undefined;
}) => {
r += `${space}${(assertion as (locals: AssertionLocals) => string)({
path,
curState,
})}\n`;
};
while (actions[i]) {
if (
!isVanilla ||
/* eslint-disable-next-line no-useless-escape */
/^┗?\s?[a-zA-Z0-9_@.\[\]-]+?$/.test(actions[i].action.type)
/^┗?\s?[a-zA-Z0-9_@.\[\]-]+?$/.test(actions[i].action.type as string)
) {
if (isFirst) isFirst = false;
else r += space;
if (!isVanilla || actions[i].action.type[0] !== '@') {
if (!isVanilla || (actions[i].action.type as string)[0] !== '@') {
r +=
dispatcher({
(dispatcher as (locals: DispatcherLocals) => string)({
action: !isVanilla
? this.getAction(actions[i].action)
: this.getMethod(actions[i].action),
@ -131,7 +173,7 @@ export default class TestGenerator extends (PureComponent || Component) {
}
}
i++;
if (i > selectedActionId) break;
if (i > selectedActionId!) break;
}
r = r.trim();
@ -139,11 +181,14 @@ export default class TestGenerator extends (PureComponent || Component) {
if (!isVanilla) r = wrap({ name, assertions: r });
else {
r = wrap({
name: /^[a-zA-Z0-9_-]+?$/.test(name) ? name : 'Store',
name: /^[a-zA-Z0-9_-]+?$/.test(name as string) ? name : 'Store',
actionName:
(selectedActionId === null || selectedActionId > 0) &&
actions[startIdx]
? actions[startIdx].action.type.replace(/[^a-zA-Z0-9_-]+/, '')
? (actions[startIdx].action.type as string).replace(
/[^a-zA-Z0-9_-]+/,
''
)
: 'should return the initial state',
initialState: stringify(computedStates[startIdx - 1].state),
assertions: r,
@ -167,25 +212,10 @@ export default class TestGenerator extends (PureComponent || Component) {
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 = {
static defaultProps = {
useCodemirror: true,
selectedActionId: null,
startActionId: null,
};
};
}

View File

@ -0,0 +1,6 @@
declare module 'es6template' {
const _default: {
compile<Locals>(template: string): (locals: Locals) => string;
};
export default _default;
}

View File

@ -8,16 +8,22 @@ import {
Notification,
Dialog,
} from 'devui';
import { formSchema, uiSchema, defaultFormData } from './templateForm';
import { MdAdd } from 'react-icons/md';
import { MdEdit } from 'react-icons/md';
import { Action } from 'redux';
import {
DevtoolsInspectorState,
TabComponentProps,
} from 'redux-devtools-inspector-monitor';
import { formSchema, uiSchema, defaultFormData } from './templateForm';
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';
import { Template } from './types';
export const getDefaultTemplates = (/* lib */) =>
export const getDefaultTemplates = (/* lib */): Template[] =>
/*
if (lib === 'redux') {
return [mochaTemplate, tapeTemplate, avaTemplate];
@ -26,15 +32,27 @@ export const getDefaultTemplates = (/* lib */) =>
*/
[jestTemplate, mochaTemplate, tapeTemplate, avaTemplate];
export default class TestTab extends Component {
constructor(props) {
super(props);
this.state = { dialogStatus: null };
}
interface TestGeneratorMonitorState {
hideTip?: boolean;
selected?: number;
templates?: Template[];
}
getPersistedState = () => this.props.monitorState.testGenerator || {};
interface State {
dialogStatus: 'Add' | 'Edit' | null;
}
handleSelectTemplate = (selectedTemplate) => {
export default class TestTab<S, A extends Action<unknown>> extends Component<
TabComponentProps<S, A>,
State
> {
state: State = { dialogStatus: null };
getPersistedState = (): TestGeneratorMonitorState =>
(this.props.monitorState as { testGenerator?: TestGeneratorMonitorState })
.testGenerator || {};
handleSelectTemplate = (selectedTemplate: Template) => {
const { templates = getDefaultTemplates() } = this.getPersistedState();
this.updateState({ selected: templates.indexOf(selectedTemplate) });
};
@ -47,7 +65,7 @@ export default class TestTab extends Component {
this.setState({ dialogStatus: null });
};
handleSubmit = ({ formData: template }) => {
handleSubmit = ({ formData: template }: { formData: Template }) => {
const {
templates = getDefaultTemplates(),
selected = 0,
@ -90,13 +108,15 @@ export default class TestTab extends Component {
this.setState({ dialogStatus: 'Edit' });
};
updateState = (newState) => {
updateState = (newState: TestGeneratorMonitorState) => {
this.props.updateMonitorState({
testGenerator: {
...this.props.monitorState.testGenerator,
...(this.props.monitorState as {
testGenerator?: TestGeneratorMonitorState;
}).testGenerator,
...newState,
},
});
} as Partial<DevtoolsInspectorState>);
};
render() {
@ -128,7 +148,7 @@ export default class TestTab extends Component {
{!assertion ? (
<Notification>No template for tests specified.</Notification>
) : (
<TestGenerator
<TestGenerator<S, A>
isVanilla={false}
assertion={assertion}
dispatcher={dispatcher}
@ -142,7 +162,7 @@ export default class TestTab extends Component {
</Notification>
)}
{dialogStatus && (
<Dialog
<Dialog<Template>
open
title={`${dialogStatus} test template`}
onDismiss={this.handleCloseDialog}
@ -164,9 +184,8 @@ export default class TestTab extends Component {
</Container>
);
}
}
TestTab.propTypes = {
static propTypes = {
monitorState: PropTypes.shape({
testGenerator: PropTypes.shape({
templates: PropTypes.array,
@ -180,4 +199,5 @@ TestTab.propTypes = {
}).isRequired,
*/
updateMonitorState: PropTypes.func.isRequired,
};
};
}

View File

@ -1,18 +0,0 @@
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,21 @@
import { AssertionLocals, DispatcherLocals, WrapLocals } from '../../types';
export const name = 'Ava template';
export const dispatcher = ({ action, prevState }: DispatcherLocals) =>
`state = reducers(${prevState!}, ${action!});`;
export const assertion = ({ curState }: AssertionLocals) =>
`t.deepEqual(state, ${curState!});`;
export const wrap = ({ assertions }: WrapLocals) =>
`import test from 'ava';
import reducers from '../../reducers';
test('reducers', (t) => {
let state;
${assertions}
});
`;
export default { name, assertion, dispatcher, wrap };

View File

@ -1,18 +0,0 @@
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,20 @@
import { AssertionLocals, DispatcherLocals, WrapLocals } from '../../types';
export const name = 'Jest template';
export const dispatcher = ({ action, prevState }: DispatcherLocals) =>
`state = reducers(${prevState!}, ${action!});`;
export const assertion = ({ curState }: AssertionLocals) =>
`expect(state).toEqual(${curState!});`;
export const wrap = ({ assertions }: WrapLocals) =>
`import reducers from '../../reducers';
test('reducers', () => {
let state;
${assertions}
});
`;
export default { name, assertion, dispatcher, wrap };

View File

@ -1,21 +0,0 @@
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,23 @@
import { AssertionLocals, DispatcherLocals, WrapLocals } from '../../types';
export const name = 'Mocha template';
export const dispatcher = ({ action, prevState }: DispatcherLocals) =>
`state = reducers(${prevState!}, ${action!});`;
export const assertion = ({ curState }: AssertionLocals) =>
`expect(state).toEqual(${curState!});`;
export const wrap = ({ assertions }: WrapLocals) =>
`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

@ -1,19 +0,0 @@
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,22 @@
import { AssertionLocals, DispatcherLocals, WrapLocals } from '../../types';
export const name = 'Tape template';
export const dispatcher = ({ action, prevState }: DispatcherLocals) =>
`state = reducers(${prevState!}, ${action!});`;
export const assertion = ({ curState }: AssertionLocals) =>
`t.deepEqual(state, ${curState!});`;
export const wrap = ({ assertions }: WrapLocals) =>
`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,64 @@
declare module 'simple-diff' {
interface AddEvent {
oldPath: (string | number)[];
newPath: (string | number)[];
type: 'add';
oldValue: undefined;
newValue: unknown;
}
interface RemoveEvent {
oldPath: (string | number)[];
newPath: (string | number)[];
type: 'remove';
oldValue: unknown;
newValue: undefined;
}
interface ChangeEvent {
oldPath: (string | number)[];
newPath: (string | number)[];
type: 'change';
oldValue: unknown;
newValue: unknown;
}
interface AddItemEvent {
oldPath: (string | number)[];
newPath: (string | number)[];
type: 'add-item';
oldIndex: -1;
curIndex: -1;
newIndex: number;
newValue: unknown;
}
interface RemoveItemEvent {
oldPath: (string | number)[];
newPath: (string | number)[];
type: 'remove-item';
oldIndex: number;
curIndex: number;
newIndex: -1;
oldValue: unknown;
}
interface MoveItemEvent {
oldPath: (string | number)[];
newPath: (string | number)[];
type: 'move-item';
oldIndex: number;
curIndex: number;
newIndex: number;
}
export type Event =
| AddEvent
| RemoveEvent
| ChangeEvent
| AddItemEvent
| RemoveItemEvent
| MoveItemEvent;
export default function (oldObj: unknown, newObj: unknown): Event[];
}

View File

@ -1,21 +1,23 @@
import { Template } from './types';
export const formSchema = {
type: 'object',
type: 'object' as const,
required: ['name'],
properties: {
name: {
type: 'string',
type: 'string' as const,
title: 'Template name',
},
dispatcher: {
type: 'string',
type: 'string' as const,
title: 'Dispatcher: ({ action, prevState }) => (`<template>`)',
},
assertion: {
type: 'string',
type: 'string' as const,
title: 'Assertion: ({ curState }) => (`<template>`)',
},
wrap: {
type: 'string',
type: 'string' as const,
title:
'Wrap code: ({ name, initialState, assertions }) => (`<template>`)',
},
@ -34,7 +36,7 @@ export const uiSchema = {
},
};
export const defaultFormData = {
export const defaultFormData: Template = {
dispatcher: 'state = reducers(${prevState}, ${action});',
assertion: 't.deepEqual(state, ${curState});',
wrap: `test('reducers', (t) => {

View File

@ -0,0 +1,23 @@
export interface DispatcherLocals {
action: string | undefined;
prevState: string | undefined;
}
export interface AssertionLocals {
path: string;
curState: number | string | undefined;
}
export interface WrapLocals {
name: string | undefined;
assertions: string;
actionName?: string;
initialState?: string | undefined;
}
export interface Template {
name?: string;
dispatcher: string;
assertion: string;
wrap: string;
}

View File

@ -1,18 +0,0 @@
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,20 @@
import { AssertionLocals, DispatcherLocals, WrapLocals } from '../../types';
export const name = 'Ava template';
export const dispatcher = ({ action }: DispatcherLocals) => `${action!};`;
export const assertion = ({ path, curState }: AssertionLocals) =>
`t.deepEqual(state${path}, ${curState!});`;
export const wrap = ({ name, initialState, assertions }: WrapLocals) =>
`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

@ -1,18 +0,0 @@
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,20 @@
import { AssertionLocals, DispatcherLocals, WrapLocals } from '../../types';
export const name = 'Mocha template';
export const dispatcher = ({ action }: DispatcherLocals) => `${action!};`;
export const assertion = ({ path, curState }: AssertionLocals) =>
`expect(store${path}).toEqual(${curState!});`;
export const wrap = ({ name, initialState, assertions }: WrapLocals) =>
`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

@ -1,20 +0,0 @@
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,27 @@
import { AssertionLocals, DispatcherLocals, WrapLocals } from '../../types';
export const name = 'Mocha template';
export const dispatcher = ({ action }: DispatcherLocals) => `${action!};`;
export const assertion = ({ path, curState }: AssertionLocals) =>
`expect(store${path}).toEqual(${curState!});`;
export const wrap = ({
name,
actionName,
initialState,
assertions,
}: WrapLocals) =>
`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

@ -1,19 +0,0 @@
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,21 @@
import { AssertionLocals, DispatcherLocals, WrapLocals } from '../../types';
export const name = 'Tape template';
export const dispatcher = ({ action }: DispatcherLocals) => `${action!};`;
export const assertion = ({ path, curState }: AssertionLocals) =>
`t.deepEqual(state${path}, ${curState!});`;
export const wrap = ({ name, initialState, assertions }: WrapLocals) =>
`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

@ -1,28 +1,42 @@
import React from 'react';
import { render } from 'enzyme';
import { renderToJson } from 'enzyme-to-json';
import { PerformAction } from 'redux-devtools-instrument';
import { Action } from 'redux';
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 actions: { [actionId: number]: PerformAction<Action<unknown>> } = {
0: {
type: 'PERFORM_ACTION',
action: { type: '@@INIT' },
timestamp: 0,
stack: undefined,
},
1: {
type: 'PERFORM_ACTION',
action: { type: 'INCREMENT_COUNTER' },
timestamp: 0,
stack: undefined,
},
};
const computedStates = [{ state: { counter: 0 } }, { state: { counter: 1 } }];
const TestGeneratorAsAny = TestGenerator as any;
describe('TestGenerator component', () => {
it('should show warning message when no params provided', () => {
const component = render(<TestGenerator useCodemirror={false} />);
const component = render(<TestGeneratorAsAny useCodemirror={false} />);
expect(renderToJson(component)).toMatchSnapshot();
});
it('should be empty when no actions provided', () => {
const component = render(
<TestGenerator
<TestGeneratorAsAny
assertion={fnTemplate.assertion}
dispatcher={fnTemplate.dispatcher}
wrap={fnTemplate.wrap}
@ -34,7 +48,7 @@ describe('TestGenerator component', () => {
it("should match function template's test for first action", () => {
const component = render(
<TestGenerator
<TestGeneratorAsAny
assertion={fnTemplate.assertion}
dispatcher={fnTemplate.dispatcher}
wrap={fnTemplate.wrap}
@ -49,7 +63,7 @@ describe('TestGenerator component', () => {
it("should match string template's test for first action", () => {
const component = render(
<TestGenerator
<TestGeneratorAsAny
assertion={strTemplate.assertion}
dispatcher={strTemplate.dispatcher}
wrap={strTemplate.wrap}
@ -64,7 +78,7 @@ describe('TestGenerator component', () => {
it('should generate test for the last action when selectedActionId not specified', () => {
const component = render(
<TestGenerator
<TestGeneratorAsAny
assertion={fnTemplate.assertion}
dispatcher={fnTemplate.dispatcher}
wrap={fnTemplate.wrap}
@ -78,7 +92,7 @@ describe('TestGenerator component', () => {
it('should generate test for vanilla js class', () => {
const component = render(
<TestGenerator
<TestGeneratorAsAny
assertion={fnVanillaTemplate.assertion}
dispatcher={fnVanillaTemplate.dispatcher}
wrap={fnVanillaTemplate.wrap}
@ -95,7 +109,7 @@ describe('TestGenerator component', () => {
it('should generate test for vanilla js class with string template', () => {
const component = render(
<TestGenerator
<TestGeneratorAsAny
assertion={strVanillaTemplate.assertion}
dispatcher={strVanillaTemplate.dispatcher}
wrap={strVanillaTemplate.wrap}

View File

@ -12,11 +12,11 @@ const computedStates = [
{ state: [0, 2, 3, 4] },
];
const test = (s1, s2) =>
const test = (s1: { state: unknown } | undefined, s2: { state: unknown }) =>
compare(s1, s2, ({ path, curState }) =>
expect(`expect(store${path}).toEqual(${curState});`).toBe(
assertion({ path, curState })
)
expect(
`expect(store${path}).toEqual(${curState as number | string});`
).toBe(assertion({ path, curState }))
);
describe('Assertions', () => {

View File

@ -0,0 +1,4 @@
{
"extends": "../../../tsconfig.react.base.json",
"include": ["../src", "."]
}

View File

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.react.base.json",
"compilerOptions": {
"outDir": "lib",
"resolveJsonModule": true
},
"include": ["src"]
}

View File

@ -387,14 +387,6 @@
"@babel/helper-plugin-utils" "^7.10.4"
"@babel/plugin-syntax-decorators" "^7.10.4"
"@babel/plugin-proposal-do-expressions@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-do-expressions/-/plugin-proposal-do-expressions-7.10.4.tgz#9a5190f3bf4818f83e41d673ee517ff76cf8e4ed"
integrity sha512-Gcc2wLVeMceRdP6m9tdDygP01lbUVmaQGBRoIRJZxzPfB5VTiUgmn1jGfORgqbEVgUpG0IQm/z4q5Y/qzG+8JQ==
dependencies:
"@babel/helper-plugin-utils" "^7.10.4"
"@babel/plugin-syntax-do-expressions" "^7.10.4"
"@babel/plugin-proposal-dynamic-import@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz#ba57a26cb98b37741e9d5bca1b8b0ddf8291f17e"
@ -530,13 +522,6 @@
dependencies:
"@babel/helper-plugin-utils" "^7.10.4"
"@babel/plugin-syntax-do-expressions@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-do-expressions/-/plugin-syntax-do-expressions-7.10.4.tgz#0c7ebb749500c6bfa99a9f926db3bfd6cdbaded9"
integrity sha512-HyvaTg1aiwGo2I+Pu0nyurRMjIP7J89GpuZ2mcQ0fhO6Jt3BnyhEPbNJFG1hRE99NAPNfPYh93/7HO+GPVkTKg==
dependencies:
"@babel/helper-plugin-utils" "^7.10.4"
"@babel/plugin-syntax-dynamic-import@^7.8.0", "@babel/plugin-syntax-dynamic-import@^7.8.3":
version "7.8.3"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3"
@ -977,14 +962,6 @@
"@babel/helper-create-regexp-features-plugin" "^7.10.4"
"@babel/helper-plugin-utils" "^7.10.4"
"@babel/polyfill@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.10.4.tgz#915e5bfe61490ac0199008e35ca9d7d151a8e45a"
integrity sha512-8BYcnVqQ5kMD2HXoHInBH7H1b/uP3KdnwCYXOqFnXqguOyuu443WXusbIUbWEfY3Z0Txk0M1uG/8YuAMhNl6zg==
dependencies:
core-js "^2.6.5"
regenerator-runtime "^0.13.4"
"@babel/preset-env@^7.11.0":
version "7.11.0"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.11.0.tgz#860ee38f2ce17ad60480c2021ba9689393efb796"
@ -1192,7 +1169,7 @@
core-js "^2.6.5"
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
version "7.11.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==
@ -3653,6 +3630,11 @@
resolved "https://registry.yarnpkg.com/@types/npmlog/-/npmlog-4.1.2.tgz#d070fe6a6b78755d1092a3dc492d34c3d8f871c4"
integrity sha512-4QQmOF5KlwfxJ5IGXFIudkeLCdMABz03RcUXu+LCb24zmln8QW6aDjuGl4d4XPVLf2j+FnjelHTP7dvceAFbhA==
"@types/object-path@^0.11.0":
version "0.11.0"
resolved "https://registry.yarnpkg.com/@types/object-path/-/object-path-0.11.0.tgz#0b744309b2573dc8bf867ef589b6288be998e602"
integrity sha512-/tuN8jDbOXcPk+VzEVZzzAgw1Byz7s/itb2YI10qkSyy6nykJH02DuhfrflxVdAdE7AZ91h5X6Cn0dmVdFw2TQ==
"@types/overlayscrollbars@^1.9.0":
version "1.12.0"
resolved "https://registry.yarnpkg.com/@types/overlayscrollbars/-/overlayscrollbars-1.12.0.tgz#98456caceca8ad73bd5bb572632a585074e70764"
@ -3828,6 +3810,11 @@
"@types/express-serve-static-core" "*"
"@types/mime" "*"
"@types/simple-element-resize-detector@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@types/simple-element-resize-detector/-/simple-element-resize-detector-1.3.0.tgz#19b40d71fefa1876ac5d4ba585197ef438946353"
integrity sha512-z89ForrCNg+4uwTHjwBCM9LjcsXYC/4O8u3tSi+82v2LCbfiYFpkjH/qQVkDewFBK6FUG7RRV7jw78EGs2maoQ==
"@types/source-list-map@*":
version "0.1.2"
resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9"
@ -11635,11 +11622,6 @@ lodash.isequal@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
lodash.isequalwith@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.isequalwith/-/lodash.isequalwith-4.4.0.tgz#266726ddd528f854f21f4ea98a065606e0fbc6b0"
integrity sha1-Jmcm3dUo+FTyH06pigZWBuD7xrA=
lodash.isinteger@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"