From cf2a10715458b6bded3ec7e8e298f79b57c70380 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Sat, 14 Sep 2024 19:05:30 +0200 Subject: [PATCH 01/61] refactor: bring config host --- package.json | 2 + src/renderer/react-fiber-config-host.ts | 324 ++++++++++++++++++++++++ yarn.lock | 23 ++ 3 files changed, 349 insertions(+) create mode 100644 src/renderer/react-fiber-config-host.ts diff --git a/package.json b/package.json index ffc4c4e65..50dcf6a76 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "dependencies": { "jest-matcher-utils": "^29.7.0", "pretty-format": "^29.7.0", + "react-reconciler": "0.29.2", "redent": "^3.0.0" }, "peerDependencies": { @@ -74,6 +75,7 @@ "@relmify/jest-serializer-strip-ansi": "^1.0.2", "@types/jest": "^29.5.12", "@types/react": "^18.3.3", + "@types/react-reconciler": "^0", "@types/react-test-renderer": "^18.3.0", "babel-jest": "^29.7.0", "del-cli": "^5.1.0", diff --git a/src/renderer/react-fiber-config-host.ts b/src/renderer/react-fiber-config-host.ts new file mode 100644 index 000000000..815949fe7 --- /dev/null +++ b/src/renderer/react-fiber-config-host.ts @@ -0,0 +1,324 @@ +/** + * 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. + * + */ + +//import isArray from 'shared/isArray'; +//import {DefaultEventPriority} from 'react-reconciler/src/ReactEventPriorities'; + +import { DefaultEventPriority } from 'react-reconciler/constants'; + +export type Type = string; +export type Props = object; + +export type Container = { + tag: 'CONTAINER'; + children: Array; + createNodeMock: Function; +}; + +export type Instance = { + tag: 'INSTANCE'; + type: string; + props: object; + isHidden: boolean; + children: Array; + internalInstanceHandle: object; + rootContainerInstance: Container; +}; + +export type TextInstance = { + tag: 'TEXT'; + text: string; + isHidden: boolean; +}; + +export type HydratableInstance = Instance | TextInstance; +export type PublicInstance = Instance | TextInstance; +export type HostContext = object; +export type UpdatePayload = object; +export type ChildSet = void; // Unused +export type TimeoutHandle = ReturnType; +export type NoTimeout = -1; +export type EventResponder = any; + +export type RendererInspectionConfig = Readonly<{}>; + +// export * from 'react-reconciler/src/ReactFiberHostConfigWithNoPersistence'; +// export * from 'react-reconciler/src/ReactFiberHostConfigWithNoHydration'; +// export * from 'react-reconciler/src/ReactFiberHostConfigWithNoTestSelectors'; +// export * from 'react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks'; + +const NO_CONTEXT = {}; +const UPDATE_SIGNAL = {}; +const nodeToInstanceMap = new WeakMap(); + +if (__DEV__) { + Object.freeze(NO_CONTEXT); + Object.freeze(UPDATE_SIGNAL); +} + +export function getPublicInstance(inst: Instance | TextInstance) { + switch (inst.tag) { + case 'INSTANCE': { + const createNodeMock = inst.rootContainerInstance.createNodeMock; + const mockNode = createNodeMock({ + type: inst.type, + props: inst.props, + }); + if (typeof mockNode === 'object' && mockNode !== null) { + nodeToInstanceMap.set(mockNode, inst); + } + + return mockNode; + } + + default: + return inst; + } +} + +export function appendChild( + parentInstance: Instance | Container, + child: Instance | TextInstance, +): void { + if (__DEV__) { + if (!Array.isArray(parentInstance.children)) { + // eslint-disable-next-line no-console + console.error( + 'An invalid container has been provided. ' + + 'This may indicate that another renderer is being used in addition to the test renderer. ' + + '(For example, ReactDOM.createPortal inside of a ReactTestRenderer tree.) ' + + 'This is not supported.', + ); + } + } + const index = parentInstance.children.indexOf(child); + if (index !== -1) { + parentInstance.children.splice(index, 1); + } + + parentInstance.children.push(child); +} + +export 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); + parentInstance.children.splice(beforeIndex, 0, child); +} + +export function removeChild( + parentInstance: Instance | Container, + child: Instance | TextInstance, +): void { + const index = parentInstance.children.indexOf(child); + parentInstance.children.splice(index, 1); +} + +export function clearContainer(container: Container): void { + container.children.splice(0); +} + +export function getRootHostContext(_rootContainerInstance: Container): HostContext { + return NO_CONTEXT; +} + +export function getChildHostContext( + _parentHostContext: HostContext, + _type: string, + _rootContainerInstance: Container, +): HostContext { + return NO_CONTEXT; +} + +export function prepareForCommit(_containerInfo: Container): object | null { + // noop + return null; +} + +export function resetAfterCommit(_containerInfo: Container): void { + // noop +} + +export function createInstance( + type: string, + props: Props, + rootContainerInstance: Container, + _hostContext: object, + internalInstanceHandle: object, +): Instance { + return { + type, + props, + isHidden: false, + children: [], + internalInstanceHandle, + rootContainerInstance, + tag: 'INSTANCE', + }; +} + +export function appendInitialChild(parentInstance: Instance, child: Instance | TextInstance): void { + const index = parentInstance.children.indexOf(child); + if (index !== -1) { + parentInstance.children.splice(index, 1); + } + + parentInstance.children.push(child); +} + +export function finalizeInitialChildren( + _testElement: Instance, + _type: string, + _props: Props, + _rootContainerInstance: Container, + _hostContext: object, +): boolean { + return false; +} + +export function prepareUpdate( + _testElement: Instance, + _type: string, + _oldProps: Props, + _newProps: Props, + _rootContainerInstance: Container, + _hostContext: object, +): object | null { + return UPDATE_SIGNAL; +} + +export function shouldSetTextContent(_type: string, _props: Props): boolean { + return false; +} + +export function createTextInstance( + text: string, + _rootContainerInstance: Container, + _hostContext: object, + _internalInstanceHandle: object, +): TextInstance { + return { + tag: 'TEXT', + text, + isHidden: false, + }; +} + +export function getCurrentEventPriority(): number { + return DefaultEventPriority; +} + +export const isPrimaryRenderer = false; +export const warnsIfNotActing = true; + +export const scheduleTimeout = setTimeout; +export const cancelTimeout = clearTimeout; + +export const noTimeout = -1; + +// ------------------- +// Mutation +// ------------------- + +export const supportsMutation = true; + +export function commitUpdate( + instance: Instance, + _updatePayload: object, + type: string, + _oldProps: Props, + newProps: Props, + _internalInstanceHandle: object, +): void { + instance.type = type; + instance.props = newProps; +} + +export function commitMount( + _instance: Instance, + _type: string, + _newProps: Props, + _internalInstanceHandle: object, +): void { + // noop +} + +export function commitTextUpdate( + textInstance: TextInstance, + _oldText: string, + newText: string, +): void { + textInstance.text = newText; +} + +export function resetTextContent(_testElement: Instance): void { + // noop +} + +export const appendChildToContainer = appendChild; +export const insertInContainerBefore = insertBefore; +export const removeChildFromContainer = removeChild; + +export function hideInstance(instance: Instance): void { + instance.isHidden = true; +} + +export function hideTextInstance(textInstance: TextInstance): void { + textInstance.isHidden = true; +} + +export function unhideInstance(instance: Instance, _props: Props): void { + instance.isHidden = false; +} + +export function unhideTextInstance(textInstance: TextInstance, _text: string): void { + textInstance.isHidden = false; +} + +export function getInstanceFromNode(mockNode: object) { + const instance = nodeToInstanceMap.get(mockNode); + if (instance !== undefined) { + return instance.internalInstanceHandle; + } + return null; +} + +export function beforeActiveInstanceBlur(_internalInstanceHandle: object) { + // noop +} + +export function afterActiveInstanceBlur() { + // noop +} + +export function preparePortalMount(_portalInstance: Instance): void { + // noop +} + +export function prepareScopeUpdate(scopeInstance: object, inst: object): void { + nodeToInstanceMap.set(scopeInstance, inst); +} + +export function getInstanceFromScope(scopeInstance: object): object | null { + return nodeToInstanceMap.get(scopeInstance) || null; +} + +export function detachDeletedInstance(_node: Instance): void { + // noop +} + +export function logRecoverableError(_error: unknown): void { + // noop +} diff --git a/yarn.lock b/yarn.lock index b90fd1e36..9c59affbe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2875,6 +2875,7 @@ __metadata: "@relmify/jest-serializer-strip-ansi": "npm:^1.0.2" "@types/jest": "npm:^29.5.12" "@types/react": "npm:^18.3.3" + "@types/react-reconciler": "npm:^0" "@types/react-test-renderer": "npm:^18.3.0" babel-jest: "npm:^29.7.0" del-cli: "npm:^5.1.0" @@ -2888,6 +2889,7 @@ __metadata: pretty-format: "npm:^29.7.0" react: "npm:18.3.1" react-native: "npm:0.75.1" + react-reconciler: "npm:0.29.2" react-test-renderer: "npm:18.3.1" redent: "npm:^3.0.0" release-it: "npm:^17.6.0" @@ -3049,6 +3051,15 @@ __metadata: languageName: node linkType: hard +"@types/react-reconciler@npm:^0": + version: 0.28.8 + resolution: "@types/react-reconciler@npm:0.28.8" + dependencies: + "@types/react": "npm:*" + checksum: 10c0/ca95cffcdf58591679c6c87dcc6f2c50cef9c6b2772d089ec0c695567656f34a30a0f2592f391d99b0e877f94afd67347082c55eb1dc5cb8000e23c8efc0fafc + languageName: node + linkType: hard + "@types/react-test-renderer@npm:^18.3.0": version: 18.3.0 resolution: "@types/react-test-renderer@npm:18.3.0" @@ -10082,6 +10093,18 @@ __metadata: languageName: node linkType: hard +"react-reconciler@npm:0.29.2": + version: 0.29.2 + resolution: "react-reconciler@npm:0.29.2" + dependencies: + loose-envify: "npm:^1.1.0" + scheduler: "npm:^0.23.2" + peerDependencies: + react: ^18.3.1 + checksum: 10c0/94f48ddc348a974256cf13c859f5a94efdb0cd72e04c51b1a4d5c72a8b960ccd35df2196057ee6a4cbcb26145e12b01e3f9ba3b183fddb901414db36a07cbf43 + languageName: node + linkType: hard + "react-refresh@npm:^0.14.0": version: 0.14.2 resolution: "react-refresh@npm:0.14.2" From e8a72c4eb806aa26decb4a77f70f5c65f7b74597 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Sat, 14 Sep 2024 19:32:41 +0200 Subject: [PATCH 02/61] chore: wip --- src/renderer/index.ts | 0 src/renderer/renderer.ts | 646 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 646 insertions(+) create mode 100644 src/renderer/index.ts create mode 100644 src/renderer/renderer.ts diff --git a/src/renderer/index.ts b/src/renderer/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts new file mode 100644 index 000000000..418ac70a2 --- /dev/null +++ b/src/renderer/renderer.ts @@ -0,0 +1,646 @@ +/** + * 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. + * + */ + +import * as React from 'react'; +// @ts-expect-error +import ReactReconciler, { Fiber, createContainer } from 'react-reconciler'; + +import createReconciler from 'react-reconciler'; + +import { getPublicInstance, Instance, TextInstance } from './react-fiber-config-host'; + +// import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; +// import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes'; + +// import * as Scheduler from 'scheduler/unstable_mock'; +// import { +// getPublicRootInstance, +// createContainer, +// updateContainer, +// flushSync, +// injectIntoDevTools, +// batchedUpdates, +// } from 'react-reconciler/src/ReactFiberReconciler'; +// import {findCurrentFiberUsingSlowPath} from 'react-reconciler/src/ReactFiberTreeReflection'; +// import { +// Fragment, +// FunctionComponent, +// ClassComponent, +// HostComponent, +// HostPortal, +// HostText, +// HostRoot, +// ContextConsumer, +// ContextProvider, +// Mode, +// ForwardRef, +// Profiler, +// MemoComponent, +// SimpleMemoComponent, +// IncompleteClassComponent, +// ScopeComponent, +// } from 'react-reconciler/src/ReactWorkTags'; +// import isArray from 'shared/isArray'; +// import getComponentNameFromType from 'shared/getComponentNameFromType'; +// import ReactVersion from 'shared/ReactVersion'; +// import {checkPropStringCoercion} from 'shared/CheckStringCoercion'; + +// import {getPublicInstance} from './ReactTestHostConfig'; +// import {ConcurrentRoot, LegacyRoot} from 'react-reconciler/src/ReactRootTags'; +// import {allowConcurrentByDefault} from 'shared/ReactFeatureFlags'; + +//const act = React.unstable_act; +const act = React.act; + +// TODO: Remove from public bundle + +type TestRendererOptions = { + createNodeMock: (element: React.ReactElement) => any, + unstable_isConcurrent: boolean, + unstable_strictMode: boolean, + unstable_concurrentUpdatesByDefault: boolean, +}; + +type ReactTestRendererJSON = { + type: string, + props: {[propName: string]: any}, + children: Array | null, + $$typeof?: Symbol, // Optional because we add it with defineProperty(). +}; + +type ReactTestRendererNode = ReactTestRendererJSON | string; + +type FindOptions = { + // performs a "greedy" search: if a matching node is found, will continue + // to search within the matching node's children. (default: true) + deep: boolean, +}; + +export type Predicate = (node: ReactTestInstance) => ?boolean; + +const allowConcurrentByDefault = false; // ? + +const defaultTestOptions = { + createNodeMock: function() { + return null; + }, +}; + +function toJSON(inst: Instance | TextInstance): ReactTestRendererNode | null { + if (inst.isHidden) { + // Omit timed out children from output entirely. This seems like the least + // surprising behavior. We could perhaps add a separate API that includes + // them, if it turns out people need it. + return null; + } + switch (inst.tag) { + case 'TEXT': + return inst.text; + case 'INSTANCE': { + // We don't include the `children` prop in JSON. + // Instead, we will include the actual rendered children. + // @ts-expect-error + const { children, ...props } = inst.props; + + let renderedChildren = null; + if (inst.children && inst.children.length) { + for (let i = 0; i < inst.children.length; i++) { + const renderedChild = toJSON(inst.children[i]); + if (renderedChild !== null) { + if (renderedChildren === null) { + renderedChildren = [renderedChild]; + } else { + renderedChildren.push(renderedChild); + } + } + } + } + + const json: ReactTestRendererJSON = { + type: inst.type, + props: props, + children: renderedChildren, + }; + Object.defineProperty(json, '$$typeof', { + value: Symbol.for('react.test.json'), + }); + return json; + } + + default: + // @ts-expect-error + throw new Error(`Unexpected node type in toJSON: ${inst.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 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(node: ?Fiber) { +// if (node == null) { +// return null; +// } +// switch (node.tag) { +// case HostRoot: +// return childrenToTree(node.child); +// case HostPortal: +// return childrenToTree(node.child); +// case ClassComponent: +// return { +// nodeType: 'component', +// type: node.type, +// props: {...node.memoizedProps}, +// instance: node.stateNode, +// rendered: childrenToTree(node.child), +// }; +// case FunctionComponent: +// case SimpleMemoComponent: +// return { +// nodeType: 'component', +// type: node.type, +// props: {...node.memoizedProps}, +// instance: null, +// rendered: childrenToTree(node.child), +// }; +// case HostComponent: { +// return { +// nodeType: 'host', +// type: node.type, +// props: {...node.memoizedProps}, +// instance: null, // TODO: use createNodeMock here somehow? +// rendered: flatten(nodeAndSiblingsArray(node.child).map(toTree)), +// }; +// } +// case HostText: +// return node.stateNode.text; +// case Fragment: +// case ContextProvider: +// case ContextConsumer: +// case Mode: +// case Profiler: +// case ForwardRef: +// case MemoComponent: +// case IncompleteClassComponent: +// case ScopeComponent: +// return childrenToTree(node.child); +// default: +// throw new Error( +// `toTree() does not yet know how to handle nodes with tag=${node.tag}`, +// ); +// } +// } + +const validWrapperTypes = new Set([ + FunctionComponent, + ClassComponent, + HostComponent, + ForwardRef, + MemoComponent, + SimpleMemoComponent, + // Normally skipped, but used when there's more than one root child. + HostRoot, +]); + +function getChildren(parent: Fiber): Array { + const children: Array = []; + const startingNode = parent; + let node: Fiber = startingNode; + if (node.child === null) { + return children; + } + + node.child.return = node; + node = node.child; + + outer: while (true) { + let descend = false; + if (validWrapperTypes.has(node.tag)) { + children.push(wrapFiber(node)); + } else if (node.tag === HostText) { + if (__DEV__) { + checkPropStringCoercion(node.memoizedProps, 'memoizedProps'); + } + children.push('' + node.memoizedProps); + } else { + descend = true; + } + if (descend && node.child !== null) { + node.child.return = node; + node = node.child; + continue; + } + while (node.sibling === null) { + if (node.return === startingNode) { + break outer; + } + node = (node.return: any); + } + (node.sibling: any).return = node.return; + node = (node.sibling: any); + } + + return children; +} + +class ReactTestInstance { + _fiber: Fiber; + + _currentFiber(): Fiber { + // Throws if this component has been unmounted. + const fiber = findCurrentFiberUsingSlowPath(this._fiber); + + if (fiber === null) { + throw new Error( + "Can't read from currently-mounting component. This error is likely " + + 'caused by a bug in React. Please file an issue.', + ); + } + + return fiber; + } + + constructor(fiber: Fiber) { + if (!validWrapperTypes.has(fiber.tag)) { + throw new Error( + `Unexpected object passed to ReactTestInstance constructor (tag: ${fiber.tag}). ` + + 'This is probably a bug in React.', + ); + } + + this._fiber = fiber; + } + + get instance() { + if (this._fiber.tag === HostComponent) { + return getPublicInstance(this._fiber.stateNode); + } else { + return this._fiber.stateNode; + } + } + + get type() { + return this._fiber.type; + } + + get props(): object { + return this._currentFiber().memoizedProps; + } + + get parent(): ?ReactTestInstance { + let parent = this._fiber.return; + while (parent !== null) { + if (validWrapperTypes.has(parent.tag)) { + if (parent.tag === HostRoot) { + // Special case: we only "materialize" instances for roots + // if they have more than a single child. So we'll check that now. + if (getChildren(parent).length < 2) { + return null; + } + } + return wrapFiber(parent); + } + parent = parent.return; + } + return null; + } + + get children(): Array { + return getChildren(this._currentFiber()); + } + + // Custom search functions + find(predicate: Predicate): ReactTestInstance { + return expectOne( + this.findAll(predicate, {deep: false}), + `matching custom predicate: ${predicate.toString()}`, + ); + } + + findByType(type: any): ReactTestInstance { + return expectOne( + this.findAllByType(type, {deep: false}), + `with node type: "${getComponentNameFromType(type) || 'Unknown'}"`, + ); + } + + findByProps(props: object): ReactTestInstance { + return expectOne( + this.findAllByProps(props, {deep: false}), + `with props: ${JSON.stringify(props)}`, + ); + } + + findAll( + predicate: Predicate, + options: ?FindOptions = null, + ): Array { + return findAll(this, predicate, options); + } + + findAllByType( + type: any, + options: ?FindOptions = null, + ): Array { + return findAll(this, node => node.type === type, options); + } + + findAllByProps( + props: object, + options: ?FindOptions = null, + ): Array { + return findAll( + this, + node => node.props && propsMatch(node.props, props), + options, + ); + } +} + +function findAll( + root: ReactTestInstance, + predicate: Predicate, + options: ?FindOptions, +): Array { + const deep = options ? options.deep : true; + const results = []; + + if (predicate(root)) { + results.push(root); + if (!deep) { + return results; + } + } + + root.children.forEach(child => { + if (typeof child === 'string') { + return; + } + results.push(...findAll(child, predicate, options)); + }); + + return results; +} + +function expectOne( + all: Array, + message: string, +): ReactTestInstance { + if (all.length === 1) { + return all[0]; + } + + const prefix = + all.length === 0 + ? 'No instances found ' + : `Expected 1 but found ${all.length} instances `; + + throw new Error(prefix + message); +} + +function propsMatch(props: object, filter: object): boolean { + for (const key in filter) { + // @ts-expect-error + if (props[key] !== filter[key]) { + return false; + } + } + return true; +} + +function onRecoverableError(error: unknown) { + // TODO: Expose onRecoverableError option to userspace + // eslint-disable-next-line react-internal/no-production-logging, react-internal/warning-args + console.error(error); +} + +function create(element: React.ReactElement, options: TestRendererOptions) { + let createNodeMock = defaultTestOptions.createNodeMock; + let isConcurrent = false; + let isStrictMode = false; + let concurrentUpdatesByDefault = null; + + if (typeof options === 'object' && options !== null) { + if (typeof options.createNodeMock === 'function') { + createNodeMock = options.createNodeMock; + } + + if (options.unstable_isConcurrent === true) { + isConcurrent = true; + } + + if (options.unstable_strictMode === true) { + isStrictMode = true; + } + + if (allowConcurrentByDefault) { + if (options.unstable_concurrentUpdatesByDefault !== undefined) { + concurrentUpdatesByDefault = + options.unstable_concurrentUpdatesByDefault; + } + } + } + + let container = { + children: [], + createNodeMock, + tag: 'CONTAINER', + }; + + let root: FiberRoot | null = createContainer( + container, + isConcurrent ? ConcurrentRoot : LegacyRoot, + null, + isStrictMode, + concurrentUpdatesByDefault, + '', + onRecoverableError, + null, + ); + + if (root == null) { + throw new Error('something went wrong'); + } + + updateContainer(element, root, null, null); + + const entry = { + _Scheduler: Scheduler, + + root: undefined, // makes flow happy + // we define a 'getter' for 'root' below using 'Object.defineProperty' + toJSON(): Array | ReactTestRendererNode | null { + if (root == null || root.current == null || container == null) { + return null; + } + if (container.children.length === 0) { + return null; + } + if (container.children.length === 1) { + return toJSON(container.children[0]); + } + if ( + container.children.length === 2 && + container.children[0].isHidden === true && + container.children[1].isHidden === false + ) { + // Omit timed out children from output entirely, including the fact that we + // temporarily wrap fallback and timed out children in an array. + return toJSON(container.children[1]); + } + let renderedChildren = null; + if (container.children && container.children.length) { + for (let i = 0; i < container.children.length; i++) { + const renderedChild = toJSON(container.children[i]); + if (renderedChild !== null) { + if (renderedChildren === null) { + renderedChildren = [renderedChild]; + } else { + renderedChildren.push(renderedChild); + } + } + } + } + return renderedChildren; + }, + + toTree() { + if (root == null || root.current == null) { + return null; + } + return toTree(root.current); + }, + + update(newElement: React$Element) { + if (root == null || root.current == null) { + return; + } + ReactReconciler.updateContainer(newElement, root, null, null); + }, + + unmount() { + if (root == null || root.current == null) { + return; + } + updateContainer(null, root, null, null); + container = null; + root = null; + }, + + getInstance() { + if (root == null || root.current == null) { + return null; + } + return getPublicRootInstance(root); + }, + + unstable_flushSync: flushSync, + }; + + Object.defineProperty( + entry, + 'root', + ({ + configurable: true, + enumerable: true, + get: function() { + if (root === null) { + throw new Error("Can't access .root on unmounted test renderer"); + } + const children = getChildren(root.current); + if (children.length === 0) { + throw new Error("Can't access .root on unmounted test renderer"); + } else if (children.length === 1) { + // Normally, we skip the root and just give you the child. + return children[0]; + } else { + // However, we give you the root if there's more than one root child. + // We could make this the behavior for all cases but it would be a breaking change. + return wrapFiber(root.current); + } + }, + }: object), + ); + + return entry; +} + +const fiberToWrapper = new WeakMap(); + +function wrapFiber(fiber: Fiber): ReactTestInstance { + let wrapper = fiberToWrapper.get(fiber); + if (wrapper === undefined && fiber.alternate !== null) { + wrapper = fiberToWrapper.get(fiber.alternate); + } + + if (wrapper === undefined) { + wrapper = new ReactTestInstance(fiber); + fiberToWrapper.set(fiber, wrapper); + } + + return wrapper; +} + +// // Enable ReactTestRenderer to be used to test DevTools integration. +// injectIntoDevTools({ +// findFiberByHostInstance: (() => { +// throw new Error('TestRenderer does not support findFiberByHostInstance()'); +// }: any), +// bundleType: __DEV__ ? 1 : 0, +// version: ReactVersion, +// rendererPackageName: 'react-test-renderer', +// }); + +export { + Scheduler as _Scheduler, + create, + /* eslint-disable-next-line camelcase */ + batchedUpdates as unstable_batchedUpdates, + act, +}; \ No newline at end of file From c321efdf2d4290f50ba955f06d5e008b07eece8c Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Sat, 14 Sep 2024 22:58:05 +0200 Subject: [PATCH 03/61] feat: basic rendered implementation --- src/renderer/renderer-orig.ts | 646 +++++++++++++++++++ src/renderer/renderer.ts | 1094 +++++++++++++++------------------ 2 files changed, 1150 insertions(+), 590 deletions(-) create mode 100644 src/renderer/renderer-orig.ts diff --git a/src/renderer/renderer-orig.ts b/src/renderer/renderer-orig.ts new file mode 100644 index 000000000..418ac70a2 --- /dev/null +++ b/src/renderer/renderer-orig.ts @@ -0,0 +1,646 @@ +/** + * 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. + * + */ + +import * as React from 'react'; +// @ts-expect-error +import ReactReconciler, { Fiber, createContainer } from 'react-reconciler'; + +import createReconciler from 'react-reconciler'; + +import { getPublicInstance, Instance, TextInstance } from './react-fiber-config-host'; + +// import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; +// import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes'; + +// import * as Scheduler from 'scheduler/unstable_mock'; +// import { +// getPublicRootInstance, +// createContainer, +// updateContainer, +// flushSync, +// injectIntoDevTools, +// batchedUpdates, +// } from 'react-reconciler/src/ReactFiberReconciler'; +// import {findCurrentFiberUsingSlowPath} from 'react-reconciler/src/ReactFiberTreeReflection'; +// import { +// Fragment, +// FunctionComponent, +// ClassComponent, +// HostComponent, +// HostPortal, +// HostText, +// HostRoot, +// ContextConsumer, +// ContextProvider, +// Mode, +// ForwardRef, +// Profiler, +// MemoComponent, +// SimpleMemoComponent, +// IncompleteClassComponent, +// ScopeComponent, +// } from 'react-reconciler/src/ReactWorkTags'; +// import isArray from 'shared/isArray'; +// import getComponentNameFromType from 'shared/getComponentNameFromType'; +// import ReactVersion from 'shared/ReactVersion'; +// import {checkPropStringCoercion} from 'shared/CheckStringCoercion'; + +// import {getPublicInstance} from './ReactTestHostConfig'; +// import {ConcurrentRoot, LegacyRoot} from 'react-reconciler/src/ReactRootTags'; +// import {allowConcurrentByDefault} from 'shared/ReactFeatureFlags'; + +//const act = React.unstable_act; +const act = React.act; + +// TODO: Remove from public bundle + +type TestRendererOptions = { + createNodeMock: (element: React.ReactElement) => any, + unstable_isConcurrent: boolean, + unstable_strictMode: boolean, + unstable_concurrentUpdatesByDefault: boolean, +}; + +type ReactTestRendererJSON = { + type: string, + props: {[propName: string]: any}, + children: Array | null, + $$typeof?: Symbol, // Optional because we add it with defineProperty(). +}; + +type ReactTestRendererNode = ReactTestRendererJSON | string; + +type FindOptions = { + // performs a "greedy" search: if a matching node is found, will continue + // to search within the matching node's children. (default: true) + deep: boolean, +}; + +export type Predicate = (node: ReactTestInstance) => ?boolean; + +const allowConcurrentByDefault = false; // ? + +const defaultTestOptions = { + createNodeMock: function() { + return null; + }, +}; + +function toJSON(inst: Instance | TextInstance): ReactTestRendererNode | null { + if (inst.isHidden) { + // Omit timed out children from output entirely. This seems like the least + // surprising behavior. We could perhaps add a separate API that includes + // them, if it turns out people need it. + return null; + } + switch (inst.tag) { + case 'TEXT': + return inst.text; + case 'INSTANCE': { + // We don't include the `children` prop in JSON. + // Instead, we will include the actual rendered children. + // @ts-expect-error + const { children, ...props } = inst.props; + + let renderedChildren = null; + if (inst.children && inst.children.length) { + for (let i = 0; i < inst.children.length; i++) { + const renderedChild = toJSON(inst.children[i]); + if (renderedChild !== null) { + if (renderedChildren === null) { + renderedChildren = [renderedChild]; + } else { + renderedChildren.push(renderedChild); + } + } + } + } + + const json: ReactTestRendererJSON = { + type: inst.type, + props: props, + children: renderedChildren, + }; + Object.defineProperty(json, '$$typeof', { + value: Symbol.for('react.test.json'), + }); + return json; + } + + default: + // @ts-expect-error + throw new Error(`Unexpected node type in toJSON: ${inst.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 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(node: ?Fiber) { +// if (node == null) { +// return null; +// } +// switch (node.tag) { +// case HostRoot: +// return childrenToTree(node.child); +// case HostPortal: +// return childrenToTree(node.child); +// case ClassComponent: +// return { +// nodeType: 'component', +// type: node.type, +// props: {...node.memoizedProps}, +// instance: node.stateNode, +// rendered: childrenToTree(node.child), +// }; +// case FunctionComponent: +// case SimpleMemoComponent: +// return { +// nodeType: 'component', +// type: node.type, +// props: {...node.memoizedProps}, +// instance: null, +// rendered: childrenToTree(node.child), +// }; +// case HostComponent: { +// return { +// nodeType: 'host', +// type: node.type, +// props: {...node.memoizedProps}, +// instance: null, // TODO: use createNodeMock here somehow? +// rendered: flatten(nodeAndSiblingsArray(node.child).map(toTree)), +// }; +// } +// case HostText: +// return node.stateNode.text; +// case Fragment: +// case ContextProvider: +// case ContextConsumer: +// case Mode: +// case Profiler: +// case ForwardRef: +// case MemoComponent: +// case IncompleteClassComponent: +// case ScopeComponent: +// return childrenToTree(node.child); +// default: +// throw new Error( +// `toTree() does not yet know how to handle nodes with tag=${node.tag}`, +// ); +// } +// } + +const validWrapperTypes = new Set([ + FunctionComponent, + ClassComponent, + HostComponent, + ForwardRef, + MemoComponent, + SimpleMemoComponent, + // Normally skipped, but used when there's more than one root child. + HostRoot, +]); + +function getChildren(parent: Fiber): Array { + const children: Array = []; + const startingNode = parent; + let node: Fiber = startingNode; + if (node.child === null) { + return children; + } + + node.child.return = node; + node = node.child; + + outer: while (true) { + let descend = false; + if (validWrapperTypes.has(node.tag)) { + children.push(wrapFiber(node)); + } else if (node.tag === HostText) { + if (__DEV__) { + checkPropStringCoercion(node.memoizedProps, 'memoizedProps'); + } + children.push('' + node.memoizedProps); + } else { + descend = true; + } + if (descend && node.child !== null) { + node.child.return = node; + node = node.child; + continue; + } + while (node.sibling === null) { + if (node.return === startingNode) { + break outer; + } + node = (node.return: any); + } + (node.sibling: any).return = node.return; + node = (node.sibling: any); + } + + return children; +} + +class ReactTestInstance { + _fiber: Fiber; + + _currentFiber(): Fiber { + // Throws if this component has been unmounted. + const fiber = findCurrentFiberUsingSlowPath(this._fiber); + + if (fiber === null) { + throw new Error( + "Can't read from currently-mounting component. This error is likely " + + 'caused by a bug in React. Please file an issue.', + ); + } + + return fiber; + } + + constructor(fiber: Fiber) { + if (!validWrapperTypes.has(fiber.tag)) { + throw new Error( + `Unexpected object passed to ReactTestInstance constructor (tag: ${fiber.tag}). ` + + 'This is probably a bug in React.', + ); + } + + this._fiber = fiber; + } + + get instance() { + if (this._fiber.tag === HostComponent) { + return getPublicInstance(this._fiber.stateNode); + } else { + return this._fiber.stateNode; + } + } + + get type() { + return this._fiber.type; + } + + get props(): object { + return this._currentFiber().memoizedProps; + } + + get parent(): ?ReactTestInstance { + let parent = this._fiber.return; + while (parent !== null) { + if (validWrapperTypes.has(parent.tag)) { + if (parent.tag === HostRoot) { + // Special case: we only "materialize" instances for roots + // if they have more than a single child. So we'll check that now. + if (getChildren(parent).length < 2) { + return null; + } + } + return wrapFiber(parent); + } + parent = parent.return; + } + return null; + } + + get children(): Array { + return getChildren(this._currentFiber()); + } + + // Custom search functions + find(predicate: Predicate): ReactTestInstance { + return expectOne( + this.findAll(predicate, {deep: false}), + `matching custom predicate: ${predicate.toString()}`, + ); + } + + findByType(type: any): ReactTestInstance { + return expectOne( + this.findAllByType(type, {deep: false}), + `with node type: "${getComponentNameFromType(type) || 'Unknown'}"`, + ); + } + + findByProps(props: object): ReactTestInstance { + return expectOne( + this.findAllByProps(props, {deep: false}), + `with props: ${JSON.stringify(props)}`, + ); + } + + findAll( + predicate: Predicate, + options: ?FindOptions = null, + ): Array { + return findAll(this, predicate, options); + } + + findAllByType( + type: any, + options: ?FindOptions = null, + ): Array { + return findAll(this, node => node.type === type, options); + } + + findAllByProps( + props: object, + options: ?FindOptions = null, + ): Array { + return findAll( + this, + node => node.props && propsMatch(node.props, props), + options, + ); + } +} + +function findAll( + root: ReactTestInstance, + predicate: Predicate, + options: ?FindOptions, +): Array { + const deep = options ? options.deep : true; + const results = []; + + if (predicate(root)) { + results.push(root); + if (!deep) { + return results; + } + } + + root.children.forEach(child => { + if (typeof child === 'string') { + return; + } + results.push(...findAll(child, predicate, options)); + }); + + return results; +} + +function expectOne( + all: Array, + message: string, +): ReactTestInstance { + if (all.length === 1) { + return all[0]; + } + + const prefix = + all.length === 0 + ? 'No instances found ' + : `Expected 1 but found ${all.length} instances `; + + throw new Error(prefix + message); +} + +function propsMatch(props: object, filter: object): boolean { + for (const key in filter) { + // @ts-expect-error + if (props[key] !== filter[key]) { + return false; + } + } + return true; +} + +function onRecoverableError(error: unknown) { + // TODO: Expose onRecoverableError option to userspace + // eslint-disable-next-line react-internal/no-production-logging, react-internal/warning-args + console.error(error); +} + +function create(element: React.ReactElement, options: TestRendererOptions) { + let createNodeMock = defaultTestOptions.createNodeMock; + let isConcurrent = false; + let isStrictMode = false; + let concurrentUpdatesByDefault = null; + + if (typeof options === 'object' && options !== null) { + if (typeof options.createNodeMock === 'function') { + createNodeMock = options.createNodeMock; + } + + if (options.unstable_isConcurrent === true) { + isConcurrent = true; + } + + if (options.unstable_strictMode === true) { + isStrictMode = true; + } + + if (allowConcurrentByDefault) { + if (options.unstable_concurrentUpdatesByDefault !== undefined) { + concurrentUpdatesByDefault = + options.unstable_concurrentUpdatesByDefault; + } + } + } + + let container = { + children: [], + createNodeMock, + tag: 'CONTAINER', + }; + + let root: FiberRoot | null = createContainer( + container, + isConcurrent ? ConcurrentRoot : LegacyRoot, + null, + isStrictMode, + concurrentUpdatesByDefault, + '', + onRecoverableError, + null, + ); + + if (root == null) { + throw new Error('something went wrong'); + } + + updateContainer(element, root, null, null); + + const entry = { + _Scheduler: Scheduler, + + root: undefined, // makes flow happy + // we define a 'getter' for 'root' below using 'Object.defineProperty' + toJSON(): Array | ReactTestRendererNode | null { + if (root == null || root.current == null || container == null) { + return null; + } + if (container.children.length === 0) { + return null; + } + if (container.children.length === 1) { + return toJSON(container.children[0]); + } + if ( + container.children.length === 2 && + container.children[0].isHidden === true && + container.children[1].isHidden === false + ) { + // Omit timed out children from output entirely, including the fact that we + // temporarily wrap fallback and timed out children in an array. + return toJSON(container.children[1]); + } + let renderedChildren = null; + if (container.children && container.children.length) { + for (let i = 0; i < container.children.length; i++) { + const renderedChild = toJSON(container.children[i]); + if (renderedChild !== null) { + if (renderedChildren === null) { + renderedChildren = [renderedChild]; + } else { + renderedChildren.push(renderedChild); + } + } + } + } + return renderedChildren; + }, + + toTree() { + if (root == null || root.current == null) { + return null; + } + return toTree(root.current); + }, + + update(newElement: React$Element) { + if (root == null || root.current == null) { + return; + } + ReactReconciler.updateContainer(newElement, root, null, null); + }, + + unmount() { + if (root == null || root.current == null) { + return; + } + updateContainer(null, root, null, null); + container = null; + root = null; + }, + + getInstance() { + if (root == null || root.current == null) { + return null; + } + return getPublicRootInstance(root); + }, + + unstable_flushSync: flushSync, + }; + + Object.defineProperty( + entry, + 'root', + ({ + configurable: true, + enumerable: true, + get: function() { + if (root === null) { + throw new Error("Can't access .root on unmounted test renderer"); + } + const children = getChildren(root.current); + if (children.length === 0) { + throw new Error("Can't access .root on unmounted test renderer"); + } else if (children.length === 1) { + // Normally, we skip the root and just give you the child. + return children[0]; + } else { + // However, we give you the root if there's more than one root child. + // We could make this the behavior for all cases but it would be a breaking change. + return wrapFiber(root.current); + } + }, + }: object), + ); + + return entry; +} + +const fiberToWrapper = new WeakMap(); + +function wrapFiber(fiber: Fiber): ReactTestInstance { + let wrapper = fiberToWrapper.get(fiber); + if (wrapper === undefined && fiber.alternate !== null) { + wrapper = fiberToWrapper.get(fiber.alternate); + } + + if (wrapper === undefined) { + wrapper = new ReactTestInstance(fiber); + fiberToWrapper.set(fiber, wrapper); + } + + return wrapper; +} + +// // Enable ReactTestRenderer to be used to test DevTools integration. +// injectIntoDevTools({ +// findFiberByHostInstance: (() => { +// throw new Error('TestRenderer does not support findFiberByHostInstance()'); +// }: any), +// bundleType: __DEV__ ? 1 : 0, +// version: ReactVersion, +// rendererPackageName: 'react-test-renderer', +// }); + +export { + Scheduler as _Scheduler, + create, + /* eslint-disable-next-line camelcase */ + batchedUpdates as unstable_batchedUpdates, + act, +}; \ No newline at end of file diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts index 418ac70a2..27fd91266 100644 --- a/src/renderer/renderer.ts +++ b/src/renderer/renderer.ts @@ -1,646 +1,560 @@ -/** - * 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. - * - */ - -import * as React from 'react'; -// @ts-expect-error -import ReactReconciler, { Fiber, createContainer } from 'react-reconciler'; - -import createReconciler from 'react-reconciler'; - -import { getPublicInstance, Instance, TextInstance } from './react-fiber-config-host'; - -// import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; -// import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes'; - -// import * as Scheduler from 'scheduler/unstable_mock'; -// import { -// getPublicRootInstance, -// createContainer, -// updateContainer, -// flushSync, -// injectIntoDevTools, -// batchedUpdates, -// } from 'react-reconciler/src/ReactFiberReconciler'; -// import {findCurrentFiberUsingSlowPath} from 'react-reconciler/src/ReactFiberTreeReflection'; -// import { -// Fragment, -// FunctionComponent, -// ClassComponent, -// HostComponent, -// HostPortal, -// HostText, -// HostRoot, -// ContextConsumer, -// ContextProvider, -// Mode, -// ForwardRef, -// Profiler, -// MemoComponent, -// SimpleMemoComponent, -// IncompleteClassComponent, -// ScopeComponent, -// } from 'react-reconciler/src/ReactWorkTags'; -// import isArray from 'shared/isArray'; -// import getComponentNameFromType from 'shared/getComponentNameFromType'; -// import ReactVersion from 'shared/ReactVersion'; -// import {checkPropStringCoercion} from 'shared/CheckStringCoercion'; - -// import {getPublicInstance} from './ReactTestHostConfig'; -// import {ConcurrentRoot, LegacyRoot} from 'react-reconciler/src/ReactRootTags'; -// import {allowConcurrentByDefault} from 'shared/ReactFeatureFlags'; - -//const act = React.unstable_act; -const act = React.act; - -// TODO: Remove from public bundle - -type TestRendererOptions = { - createNodeMock: (element: React.ReactElement) => any, - unstable_isConcurrent: boolean, - unstable_strictMode: boolean, - unstable_concurrentUpdatesByDefault: boolean, +import Reconciler, { Fiber } from 'react-reconciler'; +import { DefaultEventPriority } from 'react-reconciler/constants'; + +export type Type = string; +export type Props = object; +export type HostContext = object; +export type OpaqueHandle = Fiber; +export type PublicInstance = unknown | TextInstance; +export type SuspenseInstance = unknown; +export type UpdatePayload = unknown; + +export type Container = { + tag: 'CONTAINER'; + children: Array; // Added SuspenseInstance + createNodeMock: Function; }; -type ReactTestRendererJSON = { - type: string, - props: {[propName: string]: any}, - children: Array | null, - $$typeof?: Symbol, // Optional because we add it with defineProperty(). +export type Instance = { + tag: 'INSTANCE'; + type: string; + props: object; + isHidden: boolean; + children: Array; + rootContainer: Container; + internalHandle: OpaqueHandle; }; -type ReactTestRendererNode = ReactTestRendererJSON | string; - -type FindOptions = { - // performs a "greedy" search: if a matching node is found, will continue - // to search within the matching node's children. (default: true) - deep: boolean, +export type TextInstance = { + tag: 'TEXT'; + text: string; + isHidden: boolean; }; -export type Predicate = (node: ReactTestInstance) => ?boolean; +const NO_CONTEXT = {}; +const UPDATE_SIGNAL = {}; +const nodeToInstanceMap = new WeakMap(); -const allowConcurrentByDefault = false; // ? +if (__DEV__) { + Object.freeze(NO_CONTEXT); + Object.freeze(UPDATE_SIGNAL); +} -const defaultTestOptions = { - createNodeMock: function() { - return null; +const hostConfig = { + /** + * The reconciler has two modes: mutation mode and persistent mode. You must specify one of them. + * + * If your target platform is similar to the DOM and has methods similar to `appendChild`, `removeChild`, and so on, you'll want to use the **mutation mode**. This is the same mode used by React DOM, React ART, and the classic React Native renderer. + * + * ```js + * const HostConfig = { + * // ... + * supportsMutation: true, + * // ... + * } + * ``` + * + * Depending on the mode, the reconciler will call different methods on your host config. + * + * If you're not sure which one you want, you likely need the mutation mode. + */ + supportsMutation: true, + + /** + * The reconciler has two modes: mutation mode and persistent mode. You must specify one of them. + * + * If your target platform has immutable trees, you'll want the **persistent mode** instead. In that mode, existing nodes are never mutated, and instead every change clones the parent tree and then replaces the whole parent tree at the root. This is the node used by the new React Native renderer, codenamed "Fabric". + * + * ```js + * const HostConfig = { + * // ... + * supportsPersistence: true, + * // ... + * } + * ``` + * + * Depending on the mode, the reconciler will call different methods on your host config. + * + * If you're not sure which one you want, you likely need the mutation mode. + */ + // TODO: if RN Fabric uses that, shouldn't we use that as well? + supportsPersistence: false, + + /** + * This method should return a newly created node. For example, the DOM renderer would call `document.createElement(type)` here and then set the properties from `props`. + * + * You can use `rootContainer` to access the root container associated with that tree. For example, in the DOM renderer, this is useful to get the correct `document` reference that the root belongs to. + * + * The `hostContext` parameter lets you keep track of some information about your current place in the tree. To learn more about it, see `getChildHostContext` below. + * + * The `internalHandle` data structure is meant to be opaque. If you bend the rules and rely on its internal fields, be aware that it may change significantly between versions. You're taking on additional maintenance risk by reading from it, and giving up all guarantees if you write something to it. + * + * This method happens **in the render phase**. It can (and usually should) mutate the node it has just created before returning it, but it must not modify any other nodes. It must not register any event handlers on the parent tree. This is because an instance being created doesn't guarantee it would be placed in the tree — it could be left unused and later collected by GC. If you need to do something when an instance is definitely in the tree, look at `commitMount` instead. + */ + createInstance( + type: Type, + props: Props, + rootContainer: Container, + _hostContext: HostContext, + internalHandle: OpaqueHandle, + ): Instance { + return { + tag: 'INSTANCE', + type, + props, + isHidden: false, + children: [], + rootContainer, + internalHandle, + }; }, -}; -function toJSON(inst: Instance | TextInstance): ReactTestRendererNode | null { - if (inst.isHidden) { - // Omit timed out children from output entirely. This seems like the least - // surprising behavior. We could perhaps add a separate API that includes - // them, if it turns out people need it. - return null; - } - switch (inst.tag) { - case 'TEXT': - return inst.text; - case 'INSTANCE': { - // We don't include the `children` prop in JSON. - // Instead, we will include the actual rendered children. - // @ts-expect-error - const { children, ...props } = inst.props; - - let renderedChildren = null; - if (inst.children && inst.children.length) { - for (let i = 0; i < inst.children.length; i++) { - const renderedChild = toJSON(inst.children[i]); - if (renderedChild !== null) { - if (renderedChildren === null) { - renderedChildren = [renderedChild]; - } else { - renderedChildren.push(renderedChild); - } - } - } - } + /** + * Same as `createInstance`, but for text nodes. If your renderer doesn't support text nodes, you can throw here. + */ + createTextInstance( + text: string, + _rootContainer: Container, + _hostContext: HostContext, + _internalHandle: OpaqueHandle, + ): TextInstance { + return { + tag: 'TEXT', + text, + isHidden: false, + }; + }, - const json: ReactTestRendererJSON = { - type: inst.type, - props: props, - children: renderedChildren, - }; - Object.defineProperty(json, '$$typeof', { - value: Symbol.for('react.test.json'), - }); - return json; + appendInitialChild(parentInstance: Instance, child: Instance | TextInstance): void { + const index = parentInstance.children.indexOf(child); + if (index !== -1) { + parentInstance.children.splice(index, 1); } - default: - // @ts-expect-error - throw new Error(`Unexpected node type in toJSON: ${inst.tag}`); - } -} + parentInstance.children.push(child); + }, -function childrenToTree(node) { - if (!node) { - return null; - } + /** + * In this method, you can perform some final mutations on the `instance`. Unlike with `createInstance`, by the time `finalizeInitialChildren` is called, all the initial children have already been added to the `instance`, but the instance itself has not yet been connected to the tree on the screen. + * + * This method happens **in the render phase**. It can mutate `instance`, but it must not modify any other nodes. It's called while the tree is still being built up and not connected to the actual tree on the screen. + * + * There is a second purpose to this method. It lets you specify whether there is some work that needs to happen when the node is connected to the tree on the screen. If you return `true`, the instance will receive a `commitMount` call later. See its documentation below. + * + * If you don't want to do anything here, you should return `false`. + */ + finalizeInitialChildren( + _instance: Instance, + _type: Type, + _props: Props, + _rootContainer: Container, + _hostContext: HostContext, + ): boolean { + return false; + }, - const children = nodeAndSiblingsArray(node); - if (children.length === 0) { - return null; - } else if (children.length === 1) { - return toTree(children[0]); - } + /** + * React calls this method so that you can compare the previous and the next props, and decide whether you need to update the underlying instance or not. If you don't need to update it, return `null`. If you need to update it, you can return an arbitrary object representing the changes that need to happen. Then in `commitUpdate` you would need to apply those changes to the instance. + * + * This method happens **in the render phase**. It should only *calculate* the update — but not apply it! For example, the DOM renderer returns an array that looks like `[prop1, value1, prop2, value2, ...]` for all props that have actually changed. And only in `commitUpdate` it applies those changes. You should calculate as much as you can in `prepareUpdate` so that `commitUpdate` can be very fast and straightforward. + * + * See the meaning of `rootContainer` and `hostContext` in the `createInstance` documentation. + */ + prepareUpdate( + _instance: Instance, + _type: Type, + _oldProps: Props, + _newProps: Props, + _rootContainer: Container, + _hostContext: HostContext, + ): UpdatePayload | null { + return UPDATE_SIGNAL; + }, - return flatten(children.map(toTree)); -} + /** + * Some target platforms support setting an instance's text content without manually creating a text node. For example, in the DOM, you can set `node.textContent` instead of creating a text node and appending it. + * + * If you return `true` from this method, React will assume that this node's children are text, and will not create nodes for them. It will instead rely on you to have filled that text during `createInstance`. This is a performance optimization. For example, the DOM renderer returns `true` only if `type` is a known text-only parent (like `'textarea'`) or if `props.children` has a `'string'` type. If you return `true`, you will need to implement `resetTextContent` too. + * + * If you don't want to do anything here, you should return `false`. + * + * This method happens **in the render phase**. Do not mutate the tree from it. + */ + shouldSetTextContent(_type: Type, _props: Props): boolean { + // TODO: what should RN do here? + return false; + }, -function nodeAndSiblingsArray(nodeWithSibling) { - const array = []; - let node = nodeWithSibling; - while (node != null) { - array.push(node); - node = node.sibling; - } + /** + * This method lets you return the initial host context from the root of the tree. See `getChildHostContext` for the explanation of host context. + * + * If you don't intend to use host context, you can return `null`. + * + * This method happens **in the render phase**. Do not mutate the tree from it. + */ + getRootHostContext(_rootContainer: Container): HostContext | null { + return NO_CONTEXT; + }, - return array; -} + /** + * Host context lets you track some information about where you are in the tree so that it's available inside `createInstance` as the `hostContext` parameter. For example, the DOM renderer uses it to track whether it's inside an HTML or an SVG tree, because `createInstance` implementation needs to be different for them. + * + * If the node of this `type` does not influence the context you want to pass down, you can return `parentHostContext`. Alternatively, you can return any custom object representing the information you want to pass down. + * + * If you don't want to do anything here, return `parentHostContext`. + * + * This method happens **in the render phase**. Do not mutate the tree from it. + */ + getChildHostContext( + _parentHostContext: HostContext, + _type: Type, + _rootContainer: Container, + ): HostContext { + return NO_CONTEXT; + }, -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; -} + /** + * Determines what object gets exposed as a ref. You'll likely want to return the `instance` itself. But in some cases it might make sense to only expose some part of it. + * + * If you don't want to do anything here, return `instance`. + */ + getPublicInstance(instance: Instance | TextInstance): PublicInstance { + switch (instance.tag) { + case 'INSTANCE': { + const createNodeMock = instance.rootContainer.createNodeMock; + const mockNode = createNodeMock({ + type: instance.type, + props: instance.props, + }); + if (typeof mockNode === 'object' && mockNode !== null) { + nodeToInstanceMap.set(mockNode, instance); + } -// function toTree(node: ?Fiber) { -// if (node == null) { -// return null; -// } -// switch (node.tag) { -// case HostRoot: -// return childrenToTree(node.child); -// case HostPortal: -// return childrenToTree(node.child); -// case ClassComponent: -// return { -// nodeType: 'component', -// type: node.type, -// props: {...node.memoizedProps}, -// instance: node.stateNode, -// rendered: childrenToTree(node.child), -// }; -// case FunctionComponent: -// case SimpleMemoComponent: -// return { -// nodeType: 'component', -// type: node.type, -// props: {...node.memoizedProps}, -// instance: null, -// rendered: childrenToTree(node.child), -// }; -// case HostComponent: { -// return { -// nodeType: 'host', -// type: node.type, -// props: {...node.memoizedProps}, -// instance: null, // TODO: use createNodeMock here somehow? -// rendered: flatten(nodeAndSiblingsArray(node.child).map(toTree)), -// }; -// } -// case HostText: -// return node.stateNode.text; -// case Fragment: -// case ContextProvider: -// case ContextConsumer: -// case Mode: -// case Profiler: -// case ForwardRef: -// case MemoComponent: -// case IncompleteClassComponent: -// case ScopeComponent: -// return childrenToTree(node.child); -// default: -// throw new Error( -// `toTree() does not yet know how to handle nodes with tag=${node.tag}`, -// ); -// } -// } - -const validWrapperTypes = new Set([ - FunctionComponent, - ClassComponent, - HostComponent, - ForwardRef, - MemoComponent, - SimpleMemoComponent, - // Normally skipped, but used when there's more than one root child. - HostRoot, -]); - -function getChildren(parent: Fiber): Array { - const children: Array = []; - const startingNode = parent; - let node: Fiber = startingNode; - if (node.child === null) { - return children; - } - - node.child.return = node; - node = node.child; - - outer: while (true) { - let descend = false; - if (validWrapperTypes.has(node.tag)) { - children.push(wrapFiber(node)); - } else if (node.tag === HostText) { - if (__DEV__) { - checkPropStringCoercion(node.memoizedProps, 'memoizedProps'); + return mockNode; } - children.push('' + node.memoizedProps); - } else { - descend = true; - } - if (descend && node.child !== null) { - node.child.return = node; - node = node.child; - continue; - } - while (node.sibling === null) { - if (node.return === startingNode) { - break outer; - } - node = (node.return: any); + + default: + return instance; } - (node.sibling: any).return = node.return; - node = (node.sibling: any); - } + }, - return children; -} + /** + * This method lets you store some information before React starts making changes to the tree on the screen. For example, the DOM renderer stores the current text selection so that it can later restore it. This method is mirrored by `resetAfterCommit`. + * + * Even if you don't want to do anything here, you need to return `null` from it. + */ + prepareForCommit(_containerInfo: Container): Record | null { + return null; // noop + }, + + /** + * This method is called right after React has performed the tree mutations. You can use it to restore something you've stored in `prepareForCommit` — for example, text selection. + * + * You can leave it empty. + */ + resetAfterCommit(_containerInfo: Container): void { + // noop + }, -class ReactTestInstance { - _fiber: Fiber; + /** + * This method is called for a container that's used as a portal target. Usually you can leave it empty. + */ + preparePortalMount(_containerInfo: Container): void { + // noop + }, - _currentFiber(): Fiber { - // Throws if this component has been unmounted. - const fiber = findCurrentFiberUsingSlowPath(this._fiber); + /** + * You can proxy this to `setTimeout` or its equivalent in your environment. + */ + scheduleTimeout: setTimeout, + cancelTimeout: clearTimeout, + + /** + * This is a property (not a function) that should be set to something that can never be a valid timeout ID. For example, you can set it to `-1`. + */ + noTimeout: -1, + + /** + * Set this to `true` to indicate that your renderer supports `scheduleMicrotask`. We use microtasks as part of our discrete event implementation in React DOM. If you're not sure if your renderer should support this, you probably should. The option to not implement `scheduleMicrotask` exists so that platforms with more control over user events, like React Native, can choose to use a different mechanism. + */ + // TODO: what should RN do here? + supportsMicrotasks: true, + + /** + * Optional. You can proxy this to `queueMicrotask` or its equivalent in your environment. + */ + // TODO: what should RN do here? + scheduleMicrotask: queueMicrotask, + + /** + * This is a property (not a function) that should be set to `true` if your renderer is the main one on the page. For example, if you're writing a renderer for the Terminal, it makes sense to set it to `true`, but if your renderer is used *on top of* React DOM or some other existing renderer, set it to `false`. + */ + isPrimaryRenderer: true, + + /** + * Whether the renderer shouldn't trigger missing `act()` warnings + */ + // TODO: what should RN do here? + warnsIfNotActing: true, + + /** + * To implement this method, you'll need some constants available on the special `react-reconciler/constants` entry point: + * + * ``` + * import { + * DiscreteEventPriority, + * ContinuousEventPriority, + * DefaultEventPriority, + * } from 'react-reconciler/constants'; + * + * const HostConfig = { + * // ... + * getCurrentEventPriority() { + * return DefaultEventPriority; + * }, + * // ... + * } + * + * const MyRenderer = Reconciler(HostConfig); + * ``` + * + * The constant you return depends on which event, if any, is being handled right now. (In the browser, you can check this using `window.event && window.event.type`). + * + * - **Discrete events**: If the active event is directly caused by the user (such as mouse and keyboard events) and each event in a sequence is intentional (e.g. click), return DiscreteEventPriority. This tells React that they should interrupt any background work and cannot be batched across time. + * + * - **Continuous events**: If the active event is directly caused by the user but the user can't distinguish between individual events in a sequence (e.g. mouseover), return ContinuousEventPriority. This tells React they should interrupt any background work but can be batched across time. + * + * - **Other events / No active event**: In all other cases, return DefaultEventPriority. This tells React that this event is considered background work, and interactive events will be prioritized over it. + * + * You can consult the `getCurrentEventPriority()` implementation in `ReactDOMHostConfig.js` for a reference implementation. + */ + getCurrentEventPriority() { + return DefaultEventPriority; + }, - if (fiber === null) { - throw new Error( - "Can't read from currently-mounting component. This error is likely " + - 'caused by a bug in React. Please file an issue.', - ); + getInstanceFromNode(node: any): OpaqueHandle | null | undefined { + const instance = nodeToInstanceMap.get(node); + if (instance !== undefined) { + // TODO: why not just return the instance? + return instance.internalHandle; } - return fiber; - } + return null; + }, - constructor(fiber: Fiber) { - if (!validWrapperTypes.has(fiber.tag)) { - throw new Error( - `Unexpected object passed to ReactTestInstance constructor (tag: ${fiber.tag}). ` + - 'This is probably a bug in React.', - ); - } + beforeActiveInstanceBlur(): void { + // noop + }, - this._fiber = fiber; - } + afterActiveInstanceBlur(): void { + // noop + }, - get instance() { - if (this._fiber.tag === HostComponent) { - return getPublicInstance(this._fiber.stateNode); - } else { - return this._fiber.stateNode; - } - } - - get type() { - return this._fiber.type; - } - - get props(): object { - return this._currentFiber().memoizedProps; - } - - get parent(): ?ReactTestInstance { - let parent = this._fiber.return; - while (parent !== null) { - if (validWrapperTypes.has(parent.tag)) { - if (parent.tag === HostRoot) { - // Special case: we only "materialize" instances for roots - // if they have more than a single child. So we'll check that now. - if (getChildren(parent).length < 2) { - return null; - } - } - return wrapFiber(parent); + prepareScopeUpdate(scopeInstance: any, instance: any): void { + nodeToInstanceMap.set(scopeInstance, instance); + }, + + getInstanceFromScope(scopeInstance: any): null | Instance { + return nodeToInstanceMap.get(scopeInstance) || null; + }, + + detachDeletedInstance(_node: Instance): void { + // noop + }, + + /** + * This method should mutate the `parentInstance` and add the child to its list of children. For example, in the DOM this would translate to a `parentInstance.appendChild(child)` call. + * + * Although this method currently runs in the commit phase, you still should not mutate any other nodes in it. If you need to do some additional work when a node is definitely connected to the visible tree, look at `commitMount`. + */ + appendChild(parentInstance: Instance, child: Instance | TextInstance): void { + if (__DEV__) { + if (!Array.isArray(parentInstance.children)) { + // eslint-disable-next-line no-console + console.error( + 'An invalid container has been provided. ' + + 'This may indicate that another renderer is being used in addition to the test renderer. ' + + '(For example, ReactDOM.createPortal inside of a ReactTestRenderer tree.) ' + + 'This is not supported.', + ); } - parent = parent.return; } - return null; - } - - get children(): Array { - return getChildren(this._currentFiber()); - } - - // Custom search functions - find(predicate: Predicate): ReactTestInstance { - return expectOne( - this.findAll(predicate, {deep: false}), - `matching custom predicate: ${predicate.toString()}`, - ); - } - - findByType(type: any): ReactTestInstance { - return expectOne( - this.findAllByType(type, {deep: false}), - `with node type: "${getComponentNameFromType(type) || 'Unknown'}"`, - ); - } - - findByProps(props: object): ReactTestInstance { - return expectOne( - this.findAllByProps(props, {deep: false}), - `with props: ${JSON.stringify(props)}`, - ); - } - - findAll( - predicate: Predicate, - options: ?FindOptions = null, - ): Array { - return findAll(this, predicate, options); - } - - findAllByType( - type: any, - options: ?FindOptions = null, - ): Array { - return findAll(this, node => node.type === type, options); - } - - findAllByProps( - props: object, - options: ?FindOptions = null, - ): Array { - return findAll( - this, - node => node.props && propsMatch(node.props, props), - options, - ); - } -} -function findAll( - root: ReactTestInstance, - predicate: Predicate, - options: ?FindOptions, -): Array { - const deep = options ? options.deep : true; - const results = []; - - if (predicate(root)) { - results.push(root); - if (!deep) { - return results; + const index = parentInstance.children.indexOf(child); + if (index !== -1) { + parentInstance.children.splice(index, 1); } - } - root.children.forEach(child => { - if (typeof child === 'string') { - return; - } - results.push(...findAll(child, predicate, options)); - }); + parentInstance.children.push(child); + }, - return results; -} + /** + * Same as `appendChild`, but for when a node is attached to the root container. This is useful if attaching to the root has a slightly different implementation, or if the root container nodes are of a different type than the rest of the tree. + */ + appendChildToContainer(container: Container, child: Instance | TextInstance): void { + const index = container.children.indexOf(child); + if (index !== -1) { + container.children.splice(index, 1); + } -function expectOne( - all: Array, - message: string, -): ReactTestInstance { - if (all.length === 1) { - return all[0]; - } + container.children.push(child); + }, - const prefix = - all.length === 0 - ? 'No instances found ' - : `Expected 1 but found ${all.length} instances `; + /** + * This method should mutate the `parentInstance` and place the `child` before `beforeChild` in the list of its children. For example, in the DOM this would translate to a `parentInstance.insertBefore(child, beforeChild)` call. + * + * Note that React uses this method both for insertions and for reordering nodes. Similar to DOM, it is expected that you can call `insertBefore` to reposition an existing child. Do not mutate any other parts of the tree from it. + */ + insertBefore( + parentInstance: Instance, + child: Instance | TextInstance, + beforeChild: Instance | TextInstance | SuspenseInstance, + ): void { + const index = parentInstance.children.indexOf(child); + if (index !== -1) { + parentInstance.children.splice(index, 1); + } - throw new Error(prefix + message); -} + const beforeIndex = parentInstance.children.indexOf(beforeChild); + parentInstance.children.splice(beforeIndex, 0, child); + }, -function propsMatch(props: object, filter: object): boolean { - for (const key in filter) { - // @ts-expect-error - if (props[key] !== filter[key]) { - return false; + /** + * Same as `insertBefore`, but for when a node is attached to the root container. This is useful if attaching to the root has a slightly different implementation, or if the root container nodes are of a different type than the rest of the tree. + */ + insertInContainerBefore( + container: Container, + child: Instance | TextInstance, + beforeChild: Instance | TextInstance | SuspenseInstance, + ): void { + const index = container.children.indexOf(child); + if (index !== -1) { + container.children.splice(index, 1); } - } - return true; -} -function onRecoverableError(error: unknown) { - // TODO: Expose onRecoverableError option to userspace - // eslint-disable-next-line react-internal/no-production-logging, react-internal/warning-args - console.error(error); -} + const beforeIndex = container.children.indexOf(beforeChild); + container.children.splice(beforeIndex, 0, child); + }, -function create(element: React.ReactElement, options: TestRendererOptions) { - let createNodeMock = defaultTestOptions.createNodeMock; - let isConcurrent = false; - let isStrictMode = false; - let concurrentUpdatesByDefault = null; + /** + * This method should mutate the `parentInstance` to remove the `child` from the list of its children. + * + * React will only call it for the top-level node that is being removed. It is expected that garbage collection would take care of the whole subtree. You are not expected to traverse the child tree in it. + */ + removeChild(parentInstance: Instance, child: Instance | TextInstance | SuspenseInstance): void { + const index = parentInstance.children.indexOf(child); + parentInstance.children.splice(index, 1); + }, - if (typeof options === 'object' && options !== null) { - if (typeof options.createNodeMock === 'function') { - createNodeMock = options.createNodeMock; - } + /** + * Same as `removeChild`, but for when a node is detached from the root container. This is useful if attaching to the root has a slightly different implementation, or if the root container nodes are of a different type than the rest of the tree. + */ + removeChildFromContainer( + container: Container, + child: Instance | TextInstance | SuspenseInstance, + ): void { + const index = container.children.indexOf(child); + container.children.splice(index, 1); + }, - if (options.unstable_isConcurrent === true) { - isConcurrent = true; - } + /** + * If you returned `true` from `shouldSetTextContent` for the previous props, but returned `false` from `shouldSetTextContent` for the next props, React will call this method so that you can clear the text content you were managing manually. For example, in the DOM you could set `node.textContent = ''`. + * + * If you never return `true` from `shouldSetTextContent`, you can leave it empty. + */ + resetTextContent(_instance: Instance): void { + // noop + }, - if (options.unstable_strictMode === true) { - isStrictMode = true; - } - - if (allowConcurrentByDefault) { - if (options.unstable_concurrentUpdatesByDefault !== undefined) { - concurrentUpdatesByDefault = - options.unstable_concurrentUpdatesByDefault; - } - } - } - - let container = { - children: [], - createNodeMock, - tag: 'CONTAINER', - }; - - let root: FiberRoot | null = createContainer( - container, - isConcurrent ? ConcurrentRoot : LegacyRoot, - null, - isStrictMode, - concurrentUpdatesByDefault, - '', - onRecoverableError, - null, - ); - - if (root == null) { - throw new Error('something went wrong'); - } - - updateContainer(element, root, null, null); - - const entry = { - _Scheduler: Scheduler, - - root: undefined, // makes flow happy - // we define a 'getter' for 'root' below using 'Object.defineProperty' - toJSON(): Array | ReactTestRendererNode | null { - if (root == null || root.current == null || container == null) { - return null; - } - if (container.children.length === 0) { - return null; - } - if (container.children.length === 1) { - return toJSON(container.children[0]); - } - if ( - container.children.length === 2 && - container.children[0].isHidden === true && - container.children[1].isHidden === false - ) { - // Omit timed out children from output entirely, including the fact that we - // temporarily wrap fallback and timed out children in an array. - return toJSON(container.children[1]); - } - let renderedChildren = null; - if (container.children && container.children.length) { - for (let i = 0; i < container.children.length; i++) { - const renderedChild = toJSON(container.children[i]); - if (renderedChild !== null) { - if (renderedChildren === null) { - renderedChildren = [renderedChild]; - } else { - renderedChildren.push(renderedChild); - } - } - } - } - return renderedChildren; - }, + /** + * This method should mutate the `textInstance` and update its text content to `nextText`. + * + * Here, `textInstance` is a node created by `createTextInstance`. + */ + commitTextUpdate(textInstance: TextInstance, oldText: string, newText: string): void { + textInstance.text = newText; + }, - toTree() { - if (root == null || root.current == null) { - return null; - } - return toTree(root.current); - }, + /** + * This method is only called if you returned `true` from `finalizeInitialChildren` for this instance. + * + * It lets you do some additional work after the node is actually attached to the tree on the screen for the first time. For example, the DOM renderer uses it to trigger focus on nodes with the `autoFocus` attribute. + * + * Note that `commitMount` does not mirror `removeChild` one to one because `removeChild` is only called for the top-level removed node. This is why ideally `commitMount` should not mutate any nodes other than the `instance` itself. For example, if it registers some events on some node above, it will be your responsibility to traverse the tree in `removeChild` and clean them up, which is not ideal. + * + * The `internalHandle` data structure is meant to be opaque. If you bend the rules and rely on its internal fields, be aware that it may change significantly between versions. You're taking on additional maintenance risk by reading from it, and giving up all guarantees if you write something to it. + * + * If you never return `true` from `finalizeInitialChildren`, you can leave it empty. + */ + commitMount( + _instance: Instance, + _type: Type, + _props: Props, + _internalHandle: OpaqueHandle, + ): void { + // noop + }, - update(newElement: React$Element) { - if (root == null || root.current == null) { - return; - } - ReactReconciler.updateContainer(newElement, root, null, null); - }, + /** + * This method should mutate the `instance` according to the set of changes in `updatePayload`. Here, `updatePayload` is the object that you've returned from `prepareUpdate` and has an arbitrary structure that makes sense for your renderer. For example, the DOM renderer returns an update payload like `[prop1, value1, prop2, value2, ...]` from `prepareUpdate`, and that structure gets passed into `commitUpdate`. Ideally, all the diffing and calculation should happen inside `prepareUpdate` so that `commitUpdate` can be fast and straightforward. + * + * The `internalHandle` data structure is meant to be opaque. If you bend the rules and rely on its internal fields, be aware that it may change significantly between versions. You're taking on additional maintenance risk by reading from it, and giving up all guarantees if you write something to it. + */ + commitUpdate( + instance: Instance, + _updatePayload: UpdatePayload, + type: Type, + _prevProps: Props, + nextProps: Props, + _internalHandle: OpaqueHandle, + ): void { + instance.type = type; + instance.props = nextProps; + }, - unmount() { - if (root == null || root.current == null) { - return; - } - updateContainer(null, root, null, null); - container = null; - root = null; - }, - - getInstance() { - if (root == null || root.current == null) { - return null; - } - return getPublicRootInstance(root); - }, - - unstable_flushSync: flushSync, - }; - - Object.defineProperty( - entry, - 'root', - ({ - configurable: true, - enumerable: true, - get: function() { - if (root === null) { - throw new Error("Can't access .root on unmounted test renderer"); - } - const children = getChildren(root.current); - if (children.length === 0) { - throw new Error("Can't access .root on unmounted test renderer"); - } else if (children.length === 1) { - // Normally, we skip the root and just give you the child. - return children[0]; - } else { - // However, we give you the root if there's more than one root child. - // We could make this the behavior for all cases but it would be a breaking change. - return wrapFiber(root.current); - } - }, - }: object), - ); + /** + * This method should make the `instance` invisible without removing it from the tree. For example, it can apply visual styling to hide it. It is used by Suspense to hide the tree while the fallback is visible. + */ + hideInstance(instance: Instance): void { + instance.isHidden = true; + }, - return entry; -} + /** + * Same as `hideInstance`, but for nodes created by `createTextInstance`. + */ + hideTextInstance(textInstance: TextInstance): void { + textInstance.isHidden = true; + }, -const fiberToWrapper = new WeakMap(); + /** + * This method should make the `instance` visible, undoing what `hideInstance` did. + */ + unhideInstance(instance: Instance, _props: Props): void { + instance.isHidden = false; + }, -function wrapFiber(fiber: Fiber): ReactTestInstance { - let wrapper = fiberToWrapper.get(fiber); - if (wrapper === undefined && fiber.alternate !== null) { - wrapper = fiberToWrapper.get(fiber.alternate); - } + /** + * Same as `unhideInstance`, but for nodes created by `createTextInstance`. + */ + unhideTextInstance(textInstance: TextInstance, _text: string): void { + textInstance.isHidden = false; + }, - if (wrapper === undefined) { - wrapper = new ReactTestInstance(fiber); - fiberToWrapper.set(fiber, wrapper); - } + /** + * This method should mutate the `container` root node and remove all children from it. + */ + clearContainer(container: Container): void { + container.children.splice(0); + }, - return wrapper; -} + // ------------------- + // Hydration Methods + // (optional) + // You can optionally implement hydration to "attach" to the existing tree during the initial render instead of creating it from scratch. For example, the DOM renderer uses this to attach to an HTML markup. + // + // To support hydration, you need to declare `supportsHydration: true` and then implement the methods in the "Hydration" section [listed in this file](https://github.com/facebook/react/blob/master/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js). File an issue if you need help. + // ------------------- + supportsHydration: false, +}; -// // Enable ReactTestRenderer to be used to test DevTools integration. -// injectIntoDevTools({ -// findFiberByHostInstance: (() => { -// throw new Error('TestRenderer does not support findFiberByHostInstance()'); -// }: any), -// bundleType: __DEV__ ? 1 : 0, -// version: ReactVersion, -// rendererPackageName: 'react-test-renderer', -// }); - -export { - Scheduler as _Scheduler, - create, - /* eslint-disable-next-line camelcase */ - batchedUpdates as unstable_batchedUpdates, - act, -}; \ No newline at end of file +export const TestRenderer = Reconciler(hostConfig); From 51865526729ce81c38ad636ef549318efa2cee9b Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Sun, 15 Sep 2024 19:06:22 +0200 Subject: [PATCH 04/61] refactor: refactods --- src/renderer/react-fiber-config-host.ts | 648 +++++----- src/renderer/{renderer.ts => reconciler.ts} | 4 +- src/renderer/renderer-orig.ts | 1266 +++++++++---------- 3 files changed, 959 insertions(+), 959 deletions(-) rename src/renderer/{renderer.ts => reconciler.ts} (99%) diff --git a/src/renderer/react-fiber-config-host.ts b/src/renderer/react-fiber-config-host.ts index 815949fe7..c218fad30 100644 --- a/src/renderer/react-fiber-config-host.ts +++ b/src/renderer/react-fiber-config-host.ts @@ -1,324 +1,324 @@ -/** - * 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. - * - */ - -//import isArray from 'shared/isArray'; -//import {DefaultEventPriority} from 'react-reconciler/src/ReactEventPriorities'; - -import { DefaultEventPriority } from 'react-reconciler/constants'; - -export type Type = string; -export type Props = object; - -export type Container = { - tag: 'CONTAINER'; - children: Array; - createNodeMock: Function; -}; - -export type Instance = { - tag: 'INSTANCE'; - type: string; - props: object; - isHidden: boolean; - children: Array; - internalInstanceHandle: object; - rootContainerInstance: Container; -}; - -export type TextInstance = { - tag: 'TEXT'; - text: string; - isHidden: boolean; -}; - -export type HydratableInstance = Instance | TextInstance; -export type PublicInstance = Instance | TextInstance; -export type HostContext = object; -export type UpdatePayload = object; -export type ChildSet = void; // Unused -export type TimeoutHandle = ReturnType; -export type NoTimeout = -1; -export type EventResponder = any; - -export type RendererInspectionConfig = Readonly<{}>; - -// export * from 'react-reconciler/src/ReactFiberHostConfigWithNoPersistence'; -// export * from 'react-reconciler/src/ReactFiberHostConfigWithNoHydration'; -// export * from 'react-reconciler/src/ReactFiberHostConfigWithNoTestSelectors'; -// export * from 'react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks'; - -const NO_CONTEXT = {}; -const UPDATE_SIGNAL = {}; -const nodeToInstanceMap = new WeakMap(); - -if (__DEV__) { - Object.freeze(NO_CONTEXT); - Object.freeze(UPDATE_SIGNAL); -} - -export function getPublicInstance(inst: Instance | TextInstance) { - switch (inst.tag) { - case 'INSTANCE': { - const createNodeMock = inst.rootContainerInstance.createNodeMock; - const mockNode = createNodeMock({ - type: inst.type, - props: inst.props, - }); - if (typeof mockNode === 'object' && mockNode !== null) { - nodeToInstanceMap.set(mockNode, inst); - } - - return mockNode; - } - - default: - return inst; - } -} - -export function appendChild( - parentInstance: Instance | Container, - child: Instance | TextInstance, -): void { - if (__DEV__) { - if (!Array.isArray(parentInstance.children)) { - // eslint-disable-next-line no-console - console.error( - 'An invalid container has been provided. ' + - 'This may indicate that another renderer is being used in addition to the test renderer. ' + - '(For example, ReactDOM.createPortal inside of a ReactTestRenderer tree.) ' + - 'This is not supported.', - ); - } - } - const index = parentInstance.children.indexOf(child); - if (index !== -1) { - parentInstance.children.splice(index, 1); - } - - parentInstance.children.push(child); -} - -export 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); - parentInstance.children.splice(beforeIndex, 0, child); -} - -export function removeChild( - parentInstance: Instance | Container, - child: Instance | TextInstance, -): void { - const index = parentInstance.children.indexOf(child); - parentInstance.children.splice(index, 1); -} - -export function clearContainer(container: Container): void { - container.children.splice(0); -} - -export function getRootHostContext(_rootContainerInstance: Container): HostContext { - return NO_CONTEXT; -} - -export function getChildHostContext( - _parentHostContext: HostContext, - _type: string, - _rootContainerInstance: Container, -): HostContext { - return NO_CONTEXT; -} - -export function prepareForCommit(_containerInfo: Container): object | null { - // noop - return null; -} - -export function resetAfterCommit(_containerInfo: Container): void { - // noop -} - -export function createInstance( - type: string, - props: Props, - rootContainerInstance: Container, - _hostContext: object, - internalInstanceHandle: object, -): Instance { - return { - type, - props, - isHidden: false, - children: [], - internalInstanceHandle, - rootContainerInstance, - tag: 'INSTANCE', - }; -} - -export function appendInitialChild(parentInstance: Instance, child: Instance | TextInstance): void { - const index = parentInstance.children.indexOf(child); - if (index !== -1) { - parentInstance.children.splice(index, 1); - } - - parentInstance.children.push(child); -} - -export function finalizeInitialChildren( - _testElement: Instance, - _type: string, - _props: Props, - _rootContainerInstance: Container, - _hostContext: object, -): boolean { - return false; -} - -export function prepareUpdate( - _testElement: Instance, - _type: string, - _oldProps: Props, - _newProps: Props, - _rootContainerInstance: Container, - _hostContext: object, -): object | null { - return UPDATE_SIGNAL; -} - -export function shouldSetTextContent(_type: string, _props: Props): boolean { - return false; -} - -export function createTextInstance( - text: string, - _rootContainerInstance: Container, - _hostContext: object, - _internalInstanceHandle: object, -): TextInstance { - return { - tag: 'TEXT', - text, - isHidden: false, - }; -} - -export function getCurrentEventPriority(): number { - return DefaultEventPriority; -} - -export const isPrimaryRenderer = false; -export const warnsIfNotActing = true; - -export const scheduleTimeout = setTimeout; -export const cancelTimeout = clearTimeout; - -export const noTimeout = -1; - -// ------------------- -// Mutation -// ------------------- - -export const supportsMutation = true; - -export function commitUpdate( - instance: Instance, - _updatePayload: object, - type: string, - _oldProps: Props, - newProps: Props, - _internalInstanceHandle: object, -): void { - instance.type = type; - instance.props = newProps; -} - -export function commitMount( - _instance: Instance, - _type: string, - _newProps: Props, - _internalInstanceHandle: object, -): void { - // noop -} - -export function commitTextUpdate( - textInstance: TextInstance, - _oldText: string, - newText: string, -): void { - textInstance.text = newText; -} - -export function resetTextContent(_testElement: Instance): void { - // noop -} - -export const appendChildToContainer = appendChild; -export const insertInContainerBefore = insertBefore; -export const removeChildFromContainer = removeChild; - -export function hideInstance(instance: Instance): void { - instance.isHidden = true; -} - -export function hideTextInstance(textInstance: TextInstance): void { - textInstance.isHidden = true; -} - -export function unhideInstance(instance: Instance, _props: Props): void { - instance.isHidden = false; -} - -export function unhideTextInstance(textInstance: TextInstance, _text: string): void { - textInstance.isHidden = false; -} - -export function getInstanceFromNode(mockNode: object) { - const instance = nodeToInstanceMap.get(mockNode); - if (instance !== undefined) { - return instance.internalInstanceHandle; - } - return null; -} - -export function beforeActiveInstanceBlur(_internalInstanceHandle: object) { - // noop -} - -export function afterActiveInstanceBlur() { - // noop -} - -export function preparePortalMount(_portalInstance: Instance): void { - // noop -} - -export function prepareScopeUpdate(scopeInstance: object, inst: object): void { - nodeToInstanceMap.set(scopeInstance, inst); -} - -export function getInstanceFromScope(scopeInstance: object): object | null { - return nodeToInstanceMap.get(scopeInstance) || null; -} - -export function detachDeletedInstance(_node: Instance): void { - // noop -} - -export function logRecoverableError(_error: unknown): void { - // noop -} +// /** +// * 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. +// * +// */ + +// //import isArray from 'shared/isArray'; +// //import {DefaultEventPriority} from 'react-reconciler/src/ReactEventPriorities'; + +// import { DefaultEventPriority } from 'react-reconciler/constants'; + +// export type Type = string; +// export type Props = object; + +// export type Container = { +// tag: 'CONTAINER'; +// children: Array; +// createNodeMock: Function; +// }; + +// export type Instance = { +// tag: 'INSTANCE'; +// type: string; +// props: object; +// isHidden: boolean; +// children: Array; +// internalInstanceHandle: object; +// rootContainerInstance: Container; +// }; + +// export type TextInstance = { +// tag: 'TEXT'; +// text: string; +// isHidden: boolean; +// }; + +// export type HydratableInstance = Instance | TextInstance; +// export type PublicInstance = Instance | TextInstance; +// export type HostContext = object; +// export type UpdatePayload = object; +// export type ChildSet = void; // Unused +// export type TimeoutHandle = ReturnType; +// export type NoTimeout = -1; +// export type EventResponder = any; + +// export type RendererInspectionConfig = Readonly<{}>; + +// // export * from 'react-reconciler/src/ReactFiberHostConfigWithNoPersistence'; +// // export * from 'react-reconciler/src/ReactFiberHostConfigWithNoHydration'; +// // export * from 'react-reconciler/src/ReactFiberHostConfigWithNoTestSelectors'; +// // export * from 'react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks'; + +// const NO_CONTEXT = {}; +// const UPDATE_SIGNAL = {}; +// const nodeToInstanceMap = new WeakMap(); + +// if (__DEV__) { +// Object.freeze(NO_CONTEXT); +// Object.freeze(UPDATE_SIGNAL); +// } + +// export function getPublicInstance(inst: Instance | TextInstance) { +// switch (inst.tag) { +// case 'INSTANCE': { +// const createNodeMock = inst.rootContainerInstance.createNodeMock; +// const mockNode = createNodeMock({ +// type: inst.type, +// props: inst.props, +// }); +// if (typeof mockNode === 'object' && mockNode !== null) { +// nodeToInstanceMap.set(mockNode, inst); +// } + +// return mockNode; +// } + +// default: +// return inst; +// } +// } + +// export function appendChild( +// parentInstance: Instance | Container, +// child: Instance | TextInstance, +// ): void { +// if (__DEV__) { +// if (!Array.isArray(parentInstance.children)) { +// // eslint-disable-next-line no-console +// console.error( +// 'An invalid container has been provided. ' + +// 'This may indicate that another renderer is being used in addition to the test renderer. ' + +// '(For example, ReactDOM.createPortal inside of a ReactTestRenderer tree.) ' + +// 'This is not supported.', +// ); +// } +// } +// const index = parentInstance.children.indexOf(child); +// if (index !== -1) { +// parentInstance.children.splice(index, 1); +// } + +// parentInstance.children.push(child); +// } + +// export 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); +// parentInstance.children.splice(beforeIndex, 0, child); +// } + +// export function removeChild( +// parentInstance: Instance | Container, +// child: Instance | TextInstance, +// ): void { +// const index = parentInstance.children.indexOf(child); +// parentInstance.children.splice(index, 1); +// } + +// export function clearContainer(container: Container): void { +// container.children.splice(0); +// } + +// export function getRootHostContext(_rootContainerInstance: Container): HostContext { +// return NO_CONTEXT; +// } + +// export function getChildHostContext( +// _parentHostContext: HostContext, +// _type: string, +// _rootContainerInstance: Container, +// ): HostContext { +// return NO_CONTEXT; +// } + +// export function prepareForCommit(_containerInfo: Container): object | null { +// // noop +// return null; +// } + +// export function resetAfterCommit(_containerInfo: Container): void { +// // noop +// } + +// export function createInstance( +// type: string, +// props: Props, +// rootContainerInstance: Container, +// _hostContext: object, +// internalInstanceHandle: object, +// ): Instance { +// return { +// type, +// props, +// isHidden: false, +// children: [], +// internalInstanceHandle, +// rootContainerInstance, +// tag: 'INSTANCE', +// }; +// } + +// export function appendInitialChild(parentInstance: Instance, child: Instance | TextInstance): void { +// const index = parentInstance.children.indexOf(child); +// if (index !== -1) { +// parentInstance.children.splice(index, 1); +// } + +// parentInstance.children.push(child); +// } + +// export function finalizeInitialChildren( +// _testElement: Instance, +// _type: string, +// _props: Props, +// _rootContainerInstance: Container, +// _hostContext: object, +// ): boolean { +// return false; +// } + +// export function prepareUpdate( +// _testElement: Instance, +// _type: string, +// _oldProps: Props, +// _newProps: Props, +// _rootContainerInstance: Container, +// _hostContext: object, +// ): object | null { +// return UPDATE_SIGNAL; +// } + +// export function shouldSetTextContent(_type: string, _props: Props): boolean { +// return false; +// } + +// export function createTextInstance( +// text: string, +// _rootContainerInstance: Container, +// _hostContext: object, +// _internalInstanceHandle: object, +// ): TextInstance { +// return { +// tag: 'TEXT', +// text, +// isHidden: false, +// }; +// } + +// export function getCurrentEventPriority(): number { +// return DefaultEventPriority; +// } + +// export const isPrimaryRenderer = false; +// export const warnsIfNotActing = true; + +// export const scheduleTimeout = setTimeout; +// export const cancelTimeout = clearTimeout; + +// export const noTimeout = -1; + +// // ------------------- +// // Mutation +// // ------------------- + +// export const supportsMutation = true; + +// export function commitUpdate( +// instance: Instance, +// _updatePayload: object, +// type: string, +// _oldProps: Props, +// newProps: Props, +// _internalInstanceHandle: object, +// ): void { +// instance.type = type; +// instance.props = newProps; +// } + +// export function commitMount( +// _instance: Instance, +// _type: string, +// _newProps: Props, +// _internalInstanceHandle: object, +// ): void { +// // noop +// } + +// export function commitTextUpdate( +// textInstance: TextInstance, +// _oldText: string, +// newText: string, +// ): void { +// textInstance.text = newText; +// } + +// export function resetTextContent(_testElement: Instance): void { +// // noop +// } + +// export const appendChildToContainer = appendChild; +// export const insertInContainerBefore = insertBefore; +// export const removeChildFromContainer = removeChild; + +// export function hideInstance(instance: Instance): void { +// instance.isHidden = true; +// } + +// export function hideTextInstance(textInstance: TextInstance): void { +// textInstance.isHidden = true; +// } + +// export function unhideInstance(instance: Instance, _props: Props): void { +// instance.isHidden = false; +// } + +// export function unhideTextInstance(textInstance: TextInstance, _text: string): void { +// textInstance.isHidden = false; +// } + +// export function getInstanceFromNode(mockNode: object) { +// const instance = nodeToInstanceMap.get(mockNode); +// if (instance !== undefined) { +// return instance.internalInstanceHandle; +// } +// return null; +// } + +// export function beforeActiveInstanceBlur(_internalInstanceHandle: object) { +// // noop +// } + +// export function afterActiveInstanceBlur() { +// // noop +// } + +// export function preparePortalMount(_portalInstance: Instance): void { +// // noop +// } + +// export function prepareScopeUpdate(scopeInstance: object, inst: object): void { +// nodeToInstanceMap.set(scopeInstance, inst); +// } + +// export function getInstanceFromScope(scopeInstance: object): object | null { +// return nodeToInstanceMap.get(scopeInstance) || null; +// } + +// export function detachDeletedInstance(_node: Instance): void { +// // noop +// } + +// export function logRecoverableError(_error: unknown): void { +// // noop +// } diff --git a/src/renderer/renderer.ts b/src/renderer/reconciler.ts similarity index 99% rename from src/renderer/renderer.ts rename to src/renderer/reconciler.ts index 27fd91266..3dfab8efe 100644 --- a/src/renderer/renderer.ts +++ b/src/renderer/reconciler.ts @@ -1,4 +1,4 @@ -import Reconciler, { Fiber } from 'react-reconciler'; +import createReconciler, { Fiber } from 'react-reconciler'; import { DefaultEventPriority } from 'react-reconciler/constants'; export type Type = string; @@ -557,4 +557,4 @@ const hostConfig = { supportsHydration: false, }; -export const TestRenderer = Reconciler(hostConfig); +export const TestReconciler = createReconciler(hostConfig); diff --git a/src/renderer/renderer-orig.ts b/src/renderer/renderer-orig.ts index 418ac70a2..072c4ba17 100644 --- a/src/renderer/renderer-orig.ts +++ b/src/renderer/renderer-orig.ts @@ -1,646 +1,646 @@ -/** - * 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. - * - */ - -import * as React from 'react'; -// @ts-expect-error -import ReactReconciler, { Fiber, createContainer } from 'react-reconciler'; - -import createReconciler from 'react-reconciler'; - -import { getPublicInstance, Instance, TextInstance } from './react-fiber-config-host'; - -// import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; -// import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes'; - -// import * as Scheduler from 'scheduler/unstable_mock'; -// import { -// getPublicRootInstance, -// createContainer, -// updateContainer, -// flushSync, -// injectIntoDevTools, -// batchedUpdates, -// } from 'react-reconciler/src/ReactFiberReconciler'; -// import {findCurrentFiberUsingSlowPath} from 'react-reconciler/src/ReactFiberTreeReflection'; -// import { -// Fragment, +// /** +// * 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. +// * +// */ + +// import * as React from 'react'; +// // @ts-expect-error +// import ReactReconciler, { Fiber, createContainer } from 'react-reconciler'; + +// import createReconciler from 'react-reconciler'; + +// import { getPublicInstance, Instance, TextInstance } from './react-fiber-config-host'; + +// // import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; +// // import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes'; + +// // import * as Scheduler from 'scheduler/unstable_mock'; +// // import { +// // getPublicRootInstance, +// // createContainer, +// // updateContainer, +// // flushSync, +// // injectIntoDevTools, +// // batchedUpdates, +// // } from 'react-reconciler/src/ReactFiberReconciler'; +// // import {findCurrentFiberUsingSlowPath} from 'react-reconciler/src/ReactFiberTreeReflection'; +// // import { +// // Fragment, +// // FunctionComponent, +// // ClassComponent, +// // HostComponent, +// // HostPortal, +// // HostText, +// // HostRoot, +// // ContextConsumer, +// // ContextProvider, +// // Mode, +// // ForwardRef, +// // Profiler, +// // MemoComponent, +// // SimpleMemoComponent, +// // IncompleteClassComponent, +// // ScopeComponent, +// // } from 'react-reconciler/src/ReactWorkTags'; +// // import isArray from 'shared/isArray'; +// // import getComponentNameFromType from 'shared/getComponentNameFromType'; +// // import ReactVersion from 'shared/ReactVersion'; +// // import {checkPropStringCoercion} from 'shared/CheckStringCoercion'; + +// // import {getPublicInstance} from './ReactTestHostConfig'; +// // import {ConcurrentRoot, LegacyRoot} from 'react-reconciler/src/ReactRootTags'; +// // import {allowConcurrentByDefault} from 'shared/ReactFeatureFlags'; + +// //const act = React.unstable_act; +// const act = React.act; + +// // TODO: Remove from public bundle + +// type TestRendererOptions = { +// createNodeMock: (element: React.ReactElement) => any, +// unstable_isConcurrent: boolean, +// unstable_strictMode: boolean, +// unstable_concurrentUpdatesByDefault: boolean, +// }; + +// type ReactTestRendererJSON = { +// type: string, +// props: {[propName: string]: any}, +// children: Array | null, +// $$typeof?: Symbol, // Optional because we add it with defineProperty(). +// }; + +// type ReactTestRendererNode = ReactTestRendererJSON | string; + +// type FindOptions = { +// // performs a "greedy" search: if a matching node is found, will continue +// // to search within the matching node's children. (default: true) +// deep: boolean, +// }; + +// export type Predicate = (node: ReactTestInstance) => ?boolean; + +// const allowConcurrentByDefault = false; // ? + +// const defaultTestOptions = { +// createNodeMock: function() { +// return null; +// }, +// }; + +// function toJSON(inst: Instance | TextInstance): ReactTestRendererNode | null { +// if (inst.isHidden) { +// // Omit timed out children from output entirely. This seems like the least +// // surprising behavior. We could perhaps add a separate API that includes +// // them, if it turns out people need it. +// return null; +// } +// switch (inst.tag) { +// case 'TEXT': +// return inst.text; +// case 'INSTANCE': { +// // We don't include the `children` prop in JSON. +// // Instead, we will include the actual rendered children. +// // @ts-expect-error +// const { children, ...props } = inst.props; + +// let renderedChildren = null; +// if (inst.children && inst.children.length) { +// for (let i = 0; i < inst.children.length; i++) { +// const renderedChild = toJSON(inst.children[i]); +// if (renderedChild !== null) { +// if (renderedChildren === null) { +// renderedChildren = [renderedChild]; +// } else { +// renderedChildren.push(renderedChild); +// } +// } +// } +// } + +// const json: ReactTestRendererJSON = { +// type: inst.type, +// props: props, +// children: renderedChildren, +// }; +// Object.defineProperty(json, '$$typeof', { +// value: Symbol.for('react.test.json'), +// }); +// return json; +// } + +// default: +// // @ts-expect-error +// throw new Error(`Unexpected node type in toJSON: ${inst.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 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(node: ?Fiber) { +// // if (node == null) { +// // return null; +// // } +// // switch (node.tag) { +// // case HostRoot: +// // return childrenToTree(node.child); +// // case HostPortal: +// // return childrenToTree(node.child); +// // case ClassComponent: +// // return { +// // nodeType: 'component', +// // type: node.type, +// // props: {...node.memoizedProps}, +// // instance: node.stateNode, +// // rendered: childrenToTree(node.child), +// // }; +// // case FunctionComponent: +// // case SimpleMemoComponent: +// // return { +// // nodeType: 'component', +// // type: node.type, +// // props: {...node.memoizedProps}, +// // instance: null, +// // rendered: childrenToTree(node.child), +// // }; +// // case HostComponent: { +// // return { +// // nodeType: 'host', +// // type: node.type, +// // props: {...node.memoizedProps}, +// // instance: null, // TODO: use createNodeMock here somehow? +// // rendered: flatten(nodeAndSiblingsArray(node.child).map(toTree)), +// // }; +// // } +// // case HostText: +// // return node.stateNode.text; +// // case Fragment: +// // case ContextProvider: +// // case ContextConsumer: +// // case Mode: +// // case Profiler: +// // case ForwardRef: +// // case MemoComponent: +// // case IncompleteClassComponent: +// // case ScopeComponent: +// // return childrenToTree(node.child); +// // default: +// // throw new Error( +// // `toTree() does not yet know how to handle nodes with tag=${node.tag}`, +// // ); +// // } +// // } + +// const validWrapperTypes = new Set([ // FunctionComponent, // ClassComponent, // HostComponent, -// HostPortal, -// HostText, -// HostRoot, -// ContextConsumer, -// ContextProvider, -// Mode, // ForwardRef, -// Profiler, // MemoComponent, // SimpleMemoComponent, -// IncompleteClassComponent, -// ScopeComponent, -// } from 'react-reconciler/src/ReactWorkTags'; -// import isArray from 'shared/isArray'; -// import getComponentNameFromType from 'shared/getComponentNameFromType'; -// import ReactVersion from 'shared/ReactVersion'; -// import {checkPropStringCoercion} from 'shared/CheckStringCoercion'; - -// import {getPublicInstance} from './ReactTestHostConfig'; -// import {ConcurrentRoot, LegacyRoot} from 'react-reconciler/src/ReactRootTags'; -// import {allowConcurrentByDefault} from 'shared/ReactFeatureFlags'; - -//const act = React.unstable_act; -const act = React.act; - -// TODO: Remove from public bundle - -type TestRendererOptions = { - createNodeMock: (element: React.ReactElement) => any, - unstable_isConcurrent: boolean, - unstable_strictMode: boolean, - unstable_concurrentUpdatesByDefault: boolean, -}; - -type ReactTestRendererJSON = { - type: string, - props: {[propName: string]: any}, - children: Array | null, - $$typeof?: Symbol, // Optional because we add it with defineProperty(). -}; - -type ReactTestRendererNode = ReactTestRendererJSON | string; - -type FindOptions = { - // performs a "greedy" search: if a matching node is found, will continue - // to search within the matching node's children. (default: true) - deep: boolean, -}; - -export type Predicate = (node: ReactTestInstance) => ?boolean; - -const allowConcurrentByDefault = false; // ? - -const defaultTestOptions = { - createNodeMock: function() { - return null; - }, -}; - -function toJSON(inst: Instance | TextInstance): ReactTestRendererNode | null { - if (inst.isHidden) { - // Omit timed out children from output entirely. This seems like the least - // surprising behavior. We could perhaps add a separate API that includes - // them, if it turns out people need it. - return null; - } - switch (inst.tag) { - case 'TEXT': - return inst.text; - case 'INSTANCE': { - // We don't include the `children` prop in JSON. - // Instead, we will include the actual rendered children. - // @ts-expect-error - const { children, ...props } = inst.props; - - let renderedChildren = null; - if (inst.children && inst.children.length) { - for (let i = 0; i < inst.children.length; i++) { - const renderedChild = toJSON(inst.children[i]); - if (renderedChild !== null) { - if (renderedChildren === null) { - renderedChildren = [renderedChild]; - } else { - renderedChildren.push(renderedChild); - } - } - } - } - - const json: ReactTestRendererJSON = { - type: inst.type, - props: props, - children: renderedChildren, - }; - Object.defineProperty(json, '$$typeof', { - value: Symbol.for('react.test.json'), - }); - return json; - } - - default: - // @ts-expect-error - throw new Error(`Unexpected node type in toJSON: ${inst.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 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(node: ?Fiber) { -// if (node == null) { -// return null; +// // Normally skipped, but used when there's more than one root child. +// HostRoot, +// ]); + +// function getChildren(parent: Fiber): Array { +// const children: Array = []; +// const startingNode = parent; +// let node: Fiber = startingNode; +// if (node.child === null) { +// return children; // } -// switch (node.tag) { -// case HostRoot: -// return childrenToTree(node.child); -// case HostPortal: -// return childrenToTree(node.child); -// case ClassComponent: -// return { -// nodeType: 'component', -// type: node.type, -// props: {...node.memoizedProps}, -// instance: node.stateNode, -// rendered: childrenToTree(node.child), -// }; -// case FunctionComponent: -// case SimpleMemoComponent: -// return { -// nodeType: 'component', -// type: node.type, -// props: {...node.memoizedProps}, -// instance: null, -// rendered: childrenToTree(node.child), -// }; -// case HostComponent: { -// return { -// nodeType: 'host', -// type: node.type, -// props: {...node.memoizedProps}, -// instance: null, // TODO: use createNodeMock here somehow? -// rendered: flatten(nodeAndSiblingsArray(node.child).map(toTree)), -// }; + +// node.child.return = node; +// node = node.child; + +// outer: while (true) { +// let descend = false; +// if (validWrapperTypes.has(node.tag)) { +// children.push(wrapFiber(node)); +// } else if (node.tag === HostText) { +// if (__DEV__) { +// checkPropStringCoercion(node.memoizedProps, 'memoizedProps'); +// } +// children.push('' + node.memoizedProps); +// } else { +// descend = true; // } -// case HostText: -// return node.stateNode.text; -// case Fragment: -// case ContextProvider: -// case ContextConsumer: -// case Mode: -// case Profiler: -// case ForwardRef: -// case MemoComponent: -// case IncompleteClassComponent: -// case ScopeComponent: -// return childrenToTree(node.child); -// default: +// if (descend && node.child !== null) { +// node.child.return = node; +// node = node.child; +// continue; +// } +// while (node.sibling === null) { +// if (node.return === startingNode) { +// break outer; +// } +// node = (node.return: any); +// } +// (node.sibling: any).return = node.return; +// node = (node.sibling: any); +// } + +// return children; +// } + +// class ReactTestInstance { +// _fiber: Fiber; + +// _currentFiber(): Fiber { +// // Throws if this component has been unmounted. +// const fiber = findCurrentFiberUsingSlowPath(this._fiber); + +// if (fiber === null) { +// throw new Error( +// "Can't read from currently-mounting component. This error is likely " + +// 'caused by a bug in React. Please file an issue.', +// ); +// } + +// return fiber; +// } + +// constructor(fiber: Fiber) { +// if (!validWrapperTypes.has(fiber.tag)) { // throw new Error( -// `toTree() does not yet know how to handle nodes with tag=${node.tag}`, +// `Unexpected object passed to ReactTestInstance constructor (tag: ${fiber.tag}). ` + +// 'This is probably a bug in React.', // ); +// } + +// this._fiber = fiber; +// } + +// get instance() { +// if (this._fiber.tag === HostComponent) { +// return getPublicInstance(this._fiber.stateNode); +// } else { +// return this._fiber.stateNode; +// } +// } + +// get type() { +// return this._fiber.type; +// } + +// get props(): object { +// return this._currentFiber().memoizedProps; // } + +// get parent(): ?ReactTestInstance { +// let parent = this._fiber.return; +// while (parent !== null) { +// if (validWrapperTypes.has(parent.tag)) { +// if (parent.tag === HostRoot) { +// // Special case: we only "materialize" instances for roots +// // if they have more than a single child. So we'll check that now. +// if (getChildren(parent).length < 2) { +// return null; +// } +// } +// return wrapFiber(parent); +// } +// parent = parent.return; +// } +// return null; +// } + +// get children(): Array { +// return getChildren(this._currentFiber()); +// } + +// // Custom search functions +// find(predicate: Predicate): ReactTestInstance { +// return expectOne( +// this.findAll(predicate, {deep: false}), +// `matching custom predicate: ${predicate.toString()}`, +// ); +// } + +// findByType(type: any): ReactTestInstance { +// return expectOne( +// this.findAllByType(type, {deep: false}), +// `with node type: "${getComponentNameFromType(type) || 'Unknown'}"`, +// ); +// } + +// findByProps(props: object): ReactTestInstance { +// return expectOne( +// this.findAllByProps(props, {deep: false}), +// `with props: ${JSON.stringify(props)}`, +// ); +// } + +// findAll( +// predicate: Predicate, +// options: ?FindOptions = null, +// ): Array { +// return findAll(this, predicate, options); +// } + +// findAllByType( +// type: any, +// options: ?FindOptions = null, +// ): Array { +// return findAll(this, node => node.type === type, options); +// } + +// findAllByProps( +// props: object, +// options: ?FindOptions = null, +// ): Array { +// return findAll( +// this, +// node => node.props && propsMatch(node.props, props), +// options, +// ); +// } +// } + +// function findAll( +// root: ReactTestInstance, +// predicate: Predicate, +// options: ?FindOptions, +// ): Array { +// const deep = options ? options.deep : true; +// const results = []; + +// if (predicate(root)) { +// results.push(root); +// if (!deep) { +// return results; +// } +// } + +// root.children.forEach(child => { +// if (typeof child === 'string') { +// return; +// } +// results.push(...findAll(child, predicate, options)); +// }); + +// return results; +// } + +// function expectOne( +// all: Array, +// message: string, +// ): ReactTestInstance { +// if (all.length === 1) { +// return all[0]; +// } + +// const prefix = +// all.length === 0 +// ? 'No instances found ' +// : `Expected 1 but found ${all.length} instances `; + +// throw new Error(prefix + message); +// } + +// function propsMatch(props: object, filter: object): boolean { +// for (const key in filter) { +// // @ts-expect-error +// if (props[key] !== filter[key]) { +// return false; +// } +// } +// return true; +// } + +// function onRecoverableError(error: unknown) { +// // TODO: Expose onRecoverableError option to userspace +// // eslint-disable-next-line react-internal/no-production-logging, react-internal/warning-args +// console.error(error); +// } + +// function create(element: React.ReactElement, options: TestRendererOptions) { +// let createNodeMock = defaultTestOptions.createNodeMock; +// let isConcurrent = false; +// let isStrictMode = false; +// let concurrentUpdatesByDefault = null; + +// if (typeof options === 'object' && options !== null) { +// if (typeof options.createNodeMock === 'function') { +// createNodeMock = options.createNodeMock; +// } + +// if (options.unstable_isConcurrent === true) { +// isConcurrent = true; +// } + +// if (options.unstable_strictMode === true) { +// isStrictMode = true; +// } + +// if (allowConcurrentByDefault) { +// if (options.unstable_concurrentUpdatesByDefault !== undefined) { +// concurrentUpdatesByDefault = +// options.unstable_concurrentUpdatesByDefault; +// } +// } +// } + +// let container = { +// children: [], +// createNodeMock, +// tag: 'CONTAINER', +// }; + +// let root: FiberRoot | null = createContainer( +// container, +// isConcurrent ? ConcurrentRoot : LegacyRoot, +// null, +// isStrictMode, +// concurrentUpdatesByDefault, +// '', +// onRecoverableError, +// null, +// ); + +// if (root == null) { +// throw new Error('something went wrong'); +// } + +// updateContainer(element, root, null, null); + +// const entry = { +// _Scheduler: Scheduler, + +// root: undefined, // makes flow happy +// // we define a 'getter' for 'root' below using 'Object.defineProperty' +// toJSON(): Array | ReactTestRendererNode | null { +// if (root == null || root.current == null || container == null) { +// return null; +// } +// if (container.children.length === 0) { +// return null; +// } +// if (container.children.length === 1) { +// return toJSON(container.children[0]); +// } +// if ( +// container.children.length === 2 && +// container.children[0].isHidden === true && +// container.children[1].isHidden === false +// ) { +// // Omit timed out children from output entirely, including the fact that we +// // temporarily wrap fallback and timed out children in an array. +// return toJSON(container.children[1]); +// } +// let renderedChildren = null; +// if (container.children && container.children.length) { +// for (let i = 0; i < container.children.length; i++) { +// const renderedChild = toJSON(container.children[i]); +// if (renderedChild !== null) { +// if (renderedChildren === null) { +// renderedChildren = [renderedChild]; +// } else { +// renderedChildren.push(renderedChild); +// } +// } +// } +// } +// return renderedChildren; +// }, + +// toTree() { +// if (root == null || root.current == null) { +// return null; +// } +// return toTree(root.current); +// }, + +// update(newElement: React$Element) { +// if (root == null || root.current == null) { +// return; +// } +// ReactReconciler.updateContainer(newElement, root, null, null); +// }, + +// unmount() { +// if (root == null || root.current == null) { +// return; +// } +// updateContainer(null, root, null, null); +// container = null; +// root = null; +// }, + +// getInstance() { +// if (root == null || root.current == null) { +// return null; +// } +// return getPublicRootInstance(root); +// }, + +// unstable_flushSync: flushSync, +// }; + +// Object.defineProperty( +// entry, +// 'root', +// ({ +// configurable: true, +// enumerable: true, +// get: function() { +// if (root === null) { +// throw new Error("Can't access .root on unmounted test renderer"); +// } +// const children = getChildren(root.current); +// if (children.length === 0) { +// throw new Error("Can't access .root on unmounted test renderer"); +// } else if (children.length === 1) { +// // Normally, we skip the root and just give you the child. +// return children[0]; +// } else { +// // However, we give you the root if there's more than one root child. +// // We could make this the behavior for all cases but it would be a breaking change. +// return wrapFiber(root.current); +// } +// }, +// }: object), +// ); + +// return entry; +// } + +// const fiberToWrapper = new WeakMap(); + +// function wrapFiber(fiber: Fiber): ReactTestInstance { +// let wrapper = fiberToWrapper.get(fiber); +// if (wrapper === undefined && fiber.alternate !== null) { +// wrapper = fiberToWrapper.get(fiber.alternate); +// } + +// if (wrapper === undefined) { +// wrapper = new ReactTestInstance(fiber); +// fiberToWrapper.set(fiber, wrapper); +// } + +// return wrapper; // } -const validWrapperTypes = new Set([ - FunctionComponent, - ClassComponent, - HostComponent, - ForwardRef, - MemoComponent, - SimpleMemoComponent, - // Normally skipped, but used when there's more than one root child. - HostRoot, -]); - -function getChildren(parent: Fiber): Array { - const children: Array = []; - const startingNode = parent; - let node: Fiber = startingNode; - if (node.child === null) { - return children; - } - - node.child.return = node; - node = node.child; - - outer: while (true) { - let descend = false; - if (validWrapperTypes.has(node.tag)) { - children.push(wrapFiber(node)); - } else if (node.tag === HostText) { - if (__DEV__) { - checkPropStringCoercion(node.memoizedProps, 'memoizedProps'); - } - children.push('' + node.memoizedProps); - } else { - descend = true; - } - if (descend && node.child !== null) { - node.child.return = node; - node = node.child; - continue; - } - while (node.sibling === null) { - if (node.return === startingNode) { - break outer; - } - node = (node.return: any); - } - (node.sibling: any).return = node.return; - node = (node.sibling: any); - } - - return children; -} - -class ReactTestInstance { - _fiber: Fiber; - - _currentFiber(): Fiber { - // Throws if this component has been unmounted. - const fiber = findCurrentFiberUsingSlowPath(this._fiber); - - if (fiber === null) { - throw new Error( - "Can't read from currently-mounting component. This error is likely " + - 'caused by a bug in React. Please file an issue.', - ); - } - - return fiber; - } - - constructor(fiber: Fiber) { - if (!validWrapperTypes.has(fiber.tag)) { - throw new Error( - `Unexpected object passed to ReactTestInstance constructor (tag: ${fiber.tag}). ` + - 'This is probably a bug in React.', - ); - } - - this._fiber = fiber; - } - - get instance() { - if (this._fiber.tag === HostComponent) { - return getPublicInstance(this._fiber.stateNode); - } else { - return this._fiber.stateNode; - } - } - - get type() { - return this._fiber.type; - } - - get props(): object { - return this._currentFiber().memoizedProps; - } - - get parent(): ?ReactTestInstance { - let parent = this._fiber.return; - while (parent !== null) { - if (validWrapperTypes.has(parent.tag)) { - if (parent.tag === HostRoot) { - // Special case: we only "materialize" instances for roots - // if they have more than a single child. So we'll check that now. - if (getChildren(parent).length < 2) { - return null; - } - } - return wrapFiber(parent); - } - parent = parent.return; - } - return null; - } - - get children(): Array { - return getChildren(this._currentFiber()); - } - - // Custom search functions - find(predicate: Predicate): ReactTestInstance { - return expectOne( - this.findAll(predicate, {deep: false}), - `matching custom predicate: ${predicate.toString()}`, - ); - } - - findByType(type: any): ReactTestInstance { - return expectOne( - this.findAllByType(type, {deep: false}), - `with node type: "${getComponentNameFromType(type) || 'Unknown'}"`, - ); - } - - findByProps(props: object): ReactTestInstance { - return expectOne( - this.findAllByProps(props, {deep: false}), - `with props: ${JSON.stringify(props)}`, - ); - } - - findAll( - predicate: Predicate, - options: ?FindOptions = null, - ): Array { - return findAll(this, predicate, options); - } - - findAllByType( - type: any, - options: ?FindOptions = null, - ): Array { - return findAll(this, node => node.type === type, options); - } - - findAllByProps( - props: object, - options: ?FindOptions = null, - ): Array { - return findAll( - this, - node => node.props && propsMatch(node.props, props), - options, - ); - } -} - -function findAll( - root: ReactTestInstance, - predicate: Predicate, - options: ?FindOptions, -): Array { - const deep = options ? options.deep : true; - const results = []; - - if (predicate(root)) { - results.push(root); - if (!deep) { - return results; - } - } - - root.children.forEach(child => { - if (typeof child === 'string') { - return; - } - results.push(...findAll(child, predicate, options)); - }); - - return results; -} - -function expectOne( - all: Array, - message: string, -): ReactTestInstance { - if (all.length === 1) { - return all[0]; - } - - const prefix = - all.length === 0 - ? 'No instances found ' - : `Expected 1 but found ${all.length} instances `; - - throw new Error(prefix + message); -} - -function propsMatch(props: object, filter: object): boolean { - for (const key in filter) { - // @ts-expect-error - if (props[key] !== filter[key]) { - return false; - } - } - return true; -} - -function onRecoverableError(error: unknown) { - // TODO: Expose onRecoverableError option to userspace - // eslint-disable-next-line react-internal/no-production-logging, react-internal/warning-args - console.error(error); -} - -function create(element: React.ReactElement, options: TestRendererOptions) { - let createNodeMock = defaultTestOptions.createNodeMock; - let isConcurrent = false; - let isStrictMode = false; - let concurrentUpdatesByDefault = null; - - if (typeof options === 'object' && options !== null) { - if (typeof options.createNodeMock === 'function') { - createNodeMock = options.createNodeMock; - } - - if (options.unstable_isConcurrent === true) { - isConcurrent = true; - } - - if (options.unstable_strictMode === true) { - isStrictMode = true; - } - - if (allowConcurrentByDefault) { - if (options.unstable_concurrentUpdatesByDefault !== undefined) { - concurrentUpdatesByDefault = - options.unstable_concurrentUpdatesByDefault; - } - } - } - - let container = { - children: [], - createNodeMock, - tag: 'CONTAINER', - }; - - let root: FiberRoot | null = createContainer( - container, - isConcurrent ? ConcurrentRoot : LegacyRoot, - null, - isStrictMode, - concurrentUpdatesByDefault, - '', - onRecoverableError, - null, - ); - - if (root == null) { - throw new Error('something went wrong'); - } - - updateContainer(element, root, null, null); - - const entry = { - _Scheduler: Scheduler, - - root: undefined, // makes flow happy - // we define a 'getter' for 'root' below using 'Object.defineProperty' - toJSON(): Array | ReactTestRendererNode | null { - if (root == null || root.current == null || container == null) { - return null; - } - if (container.children.length === 0) { - return null; - } - if (container.children.length === 1) { - return toJSON(container.children[0]); - } - if ( - container.children.length === 2 && - container.children[0].isHidden === true && - container.children[1].isHidden === false - ) { - // Omit timed out children from output entirely, including the fact that we - // temporarily wrap fallback and timed out children in an array. - return toJSON(container.children[1]); - } - let renderedChildren = null; - if (container.children && container.children.length) { - for (let i = 0; i < container.children.length; i++) { - const renderedChild = toJSON(container.children[i]); - if (renderedChild !== null) { - if (renderedChildren === null) { - renderedChildren = [renderedChild]; - } else { - renderedChildren.push(renderedChild); - } - } - } - } - return renderedChildren; - }, - - toTree() { - if (root == null || root.current == null) { - return null; - } - return toTree(root.current); - }, - - update(newElement: React$Element) { - if (root == null || root.current == null) { - return; - } - ReactReconciler.updateContainer(newElement, root, null, null); - }, - - unmount() { - if (root == null || root.current == null) { - return; - } - updateContainer(null, root, null, null); - container = null; - root = null; - }, - - getInstance() { - if (root == null || root.current == null) { - return null; - } - return getPublicRootInstance(root); - }, - - unstable_flushSync: flushSync, - }; - - Object.defineProperty( - entry, - 'root', - ({ - configurable: true, - enumerable: true, - get: function() { - if (root === null) { - throw new Error("Can't access .root on unmounted test renderer"); - } - const children = getChildren(root.current); - if (children.length === 0) { - throw new Error("Can't access .root on unmounted test renderer"); - } else if (children.length === 1) { - // Normally, we skip the root and just give you the child. - return children[0]; - } else { - // However, we give you the root if there's more than one root child. - // We could make this the behavior for all cases but it would be a breaking change. - return wrapFiber(root.current); - } - }, - }: object), - ); - - return entry; -} - -const fiberToWrapper = new WeakMap(); - -function wrapFiber(fiber: Fiber): ReactTestInstance { - let wrapper = fiberToWrapper.get(fiber); - if (wrapper === undefined && fiber.alternate !== null) { - wrapper = fiberToWrapper.get(fiber.alternate); - } - - if (wrapper === undefined) { - wrapper = new ReactTestInstance(fiber); - fiberToWrapper.set(fiber, wrapper); - } - - return wrapper; -} - -// // Enable ReactTestRenderer to be used to test DevTools integration. -// injectIntoDevTools({ -// findFiberByHostInstance: (() => { -// throw new Error('TestRenderer does not support findFiberByHostInstance()'); -// }: any), -// bundleType: __DEV__ ? 1 : 0, -// version: ReactVersion, -// rendererPackageName: 'react-test-renderer', -// }); - -export { - Scheduler as _Scheduler, - create, - /* eslint-disable-next-line camelcase */ - batchedUpdates as unstable_batchedUpdates, - act, -}; \ No newline at end of file +// // // Enable ReactTestRenderer to be used to test DevTools integration. +// // injectIntoDevTools({ +// // findFiberByHostInstance: (() => { +// // throw new Error('TestRenderer does not support findFiberByHostInstance()'); +// // }: any), +// // bundleType: __DEV__ ? 1 : 0, +// // version: ReactVersion, +// // rendererPackageName: 'react-test-renderer', +// // }); + +// export { +// Scheduler as _Scheduler, +// create, +// /* eslint-disable-next-line camelcase */ +// batchedUpdates as unstable_batchedUpdates, +// act, +// }; From 38ef431a0ffb9ccd1b1fdcdba8628c2e7422110e Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Sun, 15 Sep 2024 20:55:26 +0200 Subject: [PATCH 05/61] feat: working toJSON method --- src/renderer/reconciler.ts | 36 ++++++--- src/renderer/renderer.test.tsx | 42 +++++++++++ src/renderer/renderer.ts | 133 +++++++++++++++++++++++++++++++++ 3 files changed, 202 insertions(+), 9 deletions(-) create mode 100644 src/renderer/renderer.test.tsx create mode 100644 src/renderer/renderer.ts diff --git a/src/renderer/reconciler.ts b/src/renderer/reconciler.ts index 3dfab8efe..1ff47390f 100644 --- a/src/renderer/reconciler.ts +++ b/src/renderer/reconciler.ts @@ -3,7 +3,6 @@ import { DefaultEventPriority } from 'react-reconciler/constants'; export type Type = string; export type Props = object; -export type HostContext = object; export type OpaqueHandle = Fiber; export type PublicInstance = unknown | TextInstance; export type SuspenseInstance = unknown; @@ -11,7 +10,7 @@ export type UpdatePayload = unknown; export type Container = { tag: 'CONTAINER'; - children: Array; // Added SuspenseInstance + children: Array; // Added SuspenseInstance createNodeMock: Function; }; @@ -19,9 +18,9 @@ export type Instance = { tag: 'INSTANCE'; type: string; props: object; - isHidden: boolean; - children: Array; + children: Array; rootContainer: Container; + isHidden: boolean; internalHandle: OpaqueHandle; }; @@ -31,6 +30,10 @@ export type TextInstance = { isHidden: boolean; }; +type HostContext = { + isInsideText: boolean; +}; + const NO_CONTEXT = {}; const UPDATE_SIGNAL = {}; const nodeToInstanceMap = new WeakMap(); @@ -98,6 +101,10 @@ const hostConfig = { _hostContext: HostContext, internalHandle: OpaqueHandle, ): Instance { + console.log('createInstance', type, props); + console.log('- RootContainer:', rootContainer); + console.log('- HostContext:', _hostContext); + console.log('- InternalHandle:', internalHandle); return { tag: 'INSTANCE', type, @@ -115,9 +122,13 @@ const hostConfig = { createTextInstance( text: string, _rootContainer: Container, - _hostContext: HostContext, + hostContext: HostContext, _internalHandle: OpaqueHandle, ): TextInstance { + if (!hostContext.isInsideText) { + throw new Error(`Text string "${text}" must be rendered inside component`); + } + return { tag: 'TEXT', text, @@ -193,7 +204,7 @@ const hostConfig = { * This method happens **in the render phase**. Do not mutate the tree from it. */ getRootHostContext(_rootContainer: Container): HostContext | null { - return NO_CONTEXT; + return { isInsideText: false }; }, /** @@ -206,11 +217,18 @@ const hostConfig = { * This method happens **in the render phase**. Do not mutate the tree from it. */ getChildHostContext( - _parentHostContext: HostContext, - _type: Type, + parentHostContext: HostContext, + type: Type, _rootContainer: Container, ): HostContext { - return NO_CONTEXT; + const previousIsInsideText = parentHostContext.isInsideText; + const isInsideText = type === 'Text'; + + if (previousIsInsideText === isInsideText) { + return parentHostContext; + } + + return { isInsideText }; }, /** diff --git a/src/renderer/renderer.test.tsx b/src/renderer/renderer.test.tsx new file mode 100644 index 000000000..e9d7ab3a2 --- /dev/null +++ b/src/renderer/renderer.test.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import { View, Text } from 'react-native'; +import { render } from './renderer'; + +test('renders View', () => { + render(); + expect(true).toBe(true); +}); + +test('renders Text', () => { + render(Hello world); + expect(true).toBe(true); +}); + +test('throws when rendering string inside View', () => { + expect(() => render(Hello)).toThrowErrorMatchingInlineSnapshot( + `"Text string "Hello" must be rendered inside component"`, + ); +}); + +test('implements toJSON()', () => { + const result = render( + + Hello + , + ); + expect(result.toJSON()).toMatchInlineSnapshot(` + + + Hello + + + `); +}); diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts new file mode 100644 index 000000000..79c3682fa --- /dev/null +++ b/src/renderer/renderer.ts @@ -0,0 +1,133 @@ +import { ReactElement } from 'react'; +import { Container, Instance, TestReconciler, TextInstance } from './reconciler'; + +export function render(element: ReactElement) { + const container: Container = { + tag: 'CONTAINER', + children: [], + createNodeMock: () => null, + }; + + const root = TestReconciler.createContainer( + container, + 0, // 0 = LegacyRoot, 1 = ConcurrentRoot + null, // no hydration callback + false, // isStrictMode + null, // concurrentUpdatesByDefaultOverride + 'id', // identifierPrefix + (error) => { + // eslint-disable-next-line no-console + console.log('Recoverable Error', error); + }, // onRecoverableError + null, // transitionCallbacks + ); + + TestReconciler.updateContainer(element, root, null, () => { + // eslint-disable-next-line no-console + console.log('Rendered', container.children); + }); + + const toJSON = () => { + if (root?.current == null || container == null) { + return null; + } + + if (container.children.length === 0) { + return null; + } + + if (container.children.length === 1) { + return toJson(container.children[0]); + } + + if ( + container.children.length === 2 && + container.children[0].isHidden === true && + container.children[1].isHidden === false + ) { + // Omit timed out children from output entirely, including the fact that we + // temporarily wrap fallback and timed out children in an array. + return toJson(container.children[1]); + } + + let renderedChildren = null; + if (container.children?.length) { + for (let i = 0; i < container.children.length; i++) { + const renderedChild = toJson(container.children[i]); + if (renderedChild !== null) { + if (renderedChildren === null) { + renderedChildren = [renderedChild]; + } else { + renderedChildren.push(renderedChild); + } + } + } + } + + return renderedChildren; + }; + + return { + toJSON, + }; +} + +type ToJsonNode = ToJsonInstance | string; + +type ToJsonInstance = { + type: string; + props: object; + children: Array | null; + $$typeof: Symbol; +}; + +function toJson(instance: Instance | TextInstance): ToJsonNode | null { + if (instance.isHidden) { + // Omit timed out children from output entirely. This seems like the least + // surprising behavior. We could perhaps add a separate API that includes + // them, if it turns out people need it. + return null; + } + + switch (instance.tag) { + case 'TEXT': + return instance.text; + + case 'INSTANCE': { + // We don't include the `children` prop in JSON. + // Instead, we will include the actual rendered children. + // @ts-expect-error + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { children, ...props } = instance.props; + + let renderedChildren = null; + if (instance.children?.length) { + for (let i = 0; i < instance.children.length; i++) { + const renderedChild = toJson(instance.children[i]); + if (renderedChild !== null) { + if (renderedChildren === null) { + renderedChildren = [renderedChild]; + } else { + renderedChildren.push(renderedChild); + } + } + } + } + + const result = { + type: instance.type, + props: props, + children: renderedChildren, + $$typeof: Symbol.for('react.test.json'), + }; + Object.defineProperty(result, '$$typeof', { + value: Symbol.for('react.test.json'), + }); + return result; + } + + default: + // @ts-expect-error + throw new Error(`Unexpected node type in toJSON: ${inst.tag}`); + } +} From cdc7c689ade036356aecc2077f692994fd0422fb Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Sun, 15 Sep 2024 21:46:47 +0200 Subject: [PATCH 06/61] chore: implement root, update & unmount --- src/renderer/host-element.ts | 61 +++++++++++++++ src/renderer/reconciler.ts | 15 ++-- src/renderer/render-to-json.ts | 61 +++++++++++++++ src/renderer/renderer.test.tsx | 56 ++++++++++++++ src/renderer/renderer.ts | 133 +++++++++++++++------------------ 5 files changed, 245 insertions(+), 81 deletions(-) create mode 100644 src/renderer/host-element.ts create mode 100644 src/renderer/render-to-json.ts diff --git a/src/renderer/host-element.ts b/src/renderer/host-element.ts new file mode 100644 index 000000000..6aa926cd2 --- /dev/null +++ b/src/renderer/host-element.ts @@ -0,0 +1,61 @@ +import { Container, Instance, TextInstance } from './reconciler'; + +export type HostNode = HostElement | string; +export type HostElementProps = Record; + +const instanceToHostElementMap = new WeakMap(); + +export class HostElement { + public type: string; + public props: HostElementProps; + public children: HostNode[]; + + constructor(type: string, props: HostElementProps, children: HostNode[]) { + this.type = type; + this.props = props; + this.children = children; + } + + static fromContainer(container: Container): HostElement { + const hostElement = instanceToHostElementMap.get(container); + if (hostElement) { + return hostElement; + } + + const result = new HostElement( + 'ROOT', + {}, + container.children.map((child) => HostElement.fromInstance(child)), + ); + + instanceToHostElementMap.set(container, result); + return result; + } + + static fromInstance(instance: Instance | TextInstance): HostNode { + switch (instance.tag) { + case 'TEXT': + return instance.text; + + case 'INSTANCE': { + const hostElement = instanceToHostElementMap.get(instance); + if (hostElement) { + return hostElement; + } + + const result = new HostElement( + instance.type, + instance.props, + instance.children.map((child) => HostElement.fromInstance(child)), + ); + + instanceToHostElementMap.set(instance, result); + return result; + } + + default: + // @ts-expect-error + throw new Error(`Unexpected node type in toJSON: ${instance.tag}`); + } + } +} diff --git a/src/renderer/reconciler.ts b/src/renderer/reconciler.ts index 1ff47390f..a9a1a94a5 100644 --- a/src/renderer/reconciler.ts +++ b/src/renderer/reconciler.ts @@ -2,7 +2,7 @@ import createReconciler, { Fiber } from 'react-reconciler'; import { DefaultEventPriority } from 'react-reconciler/constants'; export type Type = string; -export type Props = object; +export type Props = Record; export type OpaqueHandle = Fiber; export type PublicInstance = unknown | TextInstance; export type SuspenseInstance = unknown; @@ -17,7 +17,7 @@ export type Container = { export type Instance = { tag: 'INSTANCE'; type: string; - props: object; + props: Props; children: Array; rootContainer: Container; isHidden: boolean; @@ -426,7 +426,7 @@ const hostConfig = { insertBefore( parentInstance: Instance, child: Instance | TextInstance, - beforeChild: Instance | TextInstance | SuspenseInstance, + beforeChild: Instance | TextInstance, ): void { const index = parentInstance.children.indexOf(child); if (index !== -1) { @@ -443,7 +443,7 @@ const hostConfig = { insertInContainerBefore( container: Container, child: Instance | TextInstance, - beforeChild: Instance | TextInstance | SuspenseInstance, + beforeChild: Instance | TextInstance, ): void { const index = container.children.indexOf(child); if (index !== -1) { @@ -459,7 +459,7 @@ const hostConfig = { * * React will only call it for the top-level node that is being removed. It is expected that garbage collection would take care of the whole subtree. You are not expected to traverse the child tree in it. */ - removeChild(parentInstance: Instance, child: Instance | TextInstance | SuspenseInstance): void { + removeChild(parentInstance: Instance, child: Instance | TextInstance): void { const index = parentInstance.children.indexOf(child); parentInstance.children.splice(index, 1); }, @@ -467,10 +467,7 @@ const hostConfig = { /** * Same as `removeChild`, but for when a node is detached from the root container. This is useful if attaching to the root has a slightly different implementation, or if the root container nodes are of a different type than the rest of the tree. */ - removeChildFromContainer( - container: Container, - child: Instance | TextInstance | SuspenseInstance, - ): void { + removeChildFromContainer(container: Container, child: Instance | TextInstance): void { const index = container.children.indexOf(child); container.children.splice(index, 1); }, diff --git a/src/renderer/render-to-json.ts b/src/renderer/render-to-json.ts new file mode 100644 index 000000000..756a59cbc --- /dev/null +++ b/src/renderer/render-to-json.ts @@ -0,0 +1,61 @@ +import { Instance, TextInstance } from './reconciler'; + +export type JsonNode = JsonInstance | string; + +export type JsonInstance = { + type: string; + props: object; + children: Array | null; + $$typeof: Symbol; +}; + +export function renderToJson(instance: Instance | TextInstance): JsonNode | null { + if (instance.isHidden) { + // Omit timed out children from output entirely. This seems like the least + // surprising behavior. We could perhaps add a separate API that includes + // them, if it turns out people need it. + return null; + } + + switch (instance.tag) { + case 'TEXT': + return instance.text; + + case 'INSTANCE': { + // We don't include the `children` prop in JSON. + // Instead, we will include the actual rendered children. + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { children, ...props } = instance.props; + + let renderedChildren = null; + if (instance.children?.length) { + for (let i = 0; i < instance.children.length; i++) { + const renderedChild = renderToJson(instance.children[i]); + if (renderedChild !== null) { + if (renderedChildren === null) { + renderedChildren = [renderedChild]; + } else { + renderedChildren.push(renderedChild); + } + } + } + } + + const result = { + type: instance.type, + props: props, + children: renderedChildren, + $$typeof: Symbol.for('react.test.json'), + }; + // This is needed for JEST to format snapshot as JSX. + Object.defineProperty(result, '$$typeof', { + value: Symbol.for('react.test.json'), + }); + return result; + } + + default: + // @ts-expect-error + throw new Error(`Unexpected node type in toJSON: ${inst.tag}`); + } +} diff --git a/src/renderer/renderer.test.tsx b/src/renderer/renderer.test.tsx index e9d7ab3a2..c08db2df6 100644 --- a/src/renderer/renderer.test.tsx +++ b/src/renderer/renderer.test.tsx @@ -18,6 +18,62 @@ test('throws when rendering string inside View', () => { ); }); +test('implements update()', () => { + const result = render(); + expect(result.toJSON()).toMatchInlineSnapshot(` + + `); + + result.update( + + Hello + , + ); + expect(result.toJSON()).toMatchInlineSnapshot(` + + + Hello + + + `); +}); + +test('implements unmount()', () => { + const result = render(); + expect(result.toJSON()).toMatchInlineSnapshot(` + + `); + + result.unmount(); + expect(result.toJSON()).toBeNull(); +}); + +test('implements get root()', () => { + const result = render(); + expect(result.root).toMatchInlineSnapshot(` + HostElement { + "children": [ + HostElement { + "children": [], + "props": { + "children": undefined, + "testID": "view", + }, + "type": "View", + }, + ], + "props": {}, + "type": "ROOT", + } + `); +}); + test('implements toJSON()', () => { const result = render( diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts index 79c3682fa..3cd6cb774 100644 --- a/src/renderer/renderer.ts +++ b/src/renderer/renderer.ts @@ -1,14 +1,23 @@ import { ReactElement } from 'react'; -import { Container, Instance, TestReconciler, TextInstance } from './reconciler'; +import { Container, TestReconciler } from './reconciler'; +import { JsonNode, renderToJson } from './render-to-json'; +import { HostElement } from './host-element'; + +export type RenderResult = { + update: (element: ReactElement) => void; + unmount: () => void; + root: HostElement | null; + toJSON: () => JsonNode | JsonNode[] | null; +}; -export function render(element: ReactElement) { - const container: Container = { +export function render(element: ReactElement): RenderResult { + let container: Container | null = { tag: 'CONTAINER', children: [], createNodeMock: () => null, }; - const root = TestReconciler.createContainer( + let rootFiber = TestReconciler.createContainer( container, 0, // 0 = LegacyRoot, 1 = ConcurrentRoot null, // no hydration callback @@ -22,24 +31,53 @@ export function render(element: ReactElement) { null, // transitionCallbacks ); - TestReconciler.updateContainer(element, root, null, () => { + TestReconciler.updateContainer(element, rootFiber, null, () => { // eslint-disable-next-line no-console - console.log('Rendered', container.children); + console.log('Rendered', container?.children); }); - const toJSON = () => { - if (root?.current == null || container == null) { - return null; + // update(newElement: React$Element) { + // if (root == null || root.current == null) { + // return; + // } + // ReactReconciler.updateContainer(newElement, root, null, null); + // }, + + const update = (element: ReactElement) => { + if (rootFiber == null || container == null) { + return; + } + + TestReconciler.updateContainer(element, rootFiber, null, () => { + // eslint-disable-next-line no-console + console.log('Updated', container?.children); + }); + }; + + const unmount = () => { + if (rootFiber == null || container == null) { + return; } - if (container.children.length === 0) { + TestReconciler.updateContainer(null, rootFiber, null, () => { + // eslint-disable-next-line no-console + console.log('Unmounted', container?.children); + }); + + container = null; + rootFiber = null; + }; + + const toJSON = () => { + if (rootFiber == null || container == null || container.children.length === 0) { return null; } if (container.children.length === 1) { - return toJson(container.children[0]); + return renderToJson(container.children[0]); } + // TODO: When could that happen? if ( container.children.length === 2 && container.children[0].isHidden === true && @@ -47,13 +85,13 @@ export function render(element: ReactElement) { ) { // Omit timed out children from output entirely, including the fact that we // temporarily wrap fallback and timed out children in an array. - return toJson(container.children[1]); + return renderToJson(container.children[1]); } let renderedChildren = null; if (container.children?.length) { for (let i = 0; i < container.children.length; i++) { - const renderedChild = toJson(container.children[i]); + const renderedChild = renderToJson(container.children[i]); if (renderedChild !== null) { if (renderedChildren === null) { renderedChildren = [renderedChild]; @@ -67,67 +105,18 @@ export function render(element: ReactElement) { return renderedChildren; }; - return { + const result = { + update, + unmount, toJSON, - }; -} - -type ToJsonNode = ToJsonInstance | string; - -type ToJsonInstance = { - type: string; - props: object; - children: Array | null; - $$typeof: Symbol; -}; - -function toJson(instance: Instance | TextInstance): ToJsonNode | null { - if (instance.isHidden) { - // Omit timed out children from output entirely. This seems like the least - // surprising behavior. We could perhaps add a separate API that includes - // them, if it turns out people need it. - return null; - } - - switch (instance.tag) { - case 'TEXT': - return instance.text; - - case 'INSTANCE': { - // We don't include the `children` prop in JSON. - // Instead, we will include the actual rendered children. - // @ts-expect-error - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { children, ...props } = instance.props; - - let renderedChildren = null; - if (instance.children?.length) { - for (let i = 0; i < instance.children.length; i++) { - const renderedChild = toJson(instance.children[i]); - if (renderedChild !== null) { - if (renderedChildren === null) { - renderedChildren = [renderedChild]; - } else { - renderedChildren.push(renderedChild); - } - } - } + get root(): HostElement { + if (rootFiber == null || container == null) { + throw new Error("Can't access .root on unmounted test renderer"); } - const result = { - type: instance.type, - props: props, - children: renderedChildren, - $$typeof: Symbol.for('react.test.json'), - }; - Object.defineProperty(result, '$$typeof', { - value: Symbol.for('react.test.json'), - }); - return result; - } + return HostElement.fromContainer(container); + }, + }; - default: - // @ts-expect-error - throw new Error(`Unexpected node type in toJSON: ${inst.tag}`); - } + return result; } From f0e3acb1b57af36ffcdf7478056db0f838c38238 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Mon, 16 Sep 2024 09:36:43 +0200 Subject: [PATCH 07/61] feat: pass first tests --- src/__tests__/host-component-names.test.tsx | 31 ++++++++++++++----- src/config.ts | 6 +++- src/render-act.ts | 18 ++++++++--- src/render.tsx | 8 +++-- src/renderer/host-element.ts | 34 +++++++++++++++++++++ src/renderer/reconciler.ts | 8 ++--- src/renderer/renderer.ts | 6 ++-- 7 files changed, 88 insertions(+), 23 deletions(-) diff --git a/src/__tests__/host-component-names.test.tsx b/src/__tests__/host-component-names.test.tsx index 0e55f1a82..6e6a18eb7 100644 --- a/src/__tests__/host-component-names.test.tsx +++ b/src/__tests__/host-component-names.test.tsx @@ -1,12 +1,17 @@ import * as React from 'react'; import { View } from 'react-native'; import TestRenderer from 'react-test-renderer'; -import { configureInternal, getConfig } from '../config'; +import { configure, configureInternal, getConfig } from '../config'; import { getHostComponentNames, configureHostComponentNamesIfNeeded, } from '../helpers/host-component-names'; import { act, render } from '..'; +import * as internalRenderer from '../renderer/renderer'; + +beforeEach(() => { + configure({ renderer: 'internal' }); +}); describe('getHostComponentNames', () => { test('returns host component names from internal config', () => { @@ -102,12 +107,22 @@ describe('configureHostComponentNamesIfNeeded', () => { }); test('throw an error when auto-detection fails', () => { - const mockCreate = jest.spyOn(TestRenderer, 'create') as jest.Mock; - const renderer = TestRenderer.create(); - - mockCreate.mockReturnValue({ - root: renderer.root, - }); + let mockRender: jest.SpyInstance; + if (getConfig().renderer === 'internal') { + const result = internalRenderer.render(); + + mockRender = jest.spyOn(internalRenderer, 'render') as jest.Mock; + mockRender.mockReturnValue({ + root: result.root, + }); + } else { + const renderer = TestRenderer.create(); + + mockRender = jest.spyOn(TestRenderer, 'create') as jest.Mock; + mockRender.mockReturnValue({ + root: renderer.root, + }); + } expect(() => configureHostComponentNamesIfNeeded()).toThrowErrorMatchingInlineSnapshot(` "Trying to detect host component names triggered the following error: @@ -118,6 +133,6 @@ describe('configureHostComponentNamesIfNeeded', () => { Please check if you are using compatible versions of React Native and React Native Testing Library." `); - mockCreate.mockReset(); + mockRender.mockReset(); }); }); diff --git a/src/config.ts b/src/config.ts index c343a3e15..b13204008 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,4 +1,5 @@ -import { DebugOptions } from './helpers/debug-deep'; +import type { DebugOptions } from './helpers/debug-deep'; +import type { Renderer } from './render'; /** * Global configuration options for React Native Testing Library. @@ -13,6 +14,9 @@ export type Config = { /** Default options for `debug` helper. */ defaultDebugOptions?: Partial; + + /** Renderer to use for rendering components. */ + renderer?: Renderer; }; export type ConfigAliasOptions = { diff --git a/src/render-act.ts b/src/render-act.ts index af31c2046..013484059 100644 --- a/src/render-act.ts +++ b/src/render-act.ts @@ -1,17 +1,27 @@ import TestRenderer from 'react-test-renderer'; -import type { ReactTestRenderer, TestRendererOptions } from 'react-test-renderer'; +import type { ReactTestRenderer } from 'react-test-renderer'; import act from './act'; +import { render } from './renderer/renderer'; +import { RenderOptions } from './render'; +import { getConfig } from './config'; export function renderWithAct( component: React.ReactElement, - options?: Partial, + options?: RenderOptions, ): ReactTestRenderer { let renderer: ReactTestRenderer; + const rendererOption = options?.renderer ?? getConfig().renderer; + // This will be called synchronously. void act(() => { - // @ts-expect-error TestRenderer.create is not typed correctly - renderer = TestRenderer.create(component, options); + if (rendererOption == 'internal') { + console.log(`💠 Test "${expect.getState().currentTestName}": using internal renderer`); + renderer = render(component) as ReactTestRenderer; + } else { + // @ts-expect-error TestRenderer.create is not typed correctly + renderer = TestRenderer.create(component, options); + } }); // @ts-ignore act is synchronous, so renderer is already initialized here diff --git a/src/render.tsx b/src/render.tsx index 5f31dcb2a..c37ade6ce 100644 --- a/src/render.tsx +++ b/src/render.tsx @@ -13,10 +13,12 @@ import { renderWithAct } from './render-act'; import { setRenderResult } from './screen'; import { getQueriesForElement } from './within'; +export type Renderer = 'react-test-renderer' | 'internal'; export interface RenderOptions { wrapper?: React.ComponentType; createNodeMock?: (element: React.ReactElement) => unknown; unstable_validateStringsRenderedWithinText?: boolean; + renderer?: Renderer; } export type RenderResult = ReturnType; @@ -41,7 +43,7 @@ export function renderInternal( wrapper: Wrapper, detectHostComponentNames = true, unstable_validateStringsRenderedWithinText, - ...testRendererOptions + ...restOptions } = options || {}; if (detectHostComponentNames) { @@ -51,12 +53,12 @@ export function renderInternal( if (unstable_validateStringsRenderedWithinText) { return renderWithStringValidation(component, { wrapper: Wrapper, - ...testRendererOptions, + ...restOptions, }); } const wrap = (element: React.ReactElement) => (Wrapper ? {element} : element); - const renderer = renderWithAct(wrap(component), testRendererOptions); + const renderer = renderWithAct(wrap(component), restOptions); return buildRenderResult(renderer, wrap); } diff --git a/src/renderer/host-element.ts b/src/renderer/host-element.ts index 6aa926cd2..116ebac60 100644 --- a/src/renderer/host-element.ts +++ b/src/renderer/host-element.ts @@ -3,6 +3,10 @@ import { Container, Instance, TextInstance } from './reconciler'; export type HostNode = HostElement | string; export type HostElementProps = Record; +type FindOptions = { + deep?: boolean; +}; + const instanceToHostElementMap = new WeakMap(); export class HostElement { @@ -58,4 +62,34 @@ export class HostElement { throw new Error(`Unexpected node type in toJSON: ${instance.tag}`); } } + + findAll(predicate: (element: HostElement) => boolean, options?: FindOptions): HostElement[] { + return findAll(this, predicate, options); + } +} + +function findAll( + root: HostElement, + predicate: (element: HostElement) => boolean, + options?: FindOptions, +): HostElement[] { + const deep = options?.deep ?? true; + const results = []; + + if (predicate(root)) { + results.push(root); + if (!deep) { + return results; + } + } + + root.children.forEach((child) => { + if (typeof child === 'string') { + return; + } + + results.push(...findAll(child, predicate, options)); + }); + + return results; } diff --git a/src/renderer/reconciler.ts b/src/renderer/reconciler.ts index a9a1a94a5..aee623f56 100644 --- a/src/renderer/reconciler.ts +++ b/src/renderer/reconciler.ts @@ -101,10 +101,10 @@ const hostConfig = { _hostContext: HostContext, internalHandle: OpaqueHandle, ): Instance { - console.log('createInstance', type, props); - console.log('- RootContainer:', rootContainer); - console.log('- HostContext:', _hostContext); - console.log('- InternalHandle:', internalHandle); + // console.log('createInstance', type, props); + // console.log('- RootContainer:', rootContainer); + // console.log('- HostContext:', _hostContext); + // console.log('- InternalHandle:', internalHandle); return { tag: 'INSTANCE', type, diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts index 3cd6cb774..c061df06b 100644 --- a/src/renderer/renderer.ts +++ b/src/renderer/renderer.ts @@ -33,7 +33,7 @@ export function render(element: ReactElement): RenderResult { TestReconciler.updateContainer(element, rootFiber, null, () => { // eslint-disable-next-line no-console - console.log('Rendered', container?.children); + //console.log('Rendered', container?.children); }); // update(newElement: React$Element) { @@ -50,7 +50,7 @@ export function render(element: ReactElement): RenderResult { TestReconciler.updateContainer(element, rootFiber, null, () => { // eslint-disable-next-line no-console - console.log('Updated', container?.children); + //console.log('Updated', container?.children); }); }; @@ -61,7 +61,7 @@ export function render(element: ReactElement): RenderResult { TestReconciler.updateContainer(null, rootFiber, null, () => { // eslint-disable-next-line no-console - console.log('Unmounted', container?.children); + //console.log('Unmounted', container?.children); }); container = null; From ef086f45abea6f6455c5dc3926465677217c508f Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Mon, 16 Sep 2024 11:00:08 +0200 Subject: [PATCH 08/61] feat: dynamic HostElement prop calculation --- src/queries/__tests__/test-id.test.tsx | 6 +++- src/renderer/host-element.ts | 47 ++++++++++++++------------ src/renderer/render-to-json.ts | 34 +++++++++++++++++-- src/renderer/renderer.test.tsx | 31 +++++++++-------- src/renderer/renderer.ts | 39 ++++++++++++++------- 5 files changed, 104 insertions(+), 53 deletions(-) diff --git a/src/queries/__tests__/test-id.test.tsx b/src/queries/__tests__/test-id.test.tsx index 0b06578f0..b9c899b5b 100644 --- a/src/queries/__tests__/test-id.test.tsx +++ b/src/queries/__tests__/test-id.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Button, Text, TextInput, View } from 'react-native'; -import { render, screen } from '../..'; +import { configure, render, screen } from '../..'; const PLACEHOLDER_FRESHNESS = 'Add custom freshness'; const PLACEHOLDER_CHEF = 'Who inspected freshness?'; @@ -24,6 +24,10 @@ const Banana = () => ( const MyComponent = (_props: { testID?: string }) => My Component; +beforeEach(() => { + configure({ renderer: 'internal' }); +}); + test('getByTestId returns only native elements', () => { render( diff --git a/src/renderer/host-element.ts b/src/renderer/host-element.ts index 116ebac60..3773da569 100644 --- a/src/renderer/host-element.ts +++ b/src/renderer/host-element.ts @@ -10,14 +10,29 @@ type FindOptions = { const instanceToHostElementMap = new WeakMap(); export class HostElement { - public type: string; - public props: HostElementProps; - public children: HostNode[]; - - constructor(type: string, props: HostElementProps, children: HostNode[]) { - this.type = type; - this.props = props; - this.children = children; + private instance: Instance | Container; + + constructor(instance: Instance | Container) { + this.instance = instance; + } + + get type(): string { + return 'type' in this.instance ? this.instance.type : 'ROOT'; + } + + get props(): HostElementProps { + return 'props' in this.instance ? this.instance.props : {}; + } + + get children(): HostNode[] { + console.log('AAAA', this.instance.children); + const result = this.instance.children.map((child) => HostElement.fromInstance(child)); + console.log('BBBB', result); + return result; + } + + get $$typeof(): Symbol { + return Symbol.for('react.test.json'); } static fromContainer(container: Container): HostElement { @@ -26,12 +41,7 @@ export class HostElement { return hostElement; } - const result = new HostElement( - 'ROOT', - {}, - container.children.map((child) => HostElement.fromInstance(child)), - ); - + const result = new HostElement(container); instanceToHostElementMap.set(container, result); return result; } @@ -47,19 +57,14 @@ export class HostElement { return hostElement; } - const result = new HostElement( - instance.type, - instance.props, - instance.children.map((child) => HostElement.fromInstance(child)), - ); - + const result = new HostElement(instance); instanceToHostElementMap.set(instance, result); return result; } default: // @ts-expect-error - throw new Error(`Unexpected node type in toJSON: ${instance.tag}`); + throw new Error(`Unexpected node type in HostElement.fromInstance: ${instance.tag}`); } } diff --git a/src/renderer/render-to-json.ts b/src/renderer/render-to-json.ts index 756a59cbc..f8cebf5ba 100644 --- a/src/renderer/render-to-json.ts +++ b/src/renderer/render-to-json.ts @@ -1,4 +1,4 @@ -import { Instance, TextInstance } from './reconciler'; +import { Container, Instance, TextInstance } from './reconciler'; export type JsonNode = JsonInstance | string; @@ -9,8 +9,8 @@ export type JsonInstance = { $$typeof: Symbol; }; -export function renderToJson(instance: Instance | TextInstance): JsonNode | null { - if (instance.isHidden) { +export function renderToJson(instance: Container | Instance | TextInstance): JsonNode | null { + if (`isHidden` in instance && instance.isHidden) { // Omit timed out children from output entirely. This seems like the least // surprising behavior. We could perhaps add a separate API that includes // them, if it turns out people need it. @@ -54,6 +54,34 @@ export function renderToJson(instance: Instance | TextInstance): JsonNode | null return result; } + case 'CONTAINER': { + let renderedChildren = null; + if (instance.children?.length) { + for (let i = 0; i < instance.children.length; i++) { + const renderedChild = renderToJson(instance.children[i]); + if (renderedChild !== null) { + if (renderedChildren === null) { + renderedChildren = [renderedChild]; + } else { + renderedChildren.push(renderedChild); + } + } + } + } + + const result = { + type: 'ROOT', + props: {}, + children: renderedChildren, + $$typeof: Symbol.for('react.test.json'), + }; + // This is needed for JEST to format snapshot as JSX. + Object.defineProperty(result, '$$typeof', { + value: Symbol.for('react.test.json'), + }); + return result; + } + default: // @ts-expect-error throw new Error(`Unexpected node type in toJSON: ${inst.tag}`); diff --git a/src/renderer/renderer.test.tsx b/src/renderer/renderer.test.tsx index c08db2df6..11c5cf02b 100644 --- a/src/renderer/renderer.test.tsx +++ b/src/renderer/renderer.test.tsx @@ -2,6 +2,10 @@ import * as React from 'react'; import { View, Text } from 'react-native'; import { render } from './renderer'; +function Passthrough({ children }: { children: React.ReactNode }) { + return children; +} + test('renders View', () => { render(); expect(true).toBe(true); @@ -12,10 +16,18 @@ test('renders Text', () => { expect(true).toBe(true); }); -test('throws when rendering string inside View', () => { +test('throws when rendering string outside of Text', () => { expect(() => render(Hello)).toThrowErrorMatchingInlineSnapshot( `"Text string "Hello" must be rendered inside component"`, ); + + expect(() => render(Hello)).toThrowErrorMatchingInlineSnapshot( + `"Text string "Hello" must be rendered inside component"`, + ); + + expect(() => render(<>Hello)).toThrowErrorMatchingInlineSnapshot( + `"Text string "Hello" must be rendered inside component"`, + ); }); test('implements update()', () => { @@ -57,20 +69,9 @@ test('implements unmount()', () => { test('implements get root()', () => { const result = render(); expect(result.root).toMatchInlineSnapshot(` - HostElement { - "children": [ - HostElement { - "children": [], - "props": { - "children": undefined, - "testID": "view", - }, - "type": "View", - }, - ], - "props": {}, - "type": "ROOT", - } + `); }); diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts index c061df06b..42b0de4a0 100644 --- a/src/renderer/renderer.ts +++ b/src/renderer/renderer.ts @@ -1,12 +1,13 @@ import { ReactElement } from 'react'; import { Container, TestReconciler } from './reconciler'; import { JsonNode, renderToJson } from './render-to-json'; -import { HostElement } from './host-element'; +import { HostElement, HostNode } from './host-element'; export type RenderResult = { update: (element: ReactElement) => void; unmount: () => void; - root: HostElement | null; + container: HostElement | null; + root: HostNode | null; toJSON: () => JsonNode | JsonNode[] | null; }; @@ -17,7 +18,7 @@ export function render(element: ReactElement): RenderResult { createNodeMock: () => null, }; - let rootFiber = TestReconciler.createContainer( + let containerFiber = TestReconciler.createContainer( container, 0, // 0 = LegacyRoot, 1 = ConcurrentRoot null, // no hydration callback @@ -31,7 +32,7 @@ export function render(element: ReactElement): RenderResult { null, // transitionCallbacks ); - TestReconciler.updateContainer(element, rootFiber, null, () => { + TestReconciler.updateContainer(element, containerFiber, null, () => { // eslint-disable-next-line no-console //console.log('Rendered', container?.children); }); @@ -44,32 +45,32 @@ export function render(element: ReactElement): RenderResult { // }, const update = (element: ReactElement) => { - if (rootFiber == null || container == null) { + if (containerFiber == null || container == null) { return; } - TestReconciler.updateContainer(element, rootFiber, null, () => { + TestReconciler.updateContainer(element, containerFiber, null, () => { // eslint-disable-next-line no-console //console.log('Updated', container?.children); }); }; const unmount = () => { - if (rootFiber == null || container == null) { + if (containerFiber == null || container == null) { return; } - TestReconciler.updateContainer(null, rootFiber, null, () => { + TestReconciler.updateContainer(null, containerFiber, null, () => { // eslint-disable-next-line no-console //console.log('Unmounted', container?.children); }); container = null; - rootFiber = null; + containerFiber = null; }; const toJSON = () => { - if (rootFiber == null || container == null || container.children.length === 0) { + if (containerFiber == null || container == null || container.children.length === 0) { return null; } @@ -109,13 +110,25 @@ export function render(element: ReactElement): RenderResult { update, unmount, toJSON, - get root(): HostElement { - if (rootFiber == null || container == null) { - throw new Error("Can't access .root on unmounted test renderer"); + get container(): HostElement { + if (containerFiber == null || container == null) { + throw new Error("Can't access .container on unmounted test renderer"); } return HostElement.fromContainer(container); }, + + get root(): HostNode { + if (containerFiber == null || container == null) { + throw new Error("Can't access .root on unmounted test renderer"); + } + + if (container.children.length === 0) { + throw new Error("Can't access .root on unmounted test renderer"); + } + + return HostElement.fromInstance(container.children[0]); + }, }; return result; From eaf3e1c398462d34ec69dc7ad77a61c8302c5532 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Mon, 16 Sep 2024 13:15:52 +0200 Subject: [PATCH 09/61] feat: support more tests --- src/__tests__/auto-cleanup.test.tsx | 10 +- src/__tests__/screen.test.tsx | 6 +- src/helpers/component-tree.ts | 2 +- src/helpers/find-all.ts | 15 +- src/matchers/__tests__/extend-expect.test.tsx | 6 +- src/matchers/__tests__/to-be-busy.test.tsx | 8 +- .../__tests__/to-have-display-value.test.tsx | 6 +- src/matchers/to-be-busy.tsx | 3 + .../__tests__/accessibility-state.test.tsx | 7 +- .../__tests__/accessibility-value.test.tsx | 61 +++++- src/queries/__tests__/display-value.test.tsx | 6 +- src/queries/__tests__/hint-text.test.tsx | 50 ++++- .../__tests__/placeholder-text.test.tsx | 6 +- src/queries/__tests__/role.test.tsx | 6 +- src/queries/__tests__/test-id.test.tsx | 2 + src/queries/__tests__/text.test.tsx | 6 +- src/render.tsx | 3 +- src/renderer/host-element.ts | 24 ++- src/renderer/reconciler.ts | 184 ++++++++++-------- src/renderer/render-to-json.ts | 20 +- src/renderer/renderer.test.tsx | 4 + src/renderer/renderer.ts | 23 +-- .../__tests__/press.real-timers.test.tsx | 6 +- src/user-event/press/__tests__/press.test.tsx | 6 +- src/user-event/type/__tests__/type.test.tsx | 3 +- 25 files changed, 343 insertions(+), 130 deletions(-) diff --git a/src/__tests__/auto-cleanup.test.tsx b/src/__tests__/auto-cleanup.test.tsx index cb11f5e62..f0cba16d9 100644 --- a/src/__tests__/auto-cleanup.test.tsx +++ b/src/__tests__/auto-cleanup.test.tsx @@ -1,6 +1,10 @@ import * as React from 'react'; import { View } from 'react-native'; -import { render } from '..'; +import { configure, render } from '..'; + +beforeEach(() => { + configure({ renderer: 'internal' }); +}); let isMounted = false; @@ -26,14 +30,14 @@ afterEach(() => { // This just verifies that by importing RNTL in an environment which supports afterEach (like jest) // we'll get automatic cleanup between tests. -test('component is mounted, but not umounted before test ends', () => { +test('component is mounted, but not unmounted before test ends', () => { const fn = jest.fn(); render(); expect(isMounted).toEqual(true); expect(fn).not.toHaveBeenCalled(); }); -test('component is automatically umounted after first test ends', () => { +test('component is automatically unmounted after first test ends', () => { expect(isMounted).toEqual(false); }); diff --git a/src/__tests__/screen.test.tsx b/src/__tests__/screen.test.tsx index b22e92522..beefdf56d 100644 --- a/src/__tests__/screen.test.tsx +++ b/src/__tests__/screen.test.tsx @@ -1,6 +1,10 @@ import * as React from 'react'; import { View, Text } from 'react-native'; -import { render, screen } from '..'; +import { configure, render, screen } from '..'; + +beforeEach(() => { + configure({ renderer: 'internal' }); +}); test('screen has the same queries as render result', () => { const result = render(Mt. Everest); diff --git a/src/helpers/component-tree.ts b/src/helpers/component-tree.ts index 8387278b5..610845e3f 100644 --- a/src/helpers/component-tree.ts +++ b/src/helpers/component-tree.ts @@ -10,7 +10,7 @@ export type HostTestInstance = ReactTestInstance & { type: string }; * @param element The element to check. */ export function isHostElement(element?: ReactTestInstance | null): element is HostTestInstance { - return typeof element?.type === 'string'; + return typeof element?.type === 'string' && element.type !== 'CONTAINER'; } /** diff --git a/src/helpers/find-all.ts b/src/helpers/find-all.ts index ffd62f936..ac68c1ea0 100644 --- a/src/helpers/find-all.ts +++ b/src/helpers/find-all.ts @@ -35,28 +35,31 @@ export function findAll( // Extracted from React Test Renderer // src: https://github.com/facebook/react/blob/8e2bde6f2751aa6335f3cef488c05c3ea08e074a/packages/react-test-renderer/src/ReactTestRenderer.js#L402 function findAllInternal( - root: ReactTestInstance, + node: ReactTestInstance, predicate: (element: ReactTestInstance) => boolean, options?: FindAllOptions, + indent: string = '', ): HostTestInstance[] { const results: HostTestInstance[] = []; + //console.log(`${indent} 🟢 findAllInternal`, node.type, node.props); + // Match descendants first but do not add them to results yet. const matchingDescendants: HostTestInstance[] = []; - root.children.forEach((child) => { + node.children.forEach((child) => { if (typeof child === 'string') { return; } - matchingDescendants.push(...findAllInternal(child, predicate, options)); + matchingDescendants.push(...findAllInternal(child, predicate, options, indent + ' ')); }); if ( // When matchDeepestOnly = true: add current element only if no descendants match (!options?.matchDeepestOnly || matchingDescendants.length === 0) && - isHostElement(root) && - predicate(root) + isHostElement(node) && + predicate(node) ) { - results.push(root); + results.push(node); } // Add matching descendants after element to preserve original tree walk order. diff --git a/src/matchers/__tests__/extend-expect.test.tsx b/src/matchers/__tests__/extend-expect.test.tsx index 1889926aa..13f1ecadd 100644 --- a/src/matchers/__tests__/extend-expect.test.tsx +++ b/src/matchers/__tests__/extend-expect.test.tsx @@ -2,7 +2,11 @@ import * as React from 'react'; import { View } from 'react-native'; // Note: that must point to root of the /src to reliably replicate default import. -import { render } from '../..'; +import { configure, render } from '../..'; + +beforeEach(() => { + configure({ renderer: 'internal' }); +}); // This is check that RNTL does not extend "expect" by default, until we actually want to expose Jest matchers publically. test('does not extend "expect" by default', () => { diff --git a/src/matchers/__tests__/to-be-busy.test.tsx b/src/matchers/__tests__/to-be-busy.test.tsx index 8de385f42..8231953ba 100644 --- a/src/matchers/__tests__/to-be-busy.test.tsx +++ b/src/matchers/__tests__/to-be-busy.test.tsx @@ -1,9 +1,13 @@ import * as React from 'react'; import { View } from 'react-native'; -import { render, screen } from '../..'; +import { configure, render, screen } from '../..'; import '../extend-expect'; -test('toBeBusy() basic case', () => { +beforeEach(() => { + configure({ renderer: 'internal' }); +}); + +test.only('toBeBusy() basic case', () => { render( <> diff --git a/src/matchers/__tests__/to-have-display-value.test.tsx b/src/matchers/__tests__/to-have-display-value.test.tsx index e5ebd7e47..32881483b 100644 --- a/src/matchers/__tests__/to-have-display-value.test.tsx +++ b/src/matchers/__tests__/to-have-display-value.test.tsx @@ -1,8 +1,12 @@ import * as React from 'react'; import { TextInput, View } from 'react-native'; -import { render, screen } from '../..'; +import { configure, render, screen } from '../..'; import '../extend-expect'; +beforeEach(() => { + configure({ renderer: 'internal' }); +}); + test('toHaveDisplayValue() example test', () => { render(); diff --git a/src/matchers/to-be-busy.tsx b/src/matchers/to-be-busy.tsx index effc027c1..6107a0fd6 100644 --- a/src/matchers/to-be-busy.tsx +++ b/src/matchers/to-be-busy.tsx @@ -6,6 +6,9 @@ import { checkHostElement, formatElement } from './utils'; export function toBeBusy(this: jest.MatcherContext, element: ReactTestInstance) { checkHostElement(element, toBeBusy, this); + console.log('🔴 toBeBusy', element.type, element.props); + console.log('🔴 - computeAriaBusy', computeAriaBusy(element)); + return { pass: computeAriaBusy(element), message: () => { diff --git a/src/queries/__tests__/accessibility-state.test.tsx b/src/queries/__tests__/accessibility-state.test.tsx index 26a2b61de..6944a0442 100644 --- a/src/queries/__tests__/accessibility-state.test.tsx +++ b/src/queries/__tests__/accessibility-state.test.tsx @@ -1,11 +1,12 @@ /* eslint-disable no-console */ import * as React from 'react'; import { View, Text, Pressable, TouchableOpacity } from 'react-native'; -import { render, screen } from '../..'; +import { configure, render, screen } from '../..'; type ConsoleLogMock = jest.Mock; beforeEach(() => { + configure({ renderer: 'internal' }); jest.spyOn(console, 'warn').mockImplementation(() => {}); }); @@ -148,6 +149,10 @@ describe('disabled state matching', () => { render(); expect(screen.getByA11yState({ disabled: true })).toBeTruthy(); + expect(screen.queryByA11yState({ disabled: true })).toBeTruthy(); + + const x = screen.queryByA11yState({ disabled: false }); + expect(x).toMatchInlineSnapshot(`null`); expect(screen.queryByA11yState({ disabled: false })).toBeFalsy(); }); diff --git a/src/queries/__tests__/accessibility-value.test.tsx b/src/queries/__tests__/accessibility-value.test.tsx index 9e07ab41c..b64f39216 100644 --- a/src/queries/__tests__/accessibility-value.test.tsx +++ b/src/queries/__tests__/accessibility-value.test.tsx @@ -1,11 +1,12 @@ /* eslint-disable no-console */ import * as React from 'react'; import { Text, TouchableOpacity, View } from 'react-native'; -import { render, screen } from '../..'; +import { configure, render, screen } from '../..'; type ConsoleLogMock = jest.Mock; beforeEach(() => { + configure({ renderer: 'internal' }); jest.spyOn(console, 'warn').mockImplementation(() => {}); }); @@ -30,6 +31,64 @@ const Section = () => ( test('getByA11yValue, queryByA11yValue, findByA11yValue', async () => { render(
); + expect(screen.toJSON()).toMatchInlineSnapshot(` + [ + + Title + , + + + cool text + + , + ] + `); expect(screen.getByA11yValue({ min: 40 }).props.accessibilityValue).toEqual({ min: 40, diff --git a/src/queries/__tests__/display-value.test.tsx b/src/queries/__tests__/display-value.test.tsx index fb8d5e68c..7b3eb7ec1 100644 --- a/src/queries/__tests__/display-value.test.tsx +++ b/src/queries/__tests__/display-value.test.tsx @@ -1,8 +1,12 @@ import * as React from 'react'; import { TextInput, View } from 'react-native'; -import { fireEvent, render, screen } from '../..'; +import { configure, fireEvent, render, screen } from '../..'; import '../../matchers/extend-expect'; +beforeEach(() => { + configure({ renderer: 'internal' }); +}); + const PLACEHOLDER_FRESHNESS = 'Add custom freshness'; const PLACEHOLDER_CHEF = 'Who inspected freshness?'; const INPUT_FRESHNESS = 'Custom Freshie'; diff --git a/src/queries/__tests__/hint-text.test.tsx b/src/queries/__tests__/hint-text.test.tsx index 2ff9f8419..4be6412c4 100644 --- a/src/queries/__tests__/hint-text.test.tsx +++ b/src/queries/__tests__/hint-text.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Pressable, Text, View } from 'react-native'; -import { render, screen } from '../..'; +import { configure, render, screen } from '../..'; const BUTTON_HINT = 'click this button'; const TEXT_HINT = 'static text'; @@ -35,6 +35,54 @@ const Section = () => ( test('getByA11yHint, queryByA11yHint, findByA11yHint', async () => { render(
); + expect(screen.toJSON()).toMatchInlineSnapshot(` + [ + + Title + , + + + Hello + + , + ] + `); + expect(screen.getByA11yHint(BUTTON_HINT).props.accessibilityHint).toEqual(BUTTON_HINT); const button = screen.queryByA11yHint(BUTTON_HINT); expect(button?.props.accessibilityHint).toEqual(BUTTON_HINT); diff --git a/src/queries/__tests__/placeholder-text.test.tsx b/src/queries/__tests__/placeholder-text.test.tsx index 61a394ac1..8f46b0227 100644 --- a/src/queries/__tests__/placeholder-text.test.tsx +++ b/src/queries/__tests__/placeholder-text.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { TextInput, View } from 'react-native'; -import { render, screen } from '../..'; +import { configure, render, screen } from '../..'; const PLACEHOLDER_FRESHNESS = 'Add custom freshness'; const PLACEHOLDER_CHEF = 'Who inspected freshness?'; @@ -24,6 +24,10 @@ const Banana = () => ( ); +beforeEach(() => { + configure({ renderer: 'internal' }); +}); + test('getByPlaceholderText, queryByPlaceholderText', () => { render(); const input = screen.getByPlaceholderText(/custom/i); diff --git a/src/queries/__tests__/role.test.tsx b/src/queries/__tests__/role.test.tsx index 9feb21baf..48923a3cc 100644 --- a/src/queries/__tests__/role.test.tsx +++ b/src/queries/__tests__/role.test.tsx @@ -10,7 +10,7 @@ import { View, Switch, } from 'react-native'; -import { render, screen } from '../..'; +import { configure, render, screen } from '../..'; const TEXT_LABEL = 'cool text'; @@ -42,6 +42,10 @@ const Section = () => ( ); +beforeEach(() => { + configure({ renderer: 'internal' }); +}); + test('getByRole, queryByRole, findByRole', async () => { render(
); diff --git a/src/queries/__tests__/test-id.test.tsx b/src/queries/__tests__/test-id.test.tsx index b9c899b5b..f8170bd88 100644 --- a/src/queries/__tests__/test-id.test.tsx +++ b/src/queries/__tests__/test-id.test.tsx @@ -36,6 +36,7 @@ test('getByTestId returns only native elements', () => {