diff --git a/packages/react-test-renderer/package.json b/packages/react-test-renderer/package.json index 9fee2f49c0e37..e0f95987080d7 100644 --- a/packages/react-test-renderer/package.json +++ b/packages/react-test-renderer/package.json @@ -22,6 +22,7 @@ "object-assign": "^4.1.1", "prop-types": "^15.6.2", "react-is": "^16.8.6", + "react-shallow-renderer": "^16.12.0", "scheduler": "^0.19.0" }, "peerDependencies": { diff --git a/packages/react-test-renderer/shallow.js b/packages/react-test-renderer/shallow.js index fa548b35732db..3c455d9656ded 100644 --- a/packages/react-test-renderer/shallow.js +++ b/packages/react-test-renderer/shallow.js @@ -7,4 +7,4 @@ * @flow */ -export {default} from './src/ReactShallowRenderer'; +export {default} from 'react-shallow-renderer'; diff --git a/packages/react-test-renderer/src/ReactShallowRenderer.js b/packages/react-test-renderer/src/ReactShallowRenderer.js deleted file mode 100644 index 20751094fd28a..0000000000000 --- a/packages/react-test-renderer/src/ReactShallowRenderer.js +++ /dev/null @@ -1,863 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import * as React from 'react'; -import {isForwardRef, isMemo, ForwardRef} from 'react-is'; -import describeComponentFrame from 'shared/describeComponentFrame'; -import getComponentName from 'shared/getComponentName'; -import shallowEqual from 'shared/shallowEqual'; -import invariant from 'shared/invariant'; -import checkPropTypes from 'prop-types/checkPropTypes'; -import ReactSharedInternals from 'shared/ReactSharedInternals'; -import is from 'shared/objectIs'; - -import type {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactFiberHooks'; -import type { - ReactContext, - ReactEventResponderListener, -} from 'shared/ReactTypes'; -import type {ReactElement} from 'shared/ReactElementType'; - -type BasicStateAction = (S => S) | S; -type Dispatch = A => void; - -type Update = {| - action: A, - next: Update | null, -|}; - -type UpdateQueue = {| - first: Update | null, - dispatch: any, -|}; - -type Hook = {| - memoizedState: any, - queue: UpdateQueue | null, - next: Hook | null, -|}; - -const {ReactCurrentDispatcher} = ReactSharedInternals; - -const RE_RENDER_LIMIT = 25; - -const emptyObject = {}; -if (__DEV__) { - Object.freeze(emptyObject); -} - -// In DEV, this is the name of the currently executing primitive hook -let currentHookNameInDev: ?string; - -function areHookInputsEqual( - nextDeps: Array, - prevDeps: Array | null, -) { - if (prevDeps === null) { - if (__DEV__) { - console.error( - '%s received a final argument during this render, but not during ' + - 'the previous render. Even though the final argument is optional, ' + - 'its type cannot change between renders.', - currentHookNameInDev, - ); - } - return false; - } - - if (__DEV__) { - // Don't bother comparing lengths in prod because these arrays should be - // passed inline. - if (nextDeps.length !== prevDeps.length) { - console.error( - 'The final argument passed to %s changed size between renders. The ' + - 'order and size of this array must remain constant.\n\n' + - 'Previous: %s\n' + - 'Incoming: %s', - currentHookNameInDev, - `[${nextDeps.join(', ')}]`, - `[${prevDeps.join(', ')}]`, - ); - } - } - for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) { - if (is(nextDeps[i], prevDeps[i])) { - continue; - } - return false; - } - return true; -} - -class Updater { - constructor(renderer) { - this._renderer = renderer; - this._callbacks = []; - } - - _renderer: ReactShallowRenderer; - _callbacks: Array; - - _enqueueCallback(callback, publicInstance) { - if (typeof callback === 'function' && publicInstance) { - this._callbacks.push({ - callback, - publicInstance, - }); - } - } - - _invokeCallbacks() { - const callbacks = this._callbacks; - this._callbacks = []; - - callbacks.forEach(({callback, publicInstance}) => { - callback.call(publicInstance); - }); - } - - isMounted(publicInstance) { - return !!this._renderer._element; - } - - enqueueForceUpdate(publicInstance, callback, callerName) { - this._enqueueCallback(callback, publicInstance); - this._renderer._forcedUpdate = true; - this._renderer.render(this._renderer._element, this._renderer._context); - } - - enqueueReplaceState(publicInstance, completeState, callback, callerName) { - this._enqueueCallback(callback, publicInstance); - this._renderer._newState = completeState; - this._renderer.render(this._renderer._element, this._renderer._context); - } - - enqueueSetState(publicInstance, partialState, callback, callerName) { - this._enqueueCallback(callback, publicInstance); - const currentState = this._renderer._newState || publicInstance.state; - - if (typeof partialState === 'function') { - partialState = partialState.call( - publicInstance, - currentState, - publicInstance.props, - ); - } - - // Null and undefined are treated as no-ops. - if (partialState === null || partialState === undefined) { - return; - } - - this._renderer._newState = { - ...currentState, - ...partialState, - }; - - this._renderer.render(this._renderer._element, this._renderer._context); - } -} - -function createHook(): Hook { - return { - memoizedState: null, - queue: null, - next: null, - }; -} - -function basicStateReducer(state: S, action: BasicStateAction): S { - // $FlowFixMe: Flow doesn't like mixed types - return typeof action === 'function' ? action(state) : action; -} - -class ReactShallowRenderer { - static createRenderer = function() { - return new ReactShallowRenderer(); - }; - - constructor() { - this._reset(); - } - - _reset() { - this._context = null; - this._element = null; - this._instance = null; - this._newState = null; - this._rendered = null; - this._rendering = false; - this._forcedUpdate = false; - this._updater = new Updater(this); - this._dispatcher = this._createDispatcher(); - this._workInProgressHook = null; - this._firstWorkInProgressHook = null; - this._isReRender = false; - this._didScheduleRenderPhaseUpdate = false; - this._renderPhaseUpdates = null; - this._numberOfReRenders = 0; - } - - _context: null | Object; - _newState: null | Object; - _instance: any; - _element: null | ReactElement; - _rendered: null | mixed; - _updater: Updater; - _rendering: boolean; - _forcedUpdate: boolean; - _dispatcher: DispatcherType; - _workInProgressHook: null | Hook; - _firstWorkInProgressHook: null | Hook; - _renderPhaseUpdates: Map, Update> | null; - _isReRender: boolean; - _didScheduleRenderPhaseUpdate: boolean; - _numberOfReRenders: number; - - _validateCurrentlyRenderingComponent() { - invariant( - this._rendering && !this._instance, - 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' + - ' one of the following reasons:\n' + - '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' + - '2. You might be breaking the Rules of Hooks\n' + - '3. You might have more than one copy of React in the same app\n' + - 'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.', - ); - } - - _createDispatcher(): DispatcherType { - const useReducer = ( - reducer: (S, A) => S, - initialArg: I, - init?: I => S, - ): [S, Dispatch] => { - this._validateCurrentlyRenderingComponent(); - this._createWorkInProgressHook(); - const workInProgressHook: Hook = (this._workInProgressHook: any); - - if (this._isReRender) { - // This is a re-render. - const queue: UpdateQueue = (workInProgressHook.queue: any); - const dispatch: Dispatch = (queue.dispatch: any); - if (this._numberOfReRenders > 0) { - // Apply the new render phase updates to the previous current hook. - if (this._renderPhaseUpdates !== null) { - // Render phase updates are stored in a map of queue -> linked list - const firstRenderPhaseUpdate = this._renderPhaseUpdates.get(queue); - if (firstRenderPhaseUpdate !== undefined) { - (this._renderPhaseUpdates: any).delete(queue); - let newState = workInProgressHook.memoizedState; - let update = firstRenderPhaseUpdate; - do { - const action = update.action; - newState = reducer(newState, action); - update = update.next; - } while (update !== null); - workInProgressHook.memoizedState = newState; - return [newState, dispatch]; - } - } - return [workInProgressHook.memoizedState, dispatch]; - } - // Process updates outside of render - let newState = workInProgressHook.memoizedState; - let update = queue.first; - if (update !== null) { - do { - const action = update.action; - newState = reducer(newState, action); - update = update.next; - } while (update !== null); - queue.first = null; - workInProgressHook.memoizedState = newState; - } - return [newState, dispatch]; - } else { - let initialState; - if (reducer === basicStateReducer) { - // Special case for `useState`. - initialState = - typeof initialArg === 'function' - ? ((initialArg: any): () => S)() - : ((initialArg: any): S); - } else { - initialState = - init !== undefined ? init(initialArg) : ((initialArg: any): S); - } - workInProgressHook.memoizedState = initialState; - const queue: UpdateQueue = (workInProgressHook.queue = { - first: null, - dispatch: null, - }); - const dispatch: Dispatch = (queue.dispatch = (this._dispatchAction.bind( - this, - queue, - ): any)); - return [workInProgressHook.memoizedState, dispatch]; - } - }; - - const useState = ( - initialState: (() => S) | S, - ): [S, Dispatch>] => { - return useReducer( - basicStateReducer, - // useReducer has a special case to support lazy useState initializers - (initialState: any), - ); - }; - - const useMemo = ( - nextCreate: () => T, - deps: Array | void | null, - ): T => { - this._validateCurrentlyRenderingComponent(); - this._createWorkInProgressHook(); - - const nextDeps = deps !== undefined ? deps : null; - - if ( - this._workInProgressHook !== null && - this._workInProgressHook.memoizedState !== null - ) { - const prevState = this._workInProgressHook.memoizedState; - const prevDeps = prevState[1]; - if (nextDeps !== null) { - if (areHookInputsEqual(nextDeps, prevDeps)) { - return prevState[0]; - } - } - } - - const nextValue = nextCreate(); - (this._workInProgressHook: any).memoizedState = [nextValue, nextDeps]; - return nextValue; - }; - - const useRef = (initialValue: T): {|current: T|} => { - this._validateCurrentlyRenderingComponent(); - this._createWorkInProgressHook(); - const previousRef = (this._workInProgressHook: any).memoizedState; - if (previousRef === null) { - const ref = {current: initialValue}; - if (__DEV__) { - Object.seal(ref); - } - (this._workInProgressHook: any).memoizedState = ref; - return ref; - } else { - return previousRef; - } - }; - - const readContext = ( - context: ReactContext, - observedBits: void | number | boolean, - ): T => { - return context._currentValue; - }; - - const noOp = () => { - this._validateCurrentlyRenderingComponent(); - }; - - const identity = (fn: Function): Function => { - return fn; - }; - - const useResponder = ( - responder, - props, - ): ReactEventResponderListener => ({ - props: props, - responder, - }); - - // TODO: implement if we decide to keep the shallow renderer - const useTransition = ( - config, - ): [(callback: () => void) => void, boolean] => { - this._validateCurrentlyRenderingComponent(); - const startTransition = callback => { - callback(); - }; - return [startTransition, false]; - }; - - // TODO: implement if we decide to keep the shallow renderer - const useDeferredValue = (value: T, config): T => { - this._validateCurrentlyRenderingComponent(); - return value; - }; - - return { - readContext, - useCallback: (identity: any), - useContext: (context: ReactContext): T => { - this._validateCurrentlyRenderingComponent(); - return readContext(context); - }, - useDebugValue: noOp, - useEffect: noOp, - useImperativeHandle: noOp, - useLayoutEffect: noOp, - useMemo, - useReducer, - useRef, - useState, - useResponder, - useTransition, - useDeferredValue, - }; - } - - _dispatchAction(queue: UpdateQueue, action: A) { - invariant( - this._numberOfReRenders < RE_RENDER_LIMIT, - 'Too many re-renders. React limits the number of renders to prevent ' + - 'an infinite loop.', - ); - - if (this._rendering) { - // This is a render phase update. Stash it in a lazily-created map of - // queue -> linked list of updates. After this render pass, we'll restart - // and apply the stashed updates on top of the work-in-progress hook. - this._didScheduleRenderPhaseUpdate = true; - const update: Update = { - action, - next: null, - }; - let renderPhaseUpdates = this._renderPhaseUpdates; - if (renderPhaseUpdates === null) { - this._renderPhaseUpdates = renderPhaseUpdates = new Map(); - } - const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue); - if (firstRenderPhaseUpdate === undefined) { - renderPhaseUpdates.set(queue, update); - } else { - // Append the update to the end of the list. - let lastRenderPhaseUpdate = firstRenderPhaseUpdate; - while (lastRenderPhaseUpdate.next !== null) { - lastRenderPhaseUpdate = lastRenderPhaseUpdate.next; - } - lastRenderPhaseUpdate.next = update; - } - } else { - const update: Update = { - action, - next: null, - }; - - // Append the update to the end of the list. - let last = queue.first; - if (last === null) { - queue.first = update; - } else { - while (last.next !== null) { - last = last.next; - } - last.next = update; - } - - // Re-render now. - this.render(this._element, this._context); - } - } - - _createWorkInProgressHook(): Hook { - if (this._workInProgressHook === null) { - // This is the first hook in the list - if (this._firstWorkInProgressHook === null) { - this._isReRender = false; - this._firstWorkInProgressHook = this._workInProgressHook = createHook(); - } else { - // There's already a work-in-progress. Reuse it. - this._isReRender = true; - this._workInProgressHook = this._firstWorkInProgressHook; - } - } else { - if (this._workInProgressHook.next === null) { - this._isReRender = false; - // Append to the end of the list - this._workInProgressHook = (this - ._workInProgressHook: any).next = createHook(); - } else { - // There's already a work-in-progress. Reuse it. - this._isReRender = true; - this._workInProgressHook = this._workInProgressHook.next; - } - } - return this._workInProgressHook; - } - - _finishHooks(element: ReactElement, context: null | Object) { - if (this._didScheduleRenderPhaseUpdate) { - // Updates were scheduled during the render phase. They are stored in - // the `renderPhaseUpdates` map. Call the component again, reusing the - // work-in-progress hooks and applying the additional updates on top. Keep - // restarting until no more updates are scheduled. - this._didScheduleRenderPhaseUpdate = false; - this._numberOfReRenders += 1; - - // Start over from the beginning of the list - this._workInProgressHook = null; - this._rendering = false; - this.render(element, context); - } else { - this._workInProgressHook = null; - this._renderPhaseUpdates = null; - this._numberOfReRenders = 0; - } - } - - getMountedInstance() { - return this._instance; - } - - getRenderOutput() { - return this._rendered; - } - - render(element: ReactElement | null, context: null | Object = emptyObject) { - invariant( - React.isValidElement(element), - 'ReactShallowRenderer render(): Invalid component element.%s', - typeof element === 'function' - ? ' Instead of passing a component class, make sure to instantiate ' + - 'it by passing it to React.createElement.' - : '', - ); - element = ((element: any): ReactElement); - // Show a special message for host elements since it's a common case. - invariant( - typeof element.type !== 'string', - 'ReactShallowRenderer render(): Shallow rendering works only with custom ' + - 'components, not primitives (%s). Instead of calling `.render(el)` and ' + - 'inspecting the rendered output, look at `el.props` directly instead.', - element.type, - ); - invariant( - isForwardRef(element) || - typeof element.type === 'function' || - isMemo(element), - 'ReactShallowRenderer render(): Shallow rendering works only with custom ' + - 'components, but the provided element type was `%s`.', - Array.isArray(element.type) - ? 'array' - : element.type === null - ? 'null' - : typeof element.type, - ); - - if (this._rendering) { - return; - } - if (this._element != null && this._element.type !== element.type) { - this._reset(); - } - - const elementType = isMemo(element) ? element.type.type : element.type; - const previousElement = this._element; - - this._rendering = true; - this._element = element; - this._context = getMaskedContext(elementType.contextTypes, context); - - // Inner memo component props aren't currently validated in createElement. - if (isMemo(element) && elementType.propTypes) { - currentlyValidatingElement = element; - checkPropTypes( - elementType.propTypes, - element.props, - 'prop', - getComponentName(elementType), - getStackAddendum, - ); - } - - if (this._instance) { - this._updateClassComponent(elementType, element, this._context); - } else { - if (shouldConstruct(elementType)) { - this._instance = new elementType( - element.props, - this._context, - this._updater, - ); - if (typeof elementType.getDerivedStateFromProps === 'function') { - const partialState = elementType.getDerivedStateFromProps.call( - null, - element.props, - this._instance.state, - ); - if (partialState != null) { - this._instance.state = Object.assign( - {}, - this._instance.state, - partialState, - ); - } - } - - if (elementType.contextTypes) { - currentlyValidatingElement = element; - checkPropTypes( - elementType.contextTypes, - this._context, - 'context', - getName(elementType, this._instance), - getStackAddendum, - ); - - currentlyValidatingElement = null; - } - - this._mountClassComponent(elementType, element, this._context); - } else { - let shouldRender = true; - if (isMemo(element) && previousElement !== null) { - // This is a Memo component that is being re-rendered. - const compare = element.type.compare || shallowEqual; - if (compare(previousElement.props, element.props)) { - shouldRender = false; - } - } - if (shouldRender) { - const prevDispatcher = ReactCurrentDispatcher.current; - ReactCurrentDispatcher.current = this._dispatcher; - try { - // elementType could still be a ForwardRef if it was - // nested inside Memo. - if (elementType.$$typeof === ForwardRef) { - invariant( - typeof elementType.render === 'function', - 'forwardRef requires a render function but was given %s.', - typeof elementType.render, - ); - this._rendered = elementType.render.call( - undefined, - element.props, - element.ref, - ); - } else { - this._rendered = elementType(element.props, this._context); - } - } finally { - ReactCurrentDispatcher.current = prevDispatcher; - } - this._finishHooks(element, context); - } - } - } - - this._rendering = false; - this._updater._invokeCallbacks(); - - return this.getRenderOutput(); - } - - unmount() { - if (this._instance) { - if (typeof this._instance.componentWillUnmount === 'function') { - this._instance.componentWillUnmount(); - } - } - this._reset(); - } - - _mountClassComponent( - elementType: Function, - element: ReactElement, - context: null | Object, - ) { - this._instance.context = context; - this._instance.props = element.props; - this._instance.state = this._instance.state || null; - this._instance.updater = this._updater; - - if ( - typeof this._instance.UNSAFE_componentWillMount === 'function' || - typeof this._instance.componentWillMount === 'function' - ) { - const beforeState = this._newState; - - // In order to support react-lifecycles-compat polyfilled components, - // Unsafe lifecycles should not be invoked for components using the new APIs. - if ( - typeof elementType.getDerivedStateFromProps !== 'function' && - typeof this._instance.getSnapshotBeforeUpdate !== 'function' - ) { - if (typeof this._instance.componentWillMount === 'function') { - this._instance.componentWillMount(); - } - if (typeof this._instance.UNSAFE_componentWillMount === 'function') { - this._instance.UNSAFE_componentWillMount(); - } - } - - // setState may have been called during cWM - if (beforeState !== this._newState) { - this._instance.state = this._newState || emptyObject; - } - } - - this._rendered = this._instance.render(); - // Intentionally do not call componentDidMount() - // because DOM refs are not available. - } - - _updateClassComponent( - elementType: Function, - element: ReactElement, - context: null | Object, - ) { - const {props} = element; - - const oldState = this._instance.state || emptyObject; - const oldProps = this._instance.props; - - if (oldProps !== props) { - // In order to support react-lifecycles-compat polyfilled components, - // Unsafe lifecycles should not be invoked for components using the new APIs. - if ( - typeof elementType.getDerivedStateFromProps !== 'function' && - typeof this._instance.getSnapshotBeforeUpdate !== 'function' - ) { - if (typeof this._instance.componentWillReceiveProps === 'function') { - this._instance.componentWillReceiveProps(props, context); - } - if ( - typeof this._instance.UNSAFE_componentWillReceiveProps === 'function' - ) { - this._instance.UNSAFE_componentWillReceiveProps(props, context); - } - } - } - - // Read state after cWRP in case it calls setState - let state = this._newState || oldState; - if (typeof elementType.getDerivedStateFromProps === 'function') { - const partialState = elementType.getDerivedStateFromProps.call( - null, - props, - state, - ); - if (partialState != null) { - state = Object.assign({}, state, partialState); - } - } - - let shouldUpdate = true; - if (this._forcedUpdate) { - shouldUpdate = true; - this._forcedUpdate = false; - } else if (typeof this._instance.shouldComponentUpdate === 'function') { - shouldUpdate = !!this._instance.shouldComponentUpdate( - props, - state, - context, - ); - } else if ( - elementType.prototype && - elementType.prototype.isPureReactComponent - ) { - shouldUpdate = - !shallowEqual(oldProps, props) || !shallowEqual(oldState, state); - } - - if (shouldUpdate) { - // In order to support react-lifecycles-compat polyfilled components, - // Unsafe lifecycles should not be invoked for components using the new APIs. - if ( - typeof elementType.getDerivedStateFromProps !== 'function' && - typeof this._instance.getSnapshotBeforeUpdate !== 'function' - ) { - if (typeof this._instance.componentWillUpdate === 'function') { - this._instance.componentWillUpdate(props, state, context); - } - if (typeof this._instance.UNSAFE_componentWillUpdate === 'function') { - this._instance.UNSAFE_componentWillUpdate(props, state, context); - } - } - } - - this._instance.context = context; - this._instance.props = props; - this._instance.state = state; - this._newState = null; - - if (shouldUpdate) { - this._rendered = this._instance.render(); - } - // Intentionally do not call componentDidUpdate() - // because DOM refs are not available. - } -} - -let currentlyValidatingElement = null; - -function getDisplayName(element) { - if (element == null) { - return '#empty'; - } else if (typeof element === 'string' || typeof element === 'number') { - return '#text'; - } else if (typeof element.type === 'string') { - return element.type; - } else { - const elementType = isMemo(element) ? element.type.type : element.type; - return elementType.displayName || elementType.name || 'Unknown'; - } -} - -function getStackAddendum() { - let stack = ''; - if (currentlyValidatingElement) { - const name = getDisplayName(currentlyValidatingElement); - const owner = currentlyValidatingElement._owner; - stack += describeComponentFrame( - name, - currentlyValidatingElement._source, - owner && getComponentName(owner.type), - ); - } - return stack; -} - -function getName(type, instance) { - const constructor = instance && instance.constructor; - return ( - type.displayName || - (constructor && constructor.displayName) || - type.name || - (constructor && constructor.name) || - null - ); -} - -function shouldConstruct(Component) { - return !!(Component.prototype && Component.prototype.isReactComponent); -} - -function getMaskedContext(contextTypes, unmaskedContext) { - if (!contextTypes || !unmaskedContext) { - return emptyObject; - } - const context = {}; - for (let key in contextTypes) { - context[key] = unmaskedContext[key]; - } - return context; -} - -// This should probably be a default export and a named export. -// However, this not how any of other APIs are designed so doesn't line up -// with our build configs and makes it hard to properly support ES modules -// and CommonJS. -export default ReactShallowRenderer; diff --git a/yarn.lock b/yarn.lock index 83a4142f7f471..5a02e8407c9d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10856,6 +10856,15 @@ react-native-web@^0.11.5: prop-types "^15.6.0" react-timer-mixin "^0.13.4" +react-shallow-renderer@^16.12.0: + version "16.12.0" + resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.12.0.tgz#e68ca8fca7a616db42c36fdd6929723ba0bccef6" + integrity sha512-elIe2dhXJAO19IY1K3b4YWHBLnKE5wRH+PKx3AYzM6j2+N5UI+FGLKiRNFbLQcggrpBiy3GLhj7MqNUlRk9Z7g== + dependencies: + object-assign "^4.1.1" + prop-types "^15.7.2" + react-is "^16.12.0" + react-timer-mixin@^0.13.4: version "0.13.4" resolved "https://registry.yarnpkg.com/react-timer-mixin/-/react-timer-mixin-0.13.4.tgz#75a00c3c94c13abe29b43d63b4c65a88fc8264d3"