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
50 changes: 36 additions & 14 deletions packages/react-native-fantom/runner/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*/

import type {TestSuiteResult} from '../runtime/setup';
import type {ConsoleLogMessage} from './utils';

import entrypointTemplate from './entrypoint-template';
import getFantomTestConfig from './getFantomTestConfig';
Expand All @@ -22,6 +23,7 @@ import {
getBuckModesForPlatform,
getDebugInfoFromCommandResult,
getShortHash,
printConsoleLogs,
runBuck2,
symbolicateStackTrace,
} from './utils';
Expand All @@ -42,30 +44,50 @@ const BUILD_OUTPUT_PATH = fs.mkdtempSync(
const PRINT_FANTOM_OUTPUT: false = false;

function parseRNTesterCommandResult(result: ReturnType<typeof runBuck2>): {
logs: string,
logs: $ReadOnlyArray<ConsoleLogMessage>,
testResult: TestSuiteResult,
} {
const stdout = result.stdout.toString();

const outputArray = stdout
.trim()
.split('\n')
.filter(log => !log.startsWith('Running "')); // remove AppRegistry logs.
const logs = [];
let testResult;

// The last line should be the test output in JSON format
const testResultJSON = outputArray.pop();
const lines = stdout
.split('\n')
.map(line => line.trim())
.filter(Boolean);

for (const line of lines) {
let parsed;
try {
parsed = JSON.parse(line);
} catch {}

switch (parsed?.type) {
case 'test-result':
testResult = parsed;
break;
case 'console-log':
logs.push(parsed);
break;
default:
logs.push({
type: 'console-log',
message: line,
level: 'info',
});
break;
}
}

let testResult;
try {
testResult = JSON.parse(nullthrows(testResultJSON));
} catch (error) {
if (testResult == null) {
throw new Error(
'Failed to parse test results from RN tester binary result.\n' +
'Failed to find test results in RN tester binary output.\n' +
getDebugInfoFromCommandResult(result),
);
}

return {logs: outputArray.join('\n'), testResult};
return {logs, testResult};
}

function generateBytecodeBundle({
Expand Down Expand Up @@ -219,7 +241,7 @@ module.exports = async function runTest(
const endTime = Date.now();

if (process.env.SANDCASTLE == null) {
console.log(rnTesterParsedOutput.logs);
printConsoleLogs(rnTesterParsedOutput.logs);
}

const testResults =
Expand Down
28 changes: 28 additions & 0 deletions packages/react-native-fantom/runner/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,31 @@ export function symbolicateStackTrace(
})
.join('\n');
}

export type ConsoleLogMessage = {
type: 'console-log',
level: 'info' | 'warn' | 'error',
message: string,
};

export function printConsoleLogs(
logs: $ReadOnlyArray<ConsoleLogMessage>,
): void {
for (const log of logs) {
switch (log.type) {
case 'console-log':
switch (log.level) {
case 'info':
console.log(log.message);
break;
case 'warn':
console.warn(log.message);
break;
case 'error':
console.error(log.message);
break;
}
break;
}
}
}
8 changes: 7 additions & 1 deletion packages/react-native-fantom/runtime/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import expect from './expect';
import {createMockFunction} from './mocks';
import {setupSnapshotConfig, snapshotContext} from './snapshotContext';
import nullthrows from 'nullthrows';
import NativeFantom from 'react-native/src/private/specs/modules/NativeFantom';

export type TestCaseResult = {
ancestorTitles: Array<string>,
Expand Down Expand Up @@ -190,7 +191,12 @@ function executeTests() {
}

function reportTestSuiteResult(testSuiteResult: TestSuiteResult): void {
console.log(JSON.stringify(testSuiteResult));
NativeFantom.reportTestSuiteResultsJSON(
JSON.stringify({
type: 'test-result',
...testSuiteResult,
}),
);
}

global.$$RunTests$$ = () => {
Expand Down
68 changes: 43 additions & 25 deletions packages/react-native-fantom/src/__tests__/Fantom-itest.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,28 +76,30 @@ describe('Fantom', () => {

// TODO: when error handling is fixed, this should verify using `toThrow`
it('should throw when running a task inside another task', () => {
let lastCallbackExecuted = 0;
let threw = false;

runTask(() => {
lastCallbackExecuted = 1;
runTask(() => {
lastCallbackExecuted = 2;
throw new Error('Recursive runTask should be unreachable');
});
// TODO replace with expect(() => { ... }).toThrow() when error handling is fixed
try {
runTask(() => {});
} catch {
threw = true;
}
});
expect(lastCallbackExecuted).toBe(1);
expect(threw).toBe(true);

threw = false;

runTask(() => {
queueMicrotask(() => {
lastCallbackExecuted = 3;
runTask(() => {
lastCallbackExecuted = 4;
throw new Error(
'Recursive runTask from micro-task should be unreachable',
);
});
try {
runTask(() => {});
} catch {
threw = true;
}
});
});
expect(lastCallbackExecuted).toBe(3);
expect(threw).toBe(true);
});
});

Expand Down Expand Up @@ -125,16 +127,24 @@ describe('Fantom', () => {
runTask(() => {
root.render(
<>
<View style={{width: 100, height: 100}} collapsable={false} />
<View style={{width: 100, height: 100}} collapsable={false} />
<View
key="first"
style={{width: 100, height: 100}}
collapsable={false}
/>
<View
key="second"
style={{width: 100, height: 100}}
collapsable={false}
/>
</>,
);
});

expect(root.getRenderedOutput().toJSX()).toEqual(
<>
<rn-view width="100.000000" height="100.000000" />
<rn-view width="100.000000" height="100.000000" />
<rn-view key="0" width="100.000000" height="100.000000" />
<rn-view key="1" width="100.000000" height="100.000000" />
</>,
);

Expand Down Expand Up @@ -233,18 +243,26 @@ describe('Fantom', () => {
runTask(() => {
root.render(
<>
<View style={{width: 100, height: 100}} collapsable={false} />
<Text>hello world!</Text>
<View style={{width: 200, height: 300}} collapsable={false} />
<View
key="first"
style={{width: 100, height: 100}}
collapsable={false}
/>
<Text key="second">hello world!</Text>
<View
key="third"
style={{width: 200, height: 300}}
collapsable={false}
/>
</>,
);
});

expect(root.getRenderedOutput({props: []}).toJSX()).toEqual(
<>
<rn-view />
<rn-paragraph>hello world!</rn-paragraph>
<rn-view />
<rn-view key="0" />
<rn-paragraph key="1">hello world!</rn-paragraph>
<rn-view key="2" />
</>,
);

Expand Down
15 changes: 10 additions & 5 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 Expand Up @@ -152,16 +152,20 @@ function convertRawJsonToJSX(
function createJSXElementForTestComparison(
type: string,
props: mixed,
key?: ?string,
): React.Node {
const Tag = type;
return <Tag {...props} />;
return <Tag key={key} {...props} />;
}

function rnTypeToTestType(type: string): string {
return `rn-${type.substring(0, 1).toLowerCase() + type.substring(1)}`;
}

function jsonChildToJSXChild(jsonChild: FantomJsonObject | string): React.Node {
function jsonChildToJSXChild(
jsonChild: FantomJsonObject | string,
index?: ?number,
): React.Node {
if (typeof jsonChild === 'string') {
return jsonChild;
} else {
Expand All @@ -172,6 +176,7 @@ function jsonChildToJSXChild(jsonChild: FantomJsonObject | string): React.Node {
jsxChildren == null
? jsonChild.props
: {...jsonChild.props, children: jsxChildren},
index != null ? String(index) : undefined,
);
}
}
Expand All @@ -184,7 +189,7 @@ function jsonChildrenToJSXChildren(jsonChildren: FantomJsonObject['children']) {
let allJSXChildrenAreStrings = true;
let jsxChildrenString = '';
for (let i = 0; i < jsonChildren.length; i++) {
const jsxChild = jsonChildToJSXChild(jsonChildren[i]);
const jsxChild = jsonChildToJSXChild(jsonChildren[i], i);
jsxChildren.push(jsxChild);
if (allJSXChildrenAreStrings) {
if (typeof jsxChild === 'string') {
Expand Down
12 changes: 6 additions & 6 deletions packages/react-native-fantom/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ 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;

Expand All @@ -35,21 +35,21 @@ class Root {

render(element: MixedElement) {
if (!this.#hasRendered) {
FantomModule.startSurface(this.#surfaceId);
NativeFantom.startSurface(this.#surfaceId);
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 Down Expand Up @@ -100,7 +100,7 @@ export function runWorkLoop(): void {

try {
flushingQueue = true;
FantomModule.flushMessageQueue();
NativeFantom.flushMessageQueue();
} finally {
flushingQueue = false;
}
Expand Down
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 @@ -24,6 +24,9 @@ interface Spec extends TurboModule {
getMountingManagerLogs: (surfaceId: number) => Array<string>;
flushMessageQueue: () => void;
getRenderedOutput: (surfaceId: number, config: RenderFormatOptions) => string;
reportTestSuiteResultsJSON: (results: string) => void;
}

export default TurboModuleRegistry.getEnforcing<Spec>('Fantom') as Spec;
export default TurboModuleRegistry.getEnforcing<Spec>(
'NativeFantomCxx',
) as Spec;
Loading