UNPKG

7.61 kBJavaScriptView Raw
1import React from 'react';
2import ReactDOM from 'react-dom';
3// eslint-disable-next-line import/no-unresolved, import/extensions
4import ReactDOMServer from 'react-dom/server';
5// eslint-disable-next-line import/no-unresolved, import/extensions
6import TestUtils from 'react-addons-test-utils';
7import values from 'object.values';
8import { isElement, isValidElementType } from 'react-is';
9import { EnzymeAdapter } from 'enzyme';
10import {
11 displayNameOfNode,
12 elementToTree,
13 mapNativeEventNames,
14 propFromEvent,
15 withSetStateAllowed,
16 assertDomAvailable,
17 createRenderWrapper,
18 createMountWrapper,
19 propsWithKeysAndRef,
20 ensureKeyOrUndefined,
21} from 'enzyme-adapter-utils';
22
23function typeToNodeType(type) {
24 if (typeof type === 'function') {
25 if (type.prototype && typeof type.prototype.render === 'function') {
26 return 'class';
27 }
28 return 'function';
29 }
30 return 'host';
31}
32
33function instanceToTree(inst) {
34 if (!inst || typeof inst !== 'object') {
35 return inst;
36 }
37 const el = inst._currentElement;
38 if (!el) {
39 return null;
40 }
41 if (typeof el !== 'object') {
42 return el;
43 }
44 if (inst._tag) {
45 if (typeof el !== 'object') {
46 return el;
47 }
48 const children = inst._renderedChildren || { '.0': el.props.children };
49 return {
50 nodeType: 'host',
51 type: el.type,
52 props: el.props,
53 key: ensureKeyOrUndefined(el.key),
54 ref: el.ref,
55 instance: ReactDOM.findDOMNode(inst.getPublicInstance()) || null,
56 rendered: values(children).map(instanceToTree),
57 };
58 }
59 if (inst._renderedComponent) {
60 return {
61 nodeType: typeToNodeType(el.type),
62 type: el.type,
63 props: el.props,
64 key: ensureKeyOrUndefined(el.key),
65 ref: el.ref,
66 instance: inst._instance || null,
67 rendered: instanceToTree(inst._renderedComponent),
68 };
69 }
70 throw new Error('Enzyme Internal Error: unknown instance encountered');
71}
72
73class ReactFourteenAdapter extends EnzymeAdapter {
74 constructor() {
75 super();
76
77 const { lifecycles } = this.options;
78 this.options = {
79 ...this.options,
80 supportPrevContextArgumentOfComponentDidUpdate: true, // TODO: remove, semver-major
81 lifecycles: {
82 ...lifecycles,
83 componentDidUpdate: {
84 prevContext: true,
85 },
86 },
87 };
88 }
89
90 createMountRenderer(options) {
91 assertDomAvailable('mount');
92 const domNode = options.attachTo || global.document.createElement('div');
93 let instance = null;
94 const adapter = this;
95 return {
96 render(el, context, callback) {
97 if (instance === null) {
98 const { type, props, ref } = el;
99 const wrapperProps = {
100 Component: type,
101 props,
102 context,
103 ...(ref && { ref }),
104 };
105 const ReactWrapperComponent = createMountWrapper(el, { ...options, adapter });
106 const wrappedEl = React.createElement(ReactWrapperComponent, wrapperProps);
107 instance = ReactDOM.render(wrappedEl, domNode);
108 if (typeof callback === 'function') {
109 callback();
110 }
111 } else {
112 instance.setChildProps(el.props, context, callback);
113 }
114 },
115 unmount() {
116 ReactDOM.unmountComponentAtNode(domNode);
117 instance = null;
118 },
119 getNode() {
120 return instance ? instanceToTree(instance._reactInternalInstance).rendered : null;
121 },
122 simulateEvent(node, event, mock) {
123 const mappedEvent = mapNativeEventNames(event);
124 const eventFn = TestUtils.Simulate[mappedEvent];
125 if (!eventFn) {
126 throw new TypeError(`ReactWrapper::simulate() event '${event}' does not exist`);
127 }
128 // eslint-disable-next-line react/no-find-dom-node
129 eventFn(ReactDOM.findDOMNode(node.instance), mock);
130 },
131 batchedUpdates(fn) {
132 return ReactDOM.unstable_batchedUpdates(fn);
133 },
134 };
135 }
136
137 createShallowRenderer(/* options */) {
138 const renderer = TestUtils.createRenderer();
139 let isDOM = false;
140 let cachedNode = null;
141 return {
142 render(el, context) {
143 cachedNode = el;
144 /* eslint consistent-return: 0 */
145 if (typeof el.type === 'string') {
146 isDOM = true;
147 } else {
148 isDOM = false;
149 return withSetStateAllowed(() => renderer.render(el, context));
150 }
151 },
152 unmount() {
153 renderer.unmount();
154 },
155 getNode() {
156 if (isDOM) {
157 return elementToTree(cachedNode);
158 }
159 const output = renderer.getRenderOutput();
160 return {
161 nodeType: typeToNodeType(cachedNode.type),
162 type: cachedNode.type,
163 props: cachedNode.props,
164 key: ensureKeyOrUndefined(cachedNode.key),
165 ref: cachedNode.ref,
166 instance: renderer._instance._instance,
167 rendered: elementToTree(output),
168 };
169 },
170 simulateEvent(node, event, ...args) {
171 const handler = node.props[propFromEvent(event)];
172 if (handler) {
173 withSetStateAllowed(() => {
174 // TODO(lmr): create/use synthetic events
175 // TODO(lmr): emulate React's event propagation
176 ReactDOM.unstable_batchedUpdates(() => {
177 handler(...args);
178 });
179 });
180 }
181 },
182 batchedUpdates(fn) {
183 return withSetStateAllowed(() => ReactDOM.unstable_batchedUpdates(fn));
184 },
185 };
186 }
187
188 createStringRenderer(options) {
189 return {
190 render(el, context) {
191 if (options.context && (el.type.contextTypes || options.childContextTypes)) {
192 const childContextTypes = {
193 ...(el.type.contextTypes || {}),
194 ...options.childContextTypes,
195 };
196 const ContextWrapper = createRenderWrapper(el, context, childContextTypes);
197 return ReactDOMServer.renderToStaticMarkup(React.createElement(ContextWrapper));
198 }
199 return ReactDOMServer.renderToStaticMarkup(el);
200 },
201 };
202 }
203
204 // Provided a bag of options, return an `EnzymeRenderer`. Some options can be implementation
205 // specific, like `attach` etc. for React, but not part of this interface explicitly.
206 // eslint-disable-next-line class-methods-use-this, no-unused-vars
207 createRenderer(options) {
208 switch (options.mode) {
209 case EnzymeAdapter.MODES.MOUNT: return this.createMountRenderer(options);
210 case EnzymeAdapter.MODES.SHALLOW: return this.createShallowRenderer(options);
211 case EnzymeAdapter.MODES.STRING: return this.createStringRenderer(options);
212 default:
213 throw new Error(`Enzyme Internal Error: Unrecognized mode: ${options.mode}`);
214 }
215 }
216
217 // converts an RSTNode to the corresponding JSX Pragma Element. This will be needed
218 // in order to implement the `Wrapper.mount()` and `Wrapper.shallow()` methods, but should
219 // be pretty straightforward for people to implement.
220 // eslint-disable-next-line class-methods-use-this, no-unused-vars
221 nodeToElement(node) {
222 if (!node || typeof node !== 'object') return null;
223 return React.createElement(node.type, propsWithKeysAndRef(node));
224 }
225
226 elementToNode(element) {
227 return elementToTree(element);
228 }
229
230 nodeToHostNode(node) {
231 return ReactDOM.findDOMNode(node.instance);
232 }
233
234 displayNameOfNode(node) {
235 return displayNameOfNode(node);
236 }
237
238 isValidElement(element) {
239 return isElement(element);
240 }
241
242 isValidElementType(object) {
243 return isValidElementType(object);
244 }
245
246 createElement(...args) {
247 return React.createElement(...args);
248 }
249}
250
251module.exports = ReactFourteenAdapter;