react-native
Version:
A framework for building native apps using React
671 lines (557 loc) • 16.9 kB
JavaScript
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/
import type {
InternalInstanceHandle,
LayoutAnimationConfig,
MeasureInWindowOnSuccessCallback,
MeasureLayoutOnSuccessCallback,
MeasureOnSuccessCallback,
Node,
} from '../../Renderer/shims/ReactNativeTypes';
import type {RootTag} from '../../Types/RootTagTypes';
import type {
NodeProps,
NodeSet,
Spec as FabricUIManager,
} from '../FabricUIManager';
import {createRootTag} from '../RootTag.js';
export type NodeMock = {
children: NodeSet,
instanceHandle: InternalInstanceHandle,
props: NodeProps,
reactTag: number,
rootTag: RootTag,
viewName: string,
};
export function fromNode(node: Node): NodeMock {
// $FlowExpectedError[incompatible-return]
return node;
}
export function toNode(node: NodeMock): Node {
// $FlowExpectedError[incompatible-return]
return node;
}
// Mock of the Native Hooks
const roots: Map<RootTag, NodeSet> = new Map();
const allocatedTags: Set<number> = new Set();
function ensureHostNode(node: Node): void {
if (node == null || typeof node !== 'object') {
throw new Error(
`Expected node to be an object. Got ${
node === null ? 'null' : typeof node
} value`,
);
}
if (typeof node.viewName !== 'string') {
throw new Error(
`Expected node to be a host node. Got object with ${
node.viewName === null ? 'null' : typeof node.viewName
} viewName`,
);
}
}
function getAncestorsInChildSet(
node: Node,
childSet: NodeSet,
): ?$ReadOnlyArray<[Node, number]> {
const rootNode = toNode({
reactTag: 0,
rootTag: fromNode(node).rootTag,
viewName: 'RootNode',
// $FlowExpectedError
instanceHandle: null,
props: {},
children: childSet,
});
let position = 0;
for (const child of childSet) {
const ancestors = getAncestors(child, node);
if (ancestors) {
return [[rootNode, position]].concat(ancestors);
}
position++;
}
return null;
}
function getAncestorsInCurrentTree(
node: Node,
): ?$ReadOnlyArray<[Node, number]> {
const childSet = roots.get(fromNode(node).rootTag);
if (childSet == null) {
return null;
}
return getAncestorsInChildSet(node, childSet);
}
function getAncestors(root: Node, node: Node): ?$ReadOnlyArray<[Node, number]> {
if (fromNode(root).reactTag === fromNode(node).reactTag) {
return [];
}
let position = 0;
for (const child of fromNode(root).children) {
const ancestors = getAncestors(child, node);
if (ancestors != null) {
return [[root, position]].concat(ancestors);
}
position++;
}
return null;
}
export function getNodeInChildSet(node: Node, childSet: NodeSet): ?Node {
const ancestors = getAncestorsInChildSet(node, childSet);
if (ancestors == null) {
return null;
}
const [parent, position] = ancestors[ancestors.length - 1];
const nodeInCurrentTree = fromNode(parent).children[position];
return nodeInCurrentTree;
}
function getNodeInCurrentTree(node: Node): ?Node {
const childSet = roots.get(fromNode(node).rootTag);
if (childSet == null) {
return null;
}
return getNodeInChildSet(node, childSet);
}
function* dfs(node: ?Node): Iterator<Node> {
if (node == null) {
return;
}
yield node;
for (const child of fromNode(node).children) {
yield* dfs(child);
}
}
function hasDisplayNone(node: Node): boolean {
const props = fromNode(node).props;
// Style is flattened when passed to native, so there's no style object.
// $FlowFixMe[prop-missing]
return props != null && props.display === 'none';
}
interface IFabricUIManagerMock extends FabricUIManager {
getRoot(rootTag: RootTag | number): NodeSet;
__getInstanceHandleFromNode(node: Node): InternalInstanceHandle;
__addCommitHook(commitHook: UIManagerCommitHook): void;
__removeCommitHook(commitHook: UIManagerCommitHook): void;
}
export interface UIManagerCommitHook {
shadowTreeWillCommit: (
rootTag: RootTag,
oldChildSet: ?NodeSet,
newChildSet: NodeSet,
) => void;
}
const commitHooks: Set<UIManagerCommitHook> = new Set();
const FabricUIManagerMock: IFabricUIManagerMock = {
createNode: jest.fn(
(
reactTag: number,
viewName: string,
rootTag: RootTag,
props: NodeProps,
instanceHandle: InternalInstanceHandle,
): Node => {
if (allocatedTags.has(reactTag)) {
throw new Error(`Created two native views with tag ${reactTag}`);
}
allocatedTags.add(reactTag);
return toNode({
reactTag,
rootTag,
viewName,
instanceHandle,
props: props,
children: [],
});
},
),
cloneNode: jest.fn((node: Node): Node => {
return toNode({...fromNode(node)});
}),
cloneNodeWithNewChildren: jest.fn((node: Node): Node => {
return toNode({...fromNode(node), children: []});
}),
cloneNodeWithNewProps: jest.fn((node: Node, newProps: NodeProps): Node => {
return toNode({
...fromNode(node),
props: {
...fromNode(node).props,
...newProps,
},
});
}),
cloneNodeWithNewChildrenAndProps: jest.fn(
(node: Node, newProps: NodeProps): Node => {
return toNode({
...fromNode(node),
children: [],
props: {
...fromNode(node).props,
...newProps,
},
});
},
),
createChildSet: jest.fn((rootTag: RootTag): NodeSet => {
return [];
}),
appendChild: jest.fn((parentNode: Node, child: Node): Node => {
// Although the signature returns a Node, React expects this to be mutating.
fromNode(parentNode).children.push(child);
return parentNode;
}),
appendChildToSet: jest.fn((childSet: NodeSet, child: Node): void => {
childSet.push(child);
}),
completeRoot: jest.fn((rootTag: RootTag, childSet: NodeSet): void => {
commitHooks.forEach(hook =>
hook.shadowTreeWillCommit(rootTag, roots.get(rootTag), childSet),
);
roots.set(rootTag, childSet);
}),
measure: jest.fn((node: Node, callback: MeasureOnSuccessCallback): void => {
ensureHostNode(node);
callback(10, 10, 100, 100, 0, 0);
}),
measureInWindow: jest.fn(
(node: Node, callback: MeasureInWindowOnSuccessCallback): void => {
ensureHostNode(node);
callback(10, 10, 100, 100);
},
),
measureLayout: jest.fn(
(
node: Node,
relativeNode: Node,
onFail: () => void,
onSuccess: MeasureLayoutOnSuccessCallback,
): void => {
ensureHostNode(node);
ensureHostNode(relativeNode);
onSuccess(1, 1, 100, 100);
},
),
configureNextLayoutAnimation: jest.fn(
(
config: LayoutAnimationConfig,
callback: () => void, // check what is returned here
errorCallback: () => void,
): void => {},
),
sendAccessibilityEvent: jest.fn((node: Node, eventType: string): void => {}),
findShadowNodeByTag_DEPRECATED: jest.fn((reactTag: number): ?Node => {}),
findNodeAtPoint: jest.fn(
(
node: Node,
locationX: number,
locationY: number,
callback: (instanceHandle: ?InternalInstanceHandle) => void,
): void => {},
),
getBoundingClientRect: jest.fn(
(
node: Node,
includeTransform: boolean,
): ?[
/* x:*/ number,
/* y:*/ number,
/* width:*/ number,
/* height:*/ number,
] => {
ensureHostNode(node);
const nodeInCurrentTree = getNodeInCurrentTree(node);
const currentProps =
nodeInCurrentTree != null ? fromNode(nodeInCurrentTree).props : null;
if (currentProps == null) {
return null;
}
const boundingClientRectForTests: ?{
x: number,
y: number,
width: number,
height: number,
} =
// $FlowExpectedError[prop-missing]
currentProps.__boundingClientRectForTests;
if (boundingClientRectForTests == null) {
return null;
}
const {x, y, width, height} = boundingClientRectForTests;
return [x, y, width, height];
},
),
hasPointerCapture: jest.fn((node: Node, pointerId: number): boolean => false),
setPointerCapture: jest.fn((node: Node, pointerId: number): void => {}),
releasePointerCapture: jest.fn((node: Node, pointerId: number): void => {}),
setNativeProps: jest.fn((node: Node, newProps: NodeProps): void => {}),
dispatchCommand: jest.fn(
(node: Node, commandName: string, args: Array<mixed>): void => {},
),
getParentNode: jest.fn((node: Node): ?InternalInstanceHandle => {
const ancestors = getAncestorsInCurrentTree(node);
if (ancestors == null || ancestors.length - 2 < 0) {
return null;
}
const [parentOfParent, position] = ancestors[ancestors.length - 2];
const parentInCurrentTree = fromNode(parentOfParent).children[position];
return fromNode(parentInCurrentTree).instanceHandle;
}),
getChildNodes: jest.fn(
(node: Node): $ReadOnlyArray<InternalInstanceHandle> => {
const nodeInCurrentTree = getNodeInCurrentTree(node);
if (nodeInCurrentTree == null) {
return [];
}
return fromNode(nodeInCurrentTree).children.map(
child => fromNode(child).instanceHandle,
);
},
),
isConnected: jest.fn((node: Node): boolean => {
return getNodeInCurrentTree(node) != null;
}),
getTextContent: jest.fn((node: Node): string => {
const nodeInCurrentTree = getNodeInCurrentTree(node);
let result = '';
if (nodeInCurrentTree == null) {
return result;
}
for (const childNode of dfs(nodeInCurrentTree)) {
if (fromNode(childNode).viewName === 'RCTRawText') {
const props = fromNode(childNode).props;
// $FlowExpectedError[prop-missing]
const maybeString: ?string = props.text;
if (typeof maybeString === 'string') {
result += maybeString;
}
}
}
return result;
}),
compareDocumentPosition: jest.fn((node: Node, otherNode: Node): number => {
/* eslint-disable no-bitwise */
const ReadOnlyNode =
require('../../../src/private/webapis/dom/nodes/ReadOnlyNode').default;
// Quick check for node vs. itself
if (fromNode(node).reactTag === fromNode(otherNode).reactTag) {
return 0;
}
if (fromNode(node).rootTag !== fromNode(otherNode).rootTag) {
return ReadOnlyNode.DOCUMENT_POSITION_DISCONNECTED;
}
const ancestors = getAncestorsInCurrentTree(node);
if (ancestors == null) {
return ReadOnlyNode.DOCUMENT_POSITION_DISCONNECTED;
}
const otherAncestors = getAncestorsInCurrentTree(otherNode);
if (otherAncestors == null) {
return ReadOnlyNode.DOCUMENT_POSITION_DISCONNECTED;
}
// Consume all common ancestors
let i = 0;
while (
i < ancestors.length &&
i < otherAncestors.length &&
ancestors[i][1] === otherAncestors[i][1]
) {
i++;
}
if (i === ancestors.length) {
return (
ReadOnlyNode.DOCUMENT_POSITION_CONTAINED_BY |
ReadOnlyNode.DOCUMENT_POSITION_FOLLOWING
);
}
if (i === otherAncestors.length) {
return (
ReadOnlyNode.DOCUMENT_POSITION_CONTAINS |
ReadOnlyNode.DOCUMENT_POSITION_PRECEDING
);
}
if (ancestors[i][1] > otherAncestors[i][1]) {
return ReadOnlyNode.DOCUMENT_POSITION_PRECEDING;
}
return ReadOnlyNode.DOCUMENT_POSITION_FOLLOWING;
}),
getOffset: jest.fn(
(
node: Node,
): ?[
/* offsetParent: */ InternalInstanceHandle,
/* offsetTop: */ number,
/* offsetLeft: */ number,
] => {
const ancestors = getAncestorsInCurrentTree(node);
if (ancestors == null) {
return null;
}
const [parent, position] = ancestors[ancestors.length - 1];
const nodeInCurrentTree = fromNode(parent).children[position];
const currentProps =
nodeInCurrentTree != null ? fromNode(nodeInCurrentTree).props : null;
if (currentProps == null || hasDisplayNone(nodeInCurrentTree)) {
return null;
}
const offsetForTests: ?{
top: number,
left: number,
} =
// $FlowExpectedError[prop-missing]
currentProps.__offsetForTests;
if (offsetForTests == null) {
return null;
}
let currentIndex = ancestors.length - 1;
while (currentIndex >= 0 && !hasDisplayNone(ancestors[currentIndex][0])) {
currentIndex--;
}
if (currentIndex >= 0) {
// The node or one of its ancestors have display: none
return null;
}
return [
fromNode(parent).instanceHandle,
offsetForTests.top,
offsetForTests.left,
];
},
),
getScrollPosition: jest.fn(
(node: Node): ?[/* scrollLeft: */ number, /* scrollTop: */ number] => {
ensureHostNode(node);
const nodeInCurrentTree = getNodeInCurrentTree(node);
const currentProps =
nodeInCurrentTree != null ? fromNode(nodeInCurrentTree).props : null;
if (currentProps == null) {
return null;
}
const scrollForTests: ?{
scrollLeft: number,
scrollTop: number,
...
} =
// $FlowExpectedError[prop-missing]
currentProps.__scrollForTests;
if (scrollForTests == null) {
return null;
}
const {scrollLeft, scrollTop} = scrollForTests;
return [scrollLeft, scrollTop];
},
),
getScrollSize: jest.fn(
(node: Node): ?[/* scrollLeft: */ number, /* scrollTop: */ number] => {
ensureHostNode(node);
const nodeInCurrentTree = getNodeInCurrentTree(node);
const currentProps =
nodeInCurrentTree != null ? fromNode(nodeInCurrentTree).props : null;
if (currentProps == null) {
return null;
}
const scrollForTests: ?{
scrollWidth: number,
scrollHeight: number,
...
} =
// $FlowExpectedError[prop-missing]
currentProps.__scrollForTests;
if (scrollForTests == null) {
return null;
}
const {scrollWidth, scrollHeight} = scrollForTests;
return [scrollWidth, scrollHeight];
},
),
getInnerSize: jest.fn(
(node: Node): ?[/* width: */ number, /* height: */ number] => {
ensureHostNode(node);
const nodeInCurrentTree = getNodeInCurrentTree(node);
const currentProps =
nodeInCurrentTree != null ? fromNode(nodeInCurrentTree).props : null;
if (currentProps == null) {
return null;
}
const innerSizeForTests: ?{
width: number,
height: number,
...
} =
// $FlowExpectedError[prop-missing]
currentProps.__innerSizeForTests;
if (innerSizeForTests == null) {
return null;
}
const {width, height} = innerSizeForTests;
return [width, height];
},
),
getBorderSize: jest.fn(
(
node: Node,
): ?[
/* topWidth: */ number,
/* rightWidth: */ number,
/* bottomWidth: */ number,
/* leftWidth: */ number,
] => {
ensureHostNode(node);
const nodeInCurrentTree = getNodeInCurrentTree(node);
const currentProps =
nodeInCurrentTree != null ? fromNode(nodeInCurrentTree).props : null;
if (currentProps == null) {
return null;
}
const borderSizeForTests: ?{
topWidth?: number,
rightWidth?: number,
bottomWidth?: number,
leftWidth?: number,
...
} =
// $FlowExpectedError[prop-missing]
currentProps.__borderSizeForTests;
if (borderSizeForTests == null) {
return null;
}
const {
topWidth = 0,
rightWidth = 0,
bottomWidth = 0,
leftWidth = 0,
} = borderSizeForTests;
return [topWidth, rightWidth, bottomWidth, leftWidth];
},
),
getTagName: jest.fn((node: Node): string => {
ensureHostNode(node);
return 'RN:' + fromNode(node).viewName;
}),
getRoot(containerTag: RootTag | number): NodeSet {
const tag = createRootTag(containerTag);
const root = roots.get(tag);
if (!root) {
throw new Error('No root found for containerTag ' + Number(tag));
}
return root;
},
__getInstanceHandleFromNode(node: Node): InternalInstanceHandle {
return fromNode(node).instanceHandle;
},
__addCommitHook(commitHook: UIManagerCommitHook): void {
commitHooks.add(commitHook);
},
__removeCommitHook(commitHook: UIManagerCommitHook): void {
commitHooks.delete(commitHook);
},
};
global.nativeFabricUIManager = FabricUIManagerMock;
export function getFabricUIManager(): ?IFabricUIManagerMock {
return FabricUIManagerMock;
}