UNPKG

@pisano/enzyme-adapter-react-16

Version:

JavaScript Testing utilities for React

321 lines (304 loc) 9.79 kB
/* eslint no-use-before-define: 0 */ import React from 'react'; import ReactDOM from 'react-dom'; // eslint-disable-next-line import/no-unresolved, import/extensions import ReactDOMServer from 'react-dom/server'; // eslint-disable-next-line import/no-unresolved, import/extensions import ShallowRenderer from 'react-test-renderer/shallow'; // eslint-disable-next-line import/no-unresolved, import/extensions import TestUtils from 'react-dom/test-utils'; import { EnzymeAdapter } from '@pisano/enzyme'; import { elementToTree, nodeTypeFromType, mapNativeEventNames, propFromEvent, assertDomAvailable, withSetStateAllowed, createRenderWrapper, createMountWrapper, propsWithKeysAndRef, ensureKeyOrUndefined, } from '@pisano/enzyme-adapter-utils'; import { findCurrentFiberUsingSlowPath } from 'react-reconciler/reflection'; const HostRoot = 3; const ClassComponent = 2; const Fragment = 10; const FunctionalComponent = 1; const HostPortal = 4; const HostComponent = 5; const HostText = 6; const Mode = 11; const ContextConsumer = 12; const ContextProvider = 13; function nodeAndSiblingsArray(nodeWithSibling) { const array = []; let node = nodeWithSibling; while (node != null) { array.push(node); node = node.sibling; } return array; } function flatten(arr) { const result = []; const stack = [{ i: 0, array: arr }]; while (stack.length) { const n = stack.pop(); while (n.i < n.array.length) { const el = n.array[n.i]; n.i += 1; if (Array.isArray(el)) { stack.push(n); stack.push({ i: 0, array: el }); break; } result.push(el); } } return result; } function toTree(vnode) { if (vnode == null) { return null; } // TODO(lmr): I'm not really sure I understand whether or not this is what // i should be doing, or if this is a hack for something i'm doing wrong // somewhere else. Should talk to sebastian about this perhaps const node = findCurrentFiberUsingSlowPath(vnode); switch (node.tag) { case HostRoot: // 3 return toTree(node.child); case HostPortal: // 4 return toTree(node.child); case ClassComponent: return { nodeType: 'class', type: node.type, props: { ...node.memoizedProps }, key: ensureKeyOrUndefined(node.key), ref: node.ref, instance: node.stateNode, rendered: childrenToTree(node.child), }; case FunctionalComponent: // 1 return { nodeType: 'function', type: node.type, props: { ...node.memoizedProps }, key: ensureKeyOrUndefined(node.key), ref: node.ref, instance: null, rendered: childrenToTree(node.child), }; case HostComponent: { // 5 let renderedNodes = flatten(nodeAndSiblingsArray(node.child).map(toTree)); if (renderedNodes.length === 0) { renderedNodes = [node.memoizedProps.children]; } return { nodeType: 'host', type: node.type, props: { ...node.memoizedProps }, key: ensureKeyOrUndefined(node.key), ref: node.ref, instance: node.stateNode, rendered: renderedNodes, }; } case HostText: // 6 return node.memoizedProps; case Fragment: // 10 case Mode: // 11 case ContextProvider: // 13 case ContextConsumer: // 12 return childrenToTree(node.child); default: throw new Error(`Enzyme Internal Error: unknown node with tag ${node.tag}`); } } function childrenToTree(node) { if (!node) { return null; } const children = nodeAndSiblingsArray(node); if (children.length === 0) { return null; } else if (children.length === 1) { return toTree(children[0]); } return flatten(children.map(toTree)); } function nodeToHostNode(_node) { // NOTE(lmr): node could be a function component // which wont have an instance prop, but we can get the // host node associated with its return value at that point. // Although this breaks down if the return value is an array, // as is possible with React 16. let node = _node; while (node && !Array.isArray(node) && node.instance === null) { node = node.rendered; } if (Array.isArray(node)) { // TODO(lmr): throw warning regarding not being able to get a host node here throw new Error('Trying to get host node of an array'); } // if the SFC returned null effectively, there is no host node. if (!node) { return null; } return ReactDOM.findDOMNode(node.instance); } class ReactSixteenAdapter extends EnzymeAdapter { constructor() { super(); this.options = { ...this.options, enableComponentDidUpdateOnSetState: true, }; } createMountRenderer(options) { assertDomAvailable('mount'); const domNode = options.attachTo || global.document.createElement('div'); let instance = null; return { render(el, context, callback) { if (instance === null) { const ReactWrapperComponent = createMountWrapper(el, options); const wrappedEl = React.createElement(ReactWrapperComponent, { Component: el.type, props: el.props, context, }); instance = ReactDOM.render(wrappedEl, domNode); if (typeof callback === 'function') { callback(); } } else { instance.setChildProps(el.props, context, callback); } }, unmount() { ReactDOM.unmountComponentAtNode(domNode); instance = null; }, getNode() { return instance ? toTree(instance._reactInternalFiber).rendered : null; }, simulateEvent(node, event, mock) { const mappedEvent = mapNativeEventNames(event); const eventFn = TestUtils.Simulate[mappedEvent]; if (!eventFn) { throw new TypeError(`ReactWrapper::simulate() event '${event}' does not exist`); } // eslint-disable-next-line react/no-find-dom-node eventFn(nodeToHostNode(node), mock); }, batchedUpdates(fn) { return fn(); // return ReactDOM.unstable_batchedUpdates(fn); }, }; } createShallowRenderer(/* options */) { const renderer = new ShallowRenderer(); let isDOM = false; let cachedNode = null; return { render(el, context) { cachedNode = el; /* eslint consistent-return: 0 */ if (typeof el.type === 'string') { isDOM = true; } else { isDOM = false; return withSetStateAllowed(() => renderer.render(el, context)); } }, unmount() { renderer.unmount(); }, getNode() { if (isDOM) { return elementToTree(cachedNode); } const output = renderer.getRenderOutput(); return { nodeType: nodeTypeFromType(cachedNode.type), type: cachedNode.type, props: cachedNode.props, key: ensureKeyOrUndefined(cachedNode.key), ref: cachedNode.ref, instance: renderer._instance, rendered: Array.isArray(output) ? flatten(output).map(elementToTree) : elementToTree(output), }; }, simulateEvent(node, event, ...args) { const handler = node.props[propFromEvent(event)]; if (handler) { withSetStateAllowed(() => { // TODO(lmr): create/use synthetic events // TODO(lmr): emulate React's event propagation // ReactDOM.unstable_batchedUpdates(() => { handler(...args); // }); }); } }, batchedUpdates(fn) { return fn(); // return ReactDOM.unstable_batchedUpdates(fn); }, }; } createStringRenderer(options) { return { render(el, context) { if (options.context && (el.type.contextTypes || options.childContextTypes)) { const childContextTypes = { ...(el.type.contextTypes || {}), ...options.childContextTypes, }; const ContextWrapper = createRenderWrapper(el, context, childContextTypes); return ReactDOMServer.renderToStaticMarkup(React.createElement(ContextWrapper)); } return ReactDOMServer.renderToStaticMarkup(el); }, }; } // Provided a bag of options, return an `EnzymeRenderer`. Some options can be implementation // specific, like `attach` etc. for React, but not part of this interface explicitly. // eslint-disable-next-line class-methods-use-this, no-unused-vars createRenderer(options) { switch (options.mode) { case EnzymeAdapter.MODES.MOUNT: return this.createMountRenderer(options); case EnzymeAdapter.MODES.SHALLOW: return this.createShallowRenderer(options); case EnzymeAdapter.MODES.STRING: return this.createStringRenderer(options); default: throw new Error(`Enzyme Internal Error: Unrecognized mode: ${options.mode}`); } } // converts an RSTNode to the corresponding JSX Pragma Element. This will be needed // in order to implement the `Wrapper.mount()` and `Wrapper.shallow()` methods, but should // be pretty straightforward for people to implement. // eslint-disable-next-line class-methods-use-this, no-unused-vars nodeToElement(node) { if (!node || typeof node !== 'object') return null; return React.createElement(node.type, propsWithKeysAndRef(node)); } elementToNode(element) { return elementToTree(element); } nodeToHostNode(node) { return nodeToHostNode(node); } isValidElement(element) { return React.isValidElement(element); } createElement(...args) { return React.createElement(...args); } } module.exports = ReactSixteenAdapter;