1 |
|
2 | import functionName from 'function.prototype.name';
|
3 | import React from 'react';
|
4 | import ReactDOM from 'react-dom';
|
5 |
|
6 | import ReactDOMServer from 'react-dom/server';
|
7 |
|
8 | import ShallowRenderer from 'react-test-renderer/shallow';
|
9 |
|
10 | import TestUtils from 'react-dom/test-utils';
|
11 | import checkPropTypes from 'prop-types/checkPropTypes';
|
12 | import {
|
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';
|
27 | import { EnzymeAdapter } from 'enzyme';
|
28 | import { typeOfNode } from 'enzyme/build/Utils';
|
29 | import {
|
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';
|
46 | import findCurrentFiberUsingSlowPath from './findCurrentFiberUsingSlowPath';
|
47 | import detectFiberTags from './detectFiberTags';
|
48 |
|
49 | const is164 = !!TestUtils.Simulate.touchStart;
|
50 | const is165 = !!TestUtils.Simulate.auxClick;
|
51 | const is166 = is165 && !React.unstable_AsyncMode;
|
52 |
|
53 |
|
54 | let FiberTags = null;
|
55 |
|
56 | function 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 |
|
66 | function 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 |
|
85 | function nodeTypeFromType(type) {
|
86 | if (type === Portal) {
|
87 | return 'portal';
|
88 | }
|
89 |
|
90 | return utilNodeTypeFromType(type);
|
91 | }
|
92 |
|
93 | function 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 |
|
112 | function toTree(vnode) {
|
113 | if (vnode == null) {
|
114 | return null;
|
115 | }
|
116 |
|
117 |
|
118 |
|
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 |
|
198 | function 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 |
|
212 | function nodeToHostNode(_node) {
|
213 |
|
214 |
|
215 |
|
216 |
|
217 |
|
218 | let node = _node;
|
219 | while (node && !Array.isArray(node) && node.instance === null) {
|
220 | node = node.rendered;
|
221 | }
|
222 | if (Array.isArray(node)) {
|
223 |
|
224 | throw new Error('Trying to get host node of an array');
|
225 | }
|
226 |
|
227 | if (!node) {
|
228 | return null;
|
229 | }
|
230 | return ReactDOM.findDOMNode(node.instance);
|
231 | }
|
232 |
|
233 | const eventOptions = {
|
234 | animation: true,
|
235 | pointerEvents: is164,
|
236 | auxClick: is165,
|
237 | };
|
238 |
|
239 | function getEmptyStateValue() {
|
240 |
|
241 |
|
242 |
|
243 |
|
244 |
|
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 |
|
255 | class ReactSixteenAdapter extends EnzymeAdapter {
|
256 | constructor() {
|
257 | super();
|
258 | const { lifecycles } = this.options;
|
259 | this.options = {
|
260 | ...this.options,
|
261 | enableComponentDidUpdateOnSetState: true,
|
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 |
|
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 |
|
343 | },
|
344 | };
|
345 | }
|
346 |
|
347 | createShallowRenderer() {
|
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 |
|
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)
|
365 | );
|
366 |
|
367 | const context = getMaskedContext(Component.contextTypes, unmaskedContext);
|
368 | if (!isStateful && typeof Component === 'function') {
|
369 | const wrappedEl = Object.assign(
|
370 | (...args) => Component(...args),
|
371 | Component,
|
372 | );
|
373 | return withSetStateAllowed(() => renderer.render({ ...el, type: wrappedEl }, context));
|
374 | }
|
375 | if (isStateful) {
|
376 |
|
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 |
|
437 |
|
438 |
|
439 | handler(...args);
|
440 |
|
441 | });
|
442 | }
|
443 | },
|
444 | batchedUpdates(fn) {
|
445 | return fn();
|
446 |
|
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 |
|
477 |
|
478 |
|
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 |
|
494 |
|
495 |
|
496 |
|
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 |
|
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 |
|
568 | module.exports = ReactSixteenAdapter;
|