mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Bring ReactFabricHostComponent back to react-native (#36570)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/36570 I'm doing some preparations to implement this proposal to bring some DOM APIs to React Native refs: https://github.com/react-native-community/discussions-and-proposals/pull/607 To make it easier to iterate on the proposal, and to improve the separation of concerns between React and React Native, I'm moving the definition of `ReactFabricHostComponent` (the public instance provided by React when using refs on host conmponents) to the `react-native` package. I already did some steps in the React repository to simplify this: * Removing unused imperative events that caused increased coupling: https://github.com/facebook/react/pull/26282 * Extracting the definition of the public instance to a separate module: https://github.com/facebook/react/pull/26291 In this case, in order to be able to move the definition from React to React Native, we need to: 1. Create the definition in React Native and export it through `ReactNativePrivateInterface`. 2. Update React to use that definition instead of the one in its own module. This diff implements the first step. `ReactNativeAttributePayload` is required by this definition and by the one for Paper that still exists in React. I moved it here so we only define it where we use it when we remove Paper. Paper will access it through `ReactNativePrivateInterface` as well. That will also allow us to remove a few other fields in that interface. Changelog: [Internal] bypass-github-export-checks Reviewed By: yungsters Differential Revision: D43772356 fbshipit-source-id: 78dac152f415f19316ec90887127bf9861fe3110
This commit is contained in:
committed by
Facebook GitHub Bot
parent
64ea075141
commit
ee76107d74
+2
-1
@@ -28,7 +28,8 @@ module.exports = {
|
||||
testPathIgnorePatterns: [
|
||||
'/node_modules/',
|
||||
'<rootDir>/packages/react-native/template',
|
||||
'<rootDir>/packages/react-native/Libraries/Renderer',
|
||||
'<rootDir>/packages/react-native/Libraries/Renderer/implementations',
|
||||
'<rootDir>/packages/react-native/Libraries/Renderer/shims',
|
||||
'<rootDir>/packages/rn-tester/e2e',
|
||||
],
|
||||
transformIgnorePatterns: ['node_modules/(?!@react-native/)'],
|
||||
|
||||
@@ -15,14 +15,13 @@ import type {
|
||||
MeasureInWindowOnSuccessCallback,
|
||||
MeasureLayoutOnSuccessCallback,
|
||||
MeasureOnSuccessCallback,
|
||||
Node,
|
||||
} from '../Renderer/shims/ReactNativeTypes';
|
||||
import type {RootTag} from '../Types/RootTagTypes';
|
||||
|
||||
// TODO: type these properly.
|
||||
export opaque type Node = {...};
|
||||
type NodeSet = Array<Node>;
|
||||
type NodeProps = {...};
|
||||
type InstanceHandle = {...};
|
||||
export type NodeSet = Array<Node>;
|
||||
export type NodeProps = {...};
|
||||
export type InstanceHandle = {...};
|
||||
export type Spec = {|
|
||||
+createNode: (
|
||||
reactTag: number,
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* 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 {
|
||||
LayoutAnimationConfig,
|
||||
MeasureInWindowOnSuccessCallback,
|
||||
MeasureLayoutOnSuccessCallback,
|
||||
MeasureOnSuccessCallback,
|
||||
Node,
|
||||
} from '../../Renderer/shims/ReactNativeTypes';
|
||||
import type {RootTag} from '../../Types/RootTagTypes';
|
||||
import type {
|
||||
InstanceHandle,
|
||||
NodeProps,
|
||||
NodeSet,
|
||||
Spec as FabricUIManager,
|
||||
} from '../FabricUIManager';
|
||||
|
||||
type NodeMock = {
|
||||
reactTag: number,
|
||||
rootTag: RootTag,
|
||||
props: NodeProps,
|
||||
instanceHandle: InstanceHandle,
|
||||
children: NodeSet,
|
||||
};
|
||||
|
||||
function fromNode(node: Node): NodeMock {
|
||||
// $FlowExpectedError[incompatible-return]
|
||||
return node;
|
||||
}
|
||||
|
||||
function toNode(node: NodeMock): Node {
|
||||
// $FlowExpectedError[incompatible-return]
|
||||
return node;
|
||||
}
|
||||
|
||||
const FabricUIManagerMock: FabricUIManager = {
|
||||
createNode: jest.fn(
|
||||
(
|
||||
reactTag: number,
|
||||
viewName: string,
|
||||
rootTag: RootTag,
|
||||
props: NodeProps,
|
||||
instanceHandle: InstanceHandle,
|
||||
): Node => {
|
||||
return toNode({
|
||||
reactTag,
|
||||
rootTag,
|
||||
props,
|
||||
instanceHandle,
|
||||
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: newProps});
|
||||
}),
|
||||
cloneNodeWithNewChildrenAndProps: jest.fn(
|
||||
(node: Node, newProps: NodeProps): Node => {
|
||||
return toNode({...fromNode(node), children: [], props: newProps});
|
||||
},
|
||||
),
|
||||
createChildSet: jest.fn((rootTag: RootTag): NodeSet => {
|
||||
return [];
|
||||
}),
|
||||
appendChild: jest.fn((parentNode: Node, child: Node): Node => {
|
||||
return toNode({
|
||||
...fromNode(parentNode),
|
||||
children: fromNode(parentNode).children.concat(child),
|
||||
});
|
||||
}),
|
||||
appendChildToSet: jest.fn((childSet: NodeSet, child: Node): void => {
|
||||
childSet.push(child);
|
||||
}),
|
||||
completeRoot: jest.fn((rootTag: RootTag, childSet: NodeSet): void => {}),
|
||||
measure: jest.fn((node: Node, callback: MeasureOnSuccessCallback): void => {
|
||||
callback(10, 10, 100, 100, 0, 0);
|
||||
}),
|
||||
measureInWindow: jest.fn(
|
||||
(node: Node, callback: MeasureInWindowOnSuccessCallback): void => {
|
||||
callback(10, 10, 100, 100);
|
||||
},
|
||||
),
|
||||
measureLayout: jest.fn(
|
||||
(
|
||||
node: Node,
|
||||
relativeNode: Node,
|
||||
onFail: () => void,
|
||||
onSuccess: MeasureLayoutOnSuccessCallback,
|
||||
): void => {
|
||||
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 => {}),
|
||||
getBoundingClientRect: jest.fn(
|
||||
(
|
||||
node: Node,
|
||||
): [
|
||||
/* x:*/ number,
|
||||
/* y:*/ number,
|
||||
/* width:*/ number,
|
||||
/* height:*/ number,
|
||||
] => {
|
||||
return [1, 1, 100, 100];
|
||||
},
|
||||
),
|
||||
setNativeProps: jest.fn((node: Node, newProps: NodeProps): void => {}),
|
||||
dispatchCommand: jest.fn(
|
||||
(node: Node, commandName: string, args: Array<mixed>): void => {},
|
||||
),
|
||||
};
|
||||
|
||||
global.nativeFabricUIManager = FabricUIManagerMock;
|
||||
|
||||
export function getFabricUIManager(): ?FabricUIManager {
|
||||
return FabricUIManagerMock;
|
||||
}
|
||||
+30
-2
@@ -17,14 +17,22 @@ import typeof ReactFiberErrorDialog from '../Core/ReactFiberErrorDialog';
|
||||
import typeof RCTEventEmitter from '../EventEmitter/RCTEventEmitter';
|
||||
import typeof CustomEvent from '../Events/CustomEvent';
|
||||
import typeof UIManager from '../ReactNative/UIManager';
|
||||
import typeof {
|
||||
createPublicInstance,
|
||||
getNativeTagFromPublicInstance,
|
||||
getNodeFromPublicInstance,
|
||||
} from '../Renderer/public/ReactFabricPublicInstance';
|
||||
import typeof {
|
||||
create as createAttributePayload,
|
||||
diff as diffAttributePayloads,
|
||||
} from '../Renderer/public/ReactNativeAttributePayload';
|
||||
import typeof ReactNativeViewConfigRegistry from '../Renderer/shims/ReactNativeViewConfigRegistry';
|
||||
import typeof flattenStyle from '../StyleSheet/flattenStyle';
|
||||
import type {DangerouslyImpreciseStyleProp} from '../StyleSheet/StyleSheet';
|
||||
import typeof deepFreezeAndThrowOnMutationInDev from '../Utilities/deepFreezeAndThrowOnMutationInDev';
|
||||
import typeof deepDiffer from '../Utilities/differ/deepDiffer';
|
||||
import typeof Platform from '../Utilities/Platform';
|
||||
|
||||
import {type DangerouslyImpreciseStyleProp} from '../StyleSheet/StyleSheet';
|
||||
|
||||
// flowlint unsafe-getters-setters:off
|
||||
module.exports = {
|
||||
get BatchedBridge(): BatchedBridge {
|
||||
@@ -48,6 +56,7 @@ module.exports = {
|
||||
get UIManager(): UIManager {
|
||||
return require('../ReactNative/UIManager');
|
||||
},
|
||||
// TODO: Remove when React has migrated to `createAttributePayload` and `diffAttributePayloads`
|
||||
get deepDiffer(): deepDiffer {
|
||||
return require('../Utilities/differ/deepDiffer');
|
||||
},
|
||||
@@ -56,6 +65,7 @@ module.exports = {
|
||||
> {
|
||||
return require('../Utilities/deepFreezeAndThrowOnMutationInDev');
|
||||
},
|
||||
// TODO: Remove when React has migrated to `createAttributePayload` and `diffAttributePayloads`
|
||||
get flattenStyle(): flattenStyle<DangerouslyImpreciseStyleProp> {
|
||||
// $FlowFixMe[underconstrained-implicit-instantiation]
|
||||
return require('../StyleSheet/flattenStyle');
|
||||
@@ -72,4 +82,22 @@ module.exports = {
|
||||
get CustomEvent(): CustomEvent {
|
||||
return require('../Events/CustomEvent').default;
|
||||
},
|
||||
get createAttributePayload(): createAttributePayload {
|
||||
return require('../Renderer/public/ReactNativeAttributePayload').create;
|
||||
},
|
||||
get diffAttributePayloads(): diffAttributePayloads {
|
||||
return require('../Renderer/public/ReactNativeAttributePayload').diff;
|
||||
},
|
||||
get createPublicInstance(): createPublicInstance {
|
||||
return require('../Renderer/public/ReactFabricPublicInstance')
|
||||
.createPublicInstance;
|
||||
},
|
||||
get getNativeTagFromPublicInstance(): getNativeTagFromPublicInstance {
|
||||
return require('../Renderer/public/ReactFabricPublicInstance')
|
||||
.getNativeTagFromPublicInstance;
|
||||
},
|
||||
get getNodeFromPublicInstance(): getNodeFromPublicInstance {
|
||||
return require('../Renderer/public/ReactFabricPublicInstance')
|
||||
.getNodeFromPublicInstance;
|
||||
},
|
||||
};
|
||||
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @format
|
||||
* @flow strict-local
|
||||
*/
|
||||
|
||||
import type {
|
||||
AttributeConfiguration,
|
||||
HostComponent,
|
||||
INativeMethods,
|
||||
MeasureInWindowOnSuccessCallback,
|
||||
MeasureLayoutOnSuccessCallback,
|
||||
MeasureOnSuccessCallback,
|
||||
ViewConfig,
|
||||
} from '../shims/ReactNativeTypes';
|
||||
import type {ElementRef} from 'react';
|
||||
|
||||
import TextInputState from '../../Components/TextInput/TextInputState';
|
||||
import {getFabricUIManager} from '../../ReactNative/FabricUIManager';
|
||||
import {getNodeFromInternalInstanceHandle} from '../shims/ReactFabric';
|
||||
import {create} from './ReactNativeAttributePayload';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
const {
|
||||
measure: fabricMeasure,
|
||||
measureInWindow: fabricMeasureInWindow,
|
||||
measureLayout: fabricMeasureLayout,
|
||||
getBoundingClientRect: fabricGetBoundingClientRect,
|
||||
setNativeProps,
|
||||
} = nullthrows(getFabricUIManager());
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
/**
|
||||
* This is used for refs on host components.
|
||||
*/
|
||||
export class ReactFabricHostComponent implements INativeMethods {
|
||||
// These need to be accessible from `ReactFabricPublicInstanceUtils`.
|
||||
__nativeTag: number;
|
||||
__internalInstanceHandle: mixed;
|
||||
|
||||
_viewConfig: ViewConfig;
|
||||
|
||||
constructor(
|
||||
tag: number,
|
||||
viewConfig: ViewConfig,
|
||||
internalInstanceHandle: mixed,
|
||||
) {
|
||||
this.__nativeTag = tag;
|
||||
this._viewConfig = viewConfig;
|
||||
this.__internalInstanceHandle = internalInstanceHandle;
|
||||
}
|
||||
|
||||
blur() {
|
||||
// $FlowFixMe[incompatible-exact] Migrate all usages of `NativeMethods` to an interface to fix this.
|
||||
TextInputState.blurTextInput(this);
|
||||
}
|
||||
|
||||
focus() {
|
||||
// $FlowFixMe[incompatible-exact] Migrate all usages of `NativeMethods` to an interface to fix this.
|
||||
TextInputState.focusTextInput(this);
|
||||
}
|
||||
|
||||
measure(callback: MeasureOnSuccessCallback) {
|
||||
const node = getNodeFromInternalInstanceHandle(
|
||||
this.__internalInstanceHandle,
|
||||
);
|
||||
if (node != null) {
|
||||
fabricMeasure(node, callback);
|
||||
}
|
||||
}
|
||||
|
||||
measureInWindow(callback: MeasureInWindowOnSuccessCallback) {
|
||||
const node = getNodeFromInternalInstanceHandle(
|
||||
this.__internalInstanceHandle,
|
||||
);
|
||||
if (node != null) {
|
||||
fabricMeasureInWindow(node, callback);
|
||||
}
|
||||
}
|
||||
|
||||
measureLayout(
|
||||
relativeToNativeNode: number | ElementRef<HostComponent<mixed>>,
|
||||
onSuccess: MeasureLayoutOnSuccessCallback,
|
||||
onFail?: () => void /* currently unused */,
|
||||
) {
|
||||
if (
|
||||
typeof relativeToNativeNode === 'number' ||
|
||||
!(relativeToNativeNode instanceof ReactFabricHostComponent)
|
||||
) {
|
||||
if (__DEV__) {
|
||||
console.error(
|
||||
'Warning: ref.measureLayout must be called with a ref to a native component.',
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const toStateNode = getNodeFromInternalInstanceHandle(
|
||||
this.__internalInstanceHandle,
|
||||
);
|
||||
const fromStateNode = getNodeFromInternalInstanceHandle(
|
||||
relativeToNativeNode.__internalInstanceHandle,
|
||||
);
|
||||
|
||||
if (toStateNode != null && fromStateNode != null) {
|
||||
fabricMeasureLayout(
|
||||
toStateNode,
|
||||
fromStateNode,
|
||||
onFail != null ? onFail : noop,
|
||||
onSuccess != null ? onSuccess : noop,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
unstable_getBoundingClientRect(): DOMRect {
|
||||
const node = getNodeFromInternalInstanceHandle(
|
||||
this.__internalInstanceHandle,
|
||||
);
|
||||
if (node != null) {
|
||||
const rect = fabricGetBoundingClientRect(node);
|
||||
|
||||
if (rect) {
|
||||
return new DOMRect(rect[0], rect[1], rect[2], rect[3]);
|
||||
}
|
||||
}
|
||||
|
||||
// Empty rect if any of the above failed
|
||||
return new DOMRect(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
setNativeProps(nativeProps: {...}): void {
|
||||
if (__DEV__) {
|
||||
warnForStyleProps(nativeProps, this._viewConfig.validAttributes);
|
||||
}
|
||||
const updatePayload = create(nativeProps, this._viewConfig.validAttributes);
|
||||
|
||||
const node = getNodeFromInternalInstanceHandle(
|
||||
this.__internalInstanceHandle,
|
||||
);
|
||||
if (node != null && updatePayload != null) {
|
||||
setNativeProps(node, updatePayload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function warnForStyleProps(
|
||||
props: {...},
|
||||
validAttributes: AttributeConfiguration,
|
||||
): void {
|
||||
if (__DEV__) {
|
||||
for (const key in validAttributes.style) {
|
||||
if (!(validAttributes[key] || props[key] === undefined)) {
|
||||
console.error(
|
||||
'You are setting the style `{ %s' +
|
||||
': ... }` as a prop. You ' +
|
||||
'should nest it in a style object. ' +
|
||||
'E.g. `{ style: { %s' +
|
||||
': ... } }`',
|
||||
key,
|
||||
key,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createPublicInstance(
|
||||
tag: number,
|
||||
viewConfig: ViewConfig,
|
||||
internalInstanceHandle: mixed,
|
||||
): ReactFabricHostComponent {
|
||||
return new ReactFabricHostComponent(tag, viewConfig, internalInstanceHandle);
|
||||
}
|
||||
|
||||
export function getNativeTagFromPublicInstance(
|
||||
publicInstance: ReactFabricHostComponent,
|
||||
): number {
|
||||
return publicInstance.__nativeTag;
|
||||
}
|
||||
|
||||
export function getNodeFromPublicInstance(
|
||||
publicInstance: ReactFabricHostComponent,
|
||||
): mixed {
|
||||
return getNodeFromInternalInstanceHandle(
|
||||
publicInstance.__internalInstanceHandle,
|
||||
);
|
||||
}
|
||||
+492
@@ -0,0 +1,492 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @format
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {AttributeConfiguration} from '../shims/ReactNativeTypes';
|
||||
|
||||
import flattenStyle from '../../StyleSheet/flattenStyle';
|
||||
import deepDiffer from '../../Utilities/differ/deepDiffer';
|
||||
|
||||
const emptyObject = {};
|
||||
|
||||
/**
|
||||
* Create a payload that contains all the updates between two sets of props.
|
||||
*
|
||||
* These helpers are all encapsulated into a single module, because they use
|
||||
* mutation as a performance optimization which leads to subtle shared
|
||||
* dependencies between the code paths. To avoid this mutable state leaking
|
||||
* across modules, I've kept them isolated to this module.
|
||||
*/
|
||||
|
||||
type NestedNode = Array<NestedNode> | Object;
|
||||
|
||||
// Tracks removed keys
|
||||
let removedKeys: {[string]: boolean} | null = null;
|
||||
let removedKeyCount = 0;
|
||||
|
||||
const deepDifferOptions = {
|
||||
unsafelyIgnoreFunctions: true,
|
||||
};
|
||||
|
||||
function defaultDiffer(prevProp: mixed, nextProp: mixed): boolean {
|
||||
if (typeof nextProp !== 'object' || nextProp === null) {
|
||||
// Scalars have already been checked for equality
|
||||
return true;
|
||||
} else {
|
||||
// For objects and arrays, the default diffing algorithm is a deep compare
|
||||
return deepDiffer(prevProp, nextProp, deepDifferOptions);
|
||||
}
|
||||
}
|
||||
|
||||
function restoreDeletedValuesInNestedArray(
|
||||
updatePayload: Object,
|
||||
node: NestedNode,
|
||||
validAttributes: AttributeConfiguration,
|
||||
) {
|
||||
if (Array.isArray(node)) {
|
||||
let i = node.length;
|
||||
while (i-- && removedKeyCount > 0) {
|
||||
restoreDeletedValuesInNestedArray(
|
||||
updatePayload,
|
||||
node[i],
|
||||
validAttributes,
|
||||
);
|
||||
}
|
||||
} else if (node && removedKeyCount > 0) {
|
||||
const obj = node;
|
||||
for (const propKey in removedKeys) {
|
||||
// $FlowFixMe[incompatible-use] found when upgrading Flow
|
||||
if (!removedKeys[propKey]) {
|
||||
continue;
|
||||
}
|
||||
let nextProp = obj[propKey];
|
||||
if (nextProp === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const attributeConfig = validAttributes[propKey];
|
||||
if (!attributeConfig) {
|
||||
continue; // not a valid native prop
|
||||
}
|
||||
|
||||
if (typeof nextProp === 'function') {
|
||||
// $FlowFixMe[incompatible-type] found when upgrading Flow
|
||||
nextProp = true;
|
||||
}
|
||||
if (typeof nextProp === 'undefined') {
|
||||
// $FlowFixMe[incompatible-type] found when upgrading Flow
|
||||
nextProp = null;
|
||||
}
|
||||
|
||||
if (typeof attributeConfig !== 'object') {
|
||||
// case: !Object is the default case
|
||||
updatePayload[propKey] = nextProp;
|
||||
} else if (
|
||||
typeof attributeConfig.diff === 'function' ||
|
||||
typeof attributeConfig.process === 'function'
|
||||
) {
|
||||
// case: CustomAttributeConfiguration
|
||||
const nextValue =
|
||||
typeof attributeConfig.process === 'function'
|
||||
? attributeConfig.process(nextProp)
|
||||
: nextProp;
|
||||
updatePayload[propKey] = nextValue;
|
||||
}
|
||||
// $FlowFixMe[incompatible-use] found when upgrading Flow
|
||||
removedKeys[propKey] = false;
|
||||
removedKeyCount--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function diffNestedArrayProperty(
|
||||
updatePayload: null | Object,
|
||||
prevArray: Array<NestedNode>,
|
||||
nextArray: Array<NestedNode>,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
const minLength =
|
||||
prevArray.length < nextArray.length ? prevArray.length : nextArray.length;
|
||||
let i;
|
||||
for (i = 0; i < minLength; i++) {
|
||||
// Diff any items in the array in the forward direction. Repeated keys
|
||||
// will be overwritten by later values.
|
||||
updatePayload = diffNestedProperty(
|
||||
updatePayload,
|
||||
prevArray[i],
|
||||
nextArray[i],
|
||||
validAttributes,
|
||||
);
|
||||
}
|
||||
for (; i < prevArray.length; i++) {
|
||||
// Clear out all remaining properties.
|
||||
updatePayload = clearNestedProperty(
|
||||
updatePayload,
|
||||
prevArray[i],
|
||||
validAttributes,
|
||||
);
|
||||
}
|
||||
for (; i < nextArray.length; i++) {
|
||||
// Add all remaining properties.
|
||||
updatePayload = addNestedProperty(
|
||||
updatePayload,
|
||||
nextArray[i],
|
||||
validAttributes,
|
||||
);
|
||||
}
|
||||
return updatePayload;
|
||||
}
|
||||
|
||||
function diffNestedProperty(
|
||||
updatePayload: null | Object,
|
||||
prevProp: NestedNode,
|
||||
nextProp: NestedNode,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
if (!updatePayload && prevProp === nextProp) {
|
||||
// If no properties have been added, then we can bail out quickly on object
|
||||
// equality.
|
||||
return updatePayload;
|
||||
}
|
||||
|
||||
if (!prevProp || !nextProp) {
|
||||
if (nextProp) {
|
||||
return addNestedProperty(updatePayload, nextProp, validAttributes);
|
||||
}
|
||||
if (prevProp) {
|
||||
return clearNestedProperty(updatePayload, prevProp, validAttributes);
|
||||
}
|
||||
return updatePayload;
|
||||
}
|
||||
|
||||
if (!Array.isArray(prevProp) && !Array.isArray(nextProp)) {
|
||||
// Both are leaves, we can diff the leaves.
|
||||
return diffProperties(updatePayload, prevProp, nextProp, validAttributes);
|
||||
}
|
||||
|
||||
if (Array.isArray(prevProp) && Array.isArray(nextProp)) {
|
||||
// Both are arrays, we can diff the arrays.
|
||||
return diffNestedArrayProperty(
|
||||
updatePayload,
|
||||
prevProp,
|
||||
nextProp,
|
||||
validAttributes,
|
||||
);
|
||||
}
|
||||
|
||||
if (Array.isArray(prevProp)) {
|
||||
return diffProperties(
|
||||
updatePayload,
|
||||
// $FlowFixMe - We know that this is always an object when the input is.
|
||||
flattenStyle(prevProp),
|
||||
// $FlowFixMe - We know that this isn't an array because of above flow.
|
||||
nextProp,
|
||||
validAttributes,
|
||||
);
|
||||
}
|
||||
|
||||
return diffProperties(
|
||||
updatePayload,
|
||||
prevProp,
|
||||
// $FlowFixMe - We know that this is always an object when the input is.
|
||||
flattenStyle(nextProp),
|
||||
validAttributes,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* addNestedProperty takes a single set of props and valid attribute
|
||||
* attribute configurations. It processes each prop and adds it to the
|
||||
* updatePayload.
|
||||
*/
|
||||
function addNestedProperty(
|
||||
updatePayload: null | Object,
|
||||
nextProp: NestedNode,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): $FlowFixMe {
|
||||
if (!nextProp) {
|
||||
return updatePayload;
|
||||
}
|
||||
|
||||
if (!Array.isArray(nextProp)) {
|
||||
// Add each property of the leaf.
|
||||
return addProperties(updatePayload, nextProp, validAttributes);
|
||||
}
|
||||
|
||||
for (let i = 0; i < nextProp.length; i++) {
|
||||
// Add all the properties of the array.
|
||||
updatePayload = addNestedProperty(
|
||||
updatePayload,
|
||||
nextProp[i],
|
||||
validAttributes,
|
||||
);
|
||||
}
|
||||
|
||||
return updatePayload;
|
||||
}
|
||||
|
||||
/**
|
||||
* clearNestedProperty takes a single set of props and valid attributes. It
|
||||
* adds a null sentinel to the updatePayload, for each prop key.
|
||||
*/
|
||||
function clearNestedProperty(
|
||||
updatePayload: null | Object,
|
||||
prevProp: NestedNode,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
if (!prevProp) {
|
||||
return updatePayload;
|
||||
}
|
||||
|
||||
if (!Array.isArray(prevProp)) {
|
||||
// Add each property of the leaf.
|
||||
return clearProperties(updatePayload, prevProp, validAttributes);
|
||||
}
|
||||
|
||||
for (let i = 0; i < prevProp.length; i++) {
|
||||
// Add all the properties of the array.
|
||||
updatePayload = clearNestedProperty(
|
||||
updatePayload,
|
||||
prevProp[i],
|
||||
validAttributes,
|
||||
);
|
||||
}
|
||||
return updatePayload;
|
||||
}
|
||||
|
||||
/**
|
||||
* diffProperties takes two sets of props and a set of valid attributes
|
||||
* and write to updatePayload the values that changed or were deleted.
|
||||
* If no updatePayload is provided, a new one is created and returned if
|
||||
* anything changed.
|
||||
*/
|
||||
function diffProperties(
|
||||
updatePayload: null | Object,
|
||||
prevProps: Object,
|
||||
nextProps: Object,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
let attributeConfig;
|
||||
let nextProp;
|
||||
let prevProp;
|
||||
|
||||
for (const propKey in nextProps) {
|
||||
attributeConfig = validAttributes[propKey];
|
||||
if (!attributeConfig) {
|
||||
continue; // not a valid native prop
|
||||
}
|
||||
|
||||
prevProp = prevProps[propKey];
|
||||
nextProp = nextProps[propKey];
|
||||
|
||||
// functions are converted to booleans as markers that the associated
|
||||
// events should be sent from native.
|
||||
if (typeof nextProp === 'function') {
|
||||
nextProp = (true: any);
|
||||
// If nextProp is not a function, then don't bother changing prevProp
|
||||
// since nextProp will win and go into the updatePayload regardless.
|
||||
if (typeof prevProp === 'function') {
|
||||
prevProp = (true: any);
|
||||
}
|
||||
}
|
||||
|
||||
// An explicit value of undefined is treated as a null because it overrides
|
||||
// any other preceding value.
|
||||
if (typeof nextProp === 'undefined') {
|
||||
nextProp = (null: any);
|
||||
if (typeof prevProp === 'undefined') {
|
||||
prevProp = (null: any);
|
||||
}
|
||||
}
|
||||
|
||||
if (removedKeys) {
|
||||
removedKeys[propKey] = false;
|
||||
}
|
||||
|
||||
if (updatePayload && updatePayload[propKey] !== undefined) {
|
||||
// Something else already triggered an update to this key because another
|
||||
// value diffed. Since we're now later in the nested arrays our value is
|
||||
// more important so we need to calculate it and override the existing
|
||||
// value. It doesn't matter if nothing changed, we'll set it anyway.
|
||||
|
||||
// Pattern match on: attributeConfig
|
||||
if (typeof attributeConfig !== 'object') {
|
||||
// case: !Object is the default case
|
||||
updatePayload[propKey] = nextProp;
|
||||
} else if (
|
||||
typeof attributeConfig.diff === 'function' ||
|
||||
typeof attributeConfig.process === 'function'
|
||||
) {
|
||||
// case: CustomAttributeConfiguration
|
||||
const nextValue =
|
||||
typeof attributeConfig.process === 'function'
|
||||
? attributeConfig.process(nextProp)
|
||||
: nextProp;
|
||||
updatePayload[propKey] = nextValue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (prevProp === nextProp) {
|
||||
continue; // nothing changed
|
||||
}
|
||||
|
||||
// Pattern match on: attributeConfig
|
||||
if (typeof attributeConfig !== 'object') {
|
||||
// case: !Object is the default case
|
||||
if (defaultDiffer(prevProp, nextProp)) {
|
||||
// a normal leaf has changed
|
||||
(updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[
|
||||
propKey
|
||||
] = nextProp;
|
||||
}
|
||||
} else if (
|
||||
typeof attributeConfig.diff === 'function' ||
|
||||
typeof attributeConfig.process === 'function'
|
||||
) {
|
||||
// case: CustomAttributeConfiguration
|
||||
const shouldUpdate =
|
||||
prevProp === undefined ||
|
||||
(typeof attributeConfig.diff === 'function'
|
||||
? attributeConfig.diff(prevProp, nextProp)
|
||||
: defaultDiffer(prevProp, nextProp));
|
||||
if (shouldUpdate) {
|
||||
const nextValue =
|
||||
typeof attributeConfig.process === 'function'
|
||||
? // $FlowFixMe[incompatible-use] found when upgrading Flow
|
||||
attributeConfig.process(nextProp)
|
||||
: nextProp;
|
||||
(updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[
|
||||
propKey
|
||||
] = nextValue;
|
||||
}
|
||||
} else {
|
||||
// default: fallthrough case when nested properties are defined
|
||||
removedKeys = null;
|
||||
removedKeyCount = 0;
|
||||
// We think that attributeConfig is not CustomAttributeConfiguration at
|
||||
// this point so we assume it must be AttributeConfiguration.
|
||||
updatePayload = diffNestedProperty(
|
||||
updatePayload,
|
||||
prevProp,
|
||||
nextProp,
|
||||
((attributeConfig: any): AttributeConfiguration),
|
||||
);
|
||||
if (removedKeyCount > 0 && updatePayload) {
|
||||
restoreDeletedValuesInNestedArray(
|
||||
updatePayload,
|
||||
nextProp,
|
||||
((attributeConfig: any): AttributeConfiguration),
|
||||
);
|
||||
removedKeys = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also iterate through all the previous props to catch any that have been
|
||||
// removed and make sure native gets the signal so it can reset them to the
|
||||
// default.
|
||||
for (const propKey in prevProps) {
|
||||
if (nextProps[propKey] !== undefined) {
|
||||
continue; // we've already covered this key in the previous pass
|
||||
}
|
||||
attributeConfig = validAttributes[propKey];
|
||||
if (!attributeConfig) {
|
||||
continue; // not a valid native prop
|
||||
}
|
||||
|
||||
if (updatePayload && updatePayload[propKey] !== undefined) {
|
||||
// This was already updated to a diff result earlier.
|
||||
continue;
|
||||
}
|
||||
|
||||
prevProp = prevProps[propKey];
|
||||
if (prevProp === undefined) {
|
||||
continue; // was already empty anyway
|
||||
}
|
||||
// Pattern match on: attributeConfig
|
||||
if (
|
||||
typeof attributeConfig !== 'object' ||
|
||||
typeof attributeConfig.diff === 'function' ||
|
||||
typeof attributeConfig.process === 'function'
|
||||
) {
|
||||
// case: CustomAttributeConfiguration | !Object
|
||||
// Flag the leaf property for removal by sending a sentinel.
|
||||
(updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[
|
||||
propKey
|
||||
] = null;
|
||||
if (!removedKeys) {
|
||||
removedKeys = ({}: {[string]: boolean});
|
||||
}
|
||||
if (!removedKeys[propKey]) {
|
||||
removedKeys[propKey] = true;
|
||||
removedKeyCount++;
|
||||
}
|
||||
} else {
|
||||
// default:
|
||||
// This is a nested attribute configuration where all the properties
|
||||
// were removed so we need to go through and clear out all of them.
|
||||
updatePayload = clearNestedProperty(
|
||||
updatePayload,
|
||||
prevProp,
|
||||
((attributeConfig: any): AttributeConfiguration),
|
||||
);
|
||||
}
|
||||
}
|
||||
return updatePayload;
|
||||
}
|
||||
|
||||
/**
|
||||
* addProperties adds all the valid props to the payload after being processed.
|
||||
*/
|
||||
function addProperties(
|
||||
updatePayload: null | Object,
|
||||
props: Object,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
// TODO: Fast path
|
||||
return diffProperties(updatePayload, emptyObject, props, validAttributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* clearProperties clears all the previous props by adding a null sentinel
|
||||
* to the payload for each valid key.
|
||||
*/
|
||||
function clearProperties(
|
||||
updatePayload: null | Object,
|
||||
prevProps: Object,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
// TODO: Fast path
|
||||
return diffProperties(updatePayload, prevProps, emptyObject, validAttributes);
|
||||
}
|
||||
|
||||
export function create(
|
||||
props: Object,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
return addProperties(
|
||||
null, // updatePayload
|
||||
props,
|
||||
validAttributes,
|
||||
);
|
||||
}
|
||||
|
||||
export function diff(
|
||||
prevProps: Object,
|
||||
nextProps: Object,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
return diffProperties(
|
||||
null, // updatePayload
|
||||
prevProps,
|
||||
nextProps,
|
||||
validAttributes,
|
||||
);
|
||||
}
|
||||
Vendored
+267
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* 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 {HostComponent} from '../../shims/ReactNativeTypes';
|
||||
|
||||
import * as React from 'react';
|
||||
import {act} from 'react-test-renderer';
|
||||
|
||||
const TextInputState = require('../../../Components/TextInput/TextInputState');
|
||||
const FabricUIManager = require('../../../ReactNative/FabricUIManager');
|
||||
const ReactFabric = require('../../shims/ReactFabric');
|
||||
const ReactNativeViewConfigRegistry = require('../../shims/ReactNativeViewConfigRegistry');
|
||||
const nullthrows = require('nullthrows');
|
||||
|
||||
jest.mock('../../../ReactNative/FabricUIManager', () =>
|
||||
require('../../../ReactNative/__mocks__/FabricUIManager'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Given a mocked function, get a correctly typed mock function that preserves
|
||||
* the original function's type.
|
||||
*/
|
||||
function mockOf<TArguments: $ReadOnlyArray<mixed>, TReturn>(
|
||||
fn: (...args: TArguments) => TReturn,
|
||||
): JestMockFn<TArguments, TReturn> {
|
||||
if (!jest.isMockFunction(fn)) {
|
||||
throw new Error(`Function ${fn.name} is not a mock function`);
|
||||
}
|
||||
return (fn: $FlowFixMe);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a sequence of mock views as dictated by `keyLists`. The `keyLists`
|
||||
* argument is an array of arrays which determines the number of render passes,
|
||||
* how many views will be rendered in each pass, and what the keys are for each
|
||||
* of the views.
|
||||
*
|
||||
* If an element in `keyLists` is null, the entire root will be unmounted.
|
||||
*
|
||||
* The return value is an array of arrays with the resulting refs from rendering
|
||||
* each corresponding array of keys.
|
||||
*
|
||||
* If the corresponding array of keys is null, the returned element at that
|
||||
* index will also be null.
|
||||
*/
|
||||
async function mockRenderKeys(
|
||||
keyLists: Array<?Array<?string>>,
|
||||
): Promise<Array<?Array<?React.ElementRef<HostComponent<mixed>>>>> {
|
||||
const mockContainerTag = 11;
|
||||
const MockView = ReactNativeViewConfigRegistry.register(
|
||||
'RCTMockView',
|
||||
() => ({
|
||||
validAttributes: {foo: true, style: {}},
|
||||
uiViewClassName: 'RCTMockView',
|
||||
}),
|
||||
);
|
||||
|
||||
const result: Array<?Array<?React.ElementRef<HostComponent<mixed>>>> = [];
|
||||
for (let i = 0; i < keyLists.length; i++) {
|
||||
const keyList = keyLists[i];
|
||||
if (Array.isArray(keyList)) {
|
||||
const refs: Array<?React.ElementRef<HostComponent<mixed>>> = keyList.map(
|
||||
key => undefined,
|
||||
);
|
||||
await act(() => {
|
||||
ReactFabric.render(
|
||||
<MockView>
|
||||
{keyList.map((key, index) => (
|
||||
<MockView
|
||||
key={key}
|
||||
ref={ref => {
|
||||
refs[index] = ((ref: $FlowFixMe): ?React.ElementRef<
|
||||
HostComponent<mixed>,
|
||||
>);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</MockView>,
|
||||
mockContainerTag,
|
||||
);
|
||||
});
|
||||
// Clone `refs` to ignore future passes.
|
||||
result.push([...refs]);
|
||||
continue;
|
||||
}
|
||||
if (keyList == null) {
|
||||
await act(() => {
|
||||
// $FlowFixMe[prop-missing] This actually exists in ReactFabric
|
||||
ReactFabric.stopSurface(mockContainerTag);
|
||||
});
|
||||
result.push(null);
|
||||
continue;
|
||||
}
|
||||
throw new TypeError(
|
||||
`Invalid 'keyLists' element of type ${typeof keyList}.`,
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
describe('ReactFabricPublicInstance', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
// Installs the global `nativeFabricUIManager` pointing to the mock.
|
||||
require('../../../ReactNative/__mocks__/FabricUIManager');
|
||||
jest.spyOn(TextInputState, 'blurTextInput');
|
||||
jest.spyOn(TextInputState, 'focusTextInput');
|
||||
});
|
||||
|
||||
describe('blur', () => {
|
||||
test('blur() invokes TextInputState', async () => {
|
||||
const result = await mockRenderKeys([['foo']]);
|
||||
const fooRef = nullthrows(result?.[0]?.[0]);
|
||||
|
||||
fooRef.blur();
|
||||
|
||||
expect(mockOf(TextInputState.blurTextInput).mock.calls).toEqual([
|
||||
[fooRef],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('focus', () => {
|
||||
test('focus() invokes TextInputState', async () => {
|
||||
const result = await mockRenderKeys([['foo']]);
|
||||
const fooRef = nullthrows(result?.[0]?.[0]);
|
||||
|
||||
fooRef.focus();
|
||||
|
||||
expect(mockOf(TextInputState.focusTextInput).mock.calls).toEqual([
|
||||
[fooRef],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('measure', () => {
|
||||
test('component.measure(...) invokes callback', async () => {
|
||||
const result = await mockRenderKeys([['foo']]);
|
||||
const fooRef = nullthrows(result?.[0]?.[0]);
|
||||
|
||||
const callback = jest.fn();
|
||||
fooRef.measure(callback);
|
||||
|
||||
expect(
|
||||
nullthrows(FabricUIManager.getFabricUIManager()).measure,
|
||||
).toHaveBeenCalledTimes(1);
|
||||
expect(callback.mock.calls).toEqual([[10, 10, 100, 100, 0, 0]]);
|
||||
});
|
||||
|
||||
test('unmounted.measure(...) does nothing', async () => {
|
||||
const result = await mockRenderKeys([['foo'], null]);
|
||||
const fooRef = nullthrows(result?.[0]?.[0]);
|
||||
const callback = jest.fn();
|
||||
fooRef.measure(callback);
|
||||
|
||||
expect(
|
||||
nullthrows(FabricUIManager.getFabricUIManager()).measure,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('measureInWindow', () => {
|
||||
test('component.measureInWindow(...) invokes callback', async () => {
|
||||
const result = await mockRenderKeys([['foo']]);
|
||||
const fooRef = nullthrows(result?.[0]?.[0]);
|
||||
|
||||
const callback = jest.fn();
|
||||
fooRef.measureInWindow(callback);
|
||||
|
||||
expect(
|
||||
nullthrows(FabricUIManager.getFabricUIManager()).measureInWindow,
|
||||
).toHaveBeenCalledTimes(1);
|
||||
expect(callback.mock.calls).toEqual([[10, 10, 100, 100]]);
|
||||
});
|
||||
|
||||
test('unmounted.measureInWindow(...) does nothing', async () => {
|
||||
const result = await mockRenderKeys([['foo'], null]);
|
||||
const fooRef = nullthrows(result?.[0]?.[0]);
|
||||
|
||||
const callback = jest.fn();
|
||||
fooRef.measureInWindow(callback);
|
||||
|
||||
expect(
|
||||
nullthrows(FabricUIManager.getFabricUIManager()).measureInWindow,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('measureLayout', () => {
|
||||
test('component.measureLayout(component, ...) invokes callback', async () => {
|
||||
const result = await mockRenderKeys([['foo', 'bar']]);
|
||||
const fooRef = nullthrows(result?.[0]?.[0]);
|
||||
const barRef = nullthrows(result?.[0]?.[1]);
|
||||
|
||||
const successCallback = jest.fn();
|
||||
const failureCallback = jest.fn();
|
||||
fooRef.measureLayout(barRef, successCallback, failureCallback);
|
||||
|
||||
expect(
|
||||
nullthrows(FabricUIManager.getFabricUIManager()).measureLayout,
|
||||
).toHaveBeenCalledTimes(1);
|
||||
expect(successCallback.mock.calls).toEqual([[1, 1, 100, 100]]);
|
||||
});
|
||||
|
||||
test('unmounted.measureLayout(component, ...) does nothing', async () => {
|
||||
const result = await mockRenderKeys([
|
||||
['foo', 'bar'],
|
||||
['foo', null],
|
||||
]);
|
||||
const fooRef = nullthrows(result?.[0]?.[0]);
|
||||
const barRef = nullthrows(result?.[0]?.[1]);
|
||||
|
||||
const successCallback = jest.fn();
|
||||
const failureCallback = jest.fn();
|
||||
fooRef.measureLayout(barRef, successCallback, failureCallback);
|
||||
|
||||
expect(
|
||||
nullthrows(FabricUIManager.getFabricUIManager()).measureLayout,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(successCallback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('component.measureLayout(unmounted, ...) does nothing', async () => {
|
||||
const result = await mockRenderKeys([
|
||||
['foo', 'bar'],
|
||||
[null, 'bar'],
|
||||
]);
|
||||
const fooRef = nullthrows(result?.[0]?.[0]);
|
||||
const barRef = nullthrows(result?.[0]?.[1]);
|
||||
|
||||
const successCallback = jest.fn();
|
||||
const failureCallback = jest.fn();
|
||||
fooRef.measureLayout(barRef, successCallback, failureCallback);
|
||||
|
||||
expect(
|
||||
nullthrows(FabricUIManager.getFabricUIManager()).measureLayout,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(successCallback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('unmounted.measureLayout(unmounted, ...) does nothing', async () => {
|
||||
const result = await mockRenderKeys([['foo', 'bar'], null]);
|
||||
const fooRef = nullthrows(result?.[0]?.[0]);
|
||||
const barRef = nullthrows(result?.[0]?.[1]);
|
||||
|
||||
const successCallback = jest.fn();
|
||||
const failureCallback = jest.fn();
|
||||
fooRef.measureLayout(barRef, successCallback, failureCallback);
|
||||
|
||||
expect(
|
||||
nullthrows(FabricUIManager.getFabricUIManager()).measureLayout,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(successCallback).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user