diff --git a/packages/react-noop-renderer/npm/persistent.js b/packages/react-noop-renderer/npm/persistent.js new file mode 100644 index 00000000000..14991d5371a --- /dev/null +++ b/packages/react-noop-renderer/npm/persistent.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-noop-renderer-persistent.production.min.js'); +} else { + module.exports = require('./cjs/react-noop-renderer-persistent.development.js'); +} diff --git a/packages/react-noop-renderer/package.json b/packages/react-noop-renderer/package.json index afd4aaef715..25b3a429e56 100644 --- a/packages/react-noop-renderer/package.json +++ b/packages/react-noop-renderer/package.json @@ -20,6 +20,7 @@ "LICENSE", "README.md", "index.js", + "persistent.js", "cjs/" ] } diff --git a/packages/react-noop-renderer/persistent.js b/packages/react-noop-renderer/persistent.js new file mode 100644 index 00000000000..f235fad17b2 --- /dev/null +++ b/packages/react-noop-renderer/persistent.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +'use strict'; + +const ReactNoopPersistent = require('./src/ReactNoopPersistent'); + +// TODO: decide on the top-level export form. +// This is hacky but makes it work with both Rollup and Jest. +module.exports = ReactNoopPersistent.default + ? ReactNoopPersistent.default + : ReactNoopPersistent; diff --git a/packages/react-noop-renderer/src/ReactNoop.js b/packages/react-noop-renderer/src/ReactNoop.js index e9c82cbe9c6..91ae6ca7499 100644 --- a/packages/react-noop-renderer/src/ReactNoop.js +++ b/packages/react-noop-renderer/src/ReactNoop.js @@ -14,581 +14,10 @@ * environment. */ -import type {Fiber} from 'react-reconciler/src/ReactFiber'; -import type {UpdateQueue} from 'react-reconciler/src/ReactUpdateQueue'; -import type {ReactNodeList} from 'shared/ReactTypes'; -import ReactFiberReconciler from 'react-reconciler'; -import {enablePersistentReconciler} from 'shared/ReactFeatureFlags'; -import * as ReactPortal from 'shared/ReactPortal'; -import emptyObject from 'fbjs/lib/emptyObject'; -import expect from 'expect'; +import createReactNoop from './createReactNoop'; -const UPDATE_SIGNAL = {}; - -let scheduledCallback = null; - -type Container = {rootID: string, children: Array}; -type Props = {prop: any, hidden?: boolean}; -type Instance = {| - type: string, - id: number, - children: Array, - prop: any, -|}; -type TextInstance = {|text: string, id: number|}; - -let instanceCounter = 0; -let failInBeginPhase = false; - -function appendChild( - parentInstance: Instance | Container, - child: Instance | TextInstance, -): void { - const index = parentInstance.children.indexOf(child); - if (index !== -1) { - parentInstance.children.splice(index, 1); - } - parentInstance.children.push(child); -} - -function insertBefore( - parentInstance: Instance | Container, - child: Instance | TextInstance, - beforeChild: Instance | TextInstance, -): void { - const index = parentInstance.children.indexOf(child); - if (index !== -1) { - parentInstance.children.splice(index, 1); - } - const beforeIndex = parentInstance.children.indexOf(beforeChild); - if (beforeIndex === -1) { - throw new Error('This child does not exist.'); - } - parentInstance.children.splice(beforeIndex, 0, child); -} - -function removeChild( - parentInstance: Instance | Container, - child: Instance | TextInstance, -): void { - const index = parentInstance.children.indexOf(child); - if (index === -1) { - throw new Error('This child does not exist.'); - } - parentInstance.children.splice(index, 1); -} - -let elapsedTimeInMs = 0; - -let SharedHostConfig = { - getRootHostContext() { - if (failInBeginPhase) { - throw new Error('Error in host config.'); - } - return emptyObject; - }, - - getChildHostContext() { - return emptyObject; - }, - - getPublicInstance(instance) { - return instance; - }, - - createInstance(type: string, props: Props): Instance { - const inst = { - id: instanceCounter++, - type: type, - children: [], - prop: props.prop, - }; - // Hide from unit tests - Object.defineProperty(inst, 'id', {value: inst.id, enumerable: false}); - return inst; - }, - - appendInitialChild( - parentInstance: Instance, - child: Instance | TextInstance, - ): void { - parentInstance.children.push(child); - }, - - finalizeInitialChildren( - domElement: Instance, - type: string, - props: Props, - ): boolean { - return false; - }, - - prepareUpdate( - instance: Instance, - type: string, - oldProps: Props, - newProps: Props, - ): null | {} { - if (oldProps === null) { - throw new Error('Should have old props'); - } - if (newProps === null) { - throw new Error('Should have new props'); - } - return UPDATE_SIGNAL; - }, - - shouldSetTextContent(type: string, props: Props): boolean { - return ( - typeof props.children === 'string' || typeof props.children === 'number' - ); - }, - - shouldDeprioritizeSubtree(type: string, props: Props): boolean { - return !!props.hidden; - }, - - createTextInstance( - text: string, - rootContainerInstance: Container, - hostContext: Object, - internalInstanceHandle: Object, - ): TextInstance { - const inst = {text: text, id: instanceCounter++}; - // Hide from unit tests - Object.defineProperty(inst, 'id', {value: inst.id, enumerable: false}); - return inst; - }, - - scheduleDeferredCallback(callback) { - if (scheduledCallback) { - throw new Error( - 'Scheduling a callback twice is excessive. Instead, keep track of ' + - 'whether the callback has already been scheduled.', - ); - } - scheduledCallback = callback; - return 0; - }, - - cancelDeferredCallback() { - if (scheduledCallback === null) { - throw new Error('No callback is scheduled.'); - } - scheduledCallback = null; - }, - - prepareForCommit(): void {}, - - resetAfterCommit(): void {}, - - now(): number { - return elapsedTimeInMs; - }, - - isPrimaryRenderer: true, -}; - -const NoopRenderer = ReactFiberReconciler({ - ...SharedHostConfig, - mutation: { - commitMount(instance: Instance, type: string, newProps: Props): void { - // Noop - }, - - commitUpdate( - instance: Instance, - updatePayload: Object, - type: string, - oldProps: Props, - newProps: Props, - ): void { - if (oldProps === null) { - throw new Error('Should have old props'); - } - instance.prop = newProps.prop; - }, - - commitTextUpdate( - textInstance: TextInstance, - oldText: string, - newText: string, - ): void { - textInstance.text = newText; - }, - - appendChild: appendChild, - appendChildToContainer: appendChild, - insertBefore: insertBefore, - insertInContainerBefore: insertBefore, - removeChild: removeChild, - removeChildFromContainer: removeChild, - - resetTextContent(instance: Instance): void {}, - }, -}); - -const PersistentNoopRenderer = enablePersistentReconciler - ? ReactFiberReconciler({ - ...SharedHostConfig, - persistence: { - cloneInstance( - instance: Instance, - updatePayload: null | Object, - type: string, - oldProps: Props, - newProps: Props, - internalInstanceHandle: Object, - keepChildren: boolean, - recyclableInstance: null | Instance, - ): Instance { - const clone = { - id: instance.id, - type: type, - children: keepChildren ? instance.children : [], - prop: newProps.prop, - }; - Object.defineProperty(clone, 'id', { - value: clone.id, - enumerable: false, - }); - return clone; - }, - - createContainerChildSet( - container: Container, - ): Array { - return []; - }, - - appendChildToContainerChildSet( - childSet: Array, - child: Instance | TextInstance, - ): void { - childSet.push(child); - }, - - finalizeContainerChildren( - container: Container, - newChildren: Array, - ): void {}, - - replaceContainerChildren( - container: Container, - newChildren: Array, - ): void { - container.children = newChildren; - }, - }, - }) - : null; - -const rootContainers = new Map(); -const roots = new Map(); -const persistentRoots = new Map(); -const DEFAULT_ROOT_ID = ''; - -let yieldedValues = null; - -let unitsRemaining; - -function* flushUnitsOfWork(n: number): Generator, void, void> { - let didStop = false; - while (!didStop && scheduledCallback !== null) { - let cb = scheduledCallback; - scheduledCallback = null; - unitsRemaining = n; - cb({ - timeRemaining() { - if (yieldedValues !== null) { - return 0; - } - if (unitsRemaining-- > 0) { - return 999; - } - didStop = true; - return 0; - }, - // React's scheduler has its own way of keeping track of expired - // work and doesn't read this, so don't bother setting it to the - // correct value. - didTimeout: false, - }); - - if (yieldedValues !== null) { - const values = yieldedValues; - yieldedValues = null; - yield values; - } - } -} - -const ReactNoop = { - getChildren(rootID: string = DEFAULT_ROOT_ID) { - const container = rootContainers.get(rootID); - if (container) { - return container.children; - } else { - return null; - } - }, - - createPortal( - children: ReactNodeList, - container: Container, - key: ?string = null, - ) { - return ReactPortal.createPortal(children, container, null, key); - }, - - // Shortcut for testing a single root - render(element: React$Element, callback: ?Function) { - ReactNoop.renderToRootWithID(element, DEFAULT_ROOT_ID, callback); - }, - - renderToRootWithID( - element: React$Element, - rootID: string, - callback: ?Function, - ) { - let root = roots.get(rootID); - if (!root) { - const container = {rootID: rootID, children: []}; - rootContainers.set(rootID, container); - root = NoopRenderer.createContainer(container, true, false); - roots.set(rootID, root); - } - NoopRenderer.updateContainer(element, root, null, callback); - }, - - renderToPersistentRootWithID( - element: React$Element, - rootID: string, - callback: ?Function, - ) { - if (PersistentNoopRenderer === null) { - throw new Error( - 'Enable ReactFeatureFlags.enablePersistentReconciler to use it in tests.', - ); - } - let root = persistentRoots.get(rootID); - if (!root) { - const container = {rootID: rootID, children: []}; - rootContainers.set(rootID, container); - root = PersistentNoopRenderer.createContainer(container, true, false); - persistentRoots.set(rootID, root); - } - PersistentNoopRenderer.updateContainer(element, root, null, callback); - }, - - unmountRootWithID(rootID: string) { - const root = roots.get(rootID); - if (root) { - NoopRenderer.updateContainer(null, root, null, () => { - roots.delete(rootID); - rootContainers.delete(rootID); - }); - } - }, - - findInstance( - componentOrElement: Element | ?React$Component, - ): null | Instance | TextInstance { - if (componentOrElement == null) { - return null; - } - // Unsound duck typing. - const component = (componentOrElement: any); - if (typeof component.id === 'number') { - return component; - } - return NoopRenderer.findHostInstance(component); - }, - - flushDeferredPri(timeout: number = Infinity): Array { - // The legacy version of this function decremented the timeout before - // returning the new time. - // TODO: Convert tests to use flushUnitsOfWork or flushAndYield instead. - const n = timeout / 5 - 1; - - let values = []; - // eslint-disable-next-line no-for-of-loops/no-for-of-loops - for (const value of flushUnitsOfWork(n)) { - values.push(...value); - } - return values; - }, - - flush(): Array { - return ReactNoop.flushUnitsOfWork(Infinity); - }, - - flushAndYield( - unitsOfWork: number = Infinity, - ): Generator, void, void> { - return flushUnitsOfWork(unitsOfWork); - }, - - flushUnitsOfWork(n: number): Array { - let values = yieldedValues || []; - yieldedValues = null; - // eslint-disable-next-line no-for-of-loops/no-for-of-loops - for (const value of flushUnitsOfWork(n)) { - values.push(...value); - } - return values; - }, - - flushThrough(expected: Array): void { - let actual = []; - if (expected.length !== 0) { - // eslint-disable-next-line no-for-of-loops/no-for-of-loops - for (const value of flushUnitsOfWork(Infinity)) { - actual.push(...value); - if (actual.length >= expected.length) { - break; - } - } - } - expect(actual).toEqual(expected); - }, - - expire(ms: number): void { - elapsedTimeInMs += ms; - }, - - flushExpired(): Array { - return ReactNoop.flushUnitsOfWork(0); - }, - - yield(value: mixed) { - if (yieldedValues === null) { - yieldedValues = [value]; - } else { - yieldedValues.push(value); - } - }, - - clearYields() { - const values = yieldedValues; - yieldedValues = null; - return values; - }, - - hasScheduledCallback() { - return !!scheduledCallback; - }, - - batchedUpdates: NoopRenderer.batchedUpdates, - - deferredUpdates: NoopRenderer.deferredUpdates, - - unbatchedUpdates: NoopRenderer.unbatchedUpdates, - - interactiveUpdates: NoopRenderer.interactiveUpdates, - - flushSync(fn: () => mixed) { - yieldedValues = []; - NoopRenderer.flushSync(fn); - return yieldedValues; - }, - - // Logs the current state of the tree. - dumpTree(rootID: string = DEFAULT_ROOT_ID) { - const root = roots.get(rootID); - const rootContainer = rootContainers.get(rootID); - if (!root || !rootContainer) { - console.log('Nothing rendered yet.'); - return; - } - - let bufferedLog = []; - function log(...args) { - bufferedLog.push(...args, '\n'); - } - - function logHostInstances(children: Array, depth) { - for (let i = 0; i < children.length; i++) { - const child = children[i]; - const indent = ' '.repeat(depth); - if (typeof child.text === 'string') { - log(indent + '- ' + child.text); - } else { - // $FlowFixMe - The child should've been refined now. - log(indent + '- ' + child.type + '#' + child.id); - // $FlowFixMe - The child should've been refined now. - logHostInstances(child.children, depth + 1); - } - } - } - function logContainer(container: Container, depth) { - log(' '.repeat(depth) + '- [root#' + container.rootID + ']'); - logHostInstances(container.children, depth + 1); - } - - function logUpdateQueue(updateQueue: UpdateQueue, depth) { - log(' '.repeat(depth + 1) + 'QUEUED UPDATES'); - const firstUpdate = updateQueue.firstUpdate; - if (!firstUpdate) { - return; - } - - log(' '.repeat(depth + 1) + '~', '[' + firstUpdate.expirationTime + ']'); - while (firstUpdate.next) { - log( - ' '.repeat(depth + 1) + '~', - '[' + firstUpdate.expirationTime + ']', - ); - } - } - - function logFiber(fiber: Fiber, depth) { - log( - ' '.repeat(depth) + - '- ' + - // need to explicitly coerce Symbol to a string - (fiber.type ? fiber.type.name || fiber.type.toString() : '[root]'), - '[' + fiber.expirationTime + (fiber.pendingProps ? '*' : '') + ']', - ); - if (fiber.updateQueue) { - logUpdateQueue(fiber.updateQueue, depth); - } - // const childInProgress = fiber.progressedChild; - // if (childInProgress && childInProgress !== fiber.child) { - // log( - // ' '.repeat(depth + 1) + 'IN PROGRESS: ' + fiber.pendingWorkPriority, - // ); - // logFiber(childInProgress, depth + 1); - // if (fiber.child) { - // log(' '.repeat(depth + 1) + 'CURRENT'); - // } - // } else if (fiber.child && fiber.updateQueue) { - // log(' '.repeat(depth + 1) + 'CHILDREN'); - // } - if (fiber.child) { - logFiber(fiber.child, depth + 1); - } - if (fiber.sibling) { - logFiber(fiber.sibling, depth); - } - } - - log('HOST INSTANCES:'); - logContainer(rootContainer, 0); - log('FIBERS:'); - logFiber(root.current, 0); - - console.log(...bufferedLog); - }, - - simulateErrorInHostConfig(fn: () => void) { - failInBeginPhase = true; - try { - fn(); - } finally { - failInBeginPhase = false; - } - }, -}; +const ReactNoop = createReactNoop( + true, // useMutation +); export default ReactNoop; diff --git a/packages/react-noop-renderer/src/ReactNoopPersistent.js b/packages/react-noop-renderer/src/ReactNoopPersistent.js new file mode 100644 index 00000000000..32c6dcebcbd --- /dev/null +++ b/packages/react-noop-renderer/src/ReactNoopPersistent.js @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +/** + * This is a renderer of React that doesn't have a render target output. + * It is useful to demonstrate the internals of the reconciler in isolation + * and for testing semantics of reconciliation separate from the host + * environment. + */ + +import createReactNoop from './createReactNoop'; + +const ReactNoopPersistent = createReactNoop( + false, // useMutation +); + +export default ReactNoopPersistent; diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js new file mode 100644 index 00000000000..26671229288 --- /dev/null +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -0,0 +1,582 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +/** + * This is a renderer of React that doesn't have a render target output. + * It is useful to demonstrate the internals of the reconciler in isolation + * and for testing semantics of reconciliation separate from the host + * environment. + */ + +import type {Fiber} from 'react-reconciler/src/ReactFiber'; +import type {UpdateQueue} from 'react-reconciler/src/ReactUpdateQueue'; +import type {ReactNodeList} from 'shared/ReactTypes'; + +import ReactFiberReconciler from 'react-reconciler'; +import * as ReactPortal from 'shared/ReactPortal'; +import emptyObject from 'fbjs/lib/emptyObject'; +import expect from 'expect'; + +type Container = {rootID: string, children: Array}; +type Props = {prop: any, hidden?: boolean}; +type Instance = {| + type: string, + id: number, + children: Array, + prop: any, +|}; +type TextInstance = {|text: string, id: number|}; + +function createReactNoop(useMutation: boolean) { + const UPDATE_SIGNAL = {}; + let scheduledCallback = null; + + let instanceCounter = 0; + let failInBeginPhase = false; + + function appendChild( + parentInstance: Instance | Container, + child: Instance | TextInstance, + ): void { + const index = parentInstance.children.indexOf(child); + if (index !== -1) { + parentInstance.children.splice(index, 1); + } + parentInstance.children.push(child); + } + + function insertBefore( + parentInstance: Instance | Container, + child: Instance | TextInstance, + beforeChild: Instance | TextInstance, + ): void { + const index = parentInstance.children.indexOf(child); + if (index !== -1) { + parentInstance.children.splice(index, 1); + } + const beforeIndex = parentInstance.children.indexOf(beforeChild); + if (beforeIndex === -1) { + throw new Error('This child does not exist.'); + } + parentInstance.children.splice(beforeIndex, 0, child); + } + + function removeChild( + parentInstance: Instance | Container, + child: Instance | TextInstance, + ): void { + const index = parentInstance.children.indexOf(child); + if (index === -1) { + throw new Error('This child does not exist.'); + } + parentInstance.children.splice(index, 1); + } + + let elapsedTimeInMs = 0; + + const sharedHostConfig = { + getRootHostContext() { + if (failInBeginPhase) { + throw new Error('Error in host config.'); + } + return emptyObject; + }, + + getChildHostContext() { + return emptyObject; + }, + + getPublicInstance(instance) { + return instance; + }, + + createInstance(type: string, props: Props): Instance { + const inst = { + id: instanceCounter++, + type: type, + children: [], + prop: props.prop, + }; + // Hide from unit tests + Object.defineProperty(inst, 'id', {value: inst.id, enumerable: false}); + return inst; + }, + + appendInitialChild( + parentInstance: Instance, + child: Instance | TextInstance, + ): void { + parentInstance.children.push(child); + }, + + finalizeInitialChildren( + domElement: Instance, + type: string, + props: Props, + ): boolean { + return false; + }, + + prepareUpdate( + instance: Instance, + type: string, + oldProps: Props, + newProps: Props, + ): null | {} { + if (oldProps === null) { + throw new Error('Should have old props'); + } + if (newProps === null) { + throw new Error('Should have new props'); + } + return UPDATE_SIGNAL; + }, + + shouldSetTextContent(type: string, props: Props): boolean { + return ( + typeof props.children === 'string' || typeof props.children === 'number' + ); + }, + + shouldDeprioritizeSubtree(type: string, props: Props): boolean { + return !!props.hidden; + }, + + createTextInstance( + text: string, + rootContainerInstance: Container, + hostContext: Object, + internalInstanceHandle: Object, + ): TextInstance { + const inst = {text: text, id: instanceCounter++}; + // Hide from unit tests + Object.defineProperty(inst, 'id', {value: inst.id, enumerable: false}); + return inst; + }, + + scheduleDeferredCallback(callback) { + if (scheduledCallback) { + throw new Error( + 'Scheduling a callback twice is excessive. Instead, keep track of ' + + 'whether the callback has already been scheduled.', + ); + } + scheduledCallback = callback; + return 0; + }, + + cancelDeferredCallback() { + if (scheduledCallback === null) { + throw new Error('No callback is scheduled.'); + } + scheduledCallback = null; + }, + + prepareForCommit(): void {}, + + resetAfterCommit(): void {}, + + now(): number { + return elapsedTimeInMs; + }, + + isPrimaryRenderer: true, + }; + + const hostConfig = useMutation + ? { + ...sharedHostConfig, + mutation: { + commitMount(instance: Instance, type: string, newProps: Props): void { + // Noop + }, + + commitUpdate( + instance: Instance, + updatePayload: Object, + type: string, + oldProps: Props, + newProps: Props, + ): void { + if (oldProps === null) { + throw new Error('Should have old props'); + } + instance.prop = newProps.prop; + }, + + commitTextUpdate( + textInstance: TextInstance, + oldText: string, + newText: string, + ): void { + textInstance.text = newText; + }, + + appendChild: appendChild, + appendChildToContainer: appendChild, + insertBefore: insertBefore, + insertInContainerBefore: insertBefore, + removeChild: removeChild, + removeChildFromContainer: removeChild, + + resetTextContent(instance: Instance): void {}, + }, + } + : { + ...sharedHostConfig, + persistence: { + cloneInstance( + instance: Instance, + updatePayload: null | Object, + type: string, + oldProps: Props, + newProps: Props, + internalInstanceHandle: Object, + keepChildren: boolean, + recyclableInstance: null | Instance, + ): Instance { + const clone = { + id: instance.id, + type: type, + children: keepChildren ? instance.children : [], + prop: newProps.prop, + }; + Object.defineProperty(clone, 'id', { + value: clone.id, + enumerable: false, + }); + return clone; + }, + + createContainerChildSet( + container: Container, + ): Array { + return []; + }, + + appendChildToContainerChildSet( + childSet: Array, + child: Instance | TextInstance, + ): void { + childSet.push(child); + }, + + finalizeContainerChildren( + container: Container, + newChildren: Array, + ): void {}, + + replaceContainerChildren( + container: Container, + newChildren: Array, + ): void { + container.children = newChildren; + }, + }, + }; + + const NoopRenderer = ReactFiberReconciler(hostConfig); + + const rootContainers = new Map(); + const roots = new Map(); + const DEFAULT_ROOT_ID = ''; + + let yieldedValues = null; + + let unitsRemaining; + + function* flushUnitsOfWork(n: number): Generator, void, void> { + let didStop = false; + while (!didStop && scheduledCallback !== null) { + let cb = scheduledCallback; + scheduledCallback = null; + unitsRemaining = n; + cb({ + timeRemaining() { + if (yieldedValues !== null) { + return 0; + } + if (unitsRemaining-- > 0) { + return 999; + } + didStop = true; + return 0; + }, + // React's scheduler has its own way of keeping track of expired + // work and doesn't read this, so don't bother setting it to the + // correct value. + didTimeout: false, + }); + + if (yieldedValues !== null) { + const values = yieldedValues; + yieldedValues = null; + yield values; + } + } + } + + const ReactNoop = { + getChildren(rootID: string = DEFAULT_ROOT_ID) { + const container = rootContainers.get(rootID); + if (container) { + return container.children; + } else { + return null; + } + }, + + createPortal( + children: ReactNodeList, + container: Container, + key: ?string = null, + ) { + return ReactPortal.createPortal(children, container, null, key); + }, + + // Shortcut for testing a single root + render(element: React$Element, callback: ?Function) { + ReactNoop.renderToRootWithID(element, DEFAULT_ROOT_ID, callback); + }, + + renderToRootWithID( + element: React$Element, + rootID: string, + callback: ?Function, + ) { + let root = roots.get(rootID); + if (!root) { + const container = {rootID: rootID, children: []}; + rootContainers.set(rootID, container); + root = NoopRenderer.createContainer(container, true, false); + roots.set(rootID, root); + } + NoopRenderer.updateContainer(element, root, null, callback); + }, + + unmountRootWithID(rootID: string) { + const root = roots.get(rootID); + if (root) { + NoopRenderer.updateContainer(null, root, null, () => { + roots.delete(rootID); + rootContainers.delete(rootID); + }); + } + }, + + findInstance( + componentOrElement: Element | ?React$Component, + ): null | Instance | TextInstance { + if (componentOrElement == null) { + return null; + } + // Unsound duck typing. + const component = (componentOrElement: any); + if (typeof component.id === 'number') { + return component; + } + return NoopRenderer.findHostInstance(component); + }, + + flushDeferredPri(timeout: number = Infinity): Array { + // The legacy version of this function decremented the timeout before + // returning the new time. + // TODO: Convert tests to use flushUnitsOfWork or flushAndYield instead. + const n = timeout / 5 - 1; + + let values = []; + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const value of flushUnitsOfWork(n)) { + values.push(...value); + } + return values; + }, + + flush(): Array { + return ReactNoop.flushUnitsOfWork(Infinity); + }, + + flushAndYield( + unitsOfWork: number = Infinity, + ): Generator, void, void> { + return flushUnitsOfWork(unitsOfWork); + }, + + flushUnitsOfWork(n: number): Array { + let values = yieldedValues || []; + yieldedValues = null; + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const value of flushUnitsOfWork(n)) { + values.push(...value); + } + return values; + }, + + flushThrough(expected: Array): void { + let actual = []; + if (expected.length !== 0) { + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const value of flushUnitsOfWork(Infinity)) { + actual.push(...value); + if (actual.length >= expected.length) { + break; + } + } + } + expect(actual).toEqual(expected); + }, + + expire(ms: number): void { + elapsedTimeInMs += ms; + }, + + flushExpired(): Array { + return ReactNoop.flushUnitsOfWork(0); + }, + + yield(value: mixed) { + if (yieldedValues === null) { + yieldedValues = [value]; + } else { + yieldedValues.push(value); + } + }, + + clearYields() { + const values = yieldedValues; + yieldedValues = null; + return values; + }, + + hasScheduledCallback() { + return !!scheduledCallback; + }, + + batchedUpdates: NoopRenderer.batchedUpdates, + + deferredUpdates: NoopRenderer.deferredUpdates, + + unbatchedUpdates: NoopRenderer.unbatchedUpdates, + + interactiveUpdates: NoopRenderer.interactiveUpdates, + + flushSync(fn: () => mixed) { + yieldedValues = []; + NoopRenderer.flushSync(fn); + return yieldedValues; + }, + + // Logs the current state of the tree. + dumpTree(rootID: string = DEFAULT_ROOT_ID) { + const root = roots.get(rootID); + const rootContainer = rootContainers.get(rootID); + if (!root || !rootContainer) { + console.log('Nothing rendered yet.'); + return; + } + + let bufferedLog = []; + function log(...args) { + bufferedLog.push(...args, '\n'); + } + + function logHostInstances( + children: Array, + depth, + ) { + for (let i = 0; i < children.length; i++) { + const child = children[i]; + const indent = ' '.repeat(depth); + if (typeof child.text === 'string') { + log(indent + '- ' + child.text); + } else { + // $FlowFixMe - The child should've been refined now. + log(indent + '- ' + child.type + '#' + child.id); + // $FlowFixMe - The child should've been refined now. + logHostInstances(child.children, depth + 1); + } + } + } + function logContainer(container: Container, depth) { + log(' '.repeat(depth) + '- [root#' + container.rootID + ']'); + logHostInstances(container.children, depth + 1); + } + + function logUpdateQueue(updateQueue: UpdateQueue, depth) { + log(' '.repeat(depth + 1) + 'QUEUED UPDATES'); + const firstUpdate = updateQueue.firstUpdate; + if (!firstUpdate) { + return; + } + + log( + ' '.repeat(depth + 1) + '~', + '[' + firstUpdate.expirationTime + ']', + ); + while (firstUpdate.next) { + log( + ' '.repeat(depth + 1) + '~', + '[' + firstUpdate.expirationTime + ']', + ); + } + } + + function logFiber(fiber: Fiber, depth) { + log( + ' '.repeat(depth) + + '- ' + + // need to explicitly coerce Symbol to a string + (fiber.type ? fiber.type.name || fiber.type.toString() : '[root]'), + '[' + fiber.expirationTime + (fiber.pendingProps ? '*' : '') + ']', + ); + if (fiber.updateQueue) { + logUpdateQueue(fiber.updateQueue, depth); + } + // const childInProgress = fiber.progressedChild; + // if (childInProgress && childInProgress !== fiber.child) { + // log( + // ' '.repeat(depth + 1) + 'IN PROGRESS: ' + fiber.pendingWorkPriority, + // ); + // logFiber(childInProgress, depth + 1); + // if (fiber.child) { + // log(' '.repeat(depth + 1) + 'CURRENT'); + // } + // } else if (fiber.child && fiber.updateQueue) { + // log(' '.repeat(depth + 1) + 'CHILDREN'); + // } + if (fiber.child) { + logFiber(fiber.child, depth + 1); + } + if (fiber.sibling) { + logFiber(fiber.sibling, depth); + } + } + + log('HOST INSTANCES:'); + logContainer(rootContainer, 0); + log('FIBERS:'); + logFiber(root.current, 0); + + console.log(...bufferedLog); + }, + + simulateErrorInHostConfig(fn: () => void) { + failInBeginPhase = true; + try { + fn(); + } finally { + failInBeginPhase = false; + } + }, + }; + + return ReactNoop; +} + +export default createReactNoop; diff --git a/packages/react-reconciler/src/__tests__/ReactPersistent-test.internal.js b/packages/react-reconciler/src/__tests__/ReactPersistent-test.internal.js index 1445ddb798c..1757e95a787 100644 --- a/packages/react-reconciler/src/__tests__/ReactPersistent-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactPersistent-test.internal.js @@ -11,7 +11,7 @@ 'use strict'; let React; -let ReactNoop; +let ReactNoopPersistent; let ReactPortal; describe('ReactPersistent', () => { @@ -24,14 +24,12 @@ describe('ReactPersistent', () => { ReactFeatureFlags.enableNoopReconciler = false; React = require('react'); - ReactNoop = require('react-noop-renderer'); + ReactNoopPersistent = require('react-noop-renderer/persistent'); ReactPortal = require('shared/ReactPortal'); }); - const DEFAULT_ROOT_ID = 'persistent-test'; - function render(element) { - ReactNoop.renderToPersistentRootWithID(element, DEFAULT_ROOT_ID); + ReactNoopPersistent.render(element); } function div(...children) { @@ -44,7 +42,7 @@ describe('ReactPersistent', () => { } function getChildren() { - return ReactNoop.getChildren(DEFAULT_ROOT_ID); + return ReactNoopPersistent.getChildren(); } it('can update child nodes of a host instance', () => { @@ -62,12 +60,12 @@ describe('ReactPersistent', () => { } render(); - ReactNoop.flush(); + ReactNoopPersistent.flush(); const originalChildren = getChildren(); expect(originalChildren).toEqual([div(span())]); render(); - ReactNoop.flush(); + ReactNoopPersistent.flush(); const newChildren = getChildren(); expect(newChildren).toEqual([div(span(), span())]); @@ -96,12 +94,12 @@ describe('ReactPersistent', () => { } render(); - ReactNoop.flush(); + ReactNoopPersistent.flush(); const originalChildren = getChildren(); expect(originalChildren).toEqual([div(span('Hello'))]); render(); - ReactNoop.flush(); + ReactNoopPersistent.flush(); const newChildren = getChildren(); expect(newChildren).toEqual([div(span('Hello'), span('World'))]); @@ -122,12 +120,12 @@ describe('ReactPersistent', () => { } render(); - ReactNoop.flush(); + ReactNoopPersistent.flush(); const originalChildren = getChildren(); expect(originalChildren).toEqual([div('Hello', span())]); render(); - ReactNoop.flush(); + ReactNoopPersistent.flush(); const newChildren = getChildren(); expect(newChildren).toEqual([div('World', span())]); @@ -167,7 +165,7 @@ describe('ReactPersistent', () => { {ReactPortal.createPortal(, portalContainer, null)} , ); - ReactNoop.flush(); + ReactNoopPersistent.flush(); expect(emptyPortalChildSet).toEqual([]); @@ -185,7 +183,7 @@ describe('ReactPersistent', () => { )} , ); - ReactNoop.flush(); + ReactNoopPersistent.flush(); const newChildren = getChildren(); expect(newChildren).toEqual([div()]); @@ -202,7 +200,7 @@ describe('ReactPersistent', () => { // Deleting the Portal, should clear its children render(); - ReactNoop.flush(); + ReactNoopPersistent.flush(); const clearedPortalChildren = portalContainer.children; expect(clearedPortalChildren).toEqual([]); diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index f6681c64679..e9f54a11dee 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -275,6 +275,29 @@ const bundles = [ }), }, + /******* React Noop Persistent Renderer (used for tests) *******/ + { + label: 'noop-persistent', + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: RENDERER, + entry: 'react-noop-renderer/persistent', + global: 'ReactNoopRendererPersistent', + externals: ['react', 'expect'], + // React Noop uses generators. However GCC currently + // breaks when we attempt to use them in the output. + // So we precompile them with regenerator, and include + // it as a runtime dependency of React Noop. In practice + // this isn't an issue because React Noop is only used + // in our tests. We wouldn't want to do this for any + // public package though. + babel: opts => + Object.assign({}, opts, { + plugins: opts.plugins.concat([ + require.resolve('babel-plugin-transform-regenerator'), + ]), + }), + }, + /******* React Reconciler *******/ { label: 'react-reconciler',