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)< export default class Dialog<P> extends (PureComponent || Component)<
DialogProps | (DialogProps & FormProps<P>) DialogProps | (Omit<DialogProps, 'onSubmit'> & FormProps<P>)
> { > {
submitButton?: HTMLInputElement | null; submitButton?: HTMLInputElement | null;
onSubmit = () => { onSubmit = () => {
if (this.submitButton) this.submitButton.click(); if (this.submitButton) this.submitButton.click();
else this.props.onSubmit(); else (this.props.onSubmit as () => void)();
}; };
getFormButtonRef: React.RefCallback<HTMLInputElement> = (node) => { getFormButtonRef: React.RefCallback<HTMLInputElement> = (node) => {

View File

@ -1,3 +1,4 @@
import DevtoolsInspector from './DevtoolsInspector'; import DevtoolsInspector from './DevtoolsInspector';
export default DevtoolsInspector; export default DevtoolsInspector;
export { TabComponentProps } from './ActionPreview'; 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 { stringify } from 'javascript-stringify';
import objectPath from 'object-path'; import objectPath from 'object-path';
import jsan from 'jsan'; import jsan from 'jsan';
import diff from 'simple-diff'; import diff, { Event } from 'simple-diff';
import es6template from 'es6template'; import es6template from 'es6template';
import { Editor } from 'devui'; import { Editor } from 'devui';
import { TabComponentProps } from 'redux-devtools-inspector-monitor'; import { TabComponentProps } from 'redux-devtools-inspector-monitor';
import { Action } from 'redux'; 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(''); 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; if (!s) return defaultValue;
return JSON.parse(jsan.stringify(s.state)); return JSON.parse(jsan.stringify(s.state));
} }
export function compare<S>(s1: S, s2: S, cb, defaultValue) { export function compare<S>(
const paths = []; // Already processed s1: { state: S; error?: string },
function generate({ type, newPath, newValue, newIndex }) { s2: { state: S; error?: string },
let curState; cb: (value: { path: string; curState: number | string | undefined }) => void,
let path = fromPath(newPath); // 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; if (paths.length && paths.indexOf(path) !== -1) return;
paths.push(path); 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; curState = v.length;
path += '.length'; path += '.length';
} else if (type === 'add-item') { } else if (event.type === 'add-item') {
generate({ type: 'move-item', newPath }); generate({ type: 'move-item', newPath: event.newPath });
path += `[${newIndex}]`; path += `[${event.newIndex}]`;
curState = stringify(newValue); curState = stringify(event.newValue);
} else { } else {
curState = stringify(newValue); curState = stringify(event.newValue);
} }
// console.log(`expect(store${path}).toEqual(${curState});`); // 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'> { extends Omit<TabComponentProps<S, A>, 'monitorState' | 'updateMonitorState'> {
name?: string; name?: string;
isVanilla?: boolean; isVanilla?: boolean;
wrap?: unknown; wrap?: string | ((locals: WrapLocals) => string);
dispatcher?: unknown; dispatcher?: string | ((locals: DispatcherLocals) => string);
assertion?: unknown; assertion?: string | ((locals: AssertionLocals) => string);
useCodemirror: boolean; useCodemirror: boolean;
indentation?: number; indentation?: number;
header?: ReactNode; header?: ReactNode;
@ -118,9 +129,12 @@ export default class TestGenerator<
curState, curState,
}: { }: {
path: string; 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]) { while (actions[i]) {
@ -133,7 +147,7 @@ export default class TestGenerator<
else r += space; else r += space;
if (!isVanilla || (actions[i].action.type as string)[0] !== '@') { if (!isVanilla || (actions[i].action.type as string)[0] !== '@') {
r += r +=
dispatcher({ (dispatcher as (locals: DispatcherLocals) => string)({
action: !isVanilla action: !isVanilla
? this.getAction(actions[i].action) ? this.getAction(actions[i].action)
: this.getMethod(actions[i].action), : this.getMethod(actions[i].action),
@ -164,7 +178,7 @@ export default class TestGenerator<
if (!isVanilla) r = wrap({ name, assertions: r }); if (!isVanilla) r = wrap({ name, assertions: r });
else { else {
r = wrap({ r = wrap({
name: /^[a-zA-Z0-9_-]+?$/.test(name) ? name : 'Store', name: /^[a-zA-Z0-9_-]+?$/.test(name as string) ? name : 'Store',
actionName: actionName:
(selectedActionId === null || selectedActionId > 0) && (selectedActionId === null || selectedActionId > 0) &&
actions[startIdx] actions[startIdx]
@ -196,21 +210,6 @@ export default class TestGenerator<
return <Editor value={code} />; 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 = { static defaultProps = {
useCodemirror: true, useCodemirror: true,
selectedActionId: null, 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 { MdAdd } from 'react-icons/md';
import { MdEdit } from 'react-icons/md'; import { MdEdit } from 'react-icons/md';
import { Action } from 'redux'; 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 { formSchema, uiSchema, defaultFormData } from './templateForm';
import TestGenerator from './TestGenerator'; import TestGenerator from './TestGenerator';
import jestTemplate from './redux/jest/template'; import jestTemplate from './redux/jest/template';
@ -19,7 +22,6 @@ import mochaTemplate from './redux/mocha/template';
import tapeTemplate from './redux/tape/template'; import tapeTemplate from './redux/tape/template';
import avaTemplate from './redux/ava/template'; import avaTemplate from './redux/ava/template';
import { Template } from './types'; import { Template } from './types';
import { DevtoolsInspectorState } from 'redux-devtools-inspector-monitor/lib/redux';
export const getDefaultTemplates = (/* lib */): Template[] => export const getDefaultTemplates = (/* lib */): Template[] =>
/* /*
@ -33,7 +35,7 @@ export const getDefaultTemplates = (/* lib */): Template[] =>
interface TestGeneratorMonitorState { interface TestGeneratorMonitorState {
hideTip?: boolean; hideTip?: boolean;
selected?: number; selected?: number;
templates: Template[]; templates?: Template[];
} }
interface State { interface State {
@ -63,7 +65,7 @@ export default class TestTab<S, A extends Action<unknown>> extends Component<
this.setState({ dialogStatus: null }); this.setState({ dialogStatus: null });
}; };
handleSubmit = ({ formData: template }) => { handleSubmit = ({ formData: template }: { formData: Template }) => {
const { const {
templates = getDefaultTemplates(), templates = getDefaultTemplates(),
selected = 0, selected = 0,
@ -106,7 +108,7 @@ export default class TestTab<S, A extends Action<unknown>> extends Component<
this.setState({ dialogStatus: 'Edit' }); this.setState({ dialogStatus: 'Edit' });
}; };
updateState = (newState) => { updateState = (newState: TestGeneratorMonitorState) => {
this.props.updateMonitorState({ this.props.updateMonitorState({
testGenerator: { testGenerator: {
...(this.props.monitorState as { ...(this.props.monitorState as {
@ -146,7 +148,7 @@ export default class TestTab<S, A extends Action<unknown>> extends Component<
{!assertion ? ( {!assertion ? (
<Notification>No template for tests specified.</Notification> <Notification>No template for tests specified.</Notification>
) : ( ) : (
<TestGenerator <TestGenerator<S, A>
isVanilla={false} isVanilla={false}
assertion={assertion} assertion={assertion}
dispatcher={dispatcher} dispatcher={dispatcher}
@ -160,7 +162,7 @@ export default class TestTab<S, A extends Action<unknown>> extends Component<
</Notification> </Notification>
)} )}
{dialogStatus && ( {dialogStatus && (
<Dialog <Dialog<Template>
open open
title={`${dialogStatus} test template`} title={`${dialogStatus} test template`}
onDismiss={this.handleCloseDialog} onDismiss={this.handleCloseDialog}

View File

@ -1,11 +1,14 @@
import { AssertionLocals, DispatcherLocals, WrapLocals } from '../../types';
export const name = 'Ava template'; export const name = 'Ava template';
export const dispatcher = ({ action, prevState }) => export const dispatcher = ({ action, prevState }: DispatcherLocals) =>
`state = reducers(${prevState}, ${action});`; `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 test from 'ava';
import reducers from '../../reducers'; import reducers from '../../reducers';

View File

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

View File

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

View File

@ -1,11 +1,14 @@
import { AssertionLocals, DispatcherLocals, WrapLocals } from '../../types';
export const name = 'Tape template'; export const name = 'Tape template';
export const dispatcher = ({ action, prevState }) => export const dispatcher = ({ action, prevState }: DispatcherLocals) =>
`state = reducers(${prevState}, ${action});`; `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 test from 'tape';
import reducers from '../../reducers'; 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 = { export const formSchema = {
type: 'object', type: 'object' as const,
required: ['name'], required: ['name'],
properties: { properties: {
name: { name: {
type: 'string', type: 'string' as const,
title: 'Template name', title: 'Template name',
}, },
dispatcher: { dispatcher: {
type: 'string', type: 'string' as const,
title: 'Dispatcher: ({ action, prevState }) => (`<template>`)', title: 'Dispatcher: ({ action, prevState }) => (`<template>`)',
}, },
assertion: { assertion: {
type: 'string', type: 'string' as const,
title: 'Assertion: ({ curState }) => (`<template>`)', title: 'Assertion: ({ curState }) => (`<template>`)',
}, },
wrap: { wrap: {
type: 'string', type: 'string' as const,
title: title:
'Wrap code: ({ name, initialState, assertions }) => (`<template>`)', '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});', dispatcher: 'state = reducers(${prevState}, ${action});',
assertion: 't.deepEqual(state, ${curState});', assertion: 't.deepEqual(state, ${curState});',
wrap: `test('reducers', (t) => { 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 { export interface Template {
name: string; name?: string;
dispatcher: string; dispatcher: string;
assertion: string; assertion: string;
wrap: string; wrap: string;

View File

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

View File

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

View File

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

View File

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