diff --git a/src/rsg-components/Preview/Preview.js b/src/rsg-components/Preview/Preview.js index 87209da19..7ad4e078c 100644 --- a/src/rsg-components/Preview/Preview.js +++ b/src/rsg-components/Preview/Preview.js @@ -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};`; - -// 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 */ - render() { - return this.props.component(this.state, this.setStateBinded); - } -} +const Fragment = React.Fragment ? React.Fragment : 'div'; export default class Preview extends Component { static propTypes = { @@ -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); @@ -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 = ( - - - + ); window.requestAnimationFrame(() => { @@ -131,16 +82,6 @@ export default class Preview extends Component { }); } - compileCode(code) { - try { - const wrappedCode = code.trim().match(/^ { this.unmountPreview(); diff --git a/src/rsg-components/Preview/Preview.spec.js b/src/rsg-components/Preview/Preview.spec.js index d7b540d36..b49da2d4f 100644 --- a/src/rsg-components/Preview/Preview.spec.js +++ b/src/rsg-components/Preview/Preview.spec.js @@ -10,6 +10,7 @@ const evalInContext = a => require ); const code = ''; +const newCode = ''; const options = { context: { config: { @@ -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(, { ...options, disableLifecycleMethods: true, @@ -66,6 +69,28 @@ it('should render component renderer', () => { expect(actual).toMatchSnapshot(); }); +it('should update', () => { + const actual = mount(, options); + + actual.setProps({ code: newCode }); + + expect(actual.html()).toMatchSnapshot(); +}); + +it('should handle no code', () => { + const actual = mount(, options); + + expect(actual.html()).toMatchSnapshot(); +}); + +it('should handle errors', () => { + console.error = jest.fn(); + const actual = shallow(, options); + + expect(actual).toMatchSnapshot(); + expect(console.error).toHaveBeenCalledTimes(1); +}); + it('should not clear console on initial mount', () => { console.clear = jest.fn(); mount(, options); @@ -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}; -{state.count} - `; - const actual = mount(, options); - expect(actual.html()).toMatchSnapshot(); -}); - -it('should update state on setState', done => { - const code = ` -initialState = {count:1}; -setTimeout(() => state.count === 1 && setState({count:2})); - - `; - const actual = mount(, options); - - actual - .instance() - .mountNode.querySelector('button') - .click(); - - setTimeout(() => { - try { - expect(actual.html()).toMatchSnapshot(); - done(); - } catch (err) { - done.fail(err); - } - }); -}); diff --git a/src/rsg-components/Preview/__snapshots__/Preview.spec.js.snap b/src/rsg-components/Preview/__snapshots__/Preview.spec.js.snap index c41d547be..aeae44b19 100644 --- a/src/rsg-components/Preview/__snapshots__/Preview.spec.js.snap +++ b/src/rsg-components/Preview/__snapshots__/Preview.spec.js.snap @@ -1,26 +1,32 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should render component renderer 1`] = ` +exports[`should handle errors 1`] = `
+ `; -exports[`should set initialState before the first render 1`] = ` +exports[`should handle no code 1`] = `
- - 1 -
`; -exports[`should update state on setState 1`] = ` +exports[`should render component renderer 1`] = ` + +
+ +`; + +exports[`should update 1`] = `
diff --git a/src/rsg-components/ReactExample/ReactExample.js b/src/rsg-components/ReactExample/ReactExample.js new file mode 100644 index 000000000..529b6f0dd --- /dev/null +++ b/src/rsg-components/ReactExample/ReactExample.js @@ -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};`; + +// Wrap everything in a React component to leverage the state management +// of this component +class StateHolder 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 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(/^ + + + ); + return wrappedComponent; + } +} diff --git a/src/rsg-components/ReactExample/ReactExample.spec.js b/src/rsg-components/ReactExample/ReactExample.spec.js new file mode 100644 index 000000000..ddc8a05cf --- /dev/null +++ b/src/rsg-components/ReactExample/ReactExample.spec.js @@ -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( + OK'} evalInContext={evalInContext} onError={noop} /> + ); + + expect(actual).toMatchSnapshot(); +}); + +it('should wrap code in Fragment when it starts with <', () => { + const actual = mount( +
+ +
+ ); + + expect(actual.html()).toMatchSnapshot(); +}); + +it('should handle errors', () => { + const onError = jest.fn(); + + shallow(); + + expect(onError).toHaveBeenCalledTimes(1); +}); + +it('should set initialState before the first render', () => { + const code = ` +initialState = {count:1}; +{state.count} + `; + const actual = mount(); + expect(actual.html()).toMatchSnapshot(); +}); + +it('should update state on setState', done => { + const code = ` +initialState = {count:1}; +setTimeout(() => state.count === 1 && setState({count:2})); + + `; + const actual = mount(); + + actual.find('button').simulate('click'); + + setTimeout(() => { + try { + expect(actual.html()).toMatchSnapshot(); + done(); + } catch (err) { + done.fail(err); + } + }); +}); diff --git a/src/rsg-components/ReactExample/__snapshots__/ReactExample.spec.js.snap b/src/rsg-components/ReactExample/__snapshots__/ReactExample.spec.js.snap new file mode 100644 index 000000000..2b8b201cf --- /dev/null +++ b/src/rsg-components/ReactExample/__snapshots__/ReactExample.spec.js.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render code 1`] = ` + + + +`; + +exports[`should set initialState before the first render 1`] = ` + + + 1 + + +`; + +exports[`should update state on setState 1`] = ` + + + +`; + +exports[`should wrap code in Fragment when it starts with < 1`] = ` + +
+ + + + +
+ +`; diff --git a/src/rsg-components/ReactExample/index.js b/src/rsg-components/ReactExample/index.js new file mode 100644 index 000000000..5179f7fd5 --- /dev/null +++ b/src/rsg-components/ReactExample/index.js @@ -0,0 +1 @@ +export { default } from 'rsg-components/ReactExample/ReactExample';