diff --git a/packages/react-native-fantom/src/__tests__/Fantom-itest.js b/packages/react-native-fantom/src/__tests__/Fantom-itest.js index 773f92665b9c..ef56c4b86d85 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 f27ac7b67295..ec870571ad01 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 758ba63be6bc..2c234d4c94a4 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 @@ -10917,8 +10917,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 490a692808f5..58a5a35cfe7a 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 1fa0c0d2fb7f..64303435e3ca 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; }