mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
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:
committed by
Facebook GitHub Bot
parent
351b1bae95
commit
d05214665c
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
-39
@@ -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;
|
||||
}
|
||||
-307
@@ -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;
|
||||
}
|
||||
-413
@@ -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;
|
||||
-181
@@ -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;
|
||||
packages/react-native/src/private/webapis/mutationobserver/specs/__mocks__/NativeMutationObserver.js
Vendored
-327
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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==
|
||||
|
||||
Reference in New Issue
Block a user