Delete tests migrated to Fantom and unnecessary mocks for FabricUIManager, DOM, etc. (2nd attempt) (#48117)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48117

Changelog: [internal]

Re-land https://github.com/facebook/react-native/pull/48087 with some CI fixes.

Reviewed By: rshest

Differential Revision: D66820310

fbshipit-source-id: 1df4559c1daf5ec0085b299d702ce36deaa681b5
This commit is contained in:
Rubén Norte
2024-12-09 05:05:07 -08:00
committed by Facebook GitHub Bot
parent 351b1bae95
commit d05214665c
16 changed files with 1 additions and 2150 deletions
-1
View File
@@ -37,7 +37,6 @@ module.exports = {
'/node_modules/',
'<rootDir>/packages/react-native/sdks',
'<rootDir>/packages/react-native/Libraries/Renderer',
'<rootDir>/packages/react-native-test-renderer/src',
'<rootDir>/packages/react-native/sdks/hermes/',
...PODS_LOCATIONS,
],
@@ -1,17 +0,0 @@
/**
* 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
*/
module.exports = {
presets: [
['@babel/preset-env', {targets: {node: 'current'}}],
'@babel/preset-flow',
],
plugins: ['@babel/plugin-transform-react-jsx'],
};
@@ -1,25 +0,0 @@
/**
* 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
*/
'use strict';
module.exports = {
haste: {
defaultPlatform: 'ios',
platforms: ['android', 'ios', 'native'],
},
transform: {
'^.+\\.(js|ts|tsx)$': 'babel-jest',
},
transformIgnorePatterns: [
'node_modules/(?!((jest-)?react-native|@react-native(-community)?)/)',
],
setupFilesAfterEnv: ['./src/jest/setup-files-after-env'],
testEnvironment: './src/jest/environment',
};
@@ -1,18 +0,0 @@
{
"name": "@react-native/test-renderer",
"private": true,
"version": "0.77.0-main",
"description": "A Test rendering library for React Native",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/plugin-transform-react-jsx": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@babel/preset-flow": "^7.20.0"
},
"dependencies": {},
"main": "src/index.js",
"peerDependencies": {
"jest": "^29.7.0"
}
}
-14
View File
@@ -1,14 +0,0 @@
/**
* 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
* @format
* @oncall react_native
*/
export {render} from './renderer/index.js';
export {ReactNativeEnvironment} from './jest/environment.js';
@@ -1,74 +0,0 @@
/**
* 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
*/
'use strict';
const NodeEnv = require('jest-environment-node').TestEnvironment;
module.exports = class ReactNativeEnvironment extends NodeEnv {
customExportConditions = ['require', 'react-native'];
constructor(config, context) {
super(config, context);
}
async setup() {
await super.setup();
this.assignGlobals();
this.initializeTurboModuleRegistry();
}
assignGlobals() {
Object.defineProperties(this.global, {
__DEV__: {
configurable: true,
enumerable: true,
value: true,
writable: true,
},
});
this.global.IS_REACT_ACT_ENVIRONMENT = true;
}
initializeTurboModuleRegistry() {
const dims = {width: 100, height: 100, scale: 1, fontScale: 1};
const DIMS = {
screen: {
...dims,
},
window: {
...dims,
},
};
this.global.nativeModuleProxy = name => ({})[name];
this.global.__turboModuleProxy = name =>
({
SourceCode: {getConstants: () => ({scriptURL: ''})},
WebSocketModule: {connect: () => {}},
FileReaderModule: {},
AppState: {getConstants: () => ({}), getCurrentAppState: () => ({})},
DeviceInfo: {getConstants: () => ({Dimensions: DIMS})},
UIManager: {getConstants: () => ({})},
Timing: {},
DevSettings: {},
PlatformConstants: {
getConstants: () => ({reactNativeVersion: '1000.0.0'}),
},
Networking: {},
ImageLoader: {},
NativePerformanceCxx: {},
LogBox: {},
SettingsManager: {
getConstants: () => ({settings: {}}),
},
LinkingManager: {},
I18n: {getConstants: () => ({})},
})[name];
}
};
@@ -1,222 +0,0 @@
/**
* 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
*/
'use strict';
jest.requireActual('@react-native/js-polyfills/error-guard');
jest
.mock('react-native/Libraries/ReactNative/UIManager', () => ({
AndroidViewPager: {
Commands: {
setPage: jest.fn(),
setPageWithoutAnimation: jest.fn(),
},
},
blur: jest.fn(),
createView: jest.fn(),
customBubblingEventTypes: {},
customDirectEventTypes: {},
getConstants: () => ({
ViewManagerNames: [],
}),
getDefaultEventTypes: jest.fn(),
dispatchViewManagerCommand: jest.fn(),
focus: jest.fn(),
getViewManagerConfig: jest.fn(name => {
if (name === 'AndroidDrawerLayout') {
return {
Constants: {
DrawerPosition: {
Left: 10,
},
},
};
}
return {NativeProps: {}};
}),
hasViewManagerConfig: jest.fn(name => {
return name === 'AndroidDrawerLayout';
}),
measure: jest.fn(),
manageChildren: jest.fn(),
removeSubviewsFromContainerWithID: jest.fn(),
replaceExistingNonRootView: jest.fn(),
setChildren: jest.fn(),
updateView: jest.fn(),
AndroidDrawerLayout: {
Constants: {
DrawerPosition: {
Left: 10,
},
},
},
AndroidTextInput: {
Commands: {},
},
ScrollView: {
Constants: {},
},
View: {
Constants: {},
},
}))
// Mock modules defined by the native layer (ex: Objective-C, Java)
.mock('react-native/Libraries/BatchedBridge/NativeModules', () => ({
AlertManager: {
alertWithArgs: jest.fn(),
},
AsyncLocalStorage: {
multiGet: jest.fn((keys, callback) =>
process.nextTick(() => callback(null, [])),
),
multiSet: jest.fn((entries, callback) =>
process.nextTick(() => callback(null)),
),
multiRemove: jest.fn((keys, callback) =>
process.nextTick(() => callback(null)),
),
multiMerge: jest.fn((entries, callback) =>
process.nextTick(() => callback(null)),
),
clear: jest.fn(callback => process.nextTick(() => callback(null))),
getAllKeys: jest.fn(callback =>
process.nextTick(() => callback(null, [])),
),
},
DeviceInfo: {
getConstants() {
return {
Dimensions: {
window: {
fontScale: 2,
height: 1334,
scale: 2,
width: 750,
},
screen: {
fontScale: 2,
height: 1334,
scale: 2,
width: 750,
},
},
};
},
},
DevSettings: {
addMenuItem: jest.fn(),
reload: jest.fn(),
},
ImageLoader: {
getSize: jest.fn(url => Promise.resolve([320, 240])),
prefetchImage: jest.fn(),
},
ImageViewManager: {
getSize: jest.fn((uri, success) =>
process.nextTick(() => success(320, 240)),
),
prefetchImage: jest.fn(),
},
KeyboardObserver: {
addListener: jest.fn(),
removeListeners: jest.fn(),
},
Networking: {
sendRequest: jest.fn(),
abortRequest: jest.fn(),
addListener: jest.fn(),
removeListeners: jest.fn(),
},
PlatformConstants: {
getConstants() {
return {
reactNativeVersion: {
major: 1000,
minor: 0,
patch: 0,
},
};
},
},
PushNotificationManager: {
presentLocalNotification: jest.fn(),
scheduleLocalNotification: jest.fn(),
cancelAllLocalNotifications: jest.fn(),
removeAllDeliveredNotifications: jest.fn(),
getDeliveredNotifications: jest.fn(callback =>
process.nextTick(() => []),
),
removeDeliveredNotifications: jest.fn(),
setApplicationIconBadgeNumber: jest.fn(),
getApplicationIconBadgeNumber: jest.fn(callback =>
process.nextTick(() => callback(0)),
),
cancelLocalNotifications: jest.fn(),
getScheduledLocalNotifications: jest.fn(callback =>
process.nextTick(() => callback()),
),
requestPermissions: jest.fn(() =>
Promise.resolve({alert: true, badge: true, sound: true}),
),
abandonPermissions: jest.fn(),
checkPermissions: jest.fn(callback =>
process.nextTick(() =>
callback({alert: true, badge: true, sound: true}),
),
),
getInitialNotification: jest.fn(() => Promise.resolve(null)),
addListener: jest.fn(),
removeListeners: jest.fn(),
},
StatusBarManager: {
setColor: jest.fn(),
setStyle: jest.fn(),
setHidden: jest.fn(),
setNetworkActivityIndicatorVisible: jest.fn(),
setBackgroundColor: jest.fn(),
setTranslucent: jest.fn(),
getConstants: () => ({
HEIGHT: 42,
}),
},
Timing: {
createTimer: jest.fn(),
deleteTimer: jest.fn(),
},
UIManager: {},
BlobModule: {
getConstants: () => ({BLOB_URI_SCHEME: 'content', BLOB_URI_HOST: null}),
addNetworkingHandler: jest.fn(),
enableBlobSupport: jest.fn(),
disableBlobSupport: jest.fn(),
createFromParts: jest.fn(),
sendBlob: jest.fn(),
release: jest.fn(),
},
WebSocketModule: {
connect: jest.fn(),
send: jest.fn(),
sendBinary: jest.fn(),
ping: jest.fn(),
close: jest.fn(),
addListener: jest.fn(),
removeListeners: jest.fn(),
},
I18nManager: {
allowRTL: jest.fn(),
forceRTL: jest.fn(),
swapLeftAndRightInRTL: jest.fn(),
getConstants: () => ({
isRTL: false,
doLeftAndRightSwapInRTL: true,
}),
},
}));
@@ -1,39 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`render toJSON renders View props 1`] = `
<RCTView
pointerEvents="box-none"
>
<RCTText
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
isHighlighted={false}
selectionColor={null}
>
Hello
</RCTText>
<RCTView
style={
{
"flex": 1,
}
}
/>
</RCTView>
`;
exports[`render toJSON returns expected JSON output based on renderer component 1`] = `
<RCTView>
<RCTText
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
isHighlighted={false}
selectionColor={null}
>
Hello
</RCTText>
<RCTView />
</RCTView>
`;
@@ -1,63 +0,0 @@
/**
* 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
*/
'use strict';
import * as ReactNativeTestRenderer from '../index';
import * as React from 'react';
import {Text, View} from 'react-native';
import 'react-native/Libraries/Components/View/ViewNativeComponent';
function TestComponent() {
return (
<View>
<Text>Hello</Text>
<View />
</View>
);
}
function TestComponentWithProps() {
return (
<View pointerEvents="box-none">
<Text>Hello</Text>
<View style={{flex: 1}} />
</View>
);
}
describe('render', () => {
describe('toJSON', () => {
it('returns expected JSON output based on renderer component', () => {
const result = ReactNativeTestRenderer.render(<TestComponent />);
expect(result.toJSON()).toMatchSnapshot();
});
it('renders View props', () => {
const result = ReactNativeTestRenderer.render(<TestComponentWithProps />);
expect(result.toJSON()).toMatchSnapshot();
});
});
describe('findAll', () => {
it('returns all nodes matching the predicate', () => {
const result = ReactNativeTestRenderer.render(<TestComponent />);
const textNode = result.findAll(node => {
return node.props?.text === 'Hello';
})[0];
expect(textNode).not.toBeUndefined();
const viewNodes = result.findAll(node => {
return node.viewName === 'RCTView';
});
expect(viewNodes.length).toBe(2);
});
});
});
@@ -1,114 +0,0 @@
/**
* 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
*/
'use strict';
import * as FabricUIManager from 'react-native/Libraries/ReactNative/__mocks__/FabricUIManager';
import ReactFabric from 'react-native/Libraries/Renderer/shims/ReactFabric';
import {act} from 'react-test-renderer';
type FiberPartial = {
pendingProps: {
children: $ReadOnlyArray<ReactNode>,
...
},
...
};
type ReactNode = {
children: ?Array<ReactNode>,
props: {text?: string | null, ...},
viewName: string,
instanceHandle: FiberPartial,
};
type RenderedNodeJSON = {
type: string,
props: {[propName: string]: any, ...},
children: null | Array<RenderedJSON>,
$$typeof?: symbol, // Optional because we add it with defineProperty().
};
type RenderedJSON = RenderedNodeJSON | string;
type RenderResult = {
toJSON: () => Array<RenderedJSON> | RenderedJSON | null,
findAll: (predicate: (ReactNode) => boolean) => Array<ReactNode>,
};
function buildRenderResult(rootNode: ReactNode): RenderResult {
return {
toJSON: () => toJSON(rootNode),
findAll: (predicate: ReactNode => boolean) => findAll(rootNode, predicate),
};
}
export function render(element: React.MixedElement): RenderResult {
const manager = FabricUIManager.getFabricUIManager();
if (!manager) {
throw new Error('No FabricUIManager found');
}
const containerTag = Math.round(Math.random() * 1000000);
act(() => {
ReactFabric.render(element, containerTag, () => {}, true);
});
// $FlowFixMe
const root: [ReactNode] = manager.getRoot(containerTag);
if (root == null) {
throw new Error('No root found for containerTag ' + containerTag);
}
return buildRenderResult(root[0]);
}
function toJSON(node: ReactNode): RenderedJSON {
let renderedChildren = null;
if (node.children != null && node.children.length > 0) {
renderedChildren = node.children.map(c => toJSON(c));
}
if (node.viewName === 'RCTRawText') {
return node.props.text ?? '';
}
const {children: _children, ...props} =
node.instanceHandle?.pendingProps ?? {};
const json: RenderedNodeJSON = {
type: node.viewName,
props,
children: renderedChildren,
};
Object.defineProperty(json, '$$typeof', {
value: Symbol.for('react.test.json'),
});
return json;
}
function findAll(
node: ReactNode,
predicate: ReactNode => boolean,
): Array<ReactNode> {
const results = [];
if (predicate(node)) {
results.push(node);
}
if (node.children != null && node.children.length > 0) {
for (const child of node.children) {
results.push(...findAll(child, predicate));
}
}
return results;
}
@@ -1,307 +0,0 @@
/**
* 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
*/
// TODO(legacy-fake-timers): Fix these tests to work with modern timers.
jest.useFakeTimers({legacyFakeTimers: true});
import type {HostInstance} from '../../../Renderer/shims/ReactNativeTypes';
import * as React from 'react';
import {act} from 'react-test-renderer';
const TextInputState = require('../../../Components/TextInput/TextInputState');
const ReactFabric = require('../../../Renderer/shims/ReactFabric').default;
const ReactNativeViewConfigRegistry = require('../../../Renderer/shims/ReactNativeViewConfigRegistry');
const FabricUIManager = require('../../FabricUIManager');
const nullthrows = require('nullthrows');
const isWindows = process.platform === 'win32';
const itif = (condition: boolean) => {
return condition ? it : it.skip;
};
jest.mock('../../FabricUIManager', () =>
require('../../__mocks__/FabricUIManager'),
);
jest.mock('../../../../src/private/webapis/dom/nodes/specs/NativeDOM', () =>
require('../../../../src/private/webapis/dom/nodes/specs/__mocks__/NativeDOMMock'),
);
/**
* 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<?HostInstance>>> {
const mockContainerTag = 11;
const MockView = ReactNativeViewConfigRegistry.register(
'RCTMockView',
() => ({
validAttributes: {foo: true, style: {}},
uiViewClassName: 'RCTMockView',
}),
);
const result: Array<?Array<?HostInstance>> = [];
for (let i = 0; i < keyLists.length; i++) {
const keyList = keyLists[i];
if (Array.isArray(keyList)) {
const refs: Array<?HostInstance> = keyList.map(key => undefined);
await act(() => {
ReactFabric.render(
<MockView>
{keyList.map((key, index) => (
<MockView
key={key}
ref={ref => {
refs[index] = ((ref: $FlowFixMe): ?HostInstance);
}}
/>
))}
</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;
}
[
{enableAccessToHostTreeInFabric: false},
{enableAccessToHostTreeInFabric: true},
].forEach(flags => {
describe(`ReactFabricPublicInstance (ReactNativeFeatureFlags.enableAccessToHostTreeInFabric = ${String(
flags.enableAccessToHostTreeInFabric,
)})'`, () => {
beforeEach(() => {
jest.resetModules();
// Installs the global `nativeFabricUIManager` pointing to the mock.
require('../../../ReactNative/__mocks__/FabricUIManager');
jest.spyOn(TextInputState, 'blurTextInput');
jest.spyOn(TextInputState, 'focusTextInput');
require('../../../../src/private/featureflags/ReactNativeFeatureFlags').override(
{
enableAccessToHostTreeInFabric: () =>
flags.enableAccessToHostTreeInFabric,
},
);
});
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', () => {
itif(!isWindows)('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]]);
});
itif(!isWindows)('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', () => {
itif(!isWindows)(
'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]]);
},
);
itif(!isWindows)(
'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', () => {
itif(!isWindows)(
'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]]);
},
);
itif(!isWindows)(
'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();
},
);
itif(!isWindows)(
'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();
},
);
itif(!isWindows)(
'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();
},
);
});
});
});
@@ -1,334 +0,0 @@
/**
* 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();
export 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;
}
export 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;
}
export function getNodeInCurrentTree(node: Node): ?Node {
const childSet = roots.get(fromNode(node).rootTag);
if (childSet == null) {
return null;
}
return getNodeInChildSet(node, childSet);
}
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,
] => {},
),
setNativeProps: jest.fn((node: Node, newProps: NodeProps): void => {}),
dispatchCommand: jest.fn(
(node: Node, commandName: string, args: Array<mixed>): void => {},
),
compareDocumentPosition: jest.fn((node: Node, otherNode: Node): number => 0),
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;
}
@@ -1,413 +0,0 @@
/**
* 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,
Node,
} from '../../../../../../../Libraries/Renderer/shims/ReactNativeTypes';
import type {
MeasureInWindowOnSuccessCallback,
MeasureLayoutOnSuccessCallback,
MeasureOnSuccessCallback,
} from '../NativeDOM';
import typeof NativeDOM from '../NativeDOM';
import {
ensureHostNode,
fromNode,
getAncestorsInCurrentTree,
getNodeInCurrentTree,
} from '../../../../../../../Libraries/ReactNative/__mocks__/FabricUIManager';
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';
}
const NativeDOMMock: NativeDOM = {
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 [0, 0, 0, 0];
}
const boundingClientRectForTests: ?{
x: number,
y: number,
width: number,
height: number,
} =
// $FlowExpectedError[prop-missing]
currentProps.__boundingClientRectForTests;
if (boundingClientRectForTests == null) {
return [0, 0, 0, 0];
}
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 => {}),
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('../../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, 0, 0];
}
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, 0, 0];
}
const offsetForTests: ?{
top: number,
left: number,
} =
// $FlowExpectedError[prop-missing]
currentProps.__offsetForTests;
if (offsetForTests == null) {
return [null, 0, 0];
}
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, 0, 0];
}
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 [0, 0];
}
const scrollForTests: ?{
scrollLeft: number,
scrollTop: number,
...
} =
// $FlowExpectedError[prop-missing]
currentProps.__scrollForTests;
if (scrollForTests == null) {
return [0, 0];
}
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 [0, 0];
}
const scrollForTests: ?{
scrollWidth: number,
scrollHeight: number,
...
} =
// $FlowExpectedError[prop-missing]
currentProps.__scrollForTests;
if (scrollForTests == null) {
return [0, 0];
}
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 [0, 0];
}
const innerSizeForTests: ?{
width: number,
height: number,
...
} =
// $FlowExpectedError[prop-missing]
currentProps.__innerSizeForTests;
if (innerSizeForTests == null) {
return [0, 0];
}
const {width, height} = innerSizeForTests;
return [width, height];
},
),
getBorderWidth: 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 [0, 0, 0, 0];
}
const borderSizeForTests: ?{
topWidth?: number,
rightWidth?: number,
bottomWidth?: number,
leftWidth?: number,
...
} =
// $FlowExpectedError[prop-missing]
currentProps.__borderSizeForTests;
if (borderSizeForTests == null) {
return [0, 0, 0, 0];
}
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;
}),
/**
* Legacy layout APIs
*/
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);
},
),
};
export default NativeDOMMock;
@@ -1,181 +0,0 @@
/**
* 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
*/
import type ReactNativeElement from '../../../dom/nodes/ReactNativeElement';
import type IntersectionObserver from '../../IntersectionObserver';
import type {
NativeIntersectionObserverEntry,
NativeIntersectionObserverObserveOptions,
Spec,
} from '../NativeIntersectionObserver';
import {getFabricUIManager} from '../../../../../../Libraries/ReactNative/__mocks__/FabricUIManager';
import {getShadowNode} from '../../../dom/nodes/ReadOnlyNode';
import invariant from 'invariant';
import nullthrows from 'nullthrows';
type ObserverState = {
thresholds: $ReadOnlyArray<number>,
rootThresholds?: ?$ReadOnlyArray<number>,
intersecting: boolean,
currentThreshold: ?number,
currentRootThreshold: ?number,
};
type Observation = {
...NativeIntersectionObserverObserveOptions,
state: ObserverState,
};
let pendingRecords: Array<NativeIntersectionObserverEntry> = [];
let callback: ?() => void;
let observations: Array<Observation> = [];
const FabricUIManagerMock = nullthrows(getFabricUIManager());
function createRecordFromObservation(
observation: Observation,
): NativeIntersectionObserverEntry {
return {
intersectionObserverId: observation.intersectionObserverId,
targetInstanceHandle: FabricUIManagerMock.__getInstanceHandleFromNode(
// $FlowExpectedError[incompatible-call]
observation.targetShadowNode,
),
targetRect: observation.state.intersecting ? [0, 0, 1, 1] : [20, 20, 1, 1],
rootRect: [0, 0, 10, 10],
intersectionRect: observation.state.intersecting ? [0, 0, 1, 1] : null,
isIntersectingAboveThresholds: observation.state.intersecting,
time: performance.now(),
};
}
function notifyIntersectionObservers(): void {
callback?.();
}
const NativeIntersectionObserverMock = {
observe: (options: NativeIntersectionObserverObserveOptions): void => {
invariant(
observations.find(
observation =>
observation.intersectionObserverId ===
options.intersectionObserverId &&
observation.targetShadowNode === options.targetShadowNode,
) == null,
'unexpected duplicate call to observe',
);
const observation = {
...options,
state: {
thresholds: options.thresholds,
rootThresholds: options.rootThresholds,
intersecting: false,
currentThreshold: null,
currentRootThreshold: null,
},
};
observations.push(observation);
pendingRecords.push(createRecordFromObservation(observation));
setImmediate(notifyIntersectionObservers);
},
unobserve: (
intersectionObserverId: number,
targetShadowNode: mixed,
): void => {
const observationIndex = observations.findIndex(
observation =>
observation.intersectionObserverId === intersectionObserverId &&
observation.targetShadowNode === targetShadowNode,
);
invariant(
observationIndex !== -1,
'unexpected duplicate call to unobserve',
);
observations.splice(observationIndex, 1);
pendingRecords = pendingRecords.filter(
record =>
record.intersectionObserverId !== intersectionObserverId ||
record.targetInstanceHandle !==
FabricUIManagerMock.__getInstanceHandleFromNode(
// $FlowExpectedError[incompatible-call]
targetShadowNode,
),
);
},
connect: (notifyIntersectionObserversCallback: () => void): void => {
invariant(callback == null, 'unexpected call to connect');
invariant(
notifyIntersectionObserversCallback != null,
'unexpected null notify intersection observers callback',
);
callback = notifyIntersectionObserversCallback;
},
disconnect: (): void => {
invariant(callback != null, 'unexpected call to disconnect');
callback = null;
},
takeRecords: (): $ReadOnlyArray<NativeIntersectionObserverEntry> => {
const currentRecords = pendingRecords;
pendingRecords = [];
return currentRecords;
},
__forceTransitionForTests: (
observer: IntersectionObserver,
target: ReactNativeElement,
) => {
const targetShadowNode = getShadowNode(target);
const observation = observations.find(
obs =>
obs.intersectionObserverId === observer.__getObserverID() &&
obs.targetShadowNode === targetShadowNode,
);
invariant(
observation != null,
'cannot force transition on an unobserved target',
);
if (observation.state.intersecting) {
observation.state.intersecting = false;
observation.state.currentThreshold = null;
observation.state.currentRootThreshold = null;
} else {
observation.state.intersecting = true;
observation.state.currentThreshold = observation.thresholds[0];
observation.state.currentRootThreshold =
observation.rootThresholds != null
? observation.rootThresholds[0]
: null;
}
pendingRecords.push(createRecordFromObservation(observation));
setImmediate(notifyIntersectionObservers);
},
__getObservationsForTests: (
observer: IntersectionObserver,
): Array<{targetShadowNode: mixed, thresholds: $ReadOnlyArray<number>}> => {
const intersectionObserverId = observer.__getObserverID();
return observations
.filter(
observation =>
observation.intersectionObserverId === intersectionObserverId,
)
.map(observation => ({
targetShadowNode: observation.targetShadowNode,
thresholds: observation.thresholds,
}));
},
__isConnected: (): boolean => {
return callback != null;
},
};
(NativeIntersectionObserverMock: Spec);
export default NativeIntersectionObserverMock;
@@ -1,327 +0,0 @@
/**
* 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
*/
/**
* This is a mock of `NativeMutationObserver` implementing the same logic as the
* native module and integrating with the existing mock for `FabricUIManager`.
* This allows us to test all the JavaScript code for IntersectionObserver in
* JavaScript as an integration test using only public APIs.
*/
import type {NodeSet} from '../../../../../../Libraries/ReactNative/FabricUIManager';
import type {RootTag} from '../../../../../../Libraries/ReactNative/RootTag';
import type {
InternalInstanceHandle,
Node,
} from '../../../../../../Libraries/Renderer/shims/ReactNativeTypes';
import type {
MutationObserverId,
NativeMutationObserverObserveOptions,
NativeMutationRecord,
Spec,
} from '../NativeMutationObserver';
import {
type NodeMock,
type UIManagerCommitHook,
fromNode,
getFabricUIManager,
getNodeInChildSet,
} from '../../../../../../Libraries/ReactNative/__mocks__/FabricUIManager';
import ReadOnlyNode from '../../../dom/nodes/ReadOnlyNode';
import invariant from 'invariant';
import nullthrows from 'nullthrows';
let pendingRecords: Array<NativeMutationRecord> = [];
let callback: ?() => void;
let getPublicInstance: ?(instanceHandle: InternalInstanceHandle) => mixed;
let observersByRootTag: Map<
RootTag,
Map<MutationObserverId, {deep: Set<Node>, shallow: Set<Node>}>,
> = new Map();
const FabricUIManagerMock = nullthrows(getFabricUIManager());
function getMockDataFromShadowNode(node: mixed): NodeMock {
// $FlowExpectedError[incompatible-call]
return fromNode(node);
}
function castToNode(node: mixed): Node {
// $FlowExpectedError[incompatible-return]
return node;
}
const NativeMutationMock = {
observe: (options: NativeMutationObserverObserveOptions): void => {
const targetShadowNode = castToNode(options.targetShadowNode);
const rootTag = getMockDataFromShadowNode(options.targetShadowNode).rootTag;
let observers = observersByRootTag.get(rootTag);
if (observers == null) {
observers = new Map();
observersByRootTag.set(rootTag, observers);
}
let observations = observers.get(options.mutationObserverId);
if (observations == null) {
observations = {deep: new Set(), shallow: new Set()};
observers.set(options.mutationObserverId, observations);
}
const isTargetBeingObserved =
observations.deep.has(targetShadowNode) ||
observations.shallow.has(targetShadowNode);
invariant(!isTargetBeingObserved, 'unexpected duplicate call to observe');
if (options.subtree) {
observations.deep.add(targetShadowNode);
} else {
observations.shallow.add(targetShadowNode);
}
},
unobserve: (mutationObserverId: number, target: mixed): void => {
const targetShadowNode = castToNode(target);
const observers = observersByRootTag.get(
getMockDataFromShadowNode(targetShadowNode).rootTag,
);
const observations = observers?.get(mutationObserverId);
invariant(observations != null, 'unexpected call to unobserve');
const isTargetBeingObserved =
observations.deep.has(targetShadowNode) ||
observations.shallow.has(targetShadowNode);
invariant(isTargetBeingObserved, 'unexpected call to unobserve');
observations.deep.delete(targetShadowNode);
observations.shallow.delete(targetShadowNode);
},
connect: (
notifyMutationObserversCallback: () => void,
getPublicInstanceFromInstanceHandle: (
instanceHandle: InternalInstanceHandle,
) => mixed,
): void => {
invariant(callback == null, 'unexpected call to connect');
callback = notifyMutationObserversCallback;
getPublicInstance = getPublicInstanceFromInstanceHandle;
FabricUIManagerMock.__addCommitHook(NativeMutationObserverCommitHook);
},
disconnect: (): void => {
invariant(callback != null, 'unexpected call to disconnect');
callback = null;
FabricUIManagerMock.__removeCommitHook(NativeMutationObserverCommitHook);
},
takeRecords: (): $ReadOnlyArray<NativeMutationRecord> => {
const currentRecords = pendingRecords;
pendingRecords = [];
return currentRecords;
},
};
(NativeMutationMock: Spec);
export default NativeMutationMock;
const NativeMutationObserverCommitHook: UIManagerCommitHook = {
shadowTreeWillCommit: (rootTag, oldChildSet, newChildSet) => {
runMutationObservations(rootTag, oldChildSet, newChildSet);
},
};
function runMutationObservations(
rootTag: RootTag,
oldChildSet: ?NodeSet,
newChildSet: NodeSet,
): void {
const observers = observersByRootTag.get(rootTag);
if (!observers) {
return;
}
const newRecords: Array<NativeMutationRecord> = [];
for (const [mutationObserverId, observations] of observers) {
const processedNodes: Set<Node> = new Set();
for (const targetShadowNode of observations.deep) {
runMutationObservation({
mutationObserverId,
targetShadowNode,
subtree: true,
oldChildSet,
newChildSet,
newRecords,
processedNodes,
});
}
for (const targetShadowNode of observations.shallow) {
runMutationObservation({
mutationObserverId,
targetShadowNode,
subtree: false,
oldChildSet,
newChildSet,
newRecords,
processedNodes,
});
}
}
for (const record of newRecords) {
pendingRecords.push(record);
}
notifyObserversIfNecessary();
}
function findNodeOfSameFamily(list: NodeSet, node: Node): ?Node {
for (const current of list) {
if (fromNode(current).reactTag === fromNode(node).reactTag) {
return current;
}
}
return;
}
function recordMutations({
mutationObserverId,
targetShadowNode,
subtree,
oldNode,
newNode,
newRecords,
processedNodes,
}: {
mutationObserverId: MutationObserverId,
targetShadowNode: Node,
subtree: boolean,
oldNode: Node,
newNode: Node,
newRecords: Array<NativeMutationRecord>,
processedNodes: Set<Node>,
}): void {
// If the nodes are referentially equal, their children are also the same.
if (oldNode === newNode || processedNodes.has(newNode)) {
return;
}
processedNodes.add(newNode);
const oldChildren = fromNode(oldNode).children;
const newChildren = fromNode(newNode).children;
const addedNodes = [];
const removedNodes = [];
// Check for removed nodes (and equal nodes for further inspection later)
for (const oldChild of oldChildren) {
const newChild = findNodeOfSameFamily(newChildren, oldChild);
if (newChild == null) {
removedNodes.push(oldChild);
} else if (subtree) {
recordMutations({
mutationObserverId,
targetShadowNode,
subtree,
oldNode: oldChild,
newNode: newChild,
newRecords,
processedNodes,
});
}
}
// Check for added nodes
for (const newChild of newChildren) {
const oldChild = findNodeOfSameFamily(oldChildren, newChild);
if (oldChild == null) {
addedNodes.push(newChild);
}
}
if (addedNodes.length > 0 || removedNodes.length > 0) {
newRecords.push({
mutationObserverId: mutationObserverId,
target: nullthrows(getPublicInstance)(
getMockDataFromShadowNode(targetShadowNode).instanceHandle,
),
addedNodes: addedNodes.map(node => {
const readOnlyNode = nullthrows(getPublicInstance)(
fromNode(node).instanceHandle,
);
invariant(
readOnlyNode instanceof ReadOnlyNode,
'expected instance of ReadOnlyNode',
);
return readOnlyNode;
}),
removedNodes: removedNodes.map(node => {
const readOnlyNode = nullthrows(getPublicInstance)(
fromNode(node).instanceHandle,
);
invariant(
readOnlyNode instanceof ReadOnlyNode,
'expected instance of ReadOnlyNode',
);
return readOnlyNode;
}),
});
}
}
function runMutationObservation({
mutationObserverId,
targetShadowNode,
subtree,
oldChildSet,
newChildSet,
newRecords,
processedNodes,
}: {
mutationObserverId: MutationObserverId,
targetShadowNode: Node,
subtree: boolean,
oldChildSet: ?NodeSet,
newChildSet: NodeSet,
newRecords: Array<NativeMutationRecord>,
processedNodes: Set<Node>,
}): void {
if (!oldChildSet) {
return;
}
const oldTargetShadowNode = getNodeInChildSet(targetShadowNode, oldChildSet);
if (oldTargetShadowNode == null) {
return;
}
const newTargetShadowNode = getNodeInChildSet(targetShadowNode, newChildSet);
if (newTargetShadowNode == null) {
return;
}
recordMutations({
mutationObserverId,
targetShadowNode,
subtree,
oldNode: oldTargetShadowNode,
newNode: newTargetShadowNode,
newRecords,
processedNodes,
});
}
function notifyObserversIfNecessary(): void {
if (pendingRecords.length > 0) {
// We schedule these using regular tasks in native because microtasks are
// still not properly supported.
setTimeout(() => callback?.(), 0);
}
}
+1 -1
View File
@@ -1051,7 +1051,7 @@
core-js-compat "^3.37.1"
semver "^6.3.1"
"@babel/preset-flow@^7.13.13", "@babel/preset-flow@^7.20.0", "@babel/preset-flow@^7.24.7":
"@babel/preset-flow@^7.13.13", "@babel/preset-flow@^7.24.7":
version "7.24.7"
resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.24.7.tgz#eef5cb8e05e97a448fc50c16826f5612fe512c06"
integrity sha512-NL3Lo0NorCU607zU3NwRyJbpaB6E3t0xtd3LfAQKDfkeX4/ggcDXvkmkW42QWT5owUeW/jAe4hn+2qvkV1IbfQ==