NativeCxxModuleExample test for testing C++ TM E2E (#52477)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/52477

Changelog: [Internal]

This adds a Fantom test for

https://github.com/facebook/react-native/blob/main/packages/rn-tester/NativeCxxModuleExample/NativeCxxModuleExample.js

to test a C++ Turbo Module End 2 End (loading the C++ implementation in native code, accessing in JavaScript via Hermes VM and verifying the results of the API calls)

Reviewed By: rshest

Differential Revision: D77848654

fbshipit-source-id: 48a4ab88a330e9282ae8dab589743eaace62d124
This commit is contained in:
Christoph Purrer
2025-07-08 21:10:36 -07:00
committed by Facebook GitHub Bot
parent 6892dde363
commit b5329ecd6d
4 changed files with 379 additions and 7 deletions
@@ -7,9 +7,22 @@
#include "NativeCxxModuleExample.h"
#include <react/debug/react_native_assert.h>
#include <iomanip>
#include <ostream>
#include <sstream>
namespace facebook::react {
namespace {
std::string to_string_with_precision(double value, int precision = 2) {
std::ostringstream oss;
oss << std::setprecision(precision) << std::fixed << value;
return oss.str();
}
} // namespace
NativeCxxModuleExample::NativeCxxModuleExample(
std::shared_ptr<CallInvoker> jsInvoker)
: NativeCxxModuleExampleCxxSpec(std::move(jsInvoker)) {}
@@ -126,10 +139,12 @@ std::string NativeCxxModuleExample::getUnion(
float x,
std::string y,
jsi::Object z) {
std::string result = "x: " + std::to_string(x) + ", y: " + y + ", z: { ";
std::string result =
"x: " + to_string_with_precision(x) + ", y: " + y + ", z: { ";
if (z.hasProperty(rt, "value")) {
result += "value: ";
result += std::to_string(z.getProperty(rt, "value").getNumber());
result +=
to_string_with_precision(z.getProperty(rt, "value").getNumber(), 0);
} else if (z.hasProperty(rt, "low")) {
result += "low: ";
result += z.getProperty(rt, "low").getString(rt).utf8(rt);
@@ -201,11 +216,13 @@ void NativeCxxModuleExample::emitCustomDeviceEvent(
eventName,
[jsInvoker = jsInvoker_](
jsi::Runtime& rt, std::vector<jsi::Value>& args) {
args.emplace_back(jsi::Value(true));
args.emplace_back(jsi::Value(42));
args.emplace_back(jsi::String::createFromAscii(rt, "stringArg"));
args.emplace_back(bridging::toJs(
rt, CustomDeviceEvent{"one", 2, std::nullopt}, jsInvoker));
args.emplace_back(jsi::Array::createWithElements(
rt,
jsi::Value(true),
jsi::Value(42),
jsi::String::createFromAscii(rt, "stringArg"),
bridging::toJs(
rt, CustomDeviceEvent{"one", 2, std::nullopt}, jsInvoker)));
});
}
@@ -0,0 +1,351 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
import type {
BinaryTreeNode,
GraphNode,
ObjectStruct,
} from '../NativeCxxModuleExample';
import NativeCxxModuleExample, {
EnumInt,
EnumNone,
EnumStr,
} from '../NativeCxxModuleExample';
import RCTDeviceEventEmitter from 'react-native/Libraries/EventEmitter/RCTDeviceEventEmitter';
import NativeFantom from 'react-native/src/private/testing/fantom/specs/NativeFantom';
describe('NativeCxxModuleExample', () => {
it('verifies that the Turbo Module was loaded', () => {
expect(NativeCxxModuleExample).not.toBeNull();
});
it('verifies getArray(...) returns the correct values', () => {
expect(NativeCxxModuleExample?.getArray([])).toEqual([]);
expect(NativeCxxModuleExample?.getArray([null])).toEqual([null]);
expect(NativeCxxModuleExample?.getArray([{a: 1, b: '2'}])).toEqual([
{a: 1, b: '2'},
]);
});
it('verifies getBool(...) returns the correct values', () => {
expect(NativeCxxModuleExample?.getBool(false)).toBe(false);
expect(NativeCxxModuleExample?.getBool(true)).toBe(true);
});
it('verifies getConstants(...) returns the correct values', () => {
expect(NativeCxxModuleExample?.getConstants()).toEqual({
const1: true,
const2: 69,
const3: 'react-native',
});
});
it('verifies getCustomEnum(...) returns the correct values', () => {
expect(NativeCxxModuleExample?.getCustomEnum(EnumInt.IA)).toBe(EnumInt.IA);
});
it('verifies getCustomHostObject(...) returns the correct values', () => {
const customHostObject = NativeCxxModuleExample?.getCustomHostObject();
expect(customHostObject).not.toBe(null);
if (customHostObject != null) {
expect(
NativeCxxModuleExample?.consumeCustomHostObject(customHostObject),
).toBe('answer42');
}
});
it('verifies getBinaryTreeNode(...) returns the correct values', () => {
const binaryTreeNode: BinaryTreeNode = {
left: {value: 2},
value: 4,
right: {value: 6},
};
const result = NativeCxxModuleExample?.getBinaryTreeNode(binaryTreeNode);
expect(result).not.toBe(null);
if (result != null) {
expect(result.left?.left).toBeNull();
expect(result.left?.value).toBe(2);
expect(result.left?.right).toBeNull();
expect(result.value).toBe(4);
expect(result.right?.left).toBeNull();
expect(result.right?.value).toBe(6);
expect(result.right?.right).toBeNull();
}
});
it('verifies getGraphNode(...) returns the correct values', () => {
const graphNode: GraphNode = {
label: 'root',
neighbors: [{label: 'child1'}, {label: 'child2'}],
};
const result = NativeCxxModuleExample?.getGraphNode(graphNode);
expect(result).not.toBe(null);
if (result != null) {
expect(result.label).toBe('root');
expect(result.neighbors?.length).toBe(4);
expect(result.neighbors?.[0].label).toBe('child1');
expect(result.neighbors?.[0].neighbors).toBeNull();
expect(result.neighbors?.[1].label).toBe('child2');
expect(result.neighbors?.[1].neighbors).toBeNull();
expect(result.neighbors?.[2].label).toBe('top');
expect(result.neighbors?.[2].neighbors).toBeNull();
expect(result.neighbors?.[3].label).toBe('down');
expect(result.neighbors?.[3].neighbors).toBeNull();
}
});
it('verifies getNumEnum(...) returns the correct values', () => {
expect(NativeCxxModuleExample?.getNumEnum(EnumInt.IA)).toBe(EnumInt.IA);
expect(NativeCxxModuleExample?.getNumEnum(EnumInt.IB)).toBe(EnumInt.IB);
});
it('verifies getStrEnum(...) returns the correct values', () => {
expect(NativeCxxModuleExample?.getStrEnum(EnumNone.NA)).toBe(EnumStr.SB);
expect(NativeCxxModuleExample?.getStrEnum(EnumNone.NB)).toBe(EnumStr.SB);
});
it('verifies getMap(...) returns the correct values', () => {
expect(NativeCxxModuleExample?.getMap({a: 0, b: null, c: 3})).toEqual({
a: 0,
b: null,
c: 3,
});
});
it('verifies getNumber(...) returns the correct values', () => {
expect(NativeCxxModuleExample?.getNumber(0)).toBe(0);
expect(NativeCxxModuleExample?.getNumber(Math.pow(2, 53))).toBe(
Math.pow(2, 53),
);
});
it('verifies getObject(...) returns the correct values', () => {
expect(NativeCxxModuleExample?.getObject({a: 2, b: 'two'})).toEqual({
a: 2,
b: 'two',
});
expect(
NativeCxxModuleExample?.getObject({a: 4, b: 'four', c: 'seven'}),
).toEqual({a: 4, b: 'four', c: 'seven'});
});
it('verifies getSet(...) returns the correct values', () => {
expect(NativeCxxModuleExample?.getSet([1, 2, 3, 3, 3])).toEqual([1, 2, 3]);
});
it('verifies getString(...) returns the correct values', () => {
expect(NativeCxxModuleExample?.getString('')).toBe('');
expect(NativeCxxModuleExample?.getString('string')).toBe('string');
});
it('verifies getUnion(...) returns the correct values', () => {
expect(NativeCxxModuleExample?.getUnion(2.88, 'Two', {value: 2})).toBe(
'x: 2.88, y: Two, z: { value: 2 }',
);
expect(NativeCxxModuleExample?.getUnion(5.76, 'One', {low: 'value'})).toBe(
'x: 5.76, y: One, z: { low: value }',
);
});
it('verifies getValue(...) returns the correct values', () => {
expect(
NativeCxxModuleExample?.getValue(23, 'forty-two', {
a: 4,
b: 'four',
c: 'seven',
}),
).toEqual({x: 23, y: 'forty-two', z: {a: 4, b: 'four', c: 'seven'}});
});
it('verifies getValueWithCallback(...) returns the correct values', () => {
let result = '';
NativeCxxModuleExample?.getValueWithCallback((value: string) => {
result = value;
});
NativeFantom.flushMessageQueue(); // Flush the message queue to execute the callback
expect(result).toBe('value from callback!');
});
it('verifies setValueCallbackWithSubscription(...) returns the correct values', () => {
let result = '';
let subscription = NativeCxxModuleExample?.setValueCallbackWithSubscription(
(value: string) => {
result = value;
},
);
expect(result).toBe('');
expect(subscription).not.toBeNull();
subscription?.();
NativeFantom.flushMessageQueue(); // Flush the message queue to execute the callback
expect(result).toBe('value from callback on clean up!');
});
it('verifies getValueWithPromise(...) returns the correct values', () => {
{
let result = '';
let error = '';
NativeCxxModuleExample?.getValueWithPromise(false)
.then(value => {
result = value;
})
.catch(err => {
error = err;
});
NativeFantom.flushMessageQueue(); // Flush the message queue to execute the callbacks
expect(result).toBe('result!');
expect(error).toBe('');
}
{
let result = '';
let error = '';
NativeCxxModuleExample?.getValueWithPromise(true)
.then(value => {
result = value;
})
.catch(err => {
error = err;
});
NativeFantom.flushMessageQueue(); // Flush the message queue to execute the callbacks
expect(result).toBe('');
expect(error.toString()).toBe('Error: intentional promise rejection');
}
it('verifies getWithWithOptionalArgs(...) returns the correct values', () => {
expect(NativeCxxModuleExample?.getWithWithOptionalArgs()).toBeNull();
expect(NativeCxxModuleExample?.getWithWithOptionalArgs(true)).toBe(true);
expect(NativeCxxModuleExample?.getWithWithOptionalArgs(false)).toBe(
false,
);
});
it('verifies voidFunc(...) returns the correct for EventEmitters', () => {
let eventEmitterCalled = {
onPress: 0,
onClick: 0,
onChange: 0,
onSubmit: 0,
onEvent: 0,
};
let onClickValue: ?string = null;
let onChangeValue: ?ObjectStruct = null;
let onSubmitValue: ?(ObjectStruct[]) = null;
let onEventValue: ?EnumNone = null;
NativeCxxModuleExample?.onPress(() => {
eventEmitterCalled.onPress++;
});
NativeCxxModuleExample?.onClick((value: string) => {
eventEmitterCalled.onClick++;
onClickValue = value;
});
NativeCxxModuleExample?.onChange((value: ObjectStruct) => {
eventEmitterCalled.onChange++;
onChangeValue = value;
});
NativeCxxModuleExample?.onSubmit((value: ObjectStruct[]) => {
eventEmitterCalled.onSubmit++;
onSubmitValue = value;
});
NativeCxxModuleExample?.onEvent((value: EnumNone) => {
eventEmitterCalled.onEvent++;
onEventValue = value;
});
NativeCxxModuleExample?.voidFunc();
NativeFantom.flushMessageQueue(); // Flush the message queue to execute the callbacks
expect(eventEmitterCalled.onPress).toBe(1);
expect(eventEmitterCalled.onClick).toBe(1);
expect(eventEmitterCalled.onChange).toBe(1);
expect(eventEmitterCalled.onSubmit).toBe(1);
expect(eventEmitterCalled.onEvent).toBe(1);
expect(onClickValue).toBe('value from callback on click!');
expect(onChangeValue).toEqual({a: 1, b: 'two'});
expect(onSubmitValue).toEqual([
{a: 1, b: 'two'},
{a: 3, b: 'four'},
{a: 5, b: 'six'},
]);
expect(onEventValue).toBe(EnumNone.NA);
});
});
it('verifies voidPromise(...) returns the correct values', () => {
let promiseCalled = {
result: 0,
error: 0,
};
NativeCxxModuleExample?.voidPromise()
.then(_value => promiseCalled.result++)
.catch(_err => promiseCalled.error++);
NativeFantom.flushMessageQueue(); // Flush the message queue to execute the callbacks
expect(promiseCalled.result).toBe(1);
expect(promiseCalled.error).toBe(0);
});
it('verifies setMenu(...) returns the correct values', () => {
let result: {[key: string]: ?{value: string, flag: boolean}} = {
file: null,
new: null,
};
let menu = {
label: 'File',
onPress: (value: string, flag: boolean) => {
result.file = {value, flag};
},
items: [
{
label: 'new',
onPress: (value: string, flag: boolean) => {
result.new = {value, flag};
},
},
],
};
NativeCxxModuleExample?.setMenu(menu);
NativeFantom.flushMessageQueue(); // Flush the message queue to execute the callback
expect(result.file?.value).toBe('value');
expect(result.file?.flag).toBe(true);
expect(result.new?.value).toBe('another value');
expect(result.new?.flag).toBe(false);
});
it('verifies emitCustomDeviceEvent(...) returns the correct values', () => {
let events: {[key: string]: ?string} = {
foo: null,
bar: null,
};
RCTDeviceEventEmitter.addListener(
'foo',
(value: string) => (events.foo = value),
);
RCTDeviceEventEmitter.addListener(
'bar',
(value: string) => (events.bar = value),
);
NativeCxxModuleExample?.emitCustomDeviceEvent('foo');
NativeCxxModuleExample?.emitCustomDeviceEvent('bar');
NativeFantom.flushMessageQueue(); // Flush the message queue to execute the callbacks
expect(events.foo).toEqual([
true,
42,
'stringArg',
{type: 'one', level: 2},
]);
expect(events.foo).toEqual([
true,
42,
'stringArg',
{type: 'one', level: 2},
]);
});
});
@@ -26,6 +26,7 @@ module.exports = {
rootDir: path.resolve(__dirname, '../../..') /*:: as string */,
roots: [
'<rootDir>/packages/react-native',
'<rootDir>/packages/rn-tester',
'<rootDir>/packages/polyfills',
'<rootDir>/private/react-native-fantom',
],
@@ -7,6 +7,7 @@
#include "TesterAppDelegate.h"
#include <NativeCxxModuleExample/NativeCxxModuleExample.h>
#include <folly/dynamic.h>
#include <folly/json.h>
#include <glog/logging.h>
@@ -87,6 +88,8 @@ TesterAppDelegate::TesterAppDelegate(
return std::make_shared<NativeCPUTime>(jsInvoker);
} else if (name == NativeFantomTestSpecificMethods::kModuleName) {
return std::make_shared<NativeFantomTestSpecificMethods>(jsInvoker);
} else if (name == NativeCxxModuleExample::kModuleName) {
return std::make_shared<NativeCxxModuleExample>(jsInvoker);
} else {
return nullptr;
}