1
+ /**
2
+ * enzyme-adapter-react-16
3
+ * Temporary solution for React Fiber incompatibility issue with regard to the Context API
4
+ * @link https://github.com/diegohaz/constate/blob/93b7b5b469be4521784b51380f49e6589c3e56b9/test/ReactSixteenAdapter.js
5
+ * @Link https://github.com/airbnb/enzyme/issues/1509
6
+ */
7
+ /* eslint-disable */
8
+ import React from 'react'
9
+ import ReactDOM from 'react-dom'
10
+ import ReactDOMServer from 'react-dom/server'
11
+ import ShallowRenderer from 'react-test-renderer/shallow'
12
+ import TestUtils from 'react-dom/test-utils'
13
+ import { EnzymeAdapter } from 'enzyme'
14
+ import {
15
+ elementToTree ,
16
+ nodeTypeFromType ,
17
+ mapNativeEventNames ,
18
+ propFromEvent ,
19
+ assertDomAvailable ,
20
+ withSetStateAllowed ,
21
+ createRenderWrapper ,
22
+ createMountWrapper ,
23
+ propsWithKeysAndRef ,
24
+ } from 'enzyme-adapter-utils'
25
+ import { findCurrentFiberUsingSlowPath } from 'react-reconciler/reflection'
26
+
27
+ function ensureKeyOrUndefined ( key ) {
28
+ return key || ( key === '' ? '' : undefined ) ;
29
+ }
30
+
31
+ const HostRoot = 3
32
+ const ClassComponent = 2
33
+ const Fragment = 10
34
+ const FunctionalComponent = 1
35
+ const HostPortal = 4
36
+ const HostComponent = 5
37
+ const HostText = 6
38
+ const Mode = 11
39
+ const ContextConsumer = 12
40
+ const ContextProvider = 13
41
+
42
+ function nodeAndSiblingsArray ( nodeWithSibling ) {
43
+ const array = [ ]
44
+ let node = nodeWithSibling
45
+ while ( node != null ) {
46
+ array . push ( node )
47
+ node = node . sibling
48
+ }
49
+ return array
50
+ }
51
+
52
+ function flatten ( arr ) {
53
+ const result = [ ]
54
+ const stack = [ { i : 0 , array : arr } ]
55
+ while ( stack . length ) {
56
+ const n = stack . pop ( )
57
+ while ( n . i < n . array . length ) {
58
+ const el = n . array [ n . i ]
59
+ n . i += 1
60
+ if ( Array . isArray ( el ) ) {
61
+ stack . push ( n )
62
+ stack . push ( { i : 0 , array : el } )
63
+ break
64
+ }
65
+ result . push ( el )
66
+ }
67
+ }
68
+ return result
69
+ }
70
+
71
+ function toTree ( vnode ) {
72
+ if ( vnode == null ) {
73
+ return null
74
+ }
75
+ // TODO(lmr): I'm not really sure I understand whether or not this is what
76
+ // i should be doing, or if this is a hack for something i'm doing wrong
77
+ // somewhere else. Should talk to sebastian about this perhaps
78
+ const node = findCurrentFiberUsingSlowPath ( vnode )
79
+ switch ( node . tag ) {
80
+ case HostRoot : // 3
81
+ return toTree ( node . child )
82
+ case HostPortal : // 4
83
+ return toTree ( node . child )
84
+ case ClassComponent :
85
+ return {
86
+ nodeType : 'class' ,
87
+ type : node . type ,
88
+ props : { ...node . memoizedProps } ,
89
+ key : ensureKeyOrUndefined ( node . key ) ,
90
+ ref : node . ref ,
91
+ instance : node . stateNode ,
92
+ rendered : childrenToTree ( node . child ) ,
93
+ }
94
+ case FunctionalComponent : // 1
95
+ return {
96
+ nodeType : 'function' ,
97
+ type : node . type ,
98
+ props : { ...node . memoizedProps } ,
99
+ key : ensureKeyOrUndefined ( node . key ) ,
100
+ ref : node . ref ,
101
+ instance : null ,
102
+ rendered : childrenToTree ( node . child ) ,
103
+ }
104
+ case HostComponent : {
105
+ // 5
106
+ let renderedNodes = flatten ( nodeAndSiblingsArray ( node . child ) . map ( toTree ) )
107
+ if ( renderedNodes . length === 0 ) {
108
+ renderedNodes = [ node . memoizedProps . children ]
109
+ }
110
+ return {
111
+ nodeType : 'host' ,
112
+ type : node . type ,
113
+ props : { ...node . memoizedProps } ,
114
+ key : ensureKeyOrUndefined ( node . key ) ,
115
+ ref : node . ref ,
116
+ instance : node . stateNode ,
117
+ rendered : renderedNodes ,
118
+ }
119
+ }
120
+ case HostText : // 6
121
+ return node . memoizedProps
122
+ case Fragment : // 10
123
+ case Mode : // 11
124
+ case ContextProvider : // 13
125
+ case ContextConsumer : // 12
126
+ return childrenToTree ( node . child )
127
+ default :
128
+ throw new Error (
129
+ `Enzyme Internal Error: unknown node with tag ${ node . tag } ` ,
130
+ )
131
+ }
132
+ }
133
+
134
+ function childrenToTree ( node ) {
135
+ if ( ! node ) {
136
+ return null
137
+ }
138
+ const children = nodeAndSiblingsArray ( node )
139
+ if ( children . length === 0 ) {
140
+ return null
141
+ } else if ( children . length === 1 ) {
142
+ return toTree ( children [ 0 ] )
143
+ }
144
+ return flatten ( children . map ( toTree ) )
145
+ }
146
+
147
+ function nodeToHostNode ( _node ) {
148
+ // NOTE(lmr): node could be a function component
149
+ // which wont have an instance prop, but we can get the
150
+ // host node associated with its return value at that point.
151
+ // Although this breaks down if the return value is an array,
152
+ // as is possible with React 16.
153
+ let node = _node
154
+ while ( node && ! Array . isArray ( node ) && node . instance === null ) {
155
+ node = node . rendered
156
+ }
157
+ if ( Array . isArray ( node ) ) {
158
+ // TODO(lmr): throw warning regarding not being able to get a host node here
159
+ throw new Error ( 'Trying to get host node of an array' )
160
+ }
161
+ // if the SFC returned null effectively, there is no host node.
162
+ if ( ! node ) {
163
+ return null
164
+ }
165
+ return ReactDOM . findDOMNode ( node . instance )
166
+ }
167
+
168
+ class ReactSixteenAdapter extends EnzymeAdapter {
169
+ constructor ( ) {
170
+ super ( )
171
+ this . options = {
172
+ ...this . options ,
173
+ enableComponentDidUpdateOnSetState : true ,
174
+ }
175
+ }
176
+ createMountRenderer ( options ) {
177
+ assertDomAvailable ( 'mount' )
178
+ const domNode = options . attachTo || global . document . createElement ( 'div' )
179
+ let instance = null
180
+ return {
181
+ render ( el , context , callback ) {
182
+ if ( instance === null ) {
183
+ const ReactWrapperComponent = createMountWrapper ( el , options )
184
+ const wrappedEl = React . createElement ( ReactWrapperComponent , {
185
+ Component : el . type ,
186
+ props : el . props ,
187
+ context,
188
+ } )
189
+ instance = ReactDOM . render ( wrappedEl , domNode )
190
+ if ( typeof callback === 'function' ) {
191
+ callback ( )
192
+ }
193
+ } else {
194
+ instance . setChildProps ( el . props , context , callback )
195
+ }
196
+ } ,
197
+ unmount ( ) {
198
+ ReactDOM . unmountComponentAtNode ( domNode )
199
+ instance = null
200
+ } ,
201
+ getNode ( ) {
202
+ return instance ? toTree ( instance . _reactInternalFiber ) . rendered : null
203
+ } ,
204
+ simulateEvent ( node , event , mock ) {
205
+ const mappedEvent = mapNativeEventNames ( event )
206
+ const eventFn = TestUtils . Simulate [ mappedEvent ]
207
+ if ( ! eventFn ) {
208
+ throw new TypeError (
209
+ `ReactWrapper::simulate() event '${ event } ' does not exist` ,
210
+ )
211
+ }
212
+ // eslint-disable-next-line react/no-find-dom-node
213
+ eventFn ( nodeToHostNode ( node ) , mock )
214
+ } ,
215
+ batchedUpdates ( fn ) {
216
+ return fn ( )
217
+ // return ReactDOM.unstable_batchedUpdates(fn);
218
+ } ,
219
+ }
220
+ }
221
+
222
+ createShallowRenderer ( /* options */ ) {
223
+ const renderer = new ShallowRenderer ( )
224
+ let isDOM = false
225
+ let cachedNode = null
226
+ return {
227
+ render ( el , context ) {
228
+ cachedNode = el
229
+ /* eslint consistent-return: 0 */
230
+ if ( typeof el . type === 'string' ) {
231
+ isDOM = true
232
+ } else {
233
+ isDOM = false
234
+ return withSetStateAllowed ( ( ) => renderer . render ( el , context ) )
235
+ }
236
+ } ,
237
+ unmount ( ) {
238
+ renderer . unmount ( )
239
+ } ,
240
+ getNode ( ) {
241
+ if ( isDOM ) {
242
+ return elementToTree ( cachedNode )
243
+ }
244
+ const output = renderer . getRenderOutput ( )
245
+ return {
246
+ nodeType : nodeTypeFromType ( cachedNode . type ) ,
247
+ type : cachedNode . type ,
248
+ props : cachedNode . props ,
249
+ key : ensureKeyOrUndefined ( cachedNode . key ) ,
250
+ ref : cachedNode . ref ,
251
+ instance : renderer . _instance ,
252
+ rendered : Array . isArray ( output )
253
+ ? flatten ( output ) . map ( elementToTree )
254
+ : elementToTree ( output ) ,
255
+ }
256
+ } ,
257
+ simulateEvent ( node , event , ...args ) {
258
+ const handler = node . props [ propFromEvent ( event ) ]
259
+ if ( handler ) {
260
+ withSetStateAllowed ( ( ) => {
261
+ // TODO(lmr): create/use synthetic events
262
+ // TODO(lmr): emulate React's event propagation
263
+ // ReactDOM.unstable_batchedUpdates(() => {
264
+ handler ( ...args )
265
+ // });
266
+ } )
267
+ }
268
+ } ,
269
+ batchedUpdates ( fn ) {
270
+ return fn ( )
271
+ // return ReactDOM.unstable_batchedUpdates(fn);
272
+ } ,
273
+ }
274
+ }
275
+
276
+ createStringRenderer ( options ) {
277
+ return {
278
+ render ( el , context ) {
279
+ if (
280
+ options . context &&
281
+ ( el . type . contextTypes || options . childContextTypes )
282
+ ) {
283
+ const childContextTypes = {
284
+ ...( el . type . contextTypes || { } ) ,
285
+ ...options . childContextTypes ,
286
+ }
287
+ const ContextWrapper = createRenderWrapper (
288
+ el ,
289
+ context ,
290
+ childContextTypes ,
291
+ )
292
+ return ReactDOMServer . renderToStaticMarkup (
293
+ React . createElement ( ContextWrapper ) ,
294
+ )
295
+ }
296
+ return ReactDOMServer . renderToStaticMarkup ( el )
297
+ } ,
298
+ }
299
+ }
300
+
301
+ // Provided a bag of options, return an `EnzymeRenderer`. Some options can be implementation
302
+ // specific, like `attach` etc. for React, but not part of this interface explicitly.
303
+ // eslint-disable-next-line class-methods-use-this, no-unused-vars
304
+ createRenderer ( options ) {
305
+ switch ( options . mode ) {
306
+ case EnzymeAdapter . MODES . MOUNT :
307
+ return this . createMountRenderer ( options )
308
+ case EnzymeAdapter . MODES . SHALLOW :
309
+ return this . createShallowRenderer ( options )
310
+ case EnzymeAdapter . MODES . STRING :
311
+ return this . createStringRenderer ( options )
312
+ default :
313
+ throw new Error (
314
+ `Enzyme Internal Error: Unrecognized mode: ${ options . mode } ` ,
315
+ )
316
+ }
317
+ }
318
+
319
+ // converts an RSTNode to the corresponding JSX Pragma Element. This will be needed
320
+ // in order to implement the `Wrapper.mount()` and `Wrapper.shallow()` methods, but should
321
+ // be pretty straightforward for people to implement.
322
+ // eslint-disable-next-line class-methods-use-this, no-unused-vars
323
+ nodeToElement ( node ) {
324
+ if ( ! node || typeof node !== 'object' ) return null
325
+ return React . createElement ( node . type , propsWithKeysAndRef ( node ) )
326
+ }
327
+
328
+ elementToNode ( element ) {
329
+ return elementToTree ( element )
330
+ }
331
+
332
+ nodeToHostNode ( node ) {
333
+ return nodeToHostNode ( node )
334
+ }
335
+
336
+ isValidElement ( element ) {
337
+ return React . isValidElement ( element )
338
+ }
339
+
340
+ createElement ( ...args ) {
341
+ return React . createElement ( ...args )
342
+ }
343
+ }
344
+
345
+ module . exports = ReactSixteenAdapter
346
+
347
+ // PS: Fuck you enzyme! https://github.com/airbnb/enzyme/issues/1509
0 commit comments