From 16c1fa7bb48faf848d9b3c0f43428592fa236ced Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Branimir=20Karad=C5=BEi=C4=87=20=28via=20Copilot=29?= <223556219+Copilot@users.noreply.github.com> Date: Tue, 16 Jun 2026 13:29:19 -0700 Subject: [PATCH 1/4] Hop 3: route unhandled promise rejections to the host handler Previously a fire-and-forget rejected promise (e.g. an un-awaited fetch() that fails, or a throw inside a .then with no .catch) vanished silently: AppRuntime::Dispatch only catches synchronous Napi::Error throws, and no engine had a promise-rejection tracker wired, so the embedder's UnhandledExceptionHandler (the Sentry/Bugsnag hook) never fired and the process exited 0. This is hop 3 of BabylonJS/JsRuntimeHost#196. - AppRuntime::Options gains opt-in EnableUnhandledPromiseRejectionTracking (default false = no behavior change; no tracker is installed). When set, unhandled rejections route into the existing UnhandledExceptionHandler. - AppRuntime::OnUnhandledPromiseRejection(napi_value) wraps the rejection reason as a Napi::Error (forwarding Error-like reasons as-is, stringifying others) and invokes the handler. - V8 (tested): Isolate::SetPromiseRejectCallback. Reporting is deferred via Dispatch, so a rejection handled synchronously in the same turn (`const p = Promise.reject(e); p.catch(...)`) is dropped before it is reported -- matching Node semantics. Promise/reason held in v8::Global handles across the hop. - Chakra: the OS EdgeMode runtime (chakrart.h) exposes no host promise-rejection tracker (JsSetHostPromiseRejectionTracker is ChakraCore-only), so the option is a documented no-op on this backend. - Native tests (Shared.cpp): a fire-and-forget rejection reaches the handler; a synchronously-handled rejection does not. Skipped on the Chakra backend. JavaScriptCore and JSI trackers are follow-ups (see #196). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Core/AppRuntime/Include/Babylon/AppRuntime.h | 15 +++ Core/AppRuntime/Source/AppRuntime.cpp | 15 +++ Core/AppRuntime/Source/AppRuntime_Chakra.cpp | 6 ++ Core/AppRuntime/Source/AppRuntime_V8.cpp | 102 +++++++++++++++++++ Tests/UnitTests/CMakeLists.txt | 1 + Tests/UnitTests/Shared/Shared.cpp | 65 ++++++++++++ 6 files changed, 204 insertions(+) diff --git a/Core/AppRuntime/Include/Babylon/AppRuntime.h b/Core/AppRuntime/Include/Babylon/AppRuntime.h index f6ca7dfd..8183f134 100644 --- a/Core/AppRuntime/Include/Babylon/AppRuntime.h +++ b/Core/AppRuntime/Include/Babylon/AppRuntime.h @@ -21,6 +21,15 @@ namespace Babylon // Optional handler for unhandled exceptions. std::function UnhandledExceptionHandler{DefaultUnhandledExceptionHandler}; + // When true, unhandled promise rejections are routed to UnhandledExceptionHandler so + // the embedder's crash/telemetry pipeline can observe fire-and-forget failures (e.g. an + // un-awaited fetch() that rejects). Defaults to false to preserve existing behavior -- + // when false, no per-engine rejection tracker is installed. Reporting is deferred to the + // end of the current turn, so a rejection that is handled synchronously (e.g. + // `const p = Promise.reject(e); p.catch(...)`) does not reach the handler. Implemented for + // the Chakra and V8 engines. + bool EnableUnhandledPromiseRejectionTracking{false}; + // Defines whether to enable the debugger. Only implemented for V8 and Chakra. bool EnableDebugger{false}; @@ -45,6 +54,12 @@ namespace Babylon void Dispatch(Dispatchable callback); + // Routes an unhandled promise rejection to the embedder's UnhandledExceptionHandler. Called + // by the per-engine promise-rejection tracker installed in RunEnvironmentTier when + // Options::EnableUnhandledPromiseRejectionTracking is set. `reason` is the rejection value as + // a napi_value. Intended for internal (engine-implementation) use. + void OnUnhandledPromiseRejection(napi_value reason); + // Default unhandled exception handler that outputs the error message to the program output. static void BABYLON_API DefaultUnhandledExceptionHandler(const Napi::Error& error); diff --git a/Core/AppRuntime/Source/AppRuntime.cpp b/Core/AppRuntime/Source/AppRuntime.cpp index 99298df2..53b3dae0 100644 --- a/Core/AppRuntime/Source/AppRuntime.cpp +++ b/Core/AppRuntime/Source/AppRuntime.cpp @@ -125,4 +125,19 @@ namespace Babylon }); }); } + + void AppRuntime::OnUnhandledPromiseRejection(napi_value reason) + { + Napi::Env env = m_impl->m_env.value(); + const Napi::Value reasonValue{env, reason}; + + // A promise can be rejected with any value. If it is an Error-like object, forward it as-is + // (preserving its message/stack/cause); otherwise wrap its string form so the handler always + // receives a Napi::Error. + const Napi::Error error = reasonValue.IsObject() + ? Napi::Error{env, reason} + : Napi::Error::New(env, reasonValue.ToString().Utf8Value()); + + m_options.UnhandledExceptionHandler(error); + } } diff --git a/Core/AppRuntime/Source/AppRuntime_Chakra.cpp b/Core/AppRuntime/Source/AppRuntime_Chakra.cpp index 2329ad30..09fcb2ed 100644 --- a/Core/AppRuntime/Source/AppRuntime_Chakra.cpp +++ b/Core/AppRuntime/Source/AppRuntime_Chakra.cpp @@ -50,6 +50,12 @@ namespace Babylon }); }, &dispatchFunction)); + + // Options::EnableUnhandledPromiseRejectionTracking is not honored on this backend: the OS + // EdgeMode Chakra runtime (chakrart.h) does not expose a host promise-rejection tracker + // (JsSetHostPromiseRejectionTracker is ChakraCore-only), so there is no hook to route + // unhandled rejections from. The option is implemented for the V8 backend. + ThrowIfFailed(JsProjectWinRTNamespace(L"Windows")); if (m_options.EnableDebugger) diff --git a/Core/AppRuntime/Source/AppRuntime_V8.cpp b/Core/AppRuntime/Source/AppRuntime_V8.cpp index 1297fdbb..b0a8a1ce 100644 --- a/Core/AppRuntime/Source/AppRuntime_V8.cpp +++ b/Core/AppRuntime/Source/AppRuntime_V8.cpp @@ -8,6 +8,8 @@ #endif #include +#include +#include namespace Babylon { @@ -61,6 +63,93 @@ namespace Babylon }; std::unique_ptr Module::s_module; + + // Tracks promises rejected without a handler so they can be reported once the current turn + // settles. V8 fires kPromiseRejectWithNoHandler when a promise is rejected with no handler + // and kPromiseHandlerAddedAfterReject if one is attached afterwards; reporting is deferred + // (via AppRuntime::Dispatch) so a synchronous `Promise.reject(e); ...; p.catch(...)` is + // removed before it is ever reported. Keyed by promise identity hash; the promise and reason + // are held in v8::Global handles so they survive until the deferred flush runs. + struct V8RejectionTracker + { + AppRuntime* runtime{}; + v8::Isolate* isolate{}; + std::unordered_map, v8::Global>> unhandled{}; + bool flushScheduled{false}; + }; + + // The promise-rejection callback is a bare function pointer with no user-data argument. Each + // AppRuntime owns a dedicated isolate running on its own thread, and V8 invokes the callback + // on that thread, so a thread_local pointer associates the callback with the right tracker + // without risking isolate-data-slot collisions with the Node-API shim. + thread_local V8RejectionTracker* t_rejectionTracker{nullptr}; + + // Mirrors v8impl::JsValueFromV8LocalValue (js_native_api_v8.h), which is internal to the + // Node-API V8 shim and not on the public include path. + static_assert(sizeof(v8::Local) == sizeof(napi_value), + "Cannot convert between v8::Local and napi_value"); + napi_value JsValueFromV8LocalValue(v8::Local local) + { + return reinterpret_cast(*local); + } + + void FlushUnhandledRejections(V8RejectionTracker& tracker) + { + tracker.flushScheduled = false; + + v8::Isolate::Scope isolateScope{tracker.isolate}; + v8::HandleScope handleScope{tracker.isolate}; + for (auto& [hash, entry] : tracker.unhandled) + { + const v8::Local reason = entry.second.Get(tracker.isolate); + tracker.runtime->OnUnhandledPromiseRejection(JsValueFromV8LocalValue(reason)); + } + tracker.unhandled.clear(); + } + + void OnPromiseReject(v8::PromiseRejectMessage message) + { + V8RejectionTracker* tracker{t_rejectionTracker}; + if (tracker == nullptr) + { + return; + } + + v8::HandleScope handleScope{tracker->isolate}; + const v8::Local promise{message.GetPromise()}; + const int hash{promise->GetIdentityHash()}; + + switch (message.GetEvent()) + { + case v8::kPromiseRejectWithNoHandler: + { + const v8::Local reason{message.GetValue()}; + tracker->unhandled[hash] = { + v8::Global{tracker->isolate, promise}, + v8::Global{tracker->isolate, reason}}; + + if (!tracker->flushScheduled) + { + tracker->flushScheduled = true; + tracker->runtime->Dispatch([tracker](Napi::Env) { + FlushUnhandledRejections(*tracker); + }); + } + break; + } + case v8::kPromiseHandlerAddedAfterReject: + { + tracker->unhandled.erase(hash); + break; + } + default: + { + // kPromiseRejectAfterResolved / kPromiseResolveAfterResolved carry no actionable + // unhandled-rejection signal. + break; + } + } + } } void AppRuntime::RunEnvironmentTier(const char* executablePath) @@ -81,6 +170,13 @@ namespace Babylon Napi::Env env = Napi::Attach(context); + V8RejectionTracker rejectionTracker{this, isolate, {}, false}; + if (m_options.EnableUnhandledPromiseRejectionTracking) + { + t_rejectionTracker = &rejectionTracker; + isolate->SetPromiseRejectCallback(OnPromiseReject); + } + #ifdef ENABLE_V8_INSPECTOR std::optional agent; if (m_options.EnableDebugger) @@ -104,6 +200,12 @@ namespace Babylon } #endif + if (m_options.EnableUnhandledPromiseRejectionTracking) + { + isolate->SetPromiseRejectCallback(nullptr); + t_rejectionTracker = nullptr; + } + Napi::Detach(env); } diff --git a/Tests/UnitTests/CMakeLists.txt b/Tests/UnitTests/CMakeLists.txt index 2dbc7619..c12b12f8 100644 --- a/Tests/UnitTests/CMakeLists.txt +++ b/Tests/UnitTests/CMakeLists.txt @@ -45,6 +45,7 @@ endif() add_executable(UnitTests ${SOURCES} ${SCRIPTS} ${TYPE_SCRIPTS} ${ASSETS}) target_compile_definitions(UnitTests PRIVATE JSRUNTIMEHOST_PLATFORM="${JSRUNTIMEHOST_PLATFORM}") +target_compile_definitions(UnitTests PRIVATE JSRUNTIMEHOST_NAPI_ENGINE="${NAPI_JAVASCRIPT_ENGINE}") # The V8JSI Node-API shim does not implement napi_create_dataview, so the # CreateDataViewRejectsOverflowingRange test is compiled out on that backend. diff --git a/Tests/UnitTests/Shared/Shared.cpp b/Tests/UnitTests/Shared/Shared.cpp index a920fa1f..0cf9569d 100644 --- a/Tests/UnitTests/Shared/Shared.cpp +++ b/Tests/UnitTests/Shared/Shared.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include namespace @@ -279,6 +280,70 @@ TEST(AppRuntime, DestroyDoesNotDeadlock) testThread.join(); } +TEST(AppRuntime, UnhandledPromiseRejectionReachesHandler) +{ + // EnableUnhandledPromiseRejectionTracking is only honored on engines that expose a host + // promise-rejection hook. The OS EdgeMode Chakra runtime does not, so skip there. +#if defined(JSRUNTIMEHOST_NAPI_ENGINE) + if (std::string_view{JSRUNTIMEHOST_NAPI_ENGINE} == "Chakra") + { + GTEST_SKIP() << "EdgeMode Chakra does not expose a host promise-rejection tracker"; + } +#endif + + // A fire-and-forget rejected promise (no handler ever attached) must reach the embedder's + // UnhandledExceptionHandler when EnableUnhandledPromiseRejectionTracking is set. + Babylon::AppRuntime::Options options{}; + options.EnableUnhandledPromiseRejectionTracking = true; + + std::promise rejectionMessage; + options.UnhandledExceptionHandler = [&rejectionMessage](const Napi::Error& error) { + rejectionMessage.set_value(error.Message()); + }; + + Babylon::AppRuntime runtime{options}; + + Babylon::ScriptLoader loader{runtime}; + loader.Eval("Promise.reject(new Error('boom from fire-and-forget'));", ""); + + auto future = rejectionMessage.get_future(); + ASSERT_EQ(future.wait_for(std::chrono::seconds(30)), std::future_status::ready) + << "unhandled rejection did not reach the host handler"; + EXPECT_NE(future.get().find("boom from fire-and-forget"), std::string::npos); +} + +TEST(AppRuntime, SynchronouslyHandledRejectionDoesNotReachHandler) +{ +#if defined(JSRUNTIMEHOST_NAPI_ENGINE) + if (std::string_view{JSRUNTIMEHOST_NAPI_ENGINE} == "Chakra") + { + GTEST_SKIP() << "EdgeMode Chakra does not expose a host promise-rejection tracker"; + } +#endif + + // A rejection that is handled synchronously in the same turn must NOT reach the handler -- + // reporting is deferred to the end of the turn, by which point the .catch has been attached. + Babylon::AppRuntime::Options options{}; + options.EnableUnhandledPromiseRejectionTracking = true; + + std::atomic handlerFired{false}; + options.UnhandledExceptionHandler = [&handlerFired](const Napi::Error&) { + handlerFired = true; + }; + + Babylon::AppRuntime runtime{options}; + + Babylon::ScriptLoader loader{runtime}; + loader.Eval("const p = Promise.reject(new Error('handled')); p.catch(() => {});", ""); + + // Round-trip a dispatch so any deferred rejection-flush task has run before we check. + std::promise drained; + loader.Dispatch([&drained](Napi::Env) { drained.set_value(); }); + drained.get_future().wait(); + + EXPECT_FALSE(handlerFired.load()) << "a synchronously-handled rejection must not reach the host handler"; +} + // The V8JSI Node-API shim does not implement napi_create_dataview / // napi_get_dataview_info (its DataView::New throws "TODO"), so this native test // only builds on the Chakra, V8, and JavaScriptCore backends. The size_t-width From d91c28c37a2e35e0776ea5029af2f76699398e18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Branimir=20Karad=C5=BEi=C4=87=20=28via=20Copilot=29?= <223556219+Copilot@users.noreply.github.com> Date: Tue, 16 Jun 2026 13:44:07 -0700 Subject: [PATCH 2/4] Fix CI: gate unhandled-rejection tests to the V8 backend The tests enabled tracking and waited for a report, but only V8 implements the tracker, so on JavaScriptCore/JSI/EdgeMode-Chakra the ReachesHandler test timed out (30s) and failed the suite. Skip both unless the engine is V8. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Tests/UnitTests/Shared/Shared.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Tests/UnitTests/Shared/Shared.cpp b/Tests/UnitTests/Shared/Shared.cpp index 0cf9569d..8530b04c 100644 --- a/Tests/UnitTests/Shared/Shared.cpp +++ b/Tests/UnitTests/Shared/Shared.cpp @@ -282,12 +282,13 @@ TEST(AppRuntime, DestroyDoesNotDeadlock) TEST(AppRuntime, UnhandledPromiseRejectionReachesHandler) { - // EnableUnhandledPromiseRejectionTracking is only honored on engines that expose a host - // promise-rejection hook. The OS EdgeMode Chakra runtime does not, so skip there. + // EnableUnhandledPromiseRejectionTracking is only implemented on the V8 backend so far (the OS + // EdgeMode Chakra runtime exposes no host promise-rejection hook; JavaScriptCore/JSI are + // follow-ups). Skip elsewhere so the test doesn't hang waiting for a report that never comes. #if defined(JSRUNTIMEHOST_NAPI_ENGINE) - if (std::string_view{JSRUNTIMEHOST_NAPI_ENGINE} == "Chakra") + if (std::string_view{JSRUNTIMEHOST_NAPI_ENGINE} != "V8") { - GTEST_SKIP() << "EdgeMode Chakra does not expose a host promise-rejection tracker"; + GTEST_SKIP() << "unhandled promise rejection tracking is only implemented for the V8 backend"; } #endif @@ -314,10 +315,11 @@ TEST(AppRuntime, UnhandledPromiseRejectionReachesHandler) TEST(AppRuntime, SynchronouslyHandledRejectionDoesNotReachHandler) { + // Only the V8 backend implements unhandled promise rejection tracking (see the note above). #if defined(JSRUNTIMEHOST_NAPI_ENGINE) - if (std::string_view{JSRUNTIMEHOST_NAPI_ENGINE} == "Chakra") + if (std::string_view{JSRUNTIMEHOST_NAPI_ENGINE} != "V8") { - GTEST_SKIP() << "EdgeMode Chakra does not expose a host promise-rejection tracker"; + GTEST_SKIP() << "unhandled promise rejection tracking is only implemented for the V8 backend"; } #endif From 3fc9dc59dbe82498905ba3d54c9a7693062ba7c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Branimir=20Karad=C5=BEi=C4=87=20=28via=20Copilot=29?= <223556219+Copilot@users.noreply.github.com> Date: Tue, 16 Jun 2026 13:55:58 -0700 Subject: [PATCH 3/4] Fix JSI build: keep napi_value->Napi::Error conversion in the V8 backend The JSI Node-API shim's napi.h has no Napi::Value/Error(napi_env, napi_value) constructor (it wraps jsi::Value), so the shared AppRuntime.cpp could not compile for JSI. OnUnhandledPromiseRejection now takes a const Napi::Error&, and the napi_value->Napi::Error wrapping is done in AppRuntime_V8.cpp (the V8/standard shim that supports it). Also drops an unused structured-binding to satisfy -Werror. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Core/AppRuntime/Include/Babylon/AppRuntime.h | 6 ++--- Core/AppRuntime/Source/AppRuntime.cpp | 14 +++--------- Core/AppRuntime/Source/AppRuntime_V8.cpp | 24 +++++++++++++++----- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/Core/AppRuntime/Include/Babylon/AppRuntime.h b/Core/AppRuntime/Include/Babylon/AppRuntime.h index 8183f134..cab418e6 100644 --- a/Core/AppRuntime/Include/Babylon/AppRuntime.h +++ b/Core/AppRuntime/Include/Babylon/AppRuntime.h @@ -56,9 +56,9 @@ namespace Babylon // Routes an unhandled promise rejection to the embedder's UnhandledExceptionHandler. Called // by the per-engine promise-rejection tracker installed in RunEnvironmentTier when - // Options::EnableUnhandledPromiseRejectionTracking is set. `reason` is the rejection value as - // a napi_value. Intended for internal (engine-implementation) use. - void OnUnhandledPromiseRejection(napi_value reason); + // Options::EnableUnhandledPromiseRejectionTracking is set. Intended for internal + // (engine-implementation) use. + void OnUnhandledPromiseRejection(const Napi::Error& error); // Default unhandled exception handler that outputs the error message to the program output. static void BABYLON_API DefaultUnhandledExceptionHandler(const Napi::Error& error); diff --git a/Core/AppRuntime/Source/AppRuntime.cpp b/Core/AppRuntime/Source/AppRuntime.cpp index 53b3dae0..acb8e20e 100644 --- a/Core/AppRuntime/Source/AppRuntime.cpp +++ b/Core/AppRuntime/Source/AppRuntime.cpp @@ -126,18 +126,10 @@ namespace Babylon }); } - void AppRuntime::OnUnhandledPromiseRejection(napi_value reason) + void AppRuntime::OnUnhandledPromiseRejection(const Napi::Error& error) { - Napi::Env env = m_impl->m_env.value(); - const Napi::Value reasonValue{env, reason}; - - // A promise can be rejected with any value. If it is an Error-like object, forward it as-is - // (preserving its message/stack/cause); otherwise wrap its string form so the handler always - // receives a Napi::Error. - const Napi::Error error = reasonValue.IsObject() - ? Napi::Error{env, reason} - : Napi::Error::New(env, reasonValue.ToString().Utf8Value()); - + // The reason is wrapped into a Napi::Error by the engine implementation (the napi_value -> + // Napi::Value bridge is shim-specific), so this just forwards to the embedder's handler. m_options.UnhandledExceptionHandler(error); } } diff --git a/Core/AppRuntime/Source/AppRuntime_V8.cpp b/Core/AppRuntime/Source/AppRuntime_V8.cpp index b0a8a1ce..6a4b8958 100644 --- a/Core/AppRuntime/Source/AppRuntime_V8.cpp +++ b/Core/AppRuntime/Source/AppRuntime_V8.cpp @@ -93,16 +93,28 @@ namespace Babylon return reinterpret_cast(*local); } - void FlushUnhandledRejections(V8RejectionTracker& tracker) + // Wrap a rejection reason as a Napi::Error. An Error-like object is forwarded as-is + // (preserving message/stack/cause); any other value is stringified so the embedder's handler + // always receives a Napi::Error. Done here (not in shared code) because the napi_value -> + // Napi::Value bridge is specific to the V8/standard Node-API shim. + Napi::Error ToError(Napi::Env env, napi_value reason) + { + const Napi::Value reasonValue{env, reason}; + return reasonValue.IsObject() + ? Napi::Error{env, reason} + : Napi::Error::New(env, reasonValue.ToString().Utf8Value()); + } + + void FlushUnhandledRejections(V8RejectionTracker& tracker, Napi::Env env) { tracker.flushScheduled = false; v8::Isolate::Scope isolateScope{tracker.isolate}; v8::HandleScope handleScope{tracker.isolate}; - for (auto& [hash, entry] : tracker.unhandled) + for (auto& entry : tracker.unhandled) { - const v8::Local reason = entry.second.Get(tracker.isolate); - tracker.runtime->OnUnhandledPromiseRejection(JsValueFromV8LocalValue(reason)); + const v8::Local reason = entry.second.second.Get(tracker.isolate); + tracker.runtime->OnUnhandledPromiseRejection(ToError(env, JsValueFromV8LocalValue(reason))); } tracker.unhandled.clear(); } @@ -131,8 +143,8 @@ namespace Babylon if (!tracker->flushScheduled) { tracker->flushScheduled = true; - tracker->runtime->Dispatch([tracker](Napi::Env) { - FlushUnhandledRejections(*tracker); + tracker->runtime->Dispatch([tracker](Napi::Env env) { + FlushUnhandledRejections(*tracker, env); }); } break; From 1ec115ed7d420010eab457df4c659020e249d79c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Branimir=20Karad=C5=BEi=C4=87=20=28via=20Copilot=29?= <223556219+Copilot@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:27:15 -0700 Subject: [PATCH 4/4] Fix Android CI: define JSRUNTIMEHOST_NAPI_ENGINE for the JNI test target The Android test app compiles Shared.cpp via its own cpp/CMakeLists.txt, which did not define JSRUNTIMEHOST_NAPI_ENGINE, so the engine-gated unhandled-rejection tests were not skipped on the (non-V8) JavaScriptCore backend and the ReachesHandler test timed out (30s). Mirror the desktop test target's define. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt b/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt index 0af5caa8..c870151c 100644 --- a/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt +++ b/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt @@ -22,6 +22,7 @@ add_library(UnitTestsJNI SHARED ${UNIT_TESTS_DIR}/Shared/Shared.cpp) target_compile_definitions(UnitTestsJNI PRIVATE JSRUNTIMEHOST_PLATFORM="${JSRUNTIMEHOST_PLATFORM}") +target_compile_definitions(UnitTestsJNI PRIVATE JSRUNTIMEHOST_NAPI_ENGINE="${NAPI_JAVASCRIPT_ENGINE}") target_compile_definitions(UnitTestsJNI PRIVATE ARCANA_TEST_HOOKS) target_include_directories(UnitTestsJNI