This commit is contained in:
Nathan Bierema 2020-09-12 19:56:00 -04:00
parent c746f29e9d
commit 1d6d3867d2
16 changed files with 214 additions and 100 deletions

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

@ -1,3 +1,4 @@
import DevtoolsInspector from './DevtoolsInspector';
export default DevtoolsInspector;
export { TabComponentProps } from './ActionPreview';
export { DevtoolsInspectorState } from './redux';

View File

@ -3,38 +3,49 @@ 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) {
// eslint-disable-next-line @typescript-eslint/ban-types
function getState<S>(s: { state: S; error?: string }, defaultValue: {}) {
if (!s) return defaultValue;
return JSON.parse(jsan.stringify(s.state));
}
export function compare<S>(s1: S, s2: S, 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 },
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});`);
@ -51,9 +62,9 @@ interface Props<S, A extends Action<unknown>>
extends Omit<TabComponentProps<S, A>, 'monitorState' | 'updateMonitorState'> {
name?: string;
isVanilla?: boolean;
wrap?: unknown;
dispatcher?: unknown;
assertion?: unknown;
wrap?: string | ((locals: WrapLocals) => string);
dispatcher?: string | ((locals: DispatcherLocals) => string);
assertion?: string | ((locals: AssertionLocals) => string);
useCodemirror: boolean;
indentation?: number;
header?: ReactNode;
@ -118,9 +129,12 @@ export default class TestGenerator<
curState,
}: {
path: string;
curState: string | undefined;
curState: number | string | undefined;
}) => {
r += `${space}${assertion({ path, curState })}\n`;
r += `${space}${(assertion as (locals: AssertionLocals) => string)({
path,
curState,
})}\n`;
};
while (actions[i]) {
@ -133,7 +147,7 @@ export default class TestGenerator<
else r += space;
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),
@ -164,7 +178,7 @@ export default class TestGenerator<
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]
@ -196,21 +210,6 @@ export default class TestGenerator<
return <Editor value={code} />;
}
static 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,
};
static defaultProps = {
useCodemirror: true,
selectedActionId: null,

View File

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

View File

@ -11,7 +11,10 @@ import {
import { MdAdd } from 'react-icons/md';
import { MdEdit } from 'react-icons/md';
import { Action } from 'redux';
import { TabComponentProps } from 'redux-devtools-inspector-monitor';
import {
DevtoolsInspectorState,
TabComponentProps,
} from 'redux-devtools-inspector-monitor';
import { formSchema, uiSchema, defaultFormData } from './templateForm';
import TestGenerator from './TestGenerator';
import jestTemplate from './redux/jest/template';
@ -19,7 +22,6 @@ import mochaTemplate from './redux/mocha/template';
import tapeTemplate from './redux/tape/template';
import avaTemplate from './redux/ava/template';
import { Template } from './types';
import { DevtoolsInspectorState } from 'redux-devtools-inspector-monitor/lib/redux';
export const getDefaultTemplates = (/* lib */): Template[] =>
/*
@ -33,7 +35,7 @@ export const getDefaultTemplates = (/* lib */): Template[] =>
interface TestGeneratorMonitorState {
hideTip?: boolean;
selected?: number;
templates: Template[];
templates?: Template[];
}
interface State {
@ -63,7 +65,7 @@ export default class TestTab<S, A extends Action<unknown>> extends Component<
this.setState({ dialogStatus: null });
};
handleSubmit = ({ formData: template }) => {
handleSubmit = ({ formData: template }: { formData: Template }) => {
const {
templates = getDefaultTemplates(),
selected = 0,
@ -106,7 +108,7 @@ export default class TestTab<S, A extends Action<unknown>> extends Component<
this.setState({ dialogStatus: 'Edit' });
};
updateState = (newState) => {
updateState = (newState: TestGeneratorMonitorState) => {
this.props.updateMonitorState({
testGenerator: {
...(this.props.monitorState as {
@ -146,7 +148,7 @@ export default class TestTab<S, A extends Action<unknown>> extends Component<
{!assertion ? (
<Notification>No template for tests specified.</Notification>
) : (
<TestGenerator
<TestGenerator<S, A>
isVanilla={false}
assertion={assertion}
dispatcher={dispatcher}
@ -160,7 +162,7 @@ export default class TestTab<S, A extends Action<unknown>> extends Component<
</Notification>
)}
{dialogStatus && (
<Dialog
<Dialog<Template>
open
title={`${dialogStatus} test template`}
onDismiss={this.handleCloseDialog}

View File

@ -1,11 +1,14 @@
import { AssertionLocals, DispatcherLocals, WrapLocals } from '../../types';
export const name = 'Ava template';
export const dispatcher = ({ action, prevState }) =>
`state = reducers(${prevState}, ${action});`;
export const dispatcher = ({ action, prevState }: DispatcherLocals) =>
`state = reducers(${prevState!}, ${action!});`;
export const assertion = ({ curState }) => `t.deepEqual(state, ${curState});`;
export const assertion = ({ curState }: AssertionLocals) =>
`t.deepEqual(state, ${curState!});`;
export const wrap = ({ assertions }) =>
export const wrap = ({ assertions }: WrapLocals) =>
`import test from 'ava';
import reducers from '../../reducers';

View File

@ -1,12 +1,14 @@
import { AssertionLocals, DispatcherLocals, WrapLocals } from '../../types';
export const name = 'Jest template';
export const dispatcher = ({ action, prevState }) =>
`state = reducers(${prevState}, ${action});`;
export const dispatcher = ({ action, prevState }: DispatcherLocals) =>
`state = reducers(${prevState!}, ${action!});`;
export const assertion = ({ curState }) =>
`expect(state).toEqual(${curState});`;
export const assertion = ({ curState }: AssertionLocals) =>
`expect(state).toEqual(${curState!});`;
export const wrap = ({ assertions }) =>
export const wrap = ({ assertions }: WrapLocals) =>
`import reducers from '../../reducers';
test('reducers', () => {

View File

@ -1,12 +1,14 @@
import { AssertionLocals, DispatcherLocals, WrapLocals } from '../../types';
export const name = 'Mocha template';
export const dispatcher = ({ action, prevState }) =>
`state = reducers(${prevState}, ${action});`;
export const dispatcher = ({ action, prevState }: DispatcherLocals) =>
`state = reducers(${prevState!}, ${action!});`;
export const assertion = ({ curState }) =>
`expect(state).toEqual(${curState});`;
export const assertion = ({ curState }: AssertionLocals) =>
`expect(state).toEqual(${curState!});`;
export const wrap = ({ assertions }) =>
export const wrap = ({ assertions }: WrapLocals) =>
`import expect from 'expect';
import reducers from '../../reducers';

View File

@ -1,11 +1,14 @@
import { AssertionLocals, DispatcherLocals, WrapLocals } from '../../types';
export const name = 'Tape template';
export const dispatcher = ({ action, prevState }) =>
`state = reducers(${prevState}, ${action});`;
export const dispatcher = ({ action, prevState }: DispatcherLocals) =>
`state = reducers(${prevState!}, ${action!});`;
export const assertion = ({ curState }) => `t.deepEqual(state, ${curState});`;
export const assertion = ({ curState }: AssertionLocals) =>
`t.deepEqual(state, ${curState!});`;
export const wrap = ({ assertions }) =>
export const wrap = ({ assertions }: WrapLocals) =>
`import test from 'tape';
import reducers from '../../reducers';

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

@ -1,5 +1,22 @@
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;
name?: string;
dispatcher: string;
assertion: string;
wrap: string;

View File

@ -1,16 +1,18 @@
import { AssertionLocals, DispatcherLocals, WrapLocals } from '../../types';
export const name = 'Ava template';
export const dispatcher = ({ action }) => `${action};`;
export const dispatcher = ({ action }: DispatcherLocals) => `${action!};`;
export const assertion = ({ path, curState }) =>
`t.deepEqual(state${path}, ${curState});`;
export const assertion = ({ path, curState }: AssertionLocals) =>
`t.deepEqual(state${path}, ${curState!});`;
export const wrap = ({ name, initialState, assertions }) =>
export const wrap = ({ name, initialState, assertions }: WrapLocals) =>
`import test from 'ava';
import ${name} from '../../stores/${name}';
import ${name!} from '../../stores/${name!}';
test('${name}', (t) => {
const store = new ${name}(${initialState});
test('${name!}', (t) => {
const store = new ${name!}(${initialState!});
${assertions}
});
`;

View File

@ -1,16 +1,18 @@
import { AssertionLocals, DispatcherLocals, WrapLocals } from '../../types';
export const name = 'Mocha template';
export const dispatcher = ({ action }) => `${action};`;
export const dispatcher = ({ action }: DispatcherLocals) => `${action!};`;
export const assertion = ({ path, curState }) =>
`expect(store${path}).toEqual(${curState});`;
export const assertion = ({ path, curState }: AssertionLocals) =>
`expect(store${path}).toEqual(${curState!});`;
export const wrap = ({ name, initialState, assertions }) =>
export const wrap = ({ name, initialState, assertions }: WrapLocals) =>
`import expect from 'expect';
import ${name} from '../../stores/${name}';
import ${name!} from '../../stores/${name!}';
test('${name}', (t) => {
const store = new ${name}(${initialState});
test('${name!}', (t) => {
const store = new ${name!}(${initialState!});
${assertions}
});
`;

View File

@ -1,17 +1,24 @@
import { AssertionLocals, DispatcherLocals, WrapLocals } from '../../types';
export const name = 'Mocha template';
export const dispatcher = ({ action }) => `${action};`;
export const dispatcher = ({ action }: DispatcherLocals) => `${action!};`;
export const assertion = ({ path, curState }) =>
`expect(store${path}).toEqual(${curState});`;
export const assertion = ({ path, curState }: AssertionLocals) =>
`expect(store${path}).toEqual(${curState!});`;
export const wrap = ({ name, actionName, initialState, assertions }) =>
export const wrap = ({
name,
actionName,
initialState,
assertions,
}: WrapLocals) =>
`import expect from 'expect';
import ${name} from '../../stores/${name}';
import ${name!} from '../../stores/${name!}';
describe('${name}', () => {
it('${actionName}', () => {
const store = new ${name}(${initialState});
describe('${name!}', () => {
it('${actionName!}', () => {
const store = new ${name!}(${initialState!});
${assertions}
});
});

View File

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