diff --git a/packages/rn-tester/NativeCxxModuleExample/NativeCxxModuleExample.cpp b/packages/rn-tester/NativeCxxModuleExample/NativeCxxModuleExample.cpp index 0789565be9b..f36996e15da 100644 --- a/packages/rn-tester/NativeCxxModuleExample/NativeCxxModuleExample.cpp +++ b/packages/rn-tester/NativeCxxModuleExample/NativeCxxModuleExample.cpp @@ -7,9 +7,22 @@ #include "NativeCxxModuleExample.h" #include +#include +#include +#include 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 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& 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))); }); } diff --git a/packages/rn-tester/NativeCxxModuleExample/__tests__/NativeCxxModuleExample-itest.js b/packages/rn-tester/NativeCxxModuleExample/__tests__/NativeCxxModuleExample-itest.js new file mode 100644 index 00000000000..f75082e26e4 --- /dev/null +++ b/packages/rn-tester/NativeCxxModuleExample/__tests__/NativeCxxModuleExample-itest.js @@ -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}, + ]); + }); +}); diff --git a/private/react-native-fantom/config/jest.config.js b/private/react-native-fantom/config/jest.config.js index 63c3809c702..908ac1c586b 100644 --- a/private/react-native-fantom/config/jest.config.js +++ b/private/react-native-fantom/config/jest.config.js @@ -26,6 +26,7 @@ module.exports = { rootDir: path.resolve(__dirname, '../../..') /*:: as string */, roots: [ '/packages/react-native', + '/packages/rn-tester', '/packages/polyfills', '/private/react-native-fantom', ], diff --git a/private/react-native-fantom/tester/src/TesterAppDelegate.cpp b/private/react-native-fantom/tester/src/TesterAppDelegate.cpp index e4ec7bba2ca..757f31ffc17 100644 --- a/private/react-native-fantom/tester/src/TesterAppDelegate.cpp +++ b/private/react-native-fantom/tester/src/TesterAppDelegate.cpp @@ -7,6 +7,7 @@ #include "TesterAppDelegate.h" +#include #include #include #include @@ -87,6 +88,8 @@ TesterAppDelegate::TesterAppDelegate( return std::make_shared(jsInvoker); } else if (name == NativeFantomTestSpecificMethods::kModuleName) { return std::make_shared(jsInvoker); + } else if (name == NativeCxxModuleExample::kModuleName) { + return std::make_shared(jsInvoker); } else { return nullptr; }