From b013af73f69671939bca9c4241d50c611ac6d857 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sat, 21 Jun 2025 14:14:06 +0200 Subject: [PATCH 1/4] Assert console.log messages --- apps/test-app/App.tsx | 2 +- apps/test-app/package.json | 1 + apps/test-app/tsconfig.json | 7 +- package-lock.json | 7 ++ packages/node-addon-examples/index.js | 22 ---- packages/node-addon-examples/package.json | 1 + .../scripts/cmake-projects.mts | 2 +- packages/node-addon-examples/src/index.ts | 102 ++++++++++++++++++ packages/node-addon-examples/tsconfig.json | 11 +- .../tsconfig.node-scripts.json | 12 +++ .../node-addon-examples/tsconfig.tests.json | 12 +++ tsconfig.json | 3 +- 12 files changed, 150 insertions(+), 32 deletions(-) delete mode 100644 packages/node-addon-examples/index.js create mode 100644 packages/node-addon-examples/src/index.ts create mode 100644 packages/node-addon-examples/tsconfig.node-scripts.json create mode 100644 packages/node-addon-examples/tsconfig.tests.json diff --git a/apps/test-app/App.tsx b/apps/test-app/App.tsx index 3ca4ddc2..8496319d 100644 --- a/apps/test-app/App.tsx +++ b/apps/test-app/App.tsx @@ -8,7 +8,7 @@ import { StatusText, } from "mocha-remote-react-native"; -import nodeAddonExamples from "@react-native-node-api/node-addon-examples"; +import { examples as nodeAddonExamples } from "@react-native-node-api/node-addon-examples"; function loadTests() { for (const [suiteName, examples] of Object.entries(nodeAddonExamples)) { diff --git a/apps/test-app/package.json b/apps/test-app/package.json index 526509b4..25a81671 100644 --- a/apps/test-app/package.json +++ b/apps/test-app/package.json @@ -21,6 +21,7 @@ "@react-native/metro-config": "0.79.0", "@react-native/typescript-config": "0.79.0", "@rnx-kit/metro-config": "^2.0.1", + "@types/mocha": "^10.0.10", "@types/react": "^19.0.0", "concurrently": "^9.1.2", "ferric-example": "^0.1.0", diff --git a/apps/test-app/tsconfig.json b/apps/test-app/tsconfig.json index 304ab4e2..42a8c0b4 100644 --- a/apps/test-app/tsconfig.json +++ b/apps/test-app/tsconfig.json @@ -1,3 +1,8 @@ { - "extends": "@react-native/typescript-config/tsconfig.json" + "extends": "@react-native/typescript-config/tsconfig.json", + "compilerOptions": { + "types": ["react-native", "mocha"] + }, + "files": ["App.tsx", "index.ts"], + "references": [{ "path": "./tsconfig.node-scripts.json" }] } diff --git a/package-lock.json b/package-lock.json index 1940bfc4..2a680b79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "@react-native/metro-config": "0.79.0", "@react-native/typescript-config": "0.79.0", "@rnx-kit/metro-config": "^2.0.1", + "@types/mocha": "^10.0.10", "@types/react": "^19.0.0", "concurrently": "^9.1.2", "ferric-example": "^0.1.0", @@ -5961,6 +5962,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.16.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.3.tgz", diff --git a/packages/node-addon-examples/index.js b/packages/node-addon-examples/index.js deleted file mode 100644 index e5bf5eeb..00000000 --- a/packages/node-addon-examples/index.js +++ /dev/null @@ -1,22 +0,0 @@ -module.exports = { - "1-getting-started": { - "1_hello_world/napi": () => require("./examples/1-getting-started/1_hello_world/napi/hello.js"), - "1_hello_world/node-addon-api": () => require("./examples/1-getting-started/1_hello_world/node-addon-api/hello.js"), - "1_hello_world/node-addon-api-addon-class": () => require("./examples/1-getting-started/1_hello_world/node-addon-api-addon-class/hello.js"), - "2_function_arguments/napi": () => require("./examples/1-getting-started/2_function_arguments/napi/addon.js"), - "2_function_arguments/node-addon-api": () => require("./examples/1-getting-started/2_function_arguments/node-addon-api/addon.js"), - "3_callbacks/napi": () => require("./examples/1-getting-started/3_callbacks/napi/addon.js"), - "3_callbacks/node-addon-api": () => require("./examples/1-getting-started/3_callbacks/node-addon-api/addon.js"), - "4_object_factory/napi": () => require("./examples/1-getting-started/4_object_factory/napi/addon.js"), - "4_object_factory/node-addon-api": () => require("./examples/1-getting-started/4_object_factory/node-addon-api/addon.js"), - "5_function_factory": () => require("./examples/1-getting-started/5_function_factory/napi/addon.js"), - }, - "5-async-work": { - // TODO: This crashes (SIGABRT) - // "async_work_thread_safe_function": () => require("./examples/5-async-work/async_work_thread_safe_function/napi/index.js"), - }, - "tests": { - "buffers": () => require("./tests/buffers/addon.js"), - "async": () => require("./tests/async/addon.js"), - }, -}; diff --git a/packages/node-addon-examples/package.json b/packages/node-addon-examples/package.json index 3ebee6f5..1ca8e725 100644 --- a/packages/node-addon-examples/package.json +++ b/packages/node-addon-examples/package.json @@ -1,6 +1,7 @@ { "name": "@react-native-node-api/node-addon-examples", "type": "commonjs", + "main": "dist/index.js", "private": true, "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { diff --git a/packages/node-addon-examples/scripts/cmake-projects.mts b/packages/node-addon-examples/scripts/cmake-projects.mts index ede02e2c..56aab0f0 100644 --- a/packages/node-addon-examples/scripts/cmake-projects.mts +++ b/packages/node-addon-examples/scripts/cmake-projects.mts @@ -5,7 +5,7 @@ export const EXAMPLES_DIR = path.resolve(import.meta.dirname, "../examples"); export const TESTS_DIR = path.resolve(import.meta.dirname, "../tests"); export const DIRS = [EXAMPLES_DIR, TESTS_DIR]; -export function findCMakeProjectsRecursively(dir): string[] { +export function findCMakeProjectsRecursively(dir: string): string[] { let results: string[] = []; const files = readdirSync(dir); diff --git a/packages/node-addon-examples/src/index.ts b/packages/node-addon-examples/src/index.ts new file mode 100644 index 00000000..5cbcd04a --- /dev/null +++ b/packages/node-addon-examples/src/index.ts @@ -0,0 +1,102 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ + +function assertLogs(cb: () => void, expectedMessages: string[]) { + const errors: Error[] = []; + // Spying on the console.log function, as the examples don't assert anything themselves + const originalLog = console.log; + console.log = (message: string, ...args: unknown[]) => { + const nextMessage = expectedMessages.shift(); + const combinedMessage = [message, ...args].map(String).join(" "); + if (nextMessage !== combinedMessage) { + errors.push(new Error(`Unexpected log message '${combinedMessage}'`)); + } + }; + try { + cb(); + if (expectedMessages.length > 0) { + errors.push( + new Error(`Missing expected message(s): ${expectedMessages.join(", ")}`) + ); + } + } finally { + console.log = originalLog; + } + // Throw and first error + const [firstError] = errors; + if (firstError) { + throw firstError; + } +} + +export const examples: Record void>> = { + "1-getting-started": { + "1_hello_world/napi": () => + assertLogs( + () => + require("../examples/1-getting-started/1_hello_world/napi/hello.js"), + ["world"] + ), + "1_hello_world/node-addon-api": () => + assertLogs( + () => + require("../examples/1-getting-started/1_hello_world/node-addon-api/hello.js"), + ["world"] + ), + "1_hello_world/node-addon-api-addon-class": () => + assertLogs( + () => + require("../examples/1-getting-started/1_hello_world/node-addon-api-addon-class/hello.js"), + ["world"] + ), + "2_function_arguments/napi": () => + assertLogs( + () => + require("../examples/1-getting-started/2_function_arguments/napi/addon.js"), + ["This should be eight: 8"] + ), + "2_function_arguments/node-addon-api": () => + assertLogs( + () => + require("../examples/1-getting-started/2_function_arguments/node-addon-api/addon.js"), + ["This should be eight: 8"] + ), + "3_callbacks/napi": () => + assertLogs( + () => + require("../examples/1-getting-started/3_callbacks/napi/addon.js"), + ["hello world"] + ), + "3_callbacks/node-addon-api": () => + assertLogs( + () => + require("../examples/1-getting-started/3_callbacks/node-addon-api/addon.js"), + ["hello world"] + ), + "4_object_factory/napi": () => + assertLogs( + () => + require("../examples/1-getting-started/4_object_factory/napi/addon.js"), + ["hello world"] + ), + "4_object_factory/node-addon-api": () => + assertLogs( + () => + require("../examples/1-getting-started/4_object_factory/node-addon-api/addon.js"), + ["hello world"] + ), + "5_function_factory": () => + assertLogs( + () => + require("../examples/1-getting-started/5_function_factory/napi/addon.js"), + ["hello world"] + ), + }, + "5-async-work": { + // TODO: This crashes (SIGABRT) + // "async_work_thread_safe_function": () => require("../examples/5-async-work/async_work_thread_safe_function/napi/index.js"), + }, + tests: { + buffers: () => require("../tests/buffers/addon.js"), + async: () => require("./tests/async/addon.js"), + }, +}; diff --git a/packages/node-addon-examples/tsconfig.json b/packages/node-addon-examples/tsconfig.json index 5e638b43..4b659503 100644 --- a/packages/node-addon-examples/tsconfig.json +++ b/packages/node-addon-examples/tsconfig.json @@ -1,8 +1,7 @@ { - "extends": "@tsconfig/node22/tsconfig.json", - "compilerOptions": { - "module": "node16", - "moduleResolution": "node16" - }, - "include": ["scripts"] + "files": [], + "references": [ + { "path": "./tsconfig.tests.json" }, + { "path": "./tsconfig.node-scripts.json" } + ] } diff --git a/packages/node-addon-examples/tsconfig.node-scripts.json b/packages/node-addon-examples/tsconfig.node-scripts.json new file mode 100644 index 00000000..e4ca4a5b --- /dev/null +++ b/packages/node-addon-examples/tsconfig.node-scripts.json @@ -0,0 +1,12 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "composite": true, + "emitDeclarationOnly": true, + "outDir": "dist", + "rootDir": "scripts", + "types": ["node", "read-pkg"] + }, + "include": ["scripts/**/*.mts"], + "exclude": [] +} diff --git a/packages/node-addon-examples/tsconfig.tests.json b/packages/node-addon-examples/tsconfig.tests.json new file mode 100644 index 00000000..629f51e3 --- /dev/null +++ b/packages/node-addon-examples/tsconfig.tests.json @@ -0,0 +1,12 @@ +{ + "extends": "@tsconfig/react-native", + "compilerOptions": { + "composite": true, + "noEmit": false, + "module": "commonjs", + "outDir": "dist", + "rootDir": "src", + "types": ["react-native"] + }, + "include": ["src/*.ts"] +} diff --git a/tsconfig.json b/tsconfig.json index 4b931432..b9af5629 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ { "path": "./packages/host/tsconfig.json" }, { "path": "./packages/gyp-to-cmake/tsconfig.json" }, { "path": "./packages/cmake-rn/tsconfig.json" }, - { "path": "./packages/ferric/tsconfig.json" } + { "path": "./packages/ferric/tsconfig.json" }, + { "path": "./packages/node-addon-examples/tsconfig.json" } ] } From 2f5770e3b0c2de273b1e0d8809aa95f72726cd74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Wed, 25 Jun 2025 21:36:46 +0200 Subject: [PATCH 2/4] Run tests conditionally --- apps/test-app/App.tsx | 57 ++++++++++++++++------- apps/test-app/package.json | 8 +++- apps/test-app/tsconfig.node-scripts.json | 11 +++++ packages/node-addon-examples/src/index.ts | 7 ++- 4 files changed, 62 insertions(+), 21 deletions(-) create mode 100644 apps/test-app/tsconfig.node-scripts.json diff --git a/apps/test-app/App.tsx b/apps/test-app/App.tsx index 8496319d..f0bbff7a 100644 --- a/apps/test-app/App.tsx +++ b/apps/test-app/App.tsx @@ -8,26 +8,47 @@ import { StatusText, } from "mocha-remote-react-native"; -import { examples as nodeAddonExamples } from "@react-native-node-api/node-addon-examples"; +import { suites as nodeAddonExamplesSuites } from "@react-native-node-api/node-addon-examples"; -function loadTests() { - for (const [suiteName, examples] of Object.entries(nodeAddonExamples)) { - describe(suiteName, () => { - for (const [exampleName, requireExample] of Object.entries(examples)) { - it(exampleName, async () => { - const test = requireExample(); - if (test instanceof Function) { - await test(); - } - }); - } - }); - } +function describeIf( + condition: boolean, + title: string, + fn: (this: Mocha.Suite) => void +) { + return condition ? describe(title, fn) : describe.skip(title, fn); +} + +type Context = { + allTests?: boolean; + nodeAddonExamples?: boolean; + ferricExample?: boolean; +}; + +function loadTests({ + allTests = false, + nodeAddonExamples = allTests, + ferricExample = allTests, +}: Context) { + describeIf(nodeAddonExamples, "Node Addon Examples", () => { + for (const [suiteName, examples] of Object.entries( + nodeAddonExamplesSuites + )) { + describe(suiteName, () => { + for (const [exampleName, requireExample] of Object.entries(examples)) { + it(exampleName, async () => { + const test = requireExample(); + if (test instanceof Function) { + await test(); + } + }); + } + }); + } + }); - describe("ferric-example", () => { - it("exports a callable sum function", () => { - // eslint-disable-next-line @typescript-eslint/no-require-imports - const exampleAddon = require("ferric-example"); + describeIf(ferricExample, "ferric-example", () => { + it("exports a callable sum function", async () => { + const exampleAddon = await import("ferric-example"); const result = exampleAddon.sum(1, 3); if (result !== 4) { throw new Error(`Expected 1 + 3 to equal 4, but got ${result}`); diff --git a/apps/test-app/package.json b/apps/test-app/package.json index 25a81671..dc360b3a 100644 --- a/apps/test-app/package.json +++ b/apps/test-app/package.json @@ -8,7 +8,13 @@ "ios": "react-native run-ios --no-packager", "pod-install": "cd ios && pod install", "test:android": "mocha-remote --exit-on-error -- concurrently --kill-others-on-fail --passthrough-arguments npm:metro 'npm:android -- {@}' --", - "test:ios": "mocha-remote --exit-on-error -- concurrently --passthrough-arguments --kill-others-on-fail npm:metro 'npm:ios -- {@}' --" + "test:android:allTests": "MOCHA_REMOTE_CONTEXT=allTests npm run test:android -- ", + "test:android:nodeAddonExamples": "MOCHA_REMOTE_CONTEXT=nodeAddonExamples npm run test:android -- ", + "test:android:ferricExample": "MOCHA_REMOTE_CONTEXT=ferricExample npm run test:android -- ", + "test:ios": "mocha-remote --exit-on-error -- concurrently --passthrough-arguments --kill-others-on-fail npm:metro 'npm:ios -- {@}' --", + "test:ios:allTests": "MOCHA_REMOTE_CONTEXT=allTests npm run test:ios -- ", + "test:ios:nodeAddonExamples": "MOCHA_REMOTE_CONTEXT=nodeAddonExamples npm run test:ios -- ", + "test:ios:ferricExample": "MOCHA_REMOTE_CONTEXT=ferricExample npm run test:ios -- " }, "dependencies": { "@babel/core": "^7.26.10", diff --git a/apps/test-app/tsconfig.node-scripts.json b/apps/test-app/tsconfig.node-scripts.json new file mode 100644 index 00000000..66e44e08 --- /dev/null +++ b/apps/test-app/tsconfig.node-scripts.json @@ -0,0 +1,11 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "composite": true, + "emitDeclarationOnly": true, + "outDir": "dist", + "rootDir": "scripts", + "types": ["node"] + }, + "include": ["scripts/**/*.ts"] +} diff --git a/packages/node-addon-examples/src/index.ts b/packages/node-addon-examples/src/index.ts index 5cbcd04a..d2228f84 100644 --- a/packages/node-addon-examples/src/index.ts +++ b/packages/node-addon-examples/src/index.ts @@ -28,7 +28,10 @@ function assertLogs(cb: () => void, expectedMessages: string[]) { } } -export const examples: Record void>> = { +export const suites: Record< + string, + Record void | (() => void)> +> = { "1-getting-started": { "1_hello_world/napi": () => assertLogs( @@ -97,6 +100,6 @@ export const examples: Record void>> = { }, tests: { buffers: () => require("../tests/buffers/addon.js"), - async: () => require("./tests/async/addon.js"), + async: () => require("../tests/async/addon.js"), }, }; From 1a2536da0e7faf09d282b23d142a2b84d283d83e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Wed, 25 Jun 2025 21:37:29 +0200 Subject: [PATCH 3/4] Run all tests on CI --- .github/workflows/check.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 5c8c9199..aadcd557 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -81,7 +81,7 @@ jobs: - run: npm run pod-install working-directory: apps/test-app - name: Run tests (iOS) - run: npm run test:ios + run: npm run test:ios:allTests # TODO: Enable release mode when it works # run: npm run test:ios -- --mode Release working-directory: apps/test-app @@ -156,7 +156,7 @@ jobs: adb logcat > emulator-logcat.txt 2>&1 & LOGCAT_PID=$! # Build, install and run the app - npm run test:android -- --mode Release + npm run test:android:allTests -- --mode Release # Wait a bit for the sub-process to terminate, before terminating the emulator sleep 5 # Stop logcat From 34408c3fa768621256a80979fa2e1300fad3c19e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sat, 28 Jun 2025 20:47:21 +0200 Subject: [PATCH 4/4] Use require instead of dynamic import --- apps/test-app/App.tsx | 5 +++-- packages/ferric-example/package.json | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/test-app/App.tsx b/apps/test-app/App.tsx index f0bbff7a..27fca38e 100644 --- a/apps/test-app/App.tsx +++ b/apps/test-app/App.tsx @@ -47,8 +47,9 @@ function loadTests({ }); describeIf(ferricExample, "ferric-example", () => { - it("exports a callable sum function", async () => { - const exampleAddon = await import("ferric-example"); + it("exports a callable sum function", () => { + /* eslint-disable-next-line @typescript-eslint/no-require-imports -- TODO: Determine why a dynamic import doesn't work on Android */ + const exampleAddon = require("ferric-example"); const result = exampleAddon.sum(1, 3); if (result !== 4) { throw new Error(`Expected 1 + 3 to equal 4, but got ${result}`); diff --git a/packages/ferric-example/package.json b/packages/ferric-example/package.json index c67eaa98..0e747326 100644 --- a/packages/ferric-example/package.json +++ b/packages/ferric-example/package.json @@ -1,6 +1,7 @@ { "name": "@react-native-node-api/ferric-example", "private": true, + "type": "commonjs", "version": "0.1.0", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": {