react-rotary-knob-react19
Version:
Skinnable Rotary Knob for React
339 lines (322 loc) • 10 kB
JavaScript
/* eslint-disable */
import React from 'react'
import ReactDOM from 'react-dom'
import ReactDOMServer from 'react-dom/server'
import ShallowRenderer from 'react-test-renderer/shallow'
import TestUtils from 'react-dom/test-utils'
import { EnzymeAdapter } from 'enzyme'
import {
elementToTree,
nodeTypeFromType,
mapNativeEventNames,
propFromEvent,
assertDomAvailable,
withSetStateAllowed,
createRenderWrapper,
createMountWrapper,
propsWithKeysAndRef,
} from 'enzyme-adapter-utils'
import { findCurrentFiberUsingSlowPath } from 'react-reconciler/reflection'
function ensureKeyOrUndefined(key) {
return key || (key === '' ? '' : undefined);
}
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