From 973409fc0bbc53fdc2de516ddefffbc9c6884270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Fri, 3 Jan 2025 01:54:08 -0800 Subject: [PATCH 1/4] Follow naming convention for NativeFantom module Differential Revision: D67549203 --- .../src/getFantomRenderedOutput.js | 4 ++-- packages/react-native-fantom/src/index.js | 12 ++++++------ .../specs/{NativeFantomModule.js => NativeFantom.js} | 4 +++- 3 files changed, 11 insertions(+), 9 deletions(-) rename packages/react-native-fantom/src/specs/{NativeFantomModule.js => NativeFantom.js} (90%) diff --git a/packages/react-native-fantom/src/getFantomRenderedOutput.js b/packages/react-native-fantom/src/getFantomRenderedOutput.js index 496a462a3d32..45500fde1742 100644 --- a/packages/react-native-fantom/src/getFantomRenderedOutput.js +++ b/packages/react-native-fantom/src/getFantomRenderedOutput.js @@ -9,7 +9,7 @@ * @oncall react_native */ -import FantomModule from './specs/NativeFantomModule'; +import NativeFantom from './specs/NativeFantom'; // $FlowExpectedError[untyped-import] import micromatch from 'micromatch'; import * as React from 'react'; @@ -114,7 +114,7 @@ export default function getFantomRenderedOutput( } = config; return new FantomRenderedOutput( JSON.parse( - FantomModule.getRenderedOutput(surfaceId, { + NativeFantom.getRenderedOutput(surfaceId, { includeRoot, includeLayoutMetrics, }), diff --git a/packages/react-native-fantom/src/index.js b/packages/react-native-fantom/src/index.js index b765810f66a0..7b06e4e9a655 100644 --- a/packages/react-native-fantom/src/index.js +++ b/packages/react-native-fantom/src/index.js @@ -15,7 +15,7 @@ import type { import type {MixedElement} from 'react'; import getFantomRenderedOutput from './getFantomRenderedOutput'; -import FantomModule from './specs/NativeFantomModule'; +import NativeFantom from './specs/NativeFantom'; import ReactFabric from 'react-native/Libraries/Renderer/shims/ReactFabric'; let globalSurfaceIdCounter = 1; @@ -35,7 +35,7 @@ class Root { render(element: MixedElement) { if (!this.#hasRendered) { - FantomModule.startSurface(this.#surfaceId); + NativeFantom.startSurface(this.#surfaceId); this.#hasRendered = true; } @@ -43,13 +43,13 @@ class Root { } getMountingLogs(): Array { - return FantomModule.getMountingManagerLogs(this.#surfaceId); + return NativeFantom.getMountingManagerLogs(this.#surfaceId); } destroy() { // TODO: check for leaks. - FantomModule.stopSurface(this.#surfaceId); - FantomModule.flushMessageQueue(); + NativeFantom.stopSurface(this.#surfaceId); + NativeFantom.flushMessageQueue(); } getRenderedOutput(config: RenderOutputConfig = {}): FantomRenderedOutput { @@ -100,7 +100,7 @@ export function runWorkLoop(): void { try { flushingQueue = true; - FantomModule.flushMessageQueue(); + NativeFantom.flushMessageQueue(); } finally { flushingQueue = false; } diff --git a/packages/react-native-fantom/src/specs/NativeFantomModule.js b/packages/react-native-fantom/src/specs/NativeFantom.js similarity index 90% rename from packages/react-native-fantom/src/specs/NativeFantomModule.js rename to packages/react-native-fantom/src/specs/NativeFantom.js index da67f38a1e75..a2d2db9fc31c 100644 --- a/packages/react-native-fantom/src/specs/NativeFantomModule.js +++ b/packages/react-native-fantom/src/specs/NativeFantom.js @@ -26,4 +26,6 @@ interface Spec extends TurboModule { getRenderedOutput: (surfaceId: number, config: RenderFormatOptions) => string; } -export default TurboModuleRegistry.getEnforcing('Fantom') as Spec; +export default TurboModuleRegistry.getEnforcing( + 'NativeFantomCxx', +) as Spec; From 381291938956cb0fe264bc1adf2bdbf374bc904c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Fri, 3 Jan 2025 01:54:08 -0800 Subject: [PATCH 2/4] Use codegen for Fantom native module Differential Revision: D67759729 --- packages/react-native-fantom/src/getFantomRenderedOutput.js | 2 +- packages/react-native-fantom/src/index.js | 2 +- .../src/private/specs/modules}/NativeFantom.js | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) rename packages/{react-native-fantom/src/specs => react-native/src/private/specs/modules}/NativeFantom.js (79%) diff --git a/packages/react-native-fantom/src/getFantomRenderedOutput.js b/packages/react-native-fantom/src/getFantomRenderedOutput.js index 45500fde1742..2e7f36cc9a3d 100644 --- a/packages/react-native-fantom/src/getFantomRenderedOutput.js +++ b/packages/react-native-fantom/src/getFantomRenderedOutput.js @@ -9,10 +9,10 @@ * @oncall react_native */ -import NativeFantom from './specs/NativeFantom'; // $FlowExpectedError[untyped-import] import micromatch from 'micromatch'; import * as React from 'react'; +import NativeFantom from 'react-native/src/private/specs/modules/NativeFantom'; export type RenderOutputConfig = { ...FantomRenderedOutputConfig, diff --git a/packages/react-native-fantom/src/index.js b/packages/react-native-fantom/src/index.js index 7b06e4e9a655..67795cc5c67f 100644 --- a/packages/react-native-fantom/src/index.js +++ b/packages/react-native-fantom/src/index.js @@ -15,8 +15,8 @@ import type { import type {MixedElement} from 'react'; import getFantomRenderedOutput from './getFantomRenderedOutput'; -import NativeFantom from './specs/NativeFantom'; import ReactFabric from 'react-native/Libraries/Renderer/shims/ReactFabric'; +import NativeFantom from 'react-native/src/private/specs/modules/NativeFantom'; let globalSurfaceIdCounter = 1; diff --git a/packages/react-native-fantom/src/specs/NativeFantom.js b/packages/react-native/src/private/specs/modules/NativeFantom.js similarity index 79% rename from packages/react-native-fantom/src/specs/NativeFantom.js rename to packages/react-native/src/private/specs/modules/NativeFantom.js index a2d2db9fc31c..34c04f2eaebf 100644 --- a/packages/react-native-fantom/src/specs/NativeFantom.js +++ b/packages/react-native/src/private/specs/modules/NativeFantom.js @@ -4,13 +4,13 @@ * 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 + * @flow strict * @format */ -import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import type {TurboModule} from '../../../../Libraries/TurboModule/RCTExport'; -import {TurboModuleRegistry} from 'react-native'; +import * as TurboModuleRegistry from '../../../../Libraries/TurboModule/TurboModuleRegistry'; // match RenderFormatOptions.h export type RenderFormatOptions = { From 3da8bfdce7cacf4626252cecc0298fc52349ca4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Fri, 3 Jan 2025 01:54:08 -0800 Subject: [PATCH 3/4] Allow configuring viewport width, height and device pixel ratio Differential Revision: D67693031 --- .../src/__tests__/Fantom-itest.js | 57 +++++++++++++++++++ packages/react-native-fantom/src/index.js | 33 +++++++++-- .../src/private/specs/modules/NativeFantom.js | 7 ++- 3 files changed, 92 insertions(+), 5 deletions(-) diff --git a/packages/react-native-fantom/src/__tests__/Fantom-itest.js b/packages/react-native-fantom/src/__tests__/Fantom-itest.js index f16d30bd10fb..97c65c8eaa66 100644 --- a/packages/react-native-fantom/src/__tests__/Fantom-itest.js +++ b/packages/react-native-fantom/src/__tests__/Fantom-itest.js @@ -7,13 +7,47 @@ * @flow strict-local * @format * @oncall react_native + * @fantom_flags enableAccessToHostTreeInFabric:true */ import 'react-native/Libraries/Core/InitializeCore'; +import type {Root} from '..'; + import {createRoot, runTask} from '..'; import * as React from 'react'; import {Text, View} from 'react-native'; +import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement'; + +function getActualViewportDimensions(root: Root): { + viewportWidth: number, + viewportHeight: number, +} { + let maybeNode; + + runTask(() => { + root.render( + { + maybeNode = node; + }} + />, + ); + }); + + if (!(maybeNode instanceof ReactNativeElement)) { + throw new Error( + `Expected instance of ReactNativeElement but got ${String(maybeNode)}`, + ); + } + + const rect = maybeNode.getBoundingClientRect(); + return { + viewportWidth: rect.width, + viewportHeight: rect.height, + }; +} describe('Fantom', () => { describe('runTask', () => { @@ -101,6 +135,29 @@ describe('Fantom', () => { }); }); + describe('createRoot', () => { + it('allows creating a root with specific dimensions', () => { + const rootWithDefaults = createRoot(); + + expect(getActualViewportDimensions(rootWithDefaults)).toEqual({ + viewportWidth: 1000, + viewportHeight: 1000, + }); + + const rootWithCustomWidthAndHeight = createRoot({ + viewportWidth: 200, + viewportHeight: 600, + }); + + expect(getActualViewportDimensions(rootWithCustomWidthAndHeight)).toEqual( + { + viewportWidth: 200, + viewportHeight: 600, + }, + ); + }); + }); + describe('getRenderedOutput', () => { describe('toJSX', () => { it('default config', () => { diff --git a/packages/react-native-fantom/src/index.js b/packages/react-native-fantom/src/index.js index 67795cc5c67f..92e0bf0558ff 100644 --- a/packages/react-native-fantom/src/index.js +++ b/packages/react-native-fantom/src/index.js @@ -24,18 +24,41 @@ const nativeRuntimeScheduler = global.nativeRuntimeScheduler; const schedulerPriorityImmediate = nativeRuntimeScheduler.unstable_ImmediatePriority; +export type RootConfig = { + viewportWidth?: number, + viewportHeight?: number, + devicePixelRatio?: number, +}; + +const DEFAULT_VIEWPORT_WIDTH = 1000; +const DEFAULT_VIEWPORT_HEIGHT = 1000; +const DEFAULT_DEVICE_PIXEL_RATIO = 1; + class Root { #surfaceId: number; + #viewportWidth: number; + #viewportHeight: number; + #devicePixelRatio: number; + #hasRendered: boolean = false; - constructor() { + constructor(config?: RootConfig) { this.#surfaceId = globalSurfaceIdCounter; + this.#viewportWidth = config?.viewportWidth ?? DEFAULT_VIEWPORT_WIDTH; + this.#viewportHeight = config?.viewportHeight ?? DEFAULT_VIEWPORT_HEIGHT; + this.#devicePixelRatio = + config?.devicePixelRatio ?? DEFAULT_DEVICE_PIXEL_RATIO; globalSurfaceIdCounter += 10; } render(element: MixedElement) { if (!this.#hasRendered) { - NativeFantom.startSurface(this.#surfaceId); + NativeFantom.startSurface( + this.#surfaceId, + this.#viewportWidth, + this.#viewportHeight, + this.#devicePixelRatio, + ); this.#hasRendered = true; } @@ -59,6 +82,8 @@ class Root { // TODO: add an API to check if all surfaces were deallocated when tests are finished. } +export type {Root}; + const DEFAULT_TASK_PRIORITY = schedulerPriorityImmediate; /** @@ -108,6 +133,6 @@ export function runWorkLoop(): void { // 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(); +export function createRoot(rootConfig?: RootConfig): Root { + return new Root(rootConfig); } diff --git a/packages/react-native/src/private/specs/modules/NativeFantom.js b/packages/react-native/src/private/specs/modules/NativeFantom.js index 34c04f2eaebf..f4b5ce24d49b 100644 --- a/packages/react-native/src/private/specs/modules/NativeFantom.js +++ b/packages/react-native/src/private/specs/modules/NativeFantom.js @@ -19,7 +19,12 @@ export type RenderFormatOptions = { }; interface Spec extends TurboModule { - startSurface: (surfaceId: number) => void; + startSurface: ( + surfaceId: number, + viewportWidth: number, + viewportHeight: number, + devicePixelRatio: number, + ) => void; stopSurface: (surfaceId: number) => void; getMountingManagerLogs: (surfaceId: number) => Array; flushMessageQueue: () => void; From a7de2472bcbf92a608b1c672689c0119d0e4b06b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Fri, 3 Jan 2025 02:01:17 -0800 Subject: [PATCH 4/4] Set more sensible defaults for viewport width and height (#48436) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/48436 Changelog: [internal] Changing the defaults to use something that resembles a real device (in this case the iPhone 14 which is a very common device). Reviewed By: christophpurrer Differential Revision: D67759914 --- .../src/__tests__/Fantom-itest.js | 6 +-- packages/react-native-fantom/src/index.js | 7 +-- .../__tests__/ReactNativeElement-itest.js | 8 +-- .../__tests__/IntersectionObserver-itest.js | 50 +++++++++++++++---- 4 files changed, 51 insertions(+), 20 deletions(-) diff --git a/packages/react-native-fantom/src/__tests__/Fantom-itest.js b/packages/react-native-fantom/src/__tests__/Fantom-itest.js index 97c65c8eaa66..fd2b4abdbd59 100644 --- a/packages/react-native-fantom/src/__tests__/Fantom-itest.js +++ b/packages/react-native-fantom/src/__tests__/Fantom-itest.js @@ -140,8 +140,8 @@ describe('Fantom', () => { const rootWithDefaults = createRoot(); expect(getActualViewportDimensions(rootWithDefaults)).toEqual({ - viewportWidth: 1000, - viewportHeight: 1000, + viewportWidth: 390, + viewportHeight: 844, }); const rootWithCustomWidthAndHeight = createRoot({ @@ -236,7 +236,7 @@ describe('Fantom', () => { layoutMetrics-frame="{x:0,y:0,width:100,height:100}" layoutMetrics-layoutDirection="LeftToRight" layoutMetrics-overflowInset="{top:0,right:-0,bottom:-0,left:0}" - layoutMetrics-pointScaleFactor="1" + layoutMetrics-pointScaleFactor="3" width="100.000000" />, ); diff --git a/packages/react-native-fantom/src/index.js b/packages/react-native-fantom/src/index.js index 92e0bf0558ff..cf78e5d9fda7 100644 --- a/packages/react-native-fantom/src/index.js +++ b/packages/react-native-fantom/src/index.js @@ -30,9 +30,10 @@ export type RootConfig = { devicePixelRatio?: number, }; -const DEFAULT_VIEWPORT_WIDTH = 1000; -const DEFAULT_VIEWPORT_HEIGHT = 1000; -const DEFAULT_DEVICE_PIXEL_RATIO = 1; +// Defaults use iPhone 14 values (very common device). +const DEFAULT_VIEWPORT_WIDTH = 390; +const DEFAULT_VIEWPORT_HEIGHT = 844; +const DEFAULT_DEVICE_PIXEL_RATIO = 3; class Root { #surfaceId: number; diff --git a/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElement-itest.js b/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElement-itest.js index e7d71390e426..15d901527d39 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElement-itest.js +++ b/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElement-itest.js @@ -890,9 +890,9 @@ describe('ReactNativeElement', () => { const boundingClientRect = element.getBoundingClientRect(); expect(boundingClientRect).toBeInstanceOf(DOMRect); expect(boundingClientRect.x).toBe(5); - expect(boundingClientRect.y).toBe(10); - expect(boundingClientRect.width).toBe(50); - expect(boundingClientRect.height).toBe(101); + expect(boundingClientRect.y).toBeCloseTo(10.33); + expect(boundingClientRect.width).toBeCloseTo(50.33); + expect(boundingClientRect.height).toBeCloseTo(100.33); Fantom.runTask(() => { root.render(); @@ -1131,7 +1131,7 @@ describe('ReactNativeElement', () => { const element = ensureReactNativeElement(lastElement); expect(element.offsetWidth).toBe(50); - expect(element.offsetHeight).toBe(101); + expect(element.offsetHeight).toBe(100); Fantom.runTask(() => { root.render(); diff --git a/packages/react-native/src/private/webapis/intersectionobserver/__tests__/IntersectionObserver-itest.js b/packages/react-native/src/private/webapis/intersectionobserver/__tests__/IntersectionObserver-itest.js index a7ea8e84e8dd..b49e9caa9864 100644 --- a/packages/react-native/src/private/webapis/intersectionobserver/__tests__/IntersectionObserver-itest.js +++ b/packages/react-native/src/private/webapis/intersectionobserver/__tests__/IntersectionObserver-itest.js @@ -340,7 +340,10 @@ describe('IntersectionObserver', () => { let maybeNode; let observer: IntersectionObserver; - const root = Fantom.createRoot(); + const root = Fantom.createRoot({ + viewportWidth: 1000, + viewportHeight: 1000, + }); Fantom.runTask(() => { root.render( { let maybeNode; let observer: IntersectionObserver; - const root = Fantom.createRoot(); + const root = Fantom.createRoot({ + viewportWidth: 1000, + viewportHeight: 1000, + }); Fantom.runTask(() => { root.render( @@ -539,7 +545,10 @@ describe('IntersectionObserver', () => { let maybeNode; let observer: IntersectionObserver; - const root = Fantom.createRoot(); + const root = Fantom.createRoot({ + viewportWidth: 1000, + viewportHeight: 1000, + }); Fantom.runTask(() => { root.render( @@ -600,7 +609,10 @@ describe('IntersectionObserver', () => { let maybeNode; let observer: IntersectionObserver; - const root = Fantom.createRoot(); + const root = Fantom.createRoot({ + viewportWidth: 1000, + viewportHeight: 1000, + }); Fantom.runTask(() => { root.render( @@ -661,7 +673,10 @@ describe('IntersectionObserver', () => { let maybeNode; let observer: IntersectionObserver; - const root = Fantom.createRoot(); + const root = Fantom.createRoot({ + viewportWidth: 1000, + viewportHeight: 1000, + }); Fantom.runTask(() => { root.render( { let observer1: IntersectionObserver; let observer2: IntersectionObserver; - const root = Fantom.createRoot(); + const root = Fantom.createRoot({ + viewportWidth: 1000, + viewportHeight: 1000, + }); Fantom.runTask(() => { root.render( @@ -872,7 +890,10 @@ describe('IntersectionObserver', () => { let maybeNode; let observer: IntersectionObserver; - const root = Fantom.createRoot(); + const root = Fantom.createRoot({ + viewportWidth: 1000, + viewportHeight: 1000, + }); Fantom.runTask(() => { root.render( { let maybeNode; let observer: IntersectionObserver; - const root = Fantom.createRoot(); + const root = Fantom.createRoot({ + viewportWidth: 1000, + viewportHeight: 1000, + }); Fantom.runTask(() => { root.render( @@ -993,7 +1017,10 @@ describe('IntersectionObserver', () => { let maybeNode; let observer: IntersectionObserver; - const root = Fantom.createRoot(); + const root = Fantom.createRoot({ + viewportWidth: 1000, + viewportHeight: 1000, + }); Fantom.runTask(() => { root.render( @@ -1054,7 +1081,10 @@ describe('IntersectionObserver', () => { let maybeNode; let observer: IntersectionObserver; - const root = Fantom.createRoot(); + const root = Fantom.createRoot({ + viewportWidth: 1000, + viewportHeight: 1000, + }); Fantom.runTask(() => { root.render(