UNPKG

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