Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split new CodeEvaluator component out from Preview #976

Merged
merged 8 commits into from
May 14, 2018
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions src/rsg-components/CodeEvaluator/CodeEvaluator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import React, { Component } from 'react';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have any new code here? If not I won't read the whole file ;-)

This is React specific, so we should reflect it in the name: something like ReactExample (not the best too but I have no better ideas ;-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, right... yeah, git isn't being your friend here, this is all moved from Preview.

The only things I changed were:

  • I renamed PreviewComponent to InitialStateComponent
  • I passed in compilerConfig as a prop instead of reading it off context as discussed above.

I'm happy to rename it... I struggled to come up with a decent name to.

import PropTypes from 'prop-types';
import { transform } from 'buble';
import Wrapper from 'rsg-components/Wrapper';
import splitExampleCode from '../../utils/splitExampleCode';

/* eslint-disable no-invalid-this, react/no-multi-comp */

const FragmentTag = React.Fragment ? 'React.Fragment' : 'div';

const compileCode = (code, config) => transform(code, config).code;
const wrapCodeInFragment = code => `<${FragmentTag}>${code}</${FragmentTag}>;`;

// Wrap everything in a React component to leverage the state management
// of this component
class InitialStateComponent extends Component {
static propTypes = {
component: PropTypes.func.isRequired,
initialState: PropTypes.object.isRequired,
};

state = this.props.initialState;
setStateBinded = this.setState.bind(this);

render() {
return this.props.component(this.state, this.setStateBinded);
}
}

export default class CodeEvaluator extends Component {
static propTypes = {
code: PropTypes.string.isRequired,
evalInContext: PropTypes.func.isRequired,
onError: PropTypes.func.isRequired,
compilerConfig: PropTypes.object,
};
static contextTypes = {};

shouldComponentUpdate(nextProps) {
return this.props.code !== nextProps.code;
}

// Eval the code to extract the value of the initial state
getExampleInitialState(compiledCode) {
if (compiledCode.indexOf('initialState') === -1) {
return {};
}

return this.props.evalInContext(`
var state = {}, initialState = {};
try {
${compiledCode};
} catch (err) {}
return initialState;
`)();
}

// Run example code and return the last top-level expression
getExampleComponent(compiledCode) {
return this.props.evalInContext(`
var initialState = {};
${compiledCode}
`);
}

compileCode(code) {
try {
const wrappedCode = code.trim().match(/^</) ? wrapCodeInFragment(code) : code;
return compileCode(wrappedCode, this.props.compilerConfig);
} catch (err) {
if (this.props.onError) {
this.props.onError(err);
}
}
return false;
}

render() {
const compiledCode = this.compileCode(this.props.code);
if (!compiledCode) {
return null;
}

const { head, example } = splitExampleCode(compiledCode);
const initialState = this.getExampleInitialState(head);
const exampleComponent = this.getExampleComponent(example);
const wrappedComponent = (
<Wrapper onError={this.props.onError}>
<InitialStateComponent component={exampleComponent} initialState={initialState} />
</Wrapper>
);
return wrappedComponent;
}
}
65 changes: 65 additions & 0 deletions src/rsg-components/CodeEvaluator/CodeEvaluator.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React from 'react';
import noop from 'lodash/noop';
import CodeEvaluator from '../CodeEvaluator';

const evalInContext = a =>
// eslint-disable-next-line no-new-func
new Function('require', 'state', 'setState', 'const React = require("react");' + a).bind(
null,
require
);

it('should render code', () => {
const actual = shallow(
<CodeEvaluator code={'<button>OK</button>'} evalInContext={evalInContext} onError={noop} />
);

expect(actual).toMatchSnapshot();
});

it('should wrap code in Fragment when it starts with <', () => {
const actual = mount(
<div>
<CodeEvaluator code="<span /><span />" evalInContext={evalInContext} onError={noop} />
</div>
);

expect(actual.html()).toMatchSnapshot();
});

it('should handle errors', () => {
const onError = jest.fn();

shallow(<CodeEvaluator code={'<invalid code'} evalInContext={evalInContext} onError={onError} />);

expect(onError).toHaveBeenCalledTimes(1);
});

it('should set initialState before the first render', () => {
const code = `
initialState = {count:1};
<span>{state.count}</span>
`;
const actual = mount(<CodeEvaluator code={code} evalInContext={evalInContext} onError={noop} />);
expect(actual.html()).toMatchSnapshot();
});

it('should update state on setState', done => {
const code = `
initialState = {count:1};
setTimeout(() => state.count === 1 && setState({count:2}));
<button>{state.count}</button>
`;
const actual = mount(<CodeEvaluator code={code} evalInContext={evalInContext} onError={noop} />);

actual.find('button').simulate('click');

setTimeout(() => {
try {
expect(actual.html()).toMatchSnapshot();
done();
} catch (err) {
done.fail(err);
}
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render code 1`] = `
<Wrapper
onError={[Function]}
>
<InitialStateComponent
component={[Function]}
initialState={Object {}}
/>
</Wrapper>
`;

exports[`should set initialState before the first render 1`] = `

<span>
1
</span>

`;

exports[`should update state on setState 1`] = `

<button>
2
</button>

`;

exports[`should wrap code in Fragment when it starts with < 1`] = `

<div>
<span>
</span>
<span>
</span>
</div>

`;
1 change: 1 addition & 0 deletions src/rsg-components/CodeEvaluator/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from 'rsg-components/CodeEvaluator/CodeEvaluator';
77 changes: 9 additions & 68 deletions src/rsg-components/Preview/Preview.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,13 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import { transform } from 'buble';
import PlaygroundError from 'rsg-components/PlaygroundError';
import Wrapper from 'rsg-components/Wrapper';
import splitExampleCode from '../../utils/splitExampleCode';

/* eslint-disable no-invalid-this, react/no-multi-comp */
import CodeEvaluator from '../CodeEvaluator';

const Fragment = React.Fragment ? React.Fragment : 'div';
const FragmentTag = React.Fragment ? 'React.Fragment' : 'div';

const compileCode = (code, config) => transform(code, config).code;
const wrapCodeInFragment = code => `<${FragmentTag}>${code}</${FragmentTag}>;`;

// Wrap everything in a React component to leverage the state management
// of this component
class PreviewComponent extends Component {
static propTypes = {
component: PropTypes.func.isRequired,
initialState: PropTypes.object.isRequired,
};

state = this.props.initialState;
setStateBinded = this.setState.bind(this);
/* eslint-disable no-invalid-this */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need this?

Copy link
Contributor Author

@kidkuro kidkuro May 11, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in the handleError function.


render() {
return this.props.component(this.state, this.setStateBinded);
}
}
const Fragment = React.Fragment ? React.Fragment : 'div';

export default class Preview extends Component {
static propTypes = {
Expand Down Expand Up @@ -68,29 +47,6 @@ export default class Preview extends Component {
this.unmountPreview();
}

// Eval the code to extract the value of the initial state
getExampleInitialState(compiledCode) {
if (compiledCode.indexOf('initialState') === -1) {
return {};
}

return this.props.evalInContext(`
var state = {}, initialState = {};
try {
${compiledCode};
} catch (err) {}
return initialState;
`)();
}

// Run example code and return the last top-level expression
getExampleComponent(compiledCode) {
return this.props.evalInContext(`
var initialState = {};
${compiledCode}
`);
}

unmountPreview() {
if (this.mountNode) {
ReactDOM.unmountComponentAtNode(this.mountNode);
Expand All @@ -107,18 +63,13 @@ export default class Preview extends Component {
return;
}

const compiledCode = this.compileCode(code);
if (!compiledCode) {
return;
}

const { head, example } = splitExampleCode(compiledCode);
const initialState = this.getExampleInitialState(head);
const exampleComponent = this.getExampleComponent(example);
const wrappedComponent = (
<Wrapper onError={this.handleError}>
<PreviewComponent component={exampleComponent} initialState={initialState} />
</Wrapper>
<CodeEvaluator
code={code}
evalInContext={this.props.evalInContext}
onError={this.handleError}
compilerConfig={this.context.config.compilerConfig}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't CodeEvaluator read config from context itself, similar to what you did for the Editor?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it inherits context when it is rendered via ReactDOM.render(wrappedComponent, this.mountNode);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, true.

/>
);

window.requestAnimationFrame(() => {
Expand All @@ -131,16 +82,6 @@ export default class Preview extends Component {
});
}

compileCode(code) {
try {
const wrappedCode = code.trim().match(/^</) ? wrapCodeInFragment(code) : code;
return compileCode(wrappedCode, this.context.config.compilerConfig);
} catch (err) {
this.handleError(err);
}
return false;
}

handleError = err => {
this.unmountPreview();

Expand Down
57 changes: 25 additions & 32 deletions src/rsg-components/Preview/Preview.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const evalInContext = a =>
require
);
const code = '<button>OK</button>';
const newCode = '<button>Cancel</button>';
const options = {
context: {
config: {
Expand Down Expand Up @@ -58,6 +59,8 @@ it('should wrap code in Fragment when it starts with <', () => {
});

it('should render component renderer', () => {
console.error = jest.fn();

const actual = shallow(<Preview code={code} evalInContext={evalInContext} />, {
...options,
disableLifecycleMethods: true,
Expand All @@ -66,6 +69,28 @@ it('should render component renderer', () => {
expect(actual).toMatchSnapshot();
});

it('should update', () => {
const actual = mount(<Preview code={code} evalInContext={evalInContext} />, options);

actual.setProps({ code: newCode });

expect(actual.html()).toMatchSnapshot();
});

it('should handle no code', () => {
const actual = mount(<Preview code="" evalInContext={evalInContext} />, options);

expect(actual.html()).toMatchSnapshot();
});

it('should handle errors', () => {
console.error = jest.fn();
const actual = shallow(<Preview code={'<invalid code'} evalInContext={evalInContext} />, options);

expect(actual).toMatchSnapshot();
expect(console.error).toHaveBeenCalledTimes(1);
});

it('should not clear console on initial mount', () => {
console.clear = jest.fn();
mount(<Preview code={code} evalInContext={evalInContext} />, options);
Expand All @@ -79,35 +104,3 @@ it('should clear console on second mount', () => {
});
expect(console.clear).toHaveBeenCalledTimes(1);
});

it('should set initialState before the first render', () => {
const code = `
initialState = {count:1};
<span>{state.count}</span>
`;
const actual = mount(<Preview code={code} evalInContext={evalInContext} />, options);
expect(actual.html()).toMatchSnapshot();
});

it('should update state on setState', done => {
const code = `
initialState = {count:1};
setTimeout(() => state.count === 1 && setState({count:2}));
<button>{state.count}</button>
`;
const actual = mount(<Preview code={code} evalInContext={evalInContext} />, options);

actual
.instance()
.mountNode.querySelector('button')
.click();

setTimeout(() => {
try {
expect(actual.html()).toMatchSnapshot();
done();
} catch (err) {
done.fail(err);
}
});
});
Loading