/** * 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 * 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, children?: mixed}; type Instance = {| type: string, id: number, children: Array, prop: any, |}; type TextInstance = {|text: string, id: number|}; function createReactNoop(reconciler: Function, 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, supportsHydration: false, }; const hostConfig = useMutation ? { ...sharedHostConfig, supportsMutation: true, supportsPersistence: false, 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, supportsMutation: false, supportsPersistence: true, 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 = reconciler(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;