From 8e8d368a77feaa6d5ba7552d5b2a8f8cf6b43f7a Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Fri, 6 Jul 2018 15:37:12 -0700 Subject: [PATCH] [enzyme-adapter-react-16] [New] `mount`: add `hydrateIn` option Fixes #1316 --- docs/api/ReactWrapper/detach.md | 10 +++- .../src/ReactSixteenAdapter.js | 7 ++- .../enzyme-test-suite/test/Adapter-spec.jsx | 49 +++++++++++-------- packages/enzyme/src/Utils.js | 16 ++++++ 4 files changed, 57 insertions(+), 25 deletions(-) diff --git a/docs/api/ReactWrapper/detach.md b/docs/api/ReactWrapper/detach.md index 89999d377..82ff1cf64 100644 --- a/docs/api/ReactWrapper/detach.md +++ b/docs/api/ReactWrapper/detach.md @@ -3,12 +3,12 @@ Detaches the react tree from the DOM. Runs `ReactDOM.unmountComponentAtNode()` under the hood. This method will most commonly be used as a "cleanup" method if you decide to use the -`attachTo` option in `mount(node, options)`. +`attachTo` or `hydrateIn` option in `mount(node, options)`. The method is intentionally not "fluent" (in that it doesn't return `this`) because you should not be doing anything with this wrapper after this method is called. -Using the `attachTo` is not generally recommended unless it is absolutely necessary to test +Using `attachTo`/`hydrateIn` is not generally recommended unless it is absolutely necessary to test something. It is your responsibility to clean up after yourself at the end of the test if you do decide to use it, though. @@ -21,6 +21,10 @@ With the `attachTo` option, you can mount components to attached DOM elements: // render a component directly into document.body const wrapper = mount(, { attachTo: document.body }); +// Or, with the `hydrateIn` option, you can mount components on top of existing DOM elements: +// hydrate a component directly onto document.body +const hydratedWrapper = mount(, { hydrateIn: document.body }); + // we can see that the component is rendered into the document expect(wrapper.find('.in-bar')).to.have.length(1); expect(document.body.childNodes).to.have.length(1); @@ -44,6 +48,8 @@ expect(div.childNodes).to.have.length(0); // mount a component passing div into the `attachTo` option const wrapper = mount(, { attachTo: div }); +// or, mount a component passing div into the `hydrateIn` option +const hydratedWrapper = mount(, { hydrateIn: div }); // we can see now the component is rendered into the document expect(wrapper.find('.in-foo')).to.have.length(1); diff --git a/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js b/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js index 68ba2c1d6..d952234f7 100644 --- a/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js +++ b/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js @@ -175,7 +175,8 @@ class ReactSixteenAdapter extends EnzymeAdapter { } createMountRenderer(options) { assertDomAvailable('mount'); - const domNode = options.attachTo || global.document.createElement('div'); + const { attachTo, hydrateIn } = options; + const domNode = hydrateIn || attachTo || global.document.createElement('div'); let instance = null; return { render(el, context, callback) { @@ -189,7 +190,9 @@ class ReactSixteenAdapter extends EnzymeAdapter { }; const ReactWrapperComponent = createMountWrapper(el, options); const wrappedEl = React.createElement(ReactWrapperComponent, wrapperProps); - instance = ReactDOM.render(wrappedEl, domNode); + instance = hydrateIn + ? ReactDOM.hydrate(wrappedEl, domNode) + : ReactDOM.render(wrappedEl, domNode); if (typeof callback === 'function') { callback(); } diff --git a/packages/enzyme-test-suite/test/Adapter-spec.jsx b/packages/enzyme-test-suite/test/Adapter-spec.jsx index 66835ae7d..109ce39c4 100644 --- a/packages/enzyme-test-suite/test/Adapter-spec.jsx +++ b/packages/enzyme-test-suite/test/Adapter-spec.jsx @@ -62,13 +62,13 @@ describe('Adapter', () => { }); describeWithDOM('mounted render', () => { - function hydratedTreeMatchesUnhydrated(element) { + function hydratedTreeMatchesUnhydrated(element, hydrate = false) { const markup = renderToString(element); const dom = jsdom.jsdom(`
${markup}
`); const rendererA = adapter.createRenderer({ mode: 'mount', - attachTo: dom.querySelector('#root'), + [hydrate ? 'hydrateIn' : 'attachTo']: dom.querySelector('#root'), }); rendererA.render(element); @@ -89,32 +89,39 @@ describe('Adapter', () => { expect(prettyFormat(nodeA)).to.equal(prettyFormat(nodeB)); } - it('hydrated trees match unhydrated trees', () => { - class Bam extends React.Component { - render() { return (
{this.props.children}
); } - } - class Foo extends React.Component { - render() { return ({this.props.children}); } - } - class One extends React.Component { - render() { return (); } - } - class Two extends React.Component { - render() { return (2); } - } - class Three extends React.Component { - render() { return (
); } - } - class Four extends React.Component { - render() { return ({'some string'}4{'another string'}); } - } + class Bam extends React.Component { + render() { return (
{this.props.children}
); } + } + class Foo extends React.Component { + render() { return ({this.props.children}); } + } + class One extends React.Component { + render() { return (); } + } + class Two extends React.Component { + render() { return (2); } + } + class Three extends React.Component { + render() { return (
); } + } + class Four extends React.Component { + render() { return ({'some string'}4{'another string'}); } + } + it('hydrated trees match unhydrated trees', () => { hydratedTreeMatchesUnhydrated(); hydratedTreeMatchesUnhydrated(); hydratedTreeMatchesUnhydrated(); hydratedTreeMatchesUnhydrated(); }); + itIf(REACT16, 'works with ReactDOM.hydrate', () => { + hydratedTreeMatchesUnhydrated(, true); + hydratedTreeMatchesUnhydrated(, true); + hydratedTreeMatchesUnhydrated(, true); + hydratedTreeMatchesUnhydrated(, true); + }); + it('treats mixed children correctly', () => { class Foo extends React.Component { render() { diff --git a/packages/enzyme/src/Utils.js b/packages/enzyme/src/Utils.js index 644bf0b1a..20e295f30 100644 --- a/packages/enzyme/src/Utils.js +++ b/packages/enzyme/src/Utils.js @@ -21,9 +21,25 @@ export function getAdapter(options = {}) { } export function makeOptions(options) { + const { attachTo, hydrateIn } = options; + + if (attachTo && hydrateIn && attachTo !== hydrateIn) { + throw new TypeError('If both the `attachTo` and `hydrateIn` options are provided, they must be === (for backwards compatibility)'); + } + + // neither present: both undefined + // only attachTo present: attachTo set, hydrateIn undefined + // only hydrateIn present: both set to hydrateIn + // both present (and ===, per above): both set to hydrateIn + const mountTargets = { + attachTo: hydrateIn || attachTo || undefined, + hydrateIn: hydrateIn || undefined, + }; + return { ...configuration.get(), ...options, + ...mountTargets, }; }