UNPKG

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