diff --git a/jest.config.js b/jest.config.js index 5a36fd90a5e975..3fb2cd09000f0a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -37,7 +37,6 @@ module.exports = { '/node_modules/', '/packages/react-native/sdks', '/packages/react-native/Libraries/Renderer', - '/packages/react-native-test-renderer/src', '/packages/react-native/sdks/hermes/', ...PODS_LOCATIONS, ], diff --git a/packages/react-native-test-renderer/babel.config.js b/packages/react-native-test-renderer/babel.config.js deleted file mode 100644 index 7cd251d7825948..00000000000000 --- a/packages/react-native-test-renderer/babel.config.js +++ /dev/null @@ -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'], -}; diff --git a/packages/react-native-test-renderer/jest.config.js b/packages/react-native-test-renderer/jest.config.js deleted file mode 100644 index d1c17f8cc89caf..00000000000000 --- a/packages/react-native-test-renderer/jest.config.js +++ /dev/null @@ -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', -}; diff --git a/packages/react-native-test-renderer/package.json b/packages/react-native-test-renderer/package.json deleted file mode 100644 index 5610c999a836e1..00000000000000 --- a/packages/react-native-test-renderer/package.json +++ /dev/null @@ -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" - } -} diff --git a/packages/react-native-test-renderer/src/index.js b/packages/react-native-test-renderer/src/index.js deleted file mode 100644 index 4317334b56253c..00000000000000 --- a/packages/react-native-test-renderer/src/index.js +++ /dev/null @@ -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'; diff --git a/packages/react-native-test-renderer/src/jest/environment.js b/packages/react-native-test-renderer/src/jest/environment.js deleted file mode 100644 index 9f70431cb74e08..00000000000000 --- a/packages/react-native-test-renderer/src/jest/environment.js +++ /dev/null @@ -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]; - } -}; diff --git a/packages/react-native-test-renderer/src/jest/setup-files-after-env.js b/packages/react-native-test-renderer/src/jest/setup-files-after-env.js deleted file mode 100644 index 6dc49eedab97cb..00000000000000 --- a/packages/react-native-test-renderer/src/jest/setup-files-after-env.js +++ /dev/null @@ -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, - }), - }, - })); diff --git a/packages/react-native-test-renderer/src/renderer/__tests__/__snapshots__/render-test.js.snap b/packages/react-native-test-renderer/src/renderer/__tests__/__snapshots__/render-test.js.snap deleted file mode 100644 index edd8a0c422dfa7..00000000000000 --- a/packages/react-native-test-renderer/src/renderer/__tests__/__snapshots__/render-test.js.snap +++ /dev/null @@ -1,39 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`render toJSON renders View props 1`] = ` - - - Hello - - - -`; - -exports[`render toJSON returns expected JSON output based on renderer component 1`] = ` - - - Hello - - - -`; diff --git a/packages/react-native-test-renderer/src/renderer/__tests__/render-test.js b/packages/react-native-test-renderer/src/renderer/__tests__/render-test.js deleted file mode 100644 index f1776daee9c82b..00000000000000 --- a/packages/react-native-test-renderer/src/renderer/__tests__/render-test.js +++ /dev/null @@ -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 ( - - Hello - - - ); -} - -function TestComponentWithProps() { - return ( - - Hello - - - ); -} - -describe('render', () => { - describe('toJSON', () => { - it('returns expected JSON output based on renderer component', () => { - const result = ReactNativeTestRenderer.render(); - expect(result.toJSON()).toMatchSnapshot(); - }); - - it('renders View props', () => { - const result = ReactNativeTestRenderer.render(); - expect(result.toJSON()).toMatchSnapshot(); - }); - }); - - describe('findAll', () => { - it('returns all nodes matching the predicate', () => { - const result = ReactNativeTestRenderer.render(); - 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); - }); - }); -}); diff --git a/packages/react-native-test-renderer/src/renderer/index.js b/packages/react-native-test-renderer/src/renderer/index.js deleted file mode 100644 index a99ff9f45bba33..00000000000000 --- a/packages/react-native-test-renderer/src/renderer/index.js +++ /dev/null @@ -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, - ... - }, - ... -}; - -type ReactNode = { - children: ?Array, - props: {text?: string | null, ...}, - viewName: string, - instanceHandle: FiberPartial, -}; - -type RenderedNodeJSON = { - type: string, - props: {[propName: string]: any, ...}, - children: null | Array, - $$typeof?: symbol, // Optional because we add it with defineProperty(). -}; -type RenderedJSON = RenderedNodeJSON | string; - -type RenderResult = { - toJSON: () => Array | RenderedJSON | null, - findAll: (predicate: (ReactNode) => boolean) => Array, -}; - -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 { - 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; -} diff --git a/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/ReactFabricPublicInstance-test.js b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/ReactFabricPublicInstance-test.js deleted file mode 100644 index 8041ccccb6e92b..00000000000000 --- a/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/ReactFabricPublicInstance-test.js +++ /dev/null @@ -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, TReturn>( - fn: (...args: TArguments) => TReturn, -): JestMockFn { - 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>, -): Promise>> { - const mockContainerTag = 11; - const MockView = ReactNativeViewConfigRegistry.register( - 'RCTMockView', - () => ({ - validAttributes: {foo: true, style: {}}, - uiViewClassName: 'RCTMockView', - }), - ); - - const result: Array> = []; - for (let i = 0; i < keyLists.length; i++) { - const keyList = keyLists[i]; - if (Array.isArray(keyList)) { - const refs: Array = keyList.map(key => undefined); - await act(() => { - ReactFabric.render( - - {keyList.map((key, index) => ( - { - refs[index] = ((ref: $FlowFixMe): ?HostInstance); - }} - /> - ))} - , - 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(); - }, - ); - }); - }); -}); diff --git a/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js b/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js deleted file mode 100644 index 1ab03a083c5c15..00000000000000 --- a/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js +++ /dev/null @@ -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 = new Map(); -const allocatedTags: Set = 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 = 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): 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; -} diff --git a/packages/react-native/src/private/webapis/dom/nodes/specs/__mocks__/NativeDOMMock.js b/packages/react-native/src/private/webapis/dom/nodes/specs/__mocks__/NativeDOMMock.js deleted file mode 100644 index ab8eddc021f1f1..00000000000000 --- a/packages/react-native/src/private/webapis/dom/nodes/specs/__mocks__/NativeDOMMock.js +++ /dev/null @@ -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 { - 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 => { - 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; diff --git a/packages/react-native/src/private/webapis/intersectionobserver/specs/__mocks__/NativeIntersectionObserver.js b/packages/react-native/src/private/webapis/intersectionobserver/specs/__mocks__/NativeIntersectionObserver.js deleted file mode 100644 index 1c5e6abdf1fae5..00000000000000 --- a/packages/react-native/src/private/webapis/intersectionobserver/specs/__mocks__/NativeIntersectionObserver.js +++ /dev/null @@ -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, - rootThresholds?: ?$ReadOnlyArray, - intersecting: boolean, - currentThreshold: ?number, - currentRootThreshold: ?number, -}; - -type Observation = { - ...NativeIntersectionObserverObserveOptions, - state: ObserverState, -}; - -let pendingRecords: Array = []; -let callback: ?() => void; -let observations: Array = []; - -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 => { - 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}> => { - 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; diff --git a/packages/react-native/src/private/webapis/mutationobserver/specs/__mocks__/NativeMutationObserver.js b/packages/react-native/src/private/webapis/mutationobserver/specs/__mocks__/NativeMutationObserver.js deleted file mode 100644 index 0c6a93ae6c1221..00000000000000 --- a/packages/react-native/src/private/webapis/mutationobserver/specs/__mocks__/NativeMutationObserver.js +++ /dev/null @@ -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 = []; -let callback: ?() => void; -let getPublicInstance: ?(instanceHandle: InternalInstanceHandle) => mixed; -let observersByRootTag: Map< - RootTag, - Map, shallow: Set}>, -> = 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 => { - 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 = []; - - for (const [mutationObserverId, observations] of observers) { - const processedNodes: Set = 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, - processedNodes: Set, -}): 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, - processedNodes: Set, -}): 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); - } -} diff --git a/yarn.lock b/yarn.lock index 483036ad85d022..ec697d9e3920c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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==