From 4477587f12f6c4f61706ed9caf04c84f19470345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Mon, 2 Dec 2024 03:01:20 -0800 Subject: [PATCH 1/2] Remove unnecessary fb extension from ReactNativeTester Differential Revision: D66599071 --- .../private/__tests__/ReactNativeTester.js | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 packages/react-native/src/private/__tests__/ReactNativeTester.js diff --git a/packages/react-native/src/private/__tests__/ReactNativeTester.js b/packages/react-native/src/private/__tests__/ReactNativeTester.js new file mode 100644 index 000000000000..70e480acec8f --- /dev/null +++ b/packages/react-native/src/private/__tests__/ReactNativeTester.js @@ -0,0 +1,71 @@ +/** + * 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 {MixedElement} from 'react'; + +import ReactFabric from '../../../Libraries/Renderer/shims/ReactFabric'; + +let globalSurfaceIdCounter = 1; + +const nativeRuntimeScheduler = global.nativeRuntimeScheduler; +const schedulerPriorityImmediate = + nativeRuntimeScheduler.unstable_ImmediatePriority; + +class Root { + #surfaceId: number; + #hasRendered: boolean = false; + + constructor() { + this.#surfaceId = globalSurfaceIdCounter; + globalSurfaceIdCounter += 10; + } + + render(element: MixedElement) { + if (!this.#hasRendered) { + global.$$JSTesterModuleName$$.startSurface(this.#surfaceId); + this.#hasRendered = true; + } + + ReactFabric.render(element, this.#surfaceId, () => {}, true); + } + + getMountingLogs(): Array { + return global.$$JSTesterModuleName$$.getMountingManagerLogs( + this.#surfaceId, + ); + } + + destroy() { + // TODO: check for leaks. + global.$$JSTesterModuleName$$.stopSurface(this.#surfaceId); + global.$$JSTesterModuleName$$.flushMessageQueue(); + } + + // TODO: add an API to check if all surfaces were deallocated when tests are finished. +} + +/* + * Runs a task on on the event loop. To be used together with root.render. + * + * React must run inside of event loop to ensure scheduling environment is closer to production. + */ +export function runTask(task: () => void) { + nativeRuntimeScheduler.unstable_scheduleCallback( + schedulerPriorityImmediate, + task, + ); + global.$$JSTesterModuleName$$.flushMessageQueue(); +} + +// TODO: Add option to define surface props and pass it to startSurface +// Surfacep rops: concurrentRoot, surfaceWidth, surfaceHeight, layoutDirection, pointScaleFactor. +export function createRoot(): Root { + return new Root(); +} From 8d0a13afb55eb60266ad7489316f7bd5ed420100 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Mon, 2 Dec 2024 03:07:36 -0800 Subject: [PATCH 2/2] Migrate ReactFabricPublicInstance tests to Fantom (#48025) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/48025 Changelog: [internal] This migrates the existing tests we have for the current public API for host component refs to Fantom. After this, the only remaining test to migrate before we can clean up the legacy mocks for Fabric, etc. is the one for IntersectionObserer. Differential Revision: D66599070 --- .../ReactFabricPublicInstance-Legacy-itest.js | 14 + .../ReactFabricPublicInstance-Modern-itest.js | 15 + .../__tests__/setUpFeatureFlags.js | 16 + ...tUpReactFabricPublicInstanceFantomTests.js | 365 ++++++++++++++++++ 4 files changed, 410 insertions(+) create mode 100644 packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/ReactFabricPublicInstance-Legacy-itest.js create mode 100644 packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/ReactFabricPublicInstance-Modern-itest.js create mode 100644 packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/setUpFeatureFlags.js create mode 100644 packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/setUpReactFabricPublicInstanceFantomTests.js diff --git a/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/ReactFabricPublicInstance-Legacy-itest.js b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/ReactFabricPublicInstance-Legacy-itest.js new file mode 100644 index 000000000000..6c1477aa7b5c --- /dev/null +++ b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/ReactFabricPublicInstance-Legacy-itest.js @@ -0,0 +1,14 @@ +/** + * 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 setUpReactFabricPublicInstanceFantomTests from './setUpReactFabricPublicInstanceFantomTests'; + +setUpReactFabricPublicInstanceFantomTests({isModern: false}); diff --git a/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/ReactFabricPublicInstance-Modern-itest.js b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/ReactFabricPublicInstance-Modern-itest.js new file mode 100644 index 000000000000..9f66d43eacc6 --- /dev/null +++ b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/ReactFabricPublicInstance-Modern-itest.js @@ -0,0 +1,15 @@ +/** + * 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 './setUpFeatureFlags'; +import setUpReactFabricPublicInstanceFantomTests from './setUpReactFabricPublicInstanceFantomTests'; + +setUpReactFabricPublicInstanceFantomTests({isModern: true}); diff --git a/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/setUpFeatureFlags.js b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/setUpFeatureFlags.js new file mode 100644 index 000000000000..a5a75c44ac4b --- /dev/null +++ b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/setUpFeatureFlags.js @@ -0,0 +1,16 @@ +/** + * 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 * as ReactNativeFeatureFlags from '../../../../src/private/featureflags/ReactNativeFeatureFlags'; + +ReactNativeFeatureFlags.override({ + enableAccessToHostTreeInFabric: () => true, +}); diff --git a/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/setUpReactFabricPublicInstanceFantomTests.js b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/setUpReactFabricPublicInstanceFantomTests.js new file mode 100644 index 000000000000..c62b508a2a8b --- /dev/null +++ b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/setUpReactFabricPublicInstanceFantomTests.js @@ -0,0 +1,365 @@ +/** + * 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 '../../../Core/InitializeCore.js'; + +import * as ReactNativeTester from '../../../../src/private/__tests__/ReactNativeTester'; +import ReactNativeElement from '../../../../src/private/webapis/dom/nodes/ReactNativeElement'; +import TextInputState from '../../../Components/TextInput/TextInputState'; +import View from '../../../Components/View/View'; +import ReactFabricHostComponent from '../ReactFabricHostComponent'; +import nullthrows from 'nullthrows'; +import * as React from 'react'; + +export default function setUpTests({isModern}: {isModern: boolean}) { + // TODO: move these tests to the test file for `ReactNativeElement` when the legacy implementation is removed. + describe(`ReactFabricPublicInstance (${isModern ? 'modern' : 'legacy'})`, () => { + it('should provide instances of the right class as refs in host components', () => { + let node; + + const root = ReactNativeTester.createRoot(); + ReactNativeTester.runTask(() => { + root.render( + { + node = receivedNode; + }} + />, + ); + }); + + expect(node).toBeInstanceOf( + isModern ? ReactNativeElement : ReactFabricHostComponent, + ); + }); + + describe('blur', () => { + test('blur() invokes TextInputState', () => { + const root = ReactNativeTester.createRoot(); + + let maybeNode; + + ReactNativeTester.runTask(() => { + root.render( + { + maybeNode = node; + }} + />, + ); + }); + + const node = nullthrows(maybeNode); + + const blurTextInput = jest.fn(); + + // We don't support view commands in Fantom yet, so we have to mock this. + TextInputState.blurTextInput = blurTextInput; + + ReactNativeTester.runTask(() => { + node.blur(); + }); + + expect(blurTextInput).toHaveBeenCalledTimes(1); + expect(blurTextInput.mock.calls).toEqual([[node]]); + }); + }); + + describe('focus', () => { + test('focus() invokes TextInputState', () => { + const root = ReactNativeTester.createRoot(); + + let maybeNode; + + ReactNativeTester.runTask(() => { + root.render( + { + maybeNode = node; + }} + />, + ); + }); + + const node = nullthrows(maybeNode); + + const focusTextInput = jest.fn(); + + // We don't support view commands in Fantom yet, so we have to mock this. + TextInputState.focusTextInput = focusTextInput; + + ReactNativeTester.runTask(() => { + node.focus(); + }); + + expect(focusTextInput).toHaveBeenCalledTimes(1); + expect(focusTextInput.mock.calls).toEqual([[node]]); + }); + }); + + describe('measure', () => { + it('component.measure(...) invokes callback', () => { + const root = ReactNativeTester.createRoot(); + + let maybeNode; + + ReactNativeTester.runTask(() => { + root.render( + { + maybeNode = node; + }} + />, + ); + }); + + const node = nullthrows(maybeNode); + + const callback = jest.fn(); + node.measure(callback); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback.mock.calls).toEqual([[10, 10, 100, 100, 10, 10]]); + }); + + it('unmounted.measure(...) does nothing', () => { + const root = ReactNativeTester.createRoot(); + + let maybeNode; + + ReactNativeTester.runTask(() => { + root.render( + { + maybeNode = node; + }} + />, + ); + }); + + const node = nullthrows(maybeNode); + + ReactNativeTester.runTask(() => { + root.render(<>); + }); + + const callback = jest.fn(); + node.measure(callback); + + expect(callback).not.toHaveBeenCalled(); + }); + }); + + describe('measureInWindow', () => { + it('component.measureInWindow(...) invokes callback', () => { + const root = ReactNativeTester.createRoot(); + + let maybeNode; + + ReactNativeTester.runTask(() => { + root.render( + { + maybeNode = node; + }} + />, + ); + }); + + const node = nullthrows(maybeNode); + + const callback = jest.fn(); + node.measureInWindow(callback); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback.mock.calls).toEqual([[10, 10, 100, 100]]); + }); + + it('unmounted.measureInWindow(...) does nothing', () => { + const root = ReactNativeTester.createRoot(); + + let maybeNode; + + ReactNativeTester.runTask(() => { + root.render( + { + maybeNode = node; + }} + />, + ); + }); + + const node = nullthrows(maybeNode); + + ReactNativeTester.runTask(() => { + root.render(<>); + }); + + const callback = jest.fn(); + node.measureInWindow(callback); + + expect(callback).not.toHaveBeenCalled(); + }); + }); + + describe('measureLayout', () => { + it('component.measureLayout(component, ...) invokes callback', () => { + const root = ReactNativeTester.createRoot(); + + let maybeParentNode; + let maybeChildNode; + + ReactNativeTester.runTask(() => { + root.render( + { + maybeParentNode = node; + }}> + { + maybeChildNode = node; + }} + /> + , + ); + }); + + const parentNode = nullthrows(maybeParentNode); + const childNode = nullthrows(maybeChildNode); + + const callback = jest.fn(); + childNode.measureLayout(parentNode, callback); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback.mock.calls).toEqual([[20, 20, 10, 10]]); + }); + + it('unmounted.measureLayout(component, ...) does nothing', () => { + const root = ReactNativeTester.createRoot(); + + let maybeParentNode; + let maybeChildNode; + + ReactNativeTester.runTask(() => { + root.render( + { + maybeParentNode = node; + }}> + { + maybeChildNode = node; + }} + /> + , + ); + }); + + const parentNode = nullthrows(maybeParentNode); + const childNode = nullthrows(maybeChildNode); + + ReactNativeTester.runTask(() => { + root.render( + , + ); + }); + + const callback = jest.fn(); + childNode.measureLayout(parentNode, callback); + + expect(callback).not.toHaveBeenCalled(); + }); + + it('component.measureLayout(unmounted, ...) does nothing', () => { + const root = ReactNativeTester.createRoot(); + + let maybeParentNode; + let maybeChildNode; + + ReactNativeTester.runTask(() => { + root.render( + { + maybeParentNode = node; + }}> + { + maybeChildNode = node; + }} + /> + , + ); + }); + + const parentNode = nullthrows(maybeParentNode); + const childNode = nullthrows(maybeChildNode); + + ReactNativeTester.runTask(() => { + root.render( + , + ); + }); + + const callback = jest.fn(); + parentNode.measureLayout(childNode, callback); + + expect(callback).not.toHaveBeenCalled(); + }); + + it('unmounted.measureLayout(unmounted, ...) does nothing', () => { + const root = ReactNativeTester.createRoot(); + + let maybeParentNode; + let maybeChildNode; + + ReactNativeTester.runTask(() => { + root.render( + { + maybeParentNode = node; + }}> + { + maybeChildNode = node; + }} + /> + , + ); + }); + + const parentNode = nullthrows(maybeParentNode); + const childNode = nullthrows(maybeChildNode); + + ReactNativeTester.runTask(() => { + root.render(<>); + }); + + const callback = jest.fn(); + childNode.measureLayout(parentNode, callback); + + expect(callback).not.toHaveBeenCalled(); + }); + }); + }); +}