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 5 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
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 ReactExample from '../ReactExample';

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>
<ReactExample
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);
}
});
});
20 changes: 13 additions & 7 deletions src/rsg-components/Preview/__snapshots__/Preview.spec.js.snap
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render component renderer 1`] = `
exports[`should handle errors 1`] = `
<React.Fragment>
<div />
<Styled(PlaygroundError)
message="Invariant Violation: Target container is not a DOM element."
/>
</React.Fragment>
`;

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

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

`;

exports[`should update state on setState 1`] = `
exports[`should render component renderer 1`] = `
<React.Fragment>
<div />
</React.Fragment>
`;

exports[`should update 1`] = `

<div>
<button>
2
Cancel
</button>
</div>

Expand Down
94 changes: 94 additions & 0 deletions src/rsg-components/ReactExample/ReactExample.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import React, { Component } from 'react';
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 {
Copy link
Member

Choose a reason for hiding this comment

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

Something like StateHolder would be more correct: it's not just initial state here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Gotcha, I'll change it.

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 ReactExample 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/ReactExample/ReactExample.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 ReactExample from '../ReactExample';

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(
<ReactExample 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>
<ReactExample code="<span /><span />" evalInContext={evalInContext} onError={noop} />
</div>
);

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

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

shallow(<ReactExample 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(<ReactExample 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(<ReactExample code={code} evalInContext={evalInContext} onError={noop} />);

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

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