Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion packages/react-native-fantom/src/__tests__/Fantom-itest.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,44 @@
* @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 ensureInstance from 'react-native/src/private/utilities/ensureInstance';
import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement';

function getActualViewportDimensions(root: Root): {
viewportWidth: number,
viewportHeight: number,
} {
let maybeNode;

runTask(() => {
root.render(
<View
style={{width: '100%', height: '100%'}}
ref={node => {
maybeNode = node;
}}
/>,
);
});

const node = ensureInstance(maybeNode, ReactNativeElement);

const rect = node.getBoundingClientRect();
return {
viewportWidth: rect.width,
viewportHeight: rect.height,
};
}

describe('Fantom', () => {
describe('runTask', () => {
Expand Down Expand Up @@ -103,6 +134,29 @@ describe('Fantom', () => {
});
});

describe('createRoot', () => {
it('allows creating a root with specific dimensions', () => {
const rootWithDefaults = createRoot();

expect(getActualViewportDimensions(rootWithDefaults)).toEqual({
viewportWidth: 390,
viewportHeight: 844,
});

const rootWithCustomWidthAndHeight = createRoot({
viewportWidth: 200,
viewportHeight: 600,
});

expect(getActualViewportDimensions(rootWithCustomWidthAndHeight)).toEqual(
{
viewportWidth: 200,
viewportHeight: 600,
},
);
});
});

describe('getRenderedOutput', () => {
describe('toJSX', () => {
it('default config', () => {
Expand Down Expand Up @@ -189,7 +243,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"
/>,
);
Expand Down
34 changes: 30 additions & 4 deletions packages/react-native-fantom/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,42 @@ const nativeRuntimeScheduler = global.nativeRuntimeScheduler;
const schedulerPriorityImmediate =
nativeRuntimeScheduler.unstable_ImmediatePriority;

export type RootConfig = {
viewportWidth?: number,
viewportHeight?: number,
devicePixelRatio?: number,
};

// 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;
#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;
}

Expand All @@ -59,6 +83,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;

/**
Expand Down Expand Up @@ -108,6 +134,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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>;
flushMessageQueue: () => void;
Expand Down
21 changes: 21 additions & 0 deletions packages/react-native/src/private/utilities/ensureInstance.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* 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 strict
*/

export default function ensureInstance<T>(value: mixed, Class: Class<T>): T {
if (!(value instanceof Class)) {
// $FlowIssue[incompatible-use]
const className = Class.name;
throw new Error(
`Expected instance of ${className} but got ${String(value)}`,
);
}

return value;
}
Original file line number Diff line number Diff line change
Expand Up @@ -303,11 +303,18 @@ export function setInstanceHandle(
node[INSTANCE_HANDLE_KEY] = instanceHandle;
}

let RendererProxy;
function getRendererProxy() {
if (RendererProxy == null) {
// Lazy import Fabric here to avoid DOM Node APIs classes from having side-effects.
// With a static import we can't use these classes for Paper-only variants.
RendererProxy = require('../../../../../Libraries/ReactNative/RendererProxy');
}
return RendererProxy;
}

export function getShadowNode(node: ReadOnlyNode): ?ShadowNode {
// Lazy import Fabric here to avoid DOM Node APIs classes from having side-effects.
// With a static import we can't use these classes for Paper-only variants.
const RendererProxy = require('../../../../../Libraries/ReactNative/RendererProxy');
return RendererProxy.getNodeFromInternalInstanceHandle(
return getRendererProxy().getNodeFromInternalInstanceHandle(
getInstanceHandle(node),
);
}
Expand Down Expand Up @@ -351,11 +358,10 @@ function getNodeSiblingsAndPosition(
export function getPublicInstanceFromInternalInstanceHandle(
instanceHandle: InternalInstanceHandle,
): ?ReadOnlyNode {
// Lazy import Fabric here to avoid DOM Node APIs classes from having side-effects.
// With a static import we can't use these classes for Paper-only variants.
const RendererProxy = require('../../../../../Libraries/ReactNative/RendererProxy');
const mixedPublicInstance =
RendererProxy.getPublicInstanceFromInternalInstanceHandle(instanceHandle);
getRendererProxy().getPublicInstanceFromInternalInstanceHandle(
instanceHandle,
);
// $FlowExpectedError[incompatible-return] React defines public instances as "mixed" because it can't access the definition from React Native.
return mixedPublicInstance;
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
NativeText,
NativeVirtualText,
} from '../../../../../../Libraries/Text/TextNativeComponent';
import ensureInstance from '../../../../utilities/ensureInstance';
import HTMLCollection from '../../oldstylecollections/HTMLCollection';
import NodeList from '../../oldstylecollections/NodeList';
import ReactNativeElement from '../ReactNativeElement';
Expand All @@ -26,13 +27,7 @@ import * as Fantom from '@react-native/fantom';
import * as React from 'react';

function ensureReactNativeElement(value: mixed): ReactNativeElement {
if (!(value instanceof ReactNativeElement)) {
throw new Error(
`Expected instance of ReactNativeElement but got ${String(value)}`,
);
}

return value;
return ensureInstance(value, ReactNativeElement);
}

/* eslint-disable no-bitwise */
Expand Down Expand Up @@ -890,9 +885,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(<View key="otherParent" />);
Expand Down Expand Up @@ -1131,7 +1126,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(<View key="otherParent" />);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import '../../../../../../Libraries/Core/InitializeCore.js';

import {NativeText} from '../../../../../../Libraries/Text/TextNativeComponent';
import ensureInstance from '../../../../utilities/ensureInstance';
import ReactNativeElement from '../ReactNativeElement';
import ReadOnlyNode from '../ReadOnlyNode';
import ReadOnlyText from '../ReadOnlyText';
Expand All @@ -21,33 +22,15 @@ import invariant from 'invariant';
import * as React from 'react';

function ensureReadOnlyText(value: mixed): ReadOnlyText {
if (!(value instanceof ReadOnlyText)) {
throw new Error(
`Expected instance of ReactOnlyNode but got ${String(value)}`,
);
}

return value;
return ensureInstance(value, ReadOnlyText);
}

function ensureReadOnlyNode(value: mixed): ReadOnlyNode {
if (!(value instanceof ReadOnlyNode)) {
throw new Error(
`Expected instance of ReactOnlyNode but got ${String(value)}`,
);
}

return value;
return ensureInstance(value, ReadOnlyNode);
}

function ensureReactNativeElement(value: mixed): ReactNativeElement {
if (!(value instanceof ReactNativeElement)) {
throw new Error(
`Expected instance of ReactNativeElement but got ${String(value)}`,
);
}

return value;
return ensureInstance(value, ReactNativeElement);
}

describe('ReadOnlyText', () => {
Expand Down
Loading