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
59 changes: 58 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,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(
<View
style={{width: '100%', height: '100%'}}
ref={node => {
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', () => {
Expand Down Expand Up @@ -101,6 +135,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 @@ -179,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"
/>,
);
Expand Down
4 changes: 2 additions & 2 deletions packages/react-native-fantom/src/getFantomRenderedOutput.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
* @oncall react_native
*/

import FantomModule from './specs/NativeFantomModule';
// $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,
Expand Down Expand Up @@ -114,7 +114,7 @@ export default function getFantomRenderedOutput(
} = config;
return new FantomRenderedOutput(
JSON.parse(
FantomModule.getRenderedOutput(surfaceId, {
NativeFantom.getRenderedOutput(surfaceId, {
includeRoot,
includeLayoutMetrics,
}),
Expand Down
44 changes: 35 additions & 9 deletions packages/react-native-fantom/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,41 +15,65 @@ import type {
import type {MixedElement} from 'react';

import getFantomRenderedOutput from './getFantomRenderedOutput';
import FantomModule from './specs/NativeFantomModule';
import ReactFabric from 'react-native/Libraries/Renderer/shims/ReactFabric';
import NativeFantom from 'react-native/src/private/specs/modules/NativeFantom';

let globalSurfaceIdCounter = 1;

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) {
FantomModule.startSurface(this.#surfaceId);
NativeFantom.startSurface(
this.#surfaceId,
this.#viewportWidth,
this.#viewportHeight,
this.#devicePixelRatio,
);
this.#hasRendered = true;
}

ReactFabric.render(element, this.#surfaceId, () => {}, true);
}

getMountingLogs(): Array<string> {
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 {
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 @@ -100,14 +126,14 @@ export function runWorkLoop(): void {

try {
flushingQueue = true;
FantomModule.flushMessageQueue();
NativeFantom.flushMessageQueue();
} finally {
flushingQueue = false;
}
}

// 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 @@ -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 = {
Expand All @@ -19,11 +19,18 @@ 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;
getRenderedOutput: (surfaceId: number, config: RenderFormatOptions) => string;
}

export default TurboModuleRegistry.getEnforcing<Spec>('Fantom') as Spec;
export default TurboModuleRegistry.getEnforcing<Spec>(
'NativeFantomCxx',
) as Spec;
Original file line number Diff line number Diff line change
Expand Up @@ -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(<View key="otherParent" />);
Expand Down Expand Up @@ -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(<View key="otherParent" />);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<View
Expand Down Expand Up @@ -477,7 +480,10 @@ describe('IntersectionObserver', () => {
let maybeNode;
let observer: IntersectionObserver;

const root = Fantom.createRoot();
const root = Fantom.createRoot({
viewportWidth: 1000,
viewportHeight: 1000,
});
Fantom.runTask(() => {
root.render(
<ScrollView contentOffset={{x: 0, y: 200}}>
Expand Down Expand Up @@ -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(
<ScrollView contentOffset={{x: 0, y: 25}}>
Expand Down Expand Up @@ -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(
<ScrollView contentOffset={{x: 0, y: 25}}>
Expand Down Expand Up @@ -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(
<View
Expand Down Expand Up @@ -762,7 +777,10 @@ describe('IntersectionObserver', () => {
let observer1: IntersectionObserver;
let observer2: IntersectionObserver;

const root = Fantom.createRoot();
const root = Fantom.createRoot({
viewportWidth: 1000,
viewportHeight: 1000,
});
Fantom.runTask(() => {
root.render(
<ScrollView contentOffset={{x: 0, y: 100}}>
Expand Down Expand Up @@ -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(
<View
Expand Down Expand Up @@ -932,7 +953,10 @@ describe('IntersectionObserver', () => {
let maybeNode;
let observer: IntersectionObserver;

const root = Fantom.createRoot();
const root = Fantom.createRoot({
viewportWidth: 1000,
viewportHeight: 1000,
});
Fantom.runTask(() => {
root.render(
<ScrollView contentOffset={{x: 0, y: 25}}>
Expand Down Expand Up @@ -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(
<ScrollView contentOffset={{x: 0, y: 200}}>
Expand Down Expand Up @@ -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(
<View
Expand Down