UNPKG

14.4 kBJavaScriptView Raw
1/* eslint no-use-before-define: 0 */
2import React from 'react';
3import ReactDOM from 'react-dom';
4// eslint-disable-next-line import/no-unresolved
5import ReactDOMServer from 'react-dom/server';
6// eslint-disable-next-line import/no-unresolved
7import ShallowRenderer from 'react-test-renderer/shallow';
8// eslint-disable-next-line import/no-unresolved
9import TestUtils from 'react-dom/test-utils';
10import {
11 isElement,
12 isPortal,
13 isValidElementType,
14 Fragment,
15 Portal,
16} from 'react-is';
17import { EnzymeAdapter } from 'enzyme';
18import { typeOfNode } from 'enzyme/build/Utils';
19import {
20 displayNameOfNode,
21 elementToTree as utilElementToTree,
22 nodeTypeFromType as utilNodeTypeFromType,
23 mapNativeEventNames,
24 propFromEvent,
25 assertDomAvailable,
26 withSetStateAllowed,
27 createRenderWrapper,
28 createMountWrapper,
29 propsWithKeysAndRef,
30 ensureKeyOrUndefined,
31 simulateError,
32} from 'enzyme-adapter-utils';
33import { findCurrentFiberUsingSlowPath } from 'react-reconciler/reflection';
34
35const HostRoot = 3;
36const ClassComponent = 2;
37const FragmentType = 10;
38const FunctionalComponent = 1;
39const HostPortal = 4;
40const HostComponent = 5;
41const HostText = 6;
42const Mode = 11;
43
44function nodeAndSiblingsArray(nodeWithSibling) {
45 const array = [];
46 let node = nodeWithSibling;
47 while (node != null) {
48 array.push(node);
49 node = node.sibling;
50 }
51 return array;
52}
53
54const displayNamesByType = {
55 [Fragment]: 'Fragment',
56 [Portal]: 'Portal',
57};
58
59function flatten(arr) {
60 const result = [];
61 const stack = [{ i: 0, array: arr }];
62 while (stack.length) {
63 const n = stack.pop();
64 while (n.i < n.array.length) {
65 const el = n.array[n.i];
66 n.i += 1;
67 if (Array.isArray(el)) {
68 stack.push(n);
69 stack.push({ i: 0, array: el });
70 break;
71 }
72 result.push(el);
73 }
74 }
75 return result;
76}
77
78function nodeTypeFromType(type) {
79 if (type === Portal) {
80 return 'portal';
81 }
82
83 return utilNodeTypeFromType(type);
84}
85
86function elementToTree(el) {
87 if (!isPortal(el)) {
88 return utilElementToTree(el, elementToTree);
89 }
90
91 const { children, containerInfo } = el;
92 const props = { children, containerInfo };
93
94 return {
95 nodeType: 'portal',
96 type: Portal,
97 props,
98 key: ensureKeyOrUndefined(el.key),
99 ref: el.ref || null,
100 instance: null,
101 rendered: elementToTree(el.children),
102 };
103}
104
105function toTree(vnode) {
106 if (vnode == null) {
107 return null;
108 }
109 // TODO(lmr): I'm not really sure I understand whether or not this is what
110 // i should be doing, or if this is a hack for something i'm doing wrong
111 // somewhere else. Should talk to sebastian about this perhaps
112 const node = findCurrentFiberUsingSlowPath(vnode);
113 switch (node.tag) {
114 case HostRoot: // 3
115 return childrenToTree(node.child);
116 case HostPortal: { // 4
117 const {
118 stateNode: { containerInfo },
119 memoizedProps: children,
120 } = node;
121 const props = { containerInfo, children };
122 return {
123 nodeType: 'portal',
124 type: Portal,
125 props,
126 key: ensureKeyOrUndefined(node.key),
127 ref: node.ref,
128 instance: null,
129 rendered: childrenToTree(node.child),
130 };
131 }
132 case ClassComponent:
133 return {
134 nodeType: 'class',
135 type: node.type,
136 props: { ...node.memoizedProps },
137 key: ensureKeyOrUndefined(node.key),
138 ref: node.ref,
139 instance: node.stateNode,
140 rendered: childrenToTree(node.child),
141 };
142 case FunctionalComponent: // 1
143 return {
144 nodeType: 'function',
145 type: node.type,
146 props: { ...node.memoizedProps },
147 key: ensureKeyOrUndefined(node.key),
148 ref: node.ref,
149 instance: null,
150 rendered: childrenToTree(node.child),
151 };
152 case HostComponent: { // 5
153 let renderedNodes = flatten(nodeAndSiblingsArray(node.child).map(toTree));
154 if (renderedNodes.length === 0) {
155 renderedNodes = [node.memoizedProps.children];
156 }
157 return {
158 nodeType: 'host',
159 type: node.type,
160 props: { ...node.memoizedProps },
161 key: ensureKeyOrUndefined(node.key),
162 ref: node.ref,
163 instance: node.stateNode,
164 rendered: renderedNodes,
165 };
166 }
167 case HostText: // 6
168 return node.memoizedProps;
169 case FragmentType: // 10
170 case Mode: // 11
171 return childrenToTree(node.child);
172 default:
173 throw new Error(`Enzyme Internal Error: unknown node with tag ${node.tag}`);
174 }
175}
176
177function childrenToTree(node) {
178 if (!node) {
179 return null;
180 }
181 const children = nodeAndSiblingsArray(node);
182 if (children.length === 0) {
183 return null;
184 }
185 if (children.length === 1) {
186 return toTree(children[0]);
187 }
188 return flatten(children.map(toTree));
189}
190
191function nodeToHostNode(_node) {
192 // NOTE(lmr): node could be a function component
193 // which wont have an instance prop, but we can get the
194 // host node associated with its return value at that point.
195 // Although this breaks down if the return value is an array,
196 // as is possible with React 16.
197 let node = _node;
198 while (node && !Array.isArray(node) && node.instance === null) {
199 node = node.rendered;
200 }
201 if (Array.isArray(node)) {
202 // TODO(lmr): throw warning regarding not being able to get a host node here
203 throw new Error('Trying to get host node of an array');
204 }
205 // if the SFC returned null effectively, there is no host node.
206 if (!node) {
207 return null;
208 }
209 return ReactDOM.findDOMNode(node.instance);
210}
211
212const eventOptions = { animation: true };
213
214function getEmptyStateValue() {
215 // this handles a bug in React 16.0 - 16.2
216 // see https://github.com/facebook/react/commit/39be83565c65f9c522150e52375167568a2a1459
217 // also see https://github.com/facebook/react/pull/11965
218
219 // eslint-disable-next-line react/prefer-stateless-function
220 class EmptyState extends React.Component {
221 render() {
222 return null;
223 }
224 }
225 const testRenderer = new ShallowRenderer();
226 testRenderer.render(React.createElement(EmptyState));
227 return testRenderer._instance.state;
228}
229
230class ReactSixteenTwoAdapter extends EnzymeAdapter {
231 constructor() {
232 super();
233 const { lifecycles } = this.options;
234 this.options = {
235 ...this.options,
236 enableComponentDidUpdateOnSetState: true, // TODO: remove, semver-major
237 lifecycles: {
238 ...lifecycles,
239 componentDidUpdate: {
240 onSetState: true,
241 },
242 setState: {
243 skipsComponentDidUpdateOnNullish: true,
244 },
245 },
246 };
247 }
248
249 createMountRenderer(options) {
250 assertDomAvailable('mount');
251 const { attachTo, hydrateIn } = options;
252 const domNode = hydrateIn || attachTo || global.document.createElement('div');
253 let instance = null;
254 const adapter = this;
255 return {
256 render(el, context, callback) {
257 if (instance === null) {
258 const { type, props, ref } = el;
259 const wrapperProps = {
260 Component: type,
261 props,
262 context,
263 ...(ref && { ref }),
264 };
265 const ReactWrapperComponent = createMountWrapper(el, { ...options, adapter });
266 const wrappedEl = React.createElement(ReactWrapperComponent, wrapperProps);
267 instance = hydrateIn
268 ? ReactDOM.hydrate(wrappedEl, domNode)
269 : ReactDOM.render(wrappedEl, domNode);
270 if (typeof callback === 'function') {
271 callback();
272 }
273 } else {
274 instance.setChildProps(el.props, context, callback);
275 }
276 },
277 unmount() {
278 ReactDOM.unmountComponentAtNode(domNode);
279 instance = null;
280 },
281 getNode() {
282 return instance ? toTree(instance._reactInternalFiber).rendered : null;
283 },
284 simulateError(nodeHierarchy, rootNode, error) {
285 const { instance: catchingInstance } = nodeHierarchy
286 .find(x => x.instance && x.instance.componentDidCatch) || {};
287
288 simulateError(
289 error,
290 catchingInstance,
291 rootNode,
292 nodeHierarchy,
293 nodeTypeFromType,
294 adapter.displayNameOfNode,
295 );
296 },
297 simulateEvent(node, event, mock) {
298 const mappedEvent = mapNativeEventNames(event, eventOptions);
299 const eventFn = TestUtils.Simulate[mappedEvent];
300 if (!eventFn) {
301 throw new TypeError(`ReactWrapper::simulate() event '${event}' does not exist`);
302 }
303 // eslint-disable-next-line react/no-find-dom-node
304 eventFn(nodeToHostNode(node), mock);
305 },
306 batchedUpdates(fn) {
307 return fn();
308 // return ReactDOM.unstable_batchedUpdates(fn);
309 },
310 };
311 }
312
313 createShallowRenderer(/* options */) {
314 const adapter = this;
315 const renderer = new ShallowRenderer();
316 let isDOM = false;
317 let cachedNode = null;
318 return {
319 render(el, context) {
320 cachedNode = el;
321 /* eslint consistent-return: 0 */
322 if (typeof el.type === 'string') {
323 isDOM = true;
324 } else {
325 isDOM = false;
326 const { type: Component } = el;
327
328 const isStateful = Component.prototype && (
329 Component.prototype.isReactComponent
330 || Array.isArray(Component.__reactAutoBindPairs) // fallback for createClass components
331 );
332
333 if (!isStateful && typeof Component === 'function') {
334 const wrappedEl = Object.assign(
335 (...args) => Component(...args), // eslint-disable-line new-cap
336 Component,
337 );
338 return withSetStateAllowed(() => renderer.render({ ...el, type: wrappedEl }, context));
339 }
340 if (isStateful) {
341 // fix react bug; see implementation of `getEmptyStateValue`
342 const emptyStateValue = getEmptyStateValue();
343 if (emptyStateValue) {
344 Object.defineProperty(Component.prototype, 'state', {
345 configurable: true,
346 enumerable: true,
347 get() {
348 return null;
349 },
350 set(value) {
351 if (value !== emptyStateValue) {
352 Object.defineProperty(this, 'state', {
353 configurable: true,
354 enumerable: true,
355 value,
356 writable: true,
357 });
358 }
359 return true;
360 },
361 });
362 }
363 }
364 return withSetStateAllowed(() => renderer.render(el, context));
365 }
366 },
367 unmount() {
368 renderer.unmount();
369 },
370 getNode() {
371 if (isDOM) {
372 return elementToTree(cachedNode);
373 }
374 const output = renderer.getRenderOutput();
375 return {
376 nodeType: nodeTypeFromType(cachedNode.type),
377 type: cachedNode.type,
378 props: cachedNode.props,
379 key: ensureKeyOrUndefined(cachedNode.key),
380 ref: cachedNode.ref,
381 instance: renderer._instance,
382 rendered: Array.isArray(output)
383 ? flatten(output).map(el => elementToTree(el))
384 : elementToTree(output),
385 };
386 },
387 simulateError(nodeHierarchy, rootNode, error) {
388 simulateError(
389 error,
390 renderer._instance,
391 cachedNode,
392 nodeHierarchy.concat(cachedNode),
393 nodeTypeFromType,
394 adapter.displayNameOfNode,
395 );
396 },
397 simulateEvent(node, event, ...args) {
398 const handler = node.props[propFromEvent(event, eventOptions)];
399 if (handler) {
400 withSetStateAllowed(() => {
401 // TODO(lmr): create/use synthetic events
402 // TODO(lmr): emulate React's event propagation
403 // ReactDOM.unstable_batchedUpdates(() => {
404 handler(...args);
405 // });
406 });
407 }
408 },
409 batchedUpdates(fn) {
410 return fn();
411 // return ReactDOM.unstable_batchedUpdates(fn);
412 },
413 };
414 }
415
416 createStringRenderer(options) {
417 return {
418 render(el, context) {
419 if (options.context && (el.type.contextTypes || options.childContextTypes)) {
420 const childContextTypes = {
421 ...(el.type.contextTypes || {}),
422 ...options.childContextTypes,
423 };
424 const ContextWrapper = createRenderWrapper(el, context, childContextTypes);
425 return ReactDOMServer.renderToStaticMarkup(React.createElement(ContextWrapper));
426 }
427 return ReactDOMServer.renderToStaticMarkup(el);
428 },
429 };
430 }
431
432 // Provided a bag of options, return an `EnzymeRenderer`. Some options can be implementation
433 // specific, like `attach` etc. for React, but not part of this interface explicitly.
434 // eslint-disable-next-line class-methods-use-this, no-unused-vars
435 createRenderer(options) {
436 switch (options.mode) {
437 case EnzymeAdapter.MODES.MOUNT: return this.createMountRenderer(options);
438 case EnzymeAdapter.MODES.SHALLOW: return this.createShallowRenderer(options);
439 case EnzymeAdapter.MODES.STRING: return this.createStringRenderer(options);
440 default:
441 throw new Error(`Enzyme Internal Error: Unrecognized mode: ${options.mode}`);
442 }
443 }
444
445 // converts an RSTNode to the corresponding JSX Pragma Element. This will be needed
446 // in order to implement the `Wrapper.mount()` and `Wrapper.shallow()` methods, but should
447 // be pretty straightforward for people to implement.
448 // eslint-disable-next-line class-methods-use-this, no-unused-vars
449 nodeToElement(node) {
450 if (!node || typeof node !== 'object') return null;
451 return React.createElement(node.type, propsWithKeysAndRef(node));
452 }
453
454 elementToNode(element) {
455 return elementToTree(element);
456 }
457
458 nodeToHostNode(node) {
459 return nodeToHostNode(node);
460 }
461
462 displayNameOfNode(node) {
463 if (!node) return null;
464
465 const { type, $$typeof } = node;
466 return displayNamesByType[type || $$typeof] || displayNameOfNode(node);
467 }
468
469 isValidElement(element) {
470 return isElement(element);
471 }
472
473 isValidElementType(object) {
474 return isValidElementType(object);
475 }
476
477 isFragment(fragment) {
478 return typeOfNode(fragment) === Fragment;
479 }
480
481 createElement(...args) {
482 return React.createElement(...args);
483 }
484}
485
486module.exports = ReactSixteenTwoAdapter;