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

[enzyme-adapter-react-16] [New] mount: add hydrateIn option #1707

Merged
merged 1 commit into from
Jul 7, 2018
Merged
Show file tree
Hide file tree
Changes from all 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
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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

would there ever be a case when both are present? Do we just want to not allow this and then hydrate -> attach on pre16 versions?

Copy link
Member Author

Choose a reason for hiding this comment

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

So, I’ve normalized the options here - which means it will end up having both the best time it hits this code path. So altho users really shouldn’t supply both, i cant think of how to differentiate between user options and things passed in via shallow calls or adapter options etc.

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