Skip to content

Commit

Permalink
[enzyme-adapter-react-16] [New] mount: add hydrateIn option
Browse files Browse the repository at this point in the history
Fixes #1316
  • Loading branch information
ljharb committed Jul 6, 2018
1 parent 0b552b5 commit ee180af
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 25 deletions.
10 changes: 8 additions & 2 deletions docs/api/ReactWrapper/detach.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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(<Bar />, { 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(<Bar />, { 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);
Expand All @@ -44,6 +48,8 @@ expect(div.childNodes).to.have.length(0);

// mount a component passing div into the `attachTo` option
const wrapper = mount(<Foo />, { attachTo: div });
// or, mount a component passing div into the `hydrateIn` option
const hydratedWrapper = mount(<Foo />, { hydrateIn: div });

// we can see now the component is rendered into the document
expect(wrapper.find('.in-foo')).to.have.length(1);
Expand Down
7 changes: 5 additions & 2 deletions packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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();
}
Expand Down
49 changes: 28 additions & 21 deletions packages/enzyme-test-suite/test/Adapter-spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(`<div id="root">${markup}</div>`);

const rendererA = adapter.createRenderer({
mode: 'mount',
attachTo: dom.querySelector('#root'),
[hydrate ? 'hydrateIn' : 'attachTo']: dom.querySelector('#root'),
});

rendererA.render(element);
Expand All @@ -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 (<div>{this.props.children}</div>); }
}
class Foo extends React.Component {
render() { return (<Bam>{this.props.children}</Bam>); }
}
class One extends React.Component {
render() { return (<Foo><span><Foo /></span></Foo>); }
}
class Two extends React.Component {
render() { return (<Foo><span>2</span></Foo>); }
}
class Three extends React.Component {
render() { return (<Foo><span><div /></span></Foo>); }
}
class Four extends React.Component {
render() { return (<Foo><span>{'some string'}4{'another string'}</span></Foo>); }
}
class BamBam extends React.Component {
render() { return (<div>{this.props.children}</div>); }
}
class FooBar extends React.Component {
render() { return (<BamBam>{this.props.children}</BamBam>); }
}
class One extends React.Component {
render() { return (<FooBar><span><FooBar /></span></FooBar>); }
}
class Two extends React.Component {
render() { return (<FooBar><span>2</span></FooBar>); }
}
class Three extends React.Component {
render() { return (<FooBar><span><div /></span></FooBar>); }
}
class Four extends React.Component {
render() { return (<FooBar><span>{'some string'}4{'another string'}</span></FooBar>); }
}

it('hydrated trees match unhydrated trees', () => {
hydratedTreeMatchesUnhydrated(<One />);
hydratedTreeMatchesUnhydrated(<Two />);
hydratedTreeMatchesUnhydrated(<Three />);
hydratedTreeMatchesUnhydrated(<Four />);
});

itIf(REACT16, 'works with ReactDOM.hydrate', () => {
hydratedTreeMatchesUnhydrated(<One />, true);
hydratedTreeMatchesUnhydrated(<Two />, true);
hydratedTreeMatchesUnhydrated(<Three />, true);
hydratedTreeMatchesUnhydrated(<Four />, true);
});

it('treats mixed children correctly', () => {
class Foo extends React.Component {
render() {
Expand Down
16 changes: 16 additions & 0 deletions packages/enzyme/src/Utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}

Expand Down

0 comments on commit ee180af

Please sign in to comment.