From ff0bcb24276dff178b8b581bf29f91b0a442bced Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Wed, 22 Jan 2025 06:55:18 -0800 Subject: [PATCH] introduce Fantom.dispatchNativeEvent (#48793) Summary: changelog: [internal] Pull Request resolved: https://github.com/facebook/react-native/pull/48793 Adds new method `dispatchNativeEvent` to Fantom give option to dispatch fake native events. The API is expanded in subsequent diffs. The version introduced in this diff supports only dispatching event without payload and without any options. Reviewed By: rubennorte Differential Revision: D68331986 fbshipit-source-id: 075360e1d6874794ba6df966087fbe6a0a820cbc --- .../src/__tests__/Fantom-itest.js | 45 ++++++++++++++++++- packages/react-native-fantom/src/index.js | 17 ++++++- .../__snapshots__/public-api-test.js.snap | 2 + .../react/nativemodule/dom/NativeDOM.cpp | 1 + .../src/private/specs/modules/NativeFantom.js | 5 +++ 5 files changed, 67 insertions(+), 3 deletions(-) diff --git a/packages/react-native-fantom/src/__tests__/Fantom-itest.js b/packages/react-native-fantom/src/__tests__/Fantom-itest.js index 773f92665b9..ef56c4b86d8 100644 --- a/packages/react-native-fantom/src/__tests__/Fantom-itest.js +++ b/packages/react-native-fantom/src/__tests__/Fantom-itest.js @@ -14,9 +14,15 @@ import 'react-native/Libraries/Core/InitializeCore'; import type {Root} from '..'; -import {createRoot, runTask} from '..'; +import { + createRoot, + dispatchNativeEvent, + runOnUIThread, + runTask, + runWorkLoop, +} from '..'; import * as React from 'react'; -import {Text, View} from 'react-native'; +import {Text, TextInput, View} from 'react-native'; import ensureInstance from 'react-native/src/private/utilities/ensureInstance'; import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement'; @@ -369,4 +375,39 @@ describe('Fantom', () => { }); }); }); + + describe('runOnUIThread + dispatchNativeEvent', () => { + it('sends focus event', () => { + const root = createRoot(); + let maybeNode; + + let focusEvent = jest.fn(); + + runTask(() => { + root.render( + { + maybeNode = node; + }} + />, + ); + }); + + const element = ensureInstance(maybeNode, ReactNativeElement); + + expect(focusEvent).toHaveBeenCalledTimes(0); + + runOnUIThread(() => { + dispatchNativeEvent(element, 'focus'); + }); + + // The tasks have not run. + expect(focusEvent).toHaveBeenCalledTimes(0); + + runWorkLoop(); + + expect(focusEvent).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/packages/react-native-fantom/src/index.js b/packages/react-native-fantom/src/index.js index f27ac7b6729..ec870571ad0 100644 --- a/packages/react-native-fantom/src/index.js +++ b/packages/react-native-fantom/src/index.js @@ -14,6 +14,8 @@ import type { } from './getFantomRenderedOutput'; import type {MixedElement} from 'react'; +import ReactNativeElement from '../../react-native/src/private/webapis/dom/nodes/ReadOnlyNode'; +import {getShadowNode} from '../../react-native/src/private/webapis/dom/nodes/ReadOnlyNode'; import * as Benchmark from './Benchmark'; import getFantomRenderedOutput from './getFantomRenderedOutput'; import ReactFabric from 'react-native/Libraries/Renderer/shims/ReactFabric'; @@ -100,7 +102,7 @@ export function scheduleTask(task: () => void | Promise) { let flushingQueue = false; /* - * Runs a task on on the event loop. To be used together with root.render. + * Runs a task 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. */ @@ -115,6 +117,14 @@ export function runTask(task: () => void | Promise) { runWorkLoop(); } +/* + * Simmulates running a task on the UI thread and forces side effect to drain the event queue, dispatching events to JavaScript. + */ +export function runOnUIThread(task: () => void) { + task(); + NativeFantom.flushEventQueue(); +} + /** * Runs the event loop until all tasks are executed. */ @@ -139,6 +149,11 @@ export function createRoot(rootConfig?: RootConfig): Root { return new Root(rootConfig); } +export function dispatchNativeEvent(node: ReactNativeElement, type: string) { + const shadowNode = getShadowNode(node); + NativeFantom.dispatchNativeEvent(shadowNode, type); +} + export const unstable_benchmark = Benchmark; type FantomConstants = $ReadOnly<{ diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index 9c92af98d25..83e3bc479c3 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -10916,8 +10916,10 @@ interface Spec extends TurboModule { devicePixelRatio: number ) => void; stopSurface: (surfaceId: number) => void; + dispatchNativeEvent: (shadowNode: mixed, type: string) => void; getMountingManagerLogs: (surfaceId: number) => Array; flushMessageQueue: () => void; + flushEventQueue: () => void; getRenderedOutput: (surfaceId: number, config: RenderFormatOptions) => string; reportTestSuiteResultsJSON: (results: string) => void; } diff --git a/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.cpp b/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.cpp index 490a692808f..58a5a35cfe7 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #ifdef RN_DISABLE_OSS_PLUGIN_HEADER #include "Plugins.h" diff --git a/packages/react-native/src/private/specs/modules/NativeFantom.js b/packages/react-native/src/private/specs/modules/NativeFantom.js index 1fa0c0d2fb7..64303435e3c 100644 --- a/packages/react-native/src/private/specs/modules/NativeFantom.js +++ b/packages/react-native/src/private/specs/modules/NativeFantom.js @@ -26,8 +26,13 @@ interface Spec extends TurboModule { devicePixelRatio: number, ) => void; stopSurface: (surfaceId: number) => void; + dispatchNativeEvent: ( + shadowNode: mixed /* ShadowNode */, + type: string, + ) => void; getMountingManagerLogs: (surfaceId: number) => Array; flushMessageQueue: () => void; + flushEventQueue: () => void; getRenderedOutput: (surfaceId: number, config: RenderFormatOptions) => string; reportTestSuiteResultsJSON: (results: string) => void; }