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