diff --git a/test-app/app/src/main/assets/app/mainpage.js b/test-app/app/src/main/assets/app/mainpage.js index 06a0aa08f..b3d599c43 100644 --- a/test-app/app/src/main/assets/app/mainpage.js +++ b/test-app/app/src/main/assets/app/mainpage.js @@ -75,4 +75,4 @@ require('./tests/testQueueMicrotask'); // ES MODULE TESTS __log("=== Running ES Modules Tests ==="); -require("./tests/testESModules"); +require("./tests/testESModules.mjs"); diff --git a/test-app/app/src/main/assets/app/tests/testESModules.js b/test-app/app/src/main/assets/app/tests/testESModules.js deleted file mode 100644 index a33910b43..000000000 --- a/test-app/app/src/main/assets/app/tests/testESModules.js +++ /dev/null @@ -1,103 +0,0 @@ -function runESModuleTests() { - var passed = 0; - var failed = 0; - - // Test 1: Load .mjs files as ES modules - console.log("\n--- Test 1: Loading .mjs files as ES modules ---"); - try { - var moduleExports = require("~/testSimpleESModule.mjs"); - if (moduleExports && moduleExports !== null) { - console.log("Module exports:", JSON.stringify(moduleExports)); - passed++; - } else { - console.log("❌ FAIL: ES Module loaded but exports are null"); - failed++; - } - if (moduleExports.moduleType === "ES Module") { - console.log(" - moduleType check passed"); - passed++; - } else { - console.log("❌ FAIL: moduleType check failed"); - failed++; - } - } catch (e) { - console.log("❌ FAIL: Error loading ES module:", e.message); - failed++; - } - - // Test 2: Test import.meta functionality - console.log("\n--- Test 2: Testing import.meta functionality ---"); - try { - var importMetaModule = require("~/testImportMeta.mjs"); - if (importMetaModule && importMetaModule.default && typeof importMetaModule.default === 'function') { - var metaResults = importMetaModule.default(); - console.log("import.meta test results:", JSON.stringify(metaResults, null, 2)); - - if (metaResults && metaResults.hasImportMeta && metaResults.hasUrl && metaResults.hasDirname) { - // console.log(" - import.meta.url:", metaResults.url); - // console.log(" - import.meta.dirname:", metaResults.dirname); - passed++; - } else { - console.log("❌ FAIL: import.meta properties missing"); - console.log(" - hasImportMeta:", metaResults?.hasImportMeta); - console.log(" - hasUrl:", metaResults?.hasUrl); - console.log(" - hasDirname:", metaResults?.hasDirname); - failed++; - } - } else { - console.log("❌ FAIL: import.meta module has no default export function"); - failed++; - } - } catch (e) { - console.log("❌ FAIL: Error testing import.meta:", e.message); - console.log("Stack trace:", e.stack); - failed++; - } - - // Test 3: Test Worker enhancements - console.log("\n--- Test 3: Testing Worker enhancements ---"); - try { - var workerModule = require("~/testWorkerFeatures.mjs"); - if (workerModule && workerModule.testWorkerFeatures && typeof workerModule.testWorkerFeatures === 'function') { - var workerResults = workerModule.testWorkerFeatures(); - console.log("Worker features test results:", JSON.stringify(workerResults, null, 2)); - - if (workerResults && workerResults.stringPathSupported && workerResults.urlObjectSupported && workerResults.tildePathSupported) { - console.log(" - String path support:", workerResults.stringPathSupported); - console.log(" - URL object support:", workerResults.urlObjectSupported); - console.log(" - Tilde path support:", workerResults.tildePathSupported); - passed++; - } else { - console.log("❌ FAIL: Worker enhancement features missing"); - console.log(" - stringPathSupported:", workerResults?.stringPathSupported); - console.log(" - urlObjectSupported:", workerResults?.urlObjectSupported); - console.log(" - tildePathSupported:", workerResults?.tildePathSupported); - failed++; - } - } else { - console.log("❌ FAIL: Worker features module has no testWorkerFeatures function"); - failed++; - } - } catch (e) { - console.log("❌ FAIL: Error testing Worker features:", e.message); - console.log("Stack trace:", e.stack); - failed++; - } - - // Final results - console.log("\n=== ES MODULE TEST RESULTS ==="); - console.log("Tests passed:", passed); - console.log("Tests failed:", failed); - console.log("Total tests:", passed + failed); - - if (failed === 0) { - console.log("ALL ES MODULE TESTS PASSED!"); - } else { - console.log("SOME ES MODULE TESTS FAILED!"); - } - - return { passed: passed, failed: failed }; -} - -// Run the tests immediately -runESModuleTests(); diff --git a/test-app/app/src/main/assets/app/tests/testESModules.mjs b/test-app/app/src/main/assets/app/tests/testESModules.mjs new file mode 100644 index 000000000..64bea7e90 --- /dev/null +++ b/test-app/app/src/main/assets/app/tests/testESModules.mjs @@ -0,0 +1,167 @@ +async function runESModuleTests() { + let passed = 0; + let failed = 0; + const failureDetails = []; + + const recordPass = (message, ...args) => { + console.log(`✅ PASS: ${message}`, ...args); + passed++; + }; + + const recordFailure = (message, options = {}) => { + const { error, details = [] } = options; + const fullMessage = error?.message + ? `${message}: ${error.message}` + : message; + console.log(`❌ FAIL: ${fullMessage}`); + details.forEach((detail) => console.log(detail)); + if (error?.stack) { + console.log("Stack trace:", error.stack); + } + failed++; + failureDetails.push(fullMessage); + }; + + const logFinalSummary = () => { + console.log("\n=== ES MODULE TEST RESULTS ==="); + console.log("Tests passed:", passed); + console.log("Tests failed:", failed); + console.log("Total tests:", passed + failed); + + if (failed === 0) { + console.log("ALL ES MODULE TESTS PASSED!"); + } else { + console.log("SOME ES MODULE TESTS FAILED!"); + console.log("FAILURE DETECTED: Starting failure logging"); + failureDetails.forEach((detail) => { + console.log(` ❌ ${detail}`); + }); + } + }; + + try { + // Test 1: Load .mjs files as ES modules + console.log("\n--- Test 1: Loading .mjs files as ES modules ---"); + try { + const moduleExports = await import("~/testSimpleESModule.mjs"); + if (moduleExports) { + recordPass("Module exports:", JSON.stringify(moduleExports)); + } else { + recordFailure("ES Module loaded but exports are null"); + } + + if (moduleExports?.moduleType === "ES Module") { + recordPass("moduleType check passed"); + } else { + recordFailure("moduleType check failed"); + } + } catch (e) { + recordFailure("Error loading ES module", { error: e }); + } + + // Test 2: Test import.meta functionality + console.log("\n--- Test 2: Testing import.meta functionality ---"); + try { + const importMetaModule = await import("~/testImportMeta.mjs"); + if ( + importMetaModule && + importMetaModule.default && + typeof importMetaModule.default === "function" + ) { + const metaResults = importMetaModule.default(); + console.log( + "import.meta test results:", + JSON.stringify(metaResults, null, 2) + ); + + if ( + metaResults && + metaResults.hasImportMeta && + metaResults.hasUrl && + metaResults.hasDirname + ) { + recordPass("import.meta properties present"); + console.log(" - import.meta.url:", metaResults.url); + console.log(" - import.meta.dirname:", metaResults.dirname); + } else { + recordFailure("import.meta properties missing", { + details: [ + ` - hasImportMeta: ${metaResults?.hasImportMeta}`, + ` - hasUrl: ${metaResults?.hasUrl}`, + ` - hasDirname: ${metaResults?.hasDirname}`, + ], + }); + } + } else { + recordFailure("import.meta module has no default export function"); + } + } catch (e) { + recordFailure("Error testing import.meta", { error: e }); + } + + // Test 3: Test Worker enhancements + console.log("\n--- Test 3: Testing Worker enhancements ---"); + try { + const workerModule = await import("~/testWorkerFeatures.mjs"); + if ( + workerModule && + workerModule.testWorkerFeatures && + typeof workerModule.testWorkerFeatures === "function" + ) { + const workerResults = workerModule.testWorkerFeatures(); + console.log( + "Worker features test results:", + JSON.stringify(workerResults, null, 2) + ); + + if ( + workerResults && + workerResults.stringPathSupported && + workerResults.urlObjectSupported && + workerResults.tildePathSupported + ) { + recordPass("Worker enhancement features present"); + console.log( + " - String path support:", + workerResults.stringPathSupported + ); + console.log( + " - URL object support:", + workerResults.urlObjectSupported + ); + console.log( + " - Tilde path support:", + workerResults.tildePathSupported + ); + } else { + recordFailure("Worker enhancement features missing", { + details: [ + ` - stringPathSupported: ${workerResults?.stringPathSupported}`, + ` - urlObjectSupported: ${workerResults?.urlObjectSupported}`, + ` - tildePathSupported: ${workerResults?.tildePathSupported}`, + ], + }); + } + } else { + recordFailure( + "Worker features module has no testWorkerFeatures function" + ); + } + } catch (e) { + recordFailure("Error testing Worker features", { error: e }); + } + } catch (unexpectedError) { + recordFailure("Unexpected ES module test harness failure", { + error: unexpectedError, + }); + } finally { + logFinalSummary(); + } + + return { passed, failed }; +} + +// Run the tests immediately (avoid top-level await for broader runtime support) +runESModuleTests().catch((e) => { + console.error("ES Module top-level failure:", e?.message ?? e); +}); diff --git a/test-app/app/src/main/assets/app/tests/testRuntimeImplementedAPIs.js b/test-app/app/src/main/assets/app/tests/testRuntimeImplementedAPIs.js index d2227486d..c55d356c5 100644 --- a/test-app/app/src/main/assets/app/tests/testRuntimeImplementedAPIs.js +++ b/test-app/app/src/main/assets/app/tests/testRuntimeImplementedAPIs.js @@ -1,9 +1,9 @@ describe("Runtime exposes", function () { - it("__time a low overhead, high resolution, time in ms.", function() { + it("__time a low overhead, high resolution, time in ms.", function () { // Try to get the times using Date.now and __time and compare the results, expect them to be somewhat "close". // Sometimes GC hits after Date.now is captured but before __time or the vice-versa and the test fails, // so we are giving it several attempts. - for(var i = 0; i < 5; i++) { + for (var i = 0; i < 5; i++) { try { var dateTimeStart = Date.now(); var timeStart = __time(); @@ -11,20 +11,22 @@ describe("Runtime exposes", function () { var s = android.os.SystemClock.elapsedRealtime(); for (var i = 0; i < 1000; i++) { var c = android.os.SystemClock.elapsedRealtime(); - acc += (c - s); + acc += c - s; s = c; } var dateTimeEnd = Date.now(); var timeEnd = __time(); var dateDelta = dateTimeEnd - dateTimeStart; var timeDelta = timeEnd - timeStart; - expect(Math.abs(dateDelta - timeDelta) < dateDelta * 0.25).toBe(true); + var tolerance = Math.max(10, dateDelta * 0.5); + expect(timeDelta > 0).toBe(true); + expect(Math.abs(dateDelta - timeDelta) < tolerance).toBe(true); break; - } catch(e) { + } catch (e) { if (i == 4) { throw e; } } } }); -}); \ No newline at end of file +}); diff --git a/test-app/runtime/CMakeLists.txt b/test-app/runtime/CMakeLists.txt index 331fc051d..2eaeaa7ee 100644 --- a/test-app/runtime/CMakeLists.txt +++ b/test-app/runtime/CMakeLists.txt @@ -147,6 +147,8 @@ add_library( src/main/cpp/URLImpl.cpp src/main/cpp/URLSearchParamsImpl.cpp src/main/cpp/URLPatternImpl.cpp + src/main/cpp/HMRSupport.cpp + src/main/cpp/DevFlags.cpp # V8 inspector source files will be included only in Release mode ${INSPECTOR_SOURCES} diff --git a/test-app/runtime/src/main/cpp/DevFlags.cpp b/test-app/runtime/src/main/cpp/DevFlags.cpp new file mode 100644 index 000000000..b97abb27a --- /dev/null +++ b/test-app/runtime/src/main/cpp/DevFlags.cpp @@ -0,0 +1,38 @@ +// DevFlags.cpp +#include "DevFlags.h" +#include "JEnv.h" +#include +#include + +namespace tns { + +bool IsScriptLoadingLogEnabled() { + static std::atomic cached{-1}; // -1 unknown, 0 false, 1 true + int v = cached.load(std::memory_order_acquire); + if (v != -1) { + return v == 1; + } + + static std::once_flag initFlag; + std::call_once(initFlag, []() { + bool enabled = false; + try { + JEnv env; + jclass runtimeClass = env.FindClass("com/tns/Runtime"); + if (runtimeClass != nullptr) { + jmethodID mid = env.GetStaticMethodID(runtimeClass, "getLogScriptLoadingEnabled", "()Z"); + if (mid != nullptr) { + jboolean res = env.CallStaticBooleanMethod(runtimeClass, mid); + enabled = (res == JNI_TRUE); + } + } + } catch (...) { + // keep default false + } + cached.store(enabled ? 1 : 0, std::memory_order_release); + }); + + return cached.load(std::memory_order_acquire) == 1; +} + +} // namespace tns diff --git a/test-app/runtime/src/main/cpp/DevFlags.h b/test-app/runtime/src/main/cpp/DevFlags.h new file mode 100644 index 000000000..7436b2a1b --- /dev/null +++ b/test-app/runtime/src/main/cpp/DevFlags.h @@ -0,0 +1,10 @@ +// DevFlags.h +#pragma once + +namespace tns { + +// Fast cached flag: whether to log script loading diagnostics. +// First call queries Java once; subsequent calls are atomic loads only. +bool IsScriptLoadingLogEnabled(); + +} diff --git a/test-app/runtime/src/main/cpp/HMRSupport.cpp b/test-app/runtime/src/main/cpp/HMRSupport.cpp new file mode 100644 index 000000000..be80eae0d --- /dev/null +++ b/test-app/runtime/src/main/cpp/HMRSupport.cpp @@ -0,0 +1,305 @@ +// HMRSupport.cpp +#include "HMRSupport.h" +#include "ArgConverter.h" +#include "JEnv.h" +#include +#include +#include +#include +#include + +namespace tns { + +static inline bool StartsWith(const std::string& s, const char* prefix) { + size_t n = strlen(prefix); + return s.size() >= n && s.compare(0, n, prefix) == 0; +} + +// Per-module hot data and callbacks. Keyed by canonical module path (file path or URL). +static std::unordered_map> g_hotData; +static std::unordered_map>> g_hotAccept; +static std::unordered_map>> g_hotDispose; + +v8::Local GetOrCreateHotData(v8::Isolate* isolate, const std::string& key) { + auto it = g_hotData.find(key); + if (it != g_hotData.end() && !it->second.IsEmpty()) { + return it->second.Get(isolate); + } + v8::Local obj = v8::Object::New(isolate); + g_hotData[key].Reset(isolate, obj); + return obj; +} + +void RegisterHotAccept(v8::Isolate* isolate, const std::string& key, v8::Local cb) { + if (cb.IsEmpty()) return; + g_hotAccept[key].emplace_back(v8::Global(isolate, cb)); +} + +void RegisterHotDispose(v8::Isolate* isolate, const std::string& key, v8::Local cb) { + if (cb.IsEmpty()) return; + g_hotDispose[key].emplace_back(v8::Global(isolate, cb)); +} + +std::vector> GetHotAcceptCallbacks(v8::Isolate* isolate, const std::string& key) { + std::vector> out; + auto it = g_hotAccept.find(key); + if (it != g_hotAccept.end()) { + for (auto& gfn : it->second) { + if (!gfn.IsEmpty()) out.push_back(gfn.Get(isolate)); + } + } + return out; +} + +std::vector> GetHotDisposeCallbacks(v8::Isolate* isolate, const std::string& key) { + std::vector> out; + auto it = g_hotDispose.find(key); + if (it != g_hotDispose.end()) { + for (auto& gfn : it->second) { + if (!gfn.IsEmpty()) out.push_back(gfn.Get(isolate)); + } + } + return out; +} + +void InitializeImportMetaHot(v8::Isolate* isolate, + v8::Local context, + v8::Local importMeta, + const std::string& modulePath) { + using v8::Function; + using v8::FunctionCallbackInfo; + using v8::Local; + using v8::Object; + using v8::String; + using v8::Value; + + v8::HandleScope scope(isolate); + + auto makeKeyData = [&](const std::string& key) -> Local { + return ArgConverter::ConvertToV8String(isolate, key); + }; + + auto acceptCb = [](const FunctionCallbackInfo& info) { + v8::Isolate* iso = info.GetIsolate(); + Local data = info.Data(); + std::string key; + if (!data.IsEmpty()) { + v8::String::Utf8Value s(iso, data); + key = *s ? *s : ""; + } + v8::Local cb; + if (info.Length() >= 1 && info[0]->IsFunction()) { + cb = info[0].As(); + } else if (info.Length() >= 2 && info[1]->IsFunction()) { + cb = info[1].As(); + } + if (!cb.IsEmpty()) { + RegisterHotAccept(iso, key, cb); + } + info.GetReturnValue().Set(v8::Undefined(iso)); + }; + + auto disposeCb = [](const FunctionCallbackInfo& info) { + v8::Isolate* iso = info.GetIsolate(); + Local data = info.Data(); + std::string key; + if (!data.IsEmpty()) { v8::String::Utf8Value s(iso, data); key = *s ? *s : ""; } + if (info.Length() >= 1 && info[0]->IsFunction()) { + RegisterHotDispose(iso, key, info[0].As()); + } + info.GetReturnValue().Set(v8::Undefined(iso)); + }; + + auto declineCb = [](const FunctionCallbackInfo& info) { + info.GetReturnValue().Set(v8::Undefined(info.GetIsolate())); + }; + + auto invalidateCb = [](const FunctionCallbackInfo& info) { + info.GetReturnValue().Set(v8::Undefined(info.GetIsolate())); + }; + + Local hot = Object::New(isolate); + hot->CreateDataProperty(context, ArgConverter::ConvertToV8String(isolate, "data"), + GetOrCreateHotData(isolate, modulePath)).Check(); + hot->CreateDataProperty(context, ArgConverter::ConvertToV8String(isolate, "prune"), + v8::Boolean::New(isolate, false)).Check(); + hot->CreateDataProperty( + context, ArgConverter::ConvertToV8String(isolate, "accept"), + v8::Function::New(context, acceptCb, makeKeyData(modulePath)).ToLocalChecked()).Check(); + hot->CreateDataProperty( + context, ArgConverter::ConvertToV8String(isolate, "dispose"), + v8::Function::New(context, disposeCb, makeKeyData(modulePath)).ToLocalChecked()).Check(); + hot->CreateDataProperty( + context, ArgConverter::ConvertToV8String(isolate, "decline"), + v8::Function::New(context, declineCb, makeKeyData(modulePath)).ToLocalChecked()).Check(); + hot->CreateDataProperty( + context, ArgConverter::ConvertToV8String(isolate, "invalidate"), + v8::Function::New(context, invalidateCb, makeKeyData(modulePath)).ToLocalChecked()).Check(); + + importMeta->CreateDataProperty(context, ArgConverter::ConvertToV8String(isolate, "hot"), hot).Check(); +} + +// Drop fragments and normalize parameters for consistent registry keys. +std::string CanonicalizeHttpUrlKey(const std::string& url) { + if (!(StartsWith(url, "http://") || StartsWith(url, "https://"))) { + return url; + } + // Remove fragment + size_t hashPos = url.find('#'); + std::string noHash = (hashPos == std::string::npos) ? url : url.substr(0, hashPos); + + // Strip ?import markers and sort remaining query params for stability + size_t qPos = noHash.find('?'); + if (qPos == std::string::npos) return noHash; + std::string originAndPath = noHash.substr(0, qPos); + std::string query = noHash.substr(qPos + 1); + std::vector kept; + size_t start = 0; + while (start <= query.size()) { + size_t amp = query.find('&', start); + std::string pair = (amp == std::string::npos) ? query.substr(start) : query.substr(start, amp - start); + if (!pair.empty()) { + size_t eq = pair.find('='); + std::string name = (eq == std::string::npos) ? pair : pair.substr(0, eq); + if (!(name == "import")) kept.push_back(pair); + } + if (amp == std::string::npos) break; + start = amp + 1; + } + if (kept.empty()) return originAndPath; + std::sort(kept.begin(), kept.end()); + std::string rebuilt = originAndPath + "?"; + for (size_t i = 0; i < kept.size(); i++) { + if (i > 0) rebuilt += "&"; + rebuilt += kept[i]; + } + return rebuilt; +} + +// Minimal HTTP fetch using java.net.* via JNI. Returns true on success (2xx) and non-empty body. +bool HttpFetchText(const std::string& url, std::string& out, std::string& contentType, int& status) { + out.clear(); + contentType.clear(); + status = 0; + try { + JEnv env; + + // Allow network operations on the current thread (dev-only HMR path) + // Some Android environments enforce StrictMode which throws NetworkOnMainThreadException + // when performing network I/O on the main thread. Since this fetch runs on the JS/V8 thread + // during development, explicitly relax the policy here. + { + jclass clsStrict = env.FindClass("android/os/StrictMode"); + jclass clsPolicyBuilder = env.FindClass("android/os/StrictMode$ThreadPolicy$Builder"); + if (clsStrict && clsPolicyBuilder) { + jmethodID builderCtor = env.GetMethodID(clsPolicyBuilder, "", "()V"); + jobject builder = env.NewObject(clsPolicyBuilder, builderCtor); + if (builder) { + jmethodID permitAll = env.GetMethodID(clsPolicyBuilder, "permitAll", "()Landroid/os/StrictMode$ThreadPolicy$Builder;"); + jobject builder2 = permitAll ? env.CallObjectMethod(builder, permitAll) : builder; + jmethodID build = env.GetMethodID(clsPolicyBuilder, "build", "()Landroid/os/StrictMode$ThreadPolicy;"); + jobject policy = build ? env.CallObjectMethod(builder2 ? builder2 : builder, build) : nullptr; + if (policy) { + jmethodID setThreadPolicy = env.GetStaticMethodID(clsStrict, "setThreadPolicy", "(Landroid/os/StrictMode$ThreadPolicy;)V"); + if (setThreadPolicy) { + env.CallStaticVoidMethod(clsStrict, setThreadPolicy, policy); + } + } + } + } + } + + jclass clsURL = env.FindClass("java/net/URL"); + if (!clsURL) return false; + jmethodID urlCtor = env.GetMethodID(clsURL, "", "(Ljava/lang/String;)V"); + jmethodID openConnection = env.GetMethodID(clsURL, "openConnection", "()Ljava/net/URLConnection;"); + jstring jUrlStr = env.NewStringUTF(url.c_str()); + jobject urlObj = env.NewObject(clsURL, urlCtor, jUrlStr); + + jobject conn = env.CallObjectMethod(urlObj, openConnection); + if (!conn) return false; + + jclass clsConn = env.GetObjectClass(conn); + jmethodID setConnectTimeout = env.GetMethodID(clsConn, "setConnectTimeout", "(I)V"); + jmethodID setReadTimeout = env.GetMethodID(clsConn, "setReadTimeout", "(I)V"); + jmethodID setDoInput = env.GetMethodID(clsConn, "setDoInput", "(Z)V"); + jmethodID setUseCaches = env.GetMethodID(clsConn, "setUseCaches", "(Z)V"); + jmethodID setReqProp = env.GetMethodID(clsConn, "setRequestProperty", "(Ljava/lang/String;Ljava/lang/String;)V"); + env.CallVoidMethod(conn, setConnectTimeout, 15000); + env.CallVoidMethod(conn, setReadTimeout, 15000); + if (setDoInput) { env.CallVoidMethod(conn, setDoInput, JNI_TRUE); } + if (setUseCaches) { env.CallVoidMethod(conn, setUseCaches, JNI_FALSE); } + env.CallVoidMethod(conn, setReqProp, env.NewStringUTF("Accept"), env.NewStringUTF("application/javascript, text/javascript, */*;q=0.1")); + env.CallVoidMethod(conn, setReqProp, env.NewStringUTF("Accept-Encoding"), env.NewStringUTF("identity")); + env.CallVoidMethod(conn, setReqProp, env.NewStringUTF("Cache-Control"), env.NewStringUTF("no-cache")); + env.CallVoidMethod(conn, setReqProp, env.NewStringUTF("Connection"), env.NewStringUTF("close")); + env.CallVoidMethod(conn, setReqProp, env.NewStringUTF("User-Agent"), env.NewStringUTF("NativeScript-HTTP-ESM")); + + // Try to get status via HttpURLConnection if possible + jclass clsHttp = env.FindClass("java/net/HttpURLConnection"); + bool isHttp = clsHttp && env.IsInstanceOf(conn, clsHttp); + jmethodID getResponseCode = isHttp ? env.GetMethodID(clsHttp, "getResponseCode", "()I") : nullptr; + jmethodID getErrorStream = isHttp ? env.GetMethodID(clsHttp, "getErrorStream", "()Ljava/io/InputStream;") : nullptr; + if (isHttp && getResponseCode) { + status = env.CallIntMethod(conn, getResponseCode); + } + + // Read InputStream (prefer error stream on HTTP error codes) + jmethodID getInputStream = env.GetMethodID(clsConn, "getInputStream", "()Ljava/io/InputStream;"); + jobject inStream = nullptr; + if (isHttp && status >= 400 && getErrorStream) { + inStream = env.CallObjectMethod(conn, getErrorStream); + } + if (!inStream) { + inStream = env.CallObjectMethod(conn, getInputStream); + } + if (!inStream) return false; + + jclass clsIS = env.GetObjectClass(inStream); + jmethodID readMethod = env.GetMethodID(clsIS, "read", "([B)I"); + jmethodID closeIS = env.GetMethodID(clsIS, "close", "()V"); + + jclass clsBAOS = env.FindClass("java/io/ByteArrayOutputStream"); + jmethodID baosCtor = env.GetMethodID(clsBAOS, "", "()V"); + jmethodID baosWrite = env.GetMethodID(clsBAOS, "write", "([BII)V"); + jmethodID baosToByteArray = env.GetMethodID(clsBAOS, "toByteArray", "()[B"); + jmethodID baosClose = env.GetMethodID(clsBAOS, "close", "()V"); + jobject baos = env.NewObject(clsBAOS, baosCtor); + + jbyteArray buffer = env.NewByteArray(8192); + while (true) { + jint n = env.CallIntMethod(inStream, readMethod, buffer); + if (n < 0) break; // -1 indicates EOF + if (n == 0) { + // Defensive: continue reading if zero bytes returned + continue; + } + env.CallVoidMethod(baos, baosWrite, buffer, 0, n); + } + + env.CallVoidMethod(inStream, closeIS); + jbyteArray bytes = (jbyteArray) env.CallObjectMethod(baos, baosToByteArray); + env.CallVoidMethod(baos, baosClose); + + if (!bytes) return false; + jsize len = env.GetArrayLength(bytes); + out.resize(static_cast(len)); + if (len > 0) { + env.GetByteArrayRegion(bytes, 0, len, reinterpret_cast(&out[0])); + } + + // Content-Type if available + jmethodID getContentType = env.GetMethodID(clsConn, "getContentType", "()Ljava/lang/String;"); + jstring jct = (jstring) env.CallObjectMethod(conn, getContentType); + if (jct) { + contentType = ArgConverter::jstringToString(jct); + } + + if (status == 0) status = 200; // assume OK if not HTTP + return status >= 200 && status < 300 && !out.empty(); + } catch (...) { + return false; + } +} + +} // namespace tns diff --git a/test-app/runtime/src/main/cpp/HMRSupport.h b/test-app/runtime/src/main/cpp/HMRSupport.h new file mode 100644 index 000000000..f08e7fa09 --- /dev/null +++ b/test-app/runtime/src/main/cpp/HMRSupport.h @@ -0,0 +1,25 @@ +// HMRSupport.h +#pragma once + +#include +#include +#include + +namespace tns { + +// import.meta.hot support +v8::Local GetOrCreateHotData(v8::Isolate* isolate, const std::string& key); +void RegisterHotAccept(v8::Isolate* isolate, const std::string& key, v8::Local cb); +void RegisterHotDispose(v8::Isolate* isolate, const std::string& key, v8::Local cb); +std::vector> GetHotAcceptCallbacks(v8::Isolate* isolate, const std::string& key); +std::vector> GetHotDisposeCallbacks(v8::Isolate* isolate, const std::string& key); +void InitializeImportMetaHot(v8::Isolate* isolate, + v8::Local context, + v8::Local importMeta, + const std::string& modulePath); + +// Dev HTTP loader helpers +std::string CanonicalizeHttpUrlKey(const std::string& url); +bool HttpFetchText(const std::string& url, std::string& out, std::string& contentType, int& status); + +} // namespace tns diff --git a/test-app/runtime/src/main/cpp/ModuleInternalCallbacks.cpp b/test-app/runtime/src/main/cpp/ModuleInternalCallbacks.cpp index e1514e533..e95a2b696 100644 --- a/test-app/runtime/src/main/cpp/ModuleInternalCallbacks.cpp +++ b/test-app/runtime/src/main/cpp/ModuleInternalCallbacks.cpp @@ -9,6 +9,10 @@ #include #include #include +#include +#include "HMRSupport.h" +#include "DevFlags.h" +#include "JEnv.h" using namespace v8; using namespace std; @@ -20,34 +24,163 @@ extern std::unordered_map> g_moduleRegistry; // Forward declaration used by logging helper std::string GetApplicationPath(); -// Cached toggle for verbose script loading logs sourced from Java AppConfig via JNI -static bool ShouldLogScriptLoading() { - static std::atomic cached{-1}; // -1 unknown, 0 false, 1 true - int v = cached.load(std::memory_order_acquire); - if (v != -1) { - return v == 1; - } - - static std::once_flag initFlag; - std::call_once(initFlag, []() { - bool enabled = false; - try { - JEnv env; - jclass runtimeClass = env.FindClass("com/tns/Runtime"); - if (runtimeClass != nullptr) { - jmethodID mid = env.GetStaticMethodID(runtimeClass, "getLogScriptLoadingEnabled", "()Z"); - if (mid != nullptr) { - jboolean res = env.CallStaticBooleanMethod(runtimeClass, mid); - enabled = (res == JNI_TRUE); - } +// Logging flag now provided via DevFlags for fast cached access + +// Diagnostic helper: emit detailed V8 compile error info for HTTP ESM sources. +static void LogHttpCompileDiagnostics(v8::Isolate* isolate, + v8::Local context, + const std::string& url, + const std::string& code, + v8::TryCatch& tc) { + if (!IsScriptLoadingLogEnabled()) { + return; + } + using namespace v8; + + const char* classification = "unknown"; + std::string msgStr; + std::string srcLineStr; + int lineNum = 0; + int startCol = 0; + int endCol = 0; + + Local message = tc.Message(); + if (!message.IsEmpty()) { + String::Utf8Value m8(isolate, message->Get()); + if (*m8) msgStr = *m8; + lineNum = message->GetLineNumber(context).FromMaybe(0); + startCol = message->GetStartColumn(); + endCol = message->GetEndColumn(); + MaybeLocal maybeLine = message->GetSourceLine(context); + if (!maybeLine.IsEmpty()) { + String::Utf8Value l8(isolate, maybeLine.ToLocalChecked()); + if (*l8) srcLineStr = *l8; + } + // Heuristics similar to iOS for quick triage + if (msgStr.find("Unexpected identifier") != std::string::npos || + msgStr.find("Unexpected token") != std::string::npos) { + if (msgStr.find("export") != std::string::npos && + code.find("export default") == std::string::npos && + code.find("__sfc__") != std::string::npos) { + classification = "missing-export-default"; + } else { + classification = "syntax"; } - } catch (...) { - // ignore and keep default false + } else if (msgStr.find("Cannot use import statement") != std::string::npos) { + classification = "wrap-error"; } - cached.store(enabled ? 1 : 0, std::memory_order_release); - }); + } + if (strcmp(classification, "unknown") == 0) { + if (code.find("export default") == std::string::npos && code.find("__sfc__") != std::string::npos) classification = "missing-export-default"; + else if (code.find("__sfc__") != std::string::npos && code.find("export {") == std::string::npos && code.find("export ") == std::string::npos) classification = "no-exports"; + else if (code.find("import ") == std::string::npos && code.find("export ") == std::string::npos) classification = "not-module"; + else if (code.find("_openBlock") != std::string::npos && code.find("openBlock") == std::string::npos) classification = "underscore-helper-unmapped"; + } + + // FNV-1a 64-bit hash of source for correlation + unsigned long long h = 1469598103934665603ull; // offset basis + for (unsigned char c : code) { h ^= c; h *= 1099511628211ull; } + + // Trim the snippet for readability + std::string snippet = code.substr(0, 600); + for (char& ch : snippet) { if (ch == '\n' || ch == '\r') ch = ' '; } + if (srcLineStr.size() > 240) srcLineStr = srcLineStr.substr(0, 240); + + DEBUG_WRITE("[http-esm][compile][v8-error][%s] %s line=%d col=%d..%d hash=%llx bytes=%lu msg=%s srcLine=%s snippet=%s", + classification, + url.c_str(), + lineNum, + startCol, + endCol, + (unsigned long long)h, + (unsigned long)code.size(), + msgStr.c_str(), + srcLineStr.c_str(), + snippet.c_str()); +} + +// Helper: resolve relative or root-absolute spec against an HTTP(S) referrer URL. +// Returns empty string if resolution is not possible. +static std::string ResolveHttpRelative(const std::string& referrerUrl, const std::string& spec) { + if (referrerUrl.empty()) { + return std::string(); + } + auto startsWith = [](const std::string& s, const char* pre) -> bool { + size_t n = strlen(pre); + return s.size() >= n && s.compare(0, n, pre) == 0; + }; + if (!(startsWith(referrerUrl, "http://") || startsWith(referrerUrl, "https://"))) { + return std::string(); + } + // Normalize referrer: drop fragment and query + std::string base = referrerUrl; + size_t hashPos = base.find('#'); + if (hashPos != std::string::npos) base = base.substr(0, hashPos); + size_t qPos = base.find('?'); + if (qPos != std::string::npos) base = base.substr(0, qPos); + + // Extract origin and path + size_t schemePos = base.find("://"); + if (schemePos == std::string::npos) { + return std::string(); + } + size_t pathStart = base.find('/', schemePos + 3); + std::string origin = (pathStart == std::string::npos) ? base : base.substr(0, pathStart); + std::string path = (pathStart == std::string::npos) ? std::string("/") : base.substr(pathStart); + + // Separate query/fragment from spec + std::string specPath = spec; + std::string specSuffix; + size_t specQ = specPath.find('?'); + size_t specH = specPath.find('#'); + size_t cut = std::string::npos; + if (specQ != std::string::npos && specH != std::string::npos) { + cut = std::min(specQ, specH); + } else if (specQ != std::string::npos) { + cut = specQ; + } else if (specH != std::string::npos) { + cut = specH; + } + if (cut != std::string::npos) { + specSuffix = specPath.substr(cut); + specPath = specPath.substr(0, cut); + } - return cached.load(std::memory_order_acquire) == 1; + // Build new path + std::string newPath; + if (!specPath.empty() && specPath[0] == '/') { + // Root-absolute relative to origin + newPath = specPath; + } else { + // Relative to directory of referrer path + size_t lastSlash = path.find_last_of('/'); + std::string baseDir = (lastSlash == std::string::npos) ? std::string("/") : path.substr(0, lastSlash + 1); + newPath = baseDir + specPath; + } + + // Normalize "." and ".." segments + std::vector stack; + bool absolute = !newPath.empty() && newPath[0] == '/'; + size_t i = 0; + while (i <= newPath.size()) { + size_t j = newPath.find('/', i); + std::string seg = (j == std::string::npos) ? newPath.substr(i) : newPath.substr(i, j - i); + if (seg.empty() || seg == ".") { + // skip + } else if (seg == "..") { + if (!stack.empty()) stack.pop_back(); + } else { + stack.push_back(seg); + } + if (j == std::string::npos) break; + i = j + 1; + } + std::string normPath = absolute ? "/" : std::string(); + for (size_t k = 0; k < stack.size(); k++) { + if (k > 0) normPath += "/"; + normPath += stack[k]; + } + return origin + normPath + specSuffix; } // Import meta callback to support import.meta.url and import.meta.dirname @@ -75,23 +208,26 @@ void InitializeImportMetaObject(Local context, Local module, Lo modulePath = ""; // Will use fallback path } - if (ShouldLogScriptLoading()) { + if (IsScriptLoadingLogEnabled()) { DEBUG_WRITE("InitializeImportMetaObject: Module lookup: found path = %s", modulePath.empty() ? "(empty)" : modulePath.c_str()); DEBUG_WRITE("InitializeImportMetaObject: Registry size: %zu", g_moduleRegistry.size()); } - // Convert file path to file:// URL + // Convert to URL for import.meta.url; keep http(s) untouched, file paths with file:// std::string moduleUrl; if (!modulePath.empty()) { - // Create file:// URL from the full path - moduleUrl = "file://" + modulePath; + if (modulePath.rfind("http://", 0) == 0 || modulePath.rfind("https://", 0) == 0) { + moduleUrl = modulePath; + } else { + moduleUrl = "file://" + modulePath; + } } else { // Fallback URL if module not found in registry moduleUrl = "file:///android_asset/app/"; } - if (ShouldLogScriptLoading()) { + if (IsScriptLoadingLogEnabled()) { DEBUG_WRITE("InitializeImportMetaObject: Final URL: %s", moduleUrl.c_str()); } @@ -100,14 +236,22 @@ void InitializeImportMetaObject(Local context, Local module, Lo // Set import.meta.url property meta->CreateDataProperty(context, ArgConverter::ConvertToV8String(isolate, "url"), url).Check(); - // Add import.meta.dirname support (extract directory from path) + // Add import.meta.dirname support (extract directory) std::string dirname; if (!modulePath.empty()) { - size_t lastSlash = modulePath.find_last_of("/\\"); - if (lastSlash != std::string::npos) { - dirname = modulePath.substr(0, lastSlash); + if (modulePath.rfind("http://", 0) == 0 || modulePath.rfind("https://", 0) == 0) { + // For URLs, compute dirname by trimming after last '/' + size_t q = modulePath.find('?'); + std::string noQuery = (q == std::string::npos) ? modulePath : modulePath.substr(0, q); + size_t lastSlash = noQuery.find_last_of('/'); + dirname = (lastSlash == std::string::npos) ? modulePath : noQuery.substr(0, lastSlash); } else { - dirname = "/android_asset/app"; // fallback + size_t lastSlash = modulePath.find_last_of("/\\"); + if (lastSlash != std::string::npos) { + dirname = modulePath.substr(0, lastSlash); + } else { + dirname = "/android_asset/app"; // fallback + } } } else { dirname = "/android_asset/app"; // fallback @@ -117,6 +261,9 @@ void InitializeImportMetaObject(Local context, Local module, Lo // Set import.meta.dirname property meta->CreateDataProperty(context, ArgConverter::ConvertToV8String(isolate, "dirname"), dirnameStr).Check(); + + // Attach import.meta.hot for HMR + tns::InitializeImportMetaHot(isolate, context, meta, modulePath); } // Helper function to check if a file exists and is a regular file @@ -166,24 +313,139 @@ v8::MaybeLocal ResolveModuleCallback(v8::Local context, } // Debug logging - if (ShouldLogScriptLoading()) { + if (IsScriptLoadingLogEnabled()) { DEBUG_WRITE("ResolveModuleCallback: Resolving '%s'", spec.c_str()); } - // 2) Find which filepath the referrer was compiled under + // Normalize malformed http:/ and https:/ prefixes + if (spec.rfind("http:/", 0) == 0 && spec.rfind("http://", 0) != 0) { + spec.insert(5, "/"); + } else if (spec.rfind("https:/", 0) == 0 && spec.rfind("https://", 0) != 0) { + spec.insert(6, "/"); + } + + // Attempt to resolve relative or root-absolute specifiers against an HTTP referrer URL std::string referrerPath; for (auto& kv : g_moduleRegistry) { v8::Local registered = kv.second.Get(isolate); - if (registered == referrer) { + if (!registered.IsEmpty() && registered == referrer) { referrerPath = kv.first; break; } } + bool specIsRelative = !spec.empty() && spec[0] == '.'; + bool specIsRootAbs = !spec.empty() && spec[0] == '/'; + auto startsWithHttp = [](const std::string& s) -> bool { + return s.rfind("http://", 0) == 0 || s.rfind("https://", 0) == 0; + }; + if (!startsWithHttp(spec) && (specIsRelative || specIsRootAbs)) { + if (!referrerPath.empty() && startsWithHttp(referrerPath)) { + std::string resolved = ResolveHttpRelative(referrerPath, spec); + if (!resolved.empty()) { + if (IsScriptLoadingLogEnabled()) { + DEBUG_WRITE("ResolveModuleCallback: HTTP-relative resolved '%s' + '%s' -> '%s'", + referrerPath.c_str(), spec.c_str(), resolved.c_str()); + } + spec = resolved; + } + } else if (specIsRootAbs) { + // Fallback: use global __NS_HTTP_ORIGIN__ if present to anchor root-absolute specs + v8::Local key = ArgConverter::ConvertToV8String(isolate, "__NS_HTTP_ORIGIN__"); + v8::Local global = context->Global(); + v8::MaybeLocal maybeOriginVal = global->Get(context, key); + v8::Local originVal; + if (!maybeOriginVal.IsEmpty() && maybeOriginVal.ToLocal(&originVal) && originVal->IsString()) { + v8::String::Utf8Value o8(isolate, originVal); + std::string origin = *o8 ? *o8 : ""; + if (!origin.empty() && (origin.rfind("http://", 0) == 0 || origin.rfind("https://", 0) == 0)) { + std::string refBase = origin; + if (refBase.back() != '/') refBase += '/'; + std::string resolved = ResolveHttpRelative(refBase, spec); + if (!resolved.empty()) { + if (IsScriptLoadingLogEnabled()) { + DEBUG_WRITE("[http-esm][http-origin][fallback] origin=%s spec=%s -> %s", refBase.c_str(), spec.c_str(), resolved.c_str()); + } + spec = resolved; + } + } + } + } + } + + // HTTP(S) ESM support: resolve, fetch and compile from dev server + if (spec.rfind("http://", 0) == 0 || spec.rfind("https://", 0) == 0) { + std::string canonical = tns::CanonicalizeHttpUrlKey(spec); + if (IsScriptLoadingLogEnabled()) { + DEBUG_WRITE("[http-esm][resolve] spec=%s canonical=%s", spec.c_str(), canonical.c_str()); + } + auto it = g_moduleRegistry.find(canonical); + if (it != g_moduleRegistry.end()) { + if (IsScriptLoadingLogEnabled()) { + DEBUG_WRITE("[http-esm][cache] hit %s", canonical.c_str()); + } + return v8::MaybeLocal(it->second.Get(isolate)); + } + + std::string body, ct; + int status = 0; + if (!tns::HttpFetchText(spec, body, ct, status)) { + if (IsScriptLoadingLogEnabled()) { + DEBUG_WRITE("[http-esm][fetch][fail] url=%s status=%d", spec.c_str(), status); + } + std::string msg = std::string("Failed to fetch ") + spec + ", status=" + std::to_string(status); + isolate->ThrowException(v8::Exception::Error(ArgConverter::ConvertToV8String(isolate, msg))); + return v8::MaybeLocal(); + } + if (IsScriptLoadingLogEnabled()) { + DEBUG_WRITE("[http-esm][fetch][ok] url=%s status=%d bytes=%lu ct=%s", spec.c_str(), status, (unsigned long)body.size(), ct.c_str()); + } + + v8::Local sourceText = ArgConverter::ConvertToV8String(isolate, body); + v8::Local urlString = ArgConverter::ConvertToV8String(isolate, canonical); + v8::ScriptOrigin origin(isolate, urlString, 0, 0, false, -1, v8::Local(), false, false, true); + v8::ScriptCompiler::Source src(sourceText, origin); + v8::Local mod; + { + v8::TryCatch tc(isolate); + if (!v8::ScriptCompiler::CompileModule(isolate, &src).ToLocal(&mod)) { + LogHttpCompileDiagnostics(isolate, context, canonical, body, tc); + isolate->ThrowException(v8::Exception::Error(ArgConverter::ConvertToV8String(isolate, "HTTP module compile failed"))); + return v8::MaybeLocal(); + } + } + if (IsScriptLoadingLogEnabled()) { + DEBUG_WRITE("[http-esm][compile][ok] %s bytes=%lu", canonical.c_str(), (unsigned long)body.size()); + } + // Register before instantiation to allow cyclic imports to resolve to same instance + g_moduleRegistry[canonical].Reset(isolate, mod); + // Do not evaluate here; allow V8 to handle instantiation/evaluation in importer context. + // Instantiate proactively if desired (safe), but not required. + // if (mod->GetStatus() == v8::Module::kUninstantiated) { + // if (!mod->InstantiateModule(context, &ResolveModuleCallback).FromMaybe(false)) { + // g_moduleRegistry.erase(canonical); + // return v8::MaybeLocal(); + // } + // } + // Let V8 evaluate during importer evaluation. Returning compiled module is fine. + return v8::MaybeLocal(mod); + } + + // 2) Find which filepath the referrer was compiled under (local filesystem case) + // referrerPath may already be set above; leave as-is if found. + if (referrerPath.empty()) { + for (auto& kv : g_moduleRegistry) { + v8::Local registered = kv.second.Get(isolate); + if (registered == referrer) { + referrerPath = kv.first; + break; + } + } + } // If we couldn't identify the referrer and the specifier is relative, // assume the base directory is the application root - bool specIsRelative = !spec.empty() && spec[0] == '.'; - if (referrerPath.empty() && specIsRelative) { + bool specIsRelativeFs = !spec.empty() && spec[0] == '.'; + if (referrerPath.empty() && specIsRelativeFs) { referrerPath = GetApplicationPath() + "/index.mjs"; // Default referrer } @@ -200,7 +462,7 @@ v8::MaybeLocal ResolveModuleCallback(v8::Local context, std::string cleanSpec = spec.substr(0, 2) == "./" ? spec.substr(2) : spec; std::string candidate = baseDir + cleanSpec; candidateBases.push_back(candidate); - if (ShouldLogScriptLoading()) { + if (IsScriptLoadingLogEnabled()) { DEBUG_WRITE("ResolveModuleCallback: Relative import: '%s' + '%s' -> '%s'", baseDir.c_str(), cleanSpec.c_str(), candidate.c_str()); } @@ -219,25 +481,25 @@ v8::MaybeLocal ResolveModuleCallback(v8::Local context, if (tail.rfind(appVirtualRoot, 0) == 0) { // Drop the leading "/app/" and prepend real appPath candidate = appPath + "/" + tail.substr(appVirtualRoot.size()); - if (ShouldLogScriptLoading()) { + if (IsScriptLoadingLogEnabled()) { DEBUG_WRITE("ResolveModuleCallback: file:// to appPath mapping: '%s' -> '%s'", tail.c_str(), candidate.c_str()); } } else if (tail.rfind(androidAssetAppRoot, 0) == 0) { // Replace "/android_asset/app/" with the real appPath candidate = appPath + "/" + tail.substr(androidAssetAppRoot.size()); - if (ShouldLogScriptLoading()) { + if (IsScriptLoadingLogEnabled()) { DEBUG_WRITE("ResolveModuleCallback: file:// android_asset mapping: '%s' -> '%s'", tail.c_str(), candidate.c_str()); } } else if (tail.rfind(appPath, 0) == 0) { // Already an absolute on-disk path to the app folder candidate = tail; - if (ShouldLogScriptLoading()) { + if (IsScriptLoadingLogEnabled()) { DEBUG_WRITE("ResolveModuleCallback: file:// absolute path preserved: '%s'", candidate.c_str()); } } else { // Fallback: treat as absolute on-disk path candidate = tail; - if (ShouldLogScriptLoading()) { + if (IsScriptLoadingLogEnabled()) { DEBUG_WRITE("ResolveModuleCallback: file:// generic absolute: '%s'", candidate.c_str()); } } @@ -472,7 +734,7 @@ v8::MaybeLocal ResolveModuleCallback(v8::Local context, // 7) Handle JSON modules if (absPath.size() >= 5 && absPath.compare(absPath.size() - 5, 5, ".json") == 0) { - if (ShouldLogScriptLoading()) { + if (IsScriptLoadingLogEnabled()) { DEBUG_WRITE("ResolveModuleCallback: Handling JSON module '%s'", absPath.c_str()); } @@ -526,14 +788,14 @@ v8::MaybeLocal ResolveModuleCallback(v8::Local context, // 8) Check if we've already compiled this module auto it = g_moduleRegistry.find(absPath); if (it != g_moduleRegistry.end()) { - if (ShouldLogScriptLoading()) { + if (IsScriptLoadingLogEnabled()) { DEBUG_WRITE("ResolveModuleCallback: Found cached module '%s'", absPath.c_str()); } return v8::MaybeLocal(it->second.Get(isolate)); } // 9) Compile and register the new module - if (ShouldLogScriptLoading()) { + if (IsScriptLoadingLogEnabled()) { DEBUG_WRITE("ResolveModuleCallback: Compiling new module '%s'", absPath.c_str()); } try { @@ -568,7 +830,7 @@ v8::MaybeLocal ImportModuleDynamicallyCallback( v8::String::Utf8Value specUtf8(isolate, specifier); std::string spec = *specUtf8 ? *specUtf8 : ""; - if (ShouldLogScriptLoading()) { + if (IsScriptLoadingLogEnabled()) { DEBUG_WRITE("ImportModuleDynamicallyCallback: Dynamic import for '%s'", spec.c_str()); } @@ -581,7 +843,88 @@ v8::MaybeLocal ImportModuleDynamicallyCallback( return v8::MaybeLocal(); } - // Re-use the static resolver to locate / compile the module. + // Resolve relative or root-absolute dynamic imports against the referrer's URL when provided + auto isHttpLike = [](const std::string& s) -> bool { + return s.rfind("http://", 0) == 0 || s.rfind("https://", 0) == 0; + }; + bool specIsRelative = !spec.empty() && spec[0] == '.'; + bool specIsRootAbs = !spec.empty() && spec[0] == '/'; + std::string referrerUrl; + if (!resource_name.IsEmpty() && resource_name->IsString()) { + v8::String::Utf8Value r8(isolate, resource_name); + referrerUrl = *r8 ? *r8 : ""; + } + if ((specIsRelative || specIsRootAbs) && isHttpLike(referrerUrl)) { + std::string resolved = ResolveHttpRelative(referrerUrl, spec); + if (!resolved.empty()) { + if (IsScriptLoadingLogEnabled()) { + DEBUG_WRITE("[http-esm][dyn][http-rel] base=%s spec=%s -> %s", referrerUrl.c_str(), spec.c_str(), resolved.c_str()); + } + spec = resolved; + } else if (IsScriptLoadingLogEnabled()) { + DEBUG_WRITE("[http-esm][dyn][http-rel][skip] base=%s spec=%s", referrerUrl.c_str(), spec.c_str()); + } + } + + // Handle HTTP(S) dynamic import directly + if (!spec.empty() && isHttpLike(spec)) { + std::string canonical = tns::CanonicalizeHttpUrlKey(spec); + if (IsScriptLoadingLogEnabled()) { + DEBUG_WRITE("[http-esm][dyn][resolve] spec=%s canonical=%s", spec.c_str(), canonical.c_str()); + } + v8::Local mod; + auto it = g_moduleRegistry.find(canonical); + if (it != g_moduleRegistry.end()) { + mod = it->second.Get(isolate); + if (IsScriptLoadingLogEnabled()) { + DEBUG_WRITE("[http-esm][dyn][cache] hit %s", canonical.c_str()); + } + } else { + std::string body, ct; int status = 0; + if (!tns::HttpFetchText(spec, body, ct, status)) { + if (IsScriptLoadingLogEnabled()) { + DEBUG_WRITE("[http-esm][dyn][fetch][fail] url=%s status=%d", spec.c_str(), status); + } + resolver->Reject(context, v8::Exception::Error(ArgConverter::ConvertToV8String(isolate, std::string("Failed to fetch ")+spec))).Check(); + return scope.Escape(resolver->GetPromise()); + } + if (IsScriptLoadingLogEnabled()) { + DEBUG_WRITE("[http-esm][dyn][fetch][ok] url=%s status=%d bytes=%lu ct=%s", spec.c_str(), status, (unsigned long)body.size(), ct.c_str()); + } + v8::Local sourceText = ArgConverter::ConvertToV8String(isolate, body); + v8::Local urlString = ArgConverter::ConvertToV8String(isolate, canonical); + v8::ScriptOrigin origin(isolate, urlString, 0, 0, false, -1, v8::Local(), false, false, true); + v8::ScriptCompiler::Source src(sourceText, origin); + { + v8::TryCatch tc(isolate); + if (!v8::ScriptCompiler::CompileModule(isolate, &src).ToLocal(&mod)) { + LogHttpCompileDiagnostics(isolate, context, canonical, body, tc); + resolver->Reject(context, v8::Exception::Error(ArgConverter::ConvertToV8String(isolate, "HTTP module compile failed"))).Check(); + return scope.Escape(resolver->GetPromise()); + } + } + if (IsScriptLoadingLogEnabled()) { + DEBUG_WRITE("[http-esm][dyn][compile][ok] %s bytes=%lu", canonical.c_str(), (unsigned long)body.size()); + } + g_moduleRegistry[canonical].Reset(isolate, mod); + } + if (mod->GetStatus() == v8::Module::kUninstantiated) { + if (!mod->InstantiateModule(context, &ResolveModuleCallback).FromMaybe(false)) { + resolver->Reject(context, v8::Exception::Error(ArgConverter::ConvertToV8String(isolate, "Instantiate failed"))).Check(); + return scope.Escape(resolver->GetPromise()); + } + } + if (mod->GetStatus() != v8::Module::kEvaluated) { + if (mod->Evaluate(context).IsEmpty()) { + resolver->Reject(context, v8::Exception::Error(ArgConverter::ConvertToV8String(isolate, "Evaluation failed"))).Check(); + return scope.Escape(resolver->GetPromise()); + } + } + resolver->Resolve(context, mod->GetModuleNamespace()).Check(); + return scope.Escape(resolver->GetPromise()); + } + + // Re-use the static resolver to locate / compile the module for non-HTTP cases. try { // Pass empty referrer since this V8 version doesn't expose GetModule() on // ScriptOrModule. The resolver will fall back to absolute-path heuristics. @@ -593,7 +936,7 @@ v8::MaybeLocal ImportModuleDynamicallyCallback( v8::Local module; if (!maybeModule.ToLocal(&module)) { // Resolution failed; reject to avoid leaving a pending Promise (white screen) - if (ShouldLogScriptLoading()) { + if (IsScriptLoadingLogEnabled()) { DEBUG_WRITE("ImportModuleDynamicallyCallback: Resolution failed for '%s'", spec.c_str()); } v8::Local ex = v8::Exception::Error( @@ -605,7 +948,7 @@ v8::MaybeLocal ImportModuleDynamicallyCallback( // If not yet instantiated/evaluated, do it now if (module->GetStatus() == v8::Module::kUninstantiated) { if (!module->InstantiateModule(context, &ResolveModuleCallback).FromMaybe(false)) { - if (ShouldLogScriptLoading()) { + if (IsScriptLoadingLogEnabled()) { DEBUG_WRITE("ImportModuleDynamicallyCallback: Instantiate failed for '%s'", spec.c_str()); } resolver @@ -618,7 +961,7 @@ v8::MaybeLocal ImportModuleDynamicallyCallback( if (module->GetStatus() != v8::Module::kEvaluated) { if (module->Evaluate(context).IsEmpty()) { - if (ShouldLogScriptLoading()) { + if (IsScriptLoadingLogEnabled()) { DEBUG_WRITE("ImportModuleDynamicallyCallback: Evaluation failed for '%s'", spec.c_str()); } v8::Local ex = @@ -629,12 +972,12 @@ v8::MaybeLocal ImportModuleDynamicallyCallback( } resolver->Resolve(context, module->GetModuleNamespace()).Check(); - if (ShouldLogScriptLoading()) { + if (IsScriptLoadingLogEnabled()) { DEBUG_WRITE("ImportModuleDynamicallyCallback: Successfully resolved '%s'", spec.c_str()); } } catch (NativeScriptException& ex) { ex.ReThrowToV8(); - if (ShouldLogScriptLoading()) { + if (IsScriptLoadingLogEnabled()) { DEBUG_WRITE("ImportModuleDynamicallyCallback: Native exception for '%s'", spec.c_str()); } resolver diff --git a/test-app/tools/check_console_test_results.js b/test-app/tools/check_console_test_results.js index dc9333034..9c6c65cc6 100644 --- a/test-app/tools/check_console_test_results.js +++ b/test-app/tools/check_console_test_results.js @@ -1,293 +1,448 @@ #!/usr/bin/env node -const { execSync } = require('child_process'); +const { execSync } = require("child_process"); -console.log('\n=== Checking Console Test Results ===\n'); +console.log("\n=== Checking Console Test Results ===\n"); function checkTestResults() { - try { - console.log('Getting logcat output for test results...'); - - // Get only recent logcat entries and filter for JS entries to reduce output size - // Also include "{N} Runtime Tests" tag for Jasmine output - // Use a larger window to capture all failure details - const logcatOutput = execSync('adb -e logcat -d -s JS -s "{N} Runtime Tests" | tail -500', { - encoding: 'utf8', - maxBuffer: 4 * 1024 * 1024 // 4MB buffer limit for comprehensive logs - }); - - console.log('\n=== Analyzing Test Results ===\n'); - - // Track different types of test results - const testResults = { - esModules: { passed: false, failed: false, total: 0, passedCount: 0, failedCount: 0 }, - jasmine: { specs: 0, failures: 0 }, - manual: { tests: [], failures: [] }, - general: { passes: 0, failures: 0 } - }; - - // Look for ES Module test results - testResults.esModules.passed = logcatOutput.includes('ALL ES MODULE TESTS PASSED!'); - testResults.esModules.failed = logcatOutput.includes('SOME ES MODULE TESTS FAILED!'); - // Parse ES Module counts if present - const esmPassedMatch = logcatOutput.match(/Tests passed:\s*(\d+)/); - const esmFailedMatch = logcatOutput.match(/Tests failed:\s*(\d+)/); - const esmTotalMatch = logcatOutput.match(/Total tests:\s*(\d+)/); - if (esmPassedMatch) testResults.esModules.passedCount = parseInt(esmPassedMatch[1], 10); - if (esmFailedMatch) testResults.esModules.failedCount = parseInt(esmFailedMatch[1], 10); - if (esmTotalMatch) testResults.esModules.total = parseInt(esmTotalMatch[1], 10); - - // Look for Jasmine test results - const jasmineSuccessMatch = logcatOutput.match(/SUCCESS:\s*(\d+)\s+specs?,\s*(\d+)\s+failures?/); - const jasmineFailureMatch = logcatOutput.match(/FAILURE:\s*(\d+)\s+specs?,\s*(\d+)\s+failures?/); - - if (jasmineSuccessMatch) { - testResults.jasmine.specs = parseInt(jasmineSuccessMatch[1]); - testResults.jasmine.failures = parseInt(jasmineSuccessMatch[2]); - } else if (jasmineFailureMatch) { - testResults.jasmine.specs = parseInt(jasmineFailureMatch[1]); - testResults.jasmine.failures = parseInt(jasmineFailureMatch[2]); - } else { - // Try alternative pattern: "X of Y passed (Z skipped)" - const altJasmineMatch = logcatOutput.match(/(\d+)\s+of\s+(\d+)\s+passed\s*\((\d+)\s+skipped\)/); - if (altJasmineMatch) { - const passed = parseInt(altJasmineMatch[1]); - const total = parseInt(altJasmineMatch[2]); - const skipped = parseInt(altJasmineMatch[3]); - testResults.jasmine.specs = total; - testResults.jasmine.failures = total - passed - skipped; - } - } - - // Look for manual test patterns (TEST: prefix) - const testLines = logcatOutput.split('\n'); - testLines.forEach(line => { - if (line.includes('CONSOLE LOG') || line.includes('JS') || line.includes('{N} Runtime Tests')) { - // Handle both JS console logs and Jasmine runtime test logs - const logMatch = line.match(/(?:CONSOLE LOG|JS|{N} Runtime Tests):\s*(.+)/); - if (logMatch) { - const logContent = logMatch[1]; - - // Count manual tests (those that start with "TEST:") - if (logContent.startsWith('TEST:')) { - testResults.manual.tests.push(logContent); - } - - // Count general pass/fail indicators - if (logContent.includes('✅ PASS') || logContent.includes('PASS:')) { - testResults.general.passes++; - } - if (logContent.includes('❌ FAIL') || logContent.includes('FAIL:')) { - testResults.general.failures++; - testResults.manual.failures.push(logContent); - } - } - } - }); - - // Report results - console.log('📊 Test Results Summary:'); - console.log('='.repeat(50)); - - // ES Module tests - if (testResults.esModules.passed || testResults.esModules.failed) { - const esmCounts = testResults.esModules.total - ? ` (${testResults.esModules.passedCount}/${testResults.esModules.total} passed, ${testResults.esModules.failedCount} failed)` - : ''; - if (testResults.esModules.passed) { - console.log(`✅ ES Module Tests: PASSED${esmCounts}`); - } else if (testResults.esModules.failed) { - console.log(`❌ ES Module Tests: FAILED${esmCounts}`); - } - } else { - console.log('ES Module Tests: No clear results found'); + try { + console.log("Getting logcat output for test results..."); + + // Get only recent logcat entries and filter for JS entries to reduce output size + // Also include "{N} Runtime Tests" tag for Jasmine output + // Use a larger window to capture all failure details + const logcatOutput = execSync( + 'adb -e logcat -d -s JS -s "{N} Runtime Tests"', + { + encoding: "utf8", + maxBuffer: 4 * 1024 * 1024, // 4MB buffer limit for comprehensive logs + } + ); + + console.log("\n=== Analyzing Test Results ===\n"); + + // Track different types of test results + const testResults = { + esModules: { + status: "unknown", + total: 0, + passedCount: 0, + failedCount: 0, + detailMessages: [], + }, + jasmine: { specs: 0, failures: 0 }, + manual: { tests: [], failures: [] }, + general: { passes: 0, failures: 0 }, + }; + + // Look for ES Module test results (use the latest summary block) + const esModuleMatches = Array.from( + logcatOutput.matchAll( + /=== ES MODULE TEST RESULTS ===([\s\S]*?)(?=\n===|$)/g + ) + ); + if (esModuleMatches.length > 0) { + const lastMatch = esModuleMatches[esModuleMatches.length - 1]; + const blockText = lastMatch[1]; + + const passedMatch = blockText.match(/Tests passed:\s*(\d+)/); + const failedMatch = blockText.match(/Tests failed:\s*(\d+)/); + const totalMatch = blockText.match(/Total tests:\s*(\d+)/); + const statusMatch = blockText.match( + /(ALL ES MODULE TESTS PASSED!|SOME ES MODULE TESTS FAILED!)/ + ); + + if (passedMatch) + testResults.esModules.passedCount = parseInt(passedMatch[1], 10); + if (failedMatch) + testResults.esModules.failedCount = parseInt(failedMatch[1], 10); + if (totalMatch) testResults.esModules.total = parseInt(totalMatch[1], 10); + + if (statusMatch) { + testResults.esModules.status = statusMatch[1].includes("SOME") + ? "failed" + : "passed"; + } + + const detailMatches = Array.from(blockText.matchAll(/^\s*❌\s+(.+)$/gm)); + if (detailMatches.length > 0) { + testResults.esModules.detailMessages = detailMatches.map( + (match) => match[1] + ); + } + } else { + // Fallback: attempt to infer results from the latest counters if the block is missing + const fallbackPassed = Array.from( + logcatOutput.matchAll(/Tests passed:\s*(\d+)/g) + ).pop(); + const fallbackFailed = Array.from( + logcatOutput.matchAll(/Tests failed:\s*(\d+)/g) + ).pop(); + const fallbackTotal = Array.from( + logcatOutput.matchAll(/Total tests:\s*(\d+)/g) + ).pop(); + + if (fallbackPassed) + testResults.esModules.passedCount = parseInt(fallbackPassed[1], 10); + if (fallbackFailed) + testResults.esModules.failedCount = parseInt(fallbackFailed[1], 10); + if (fallbackTotal) + testResults.esModules.total = parseInt(fallbackTotal[1], 10); + + if (logcatOutput.includes("SOME ES MODULE TESTS FAILED!")) { + testResults.esModules.status = "failed"; + } else if ( + logcatOutput.includes("ALL ES MODULE TESTS PASSED!") && + testResults.esModules.total > 0 + ) { + testResults.esModules.status = "passed"; + } + } + + if (testResults.esModules.status === "unknown") { + if (testResults.esModules.failedCount > 0) { + testResults.esModules.status = "failed"; + } else if (testResults.esModules.total > 0) { + testResults.esModules.status = "passed"; + } + } + + // Look for Jasmine test results + const jasmineSuccessMatch = logcatOutput.match( + /SUCCESS:\s*(\d+)\s+specs?,\s*(\d+)\s+failures?/ + ); + const jasmineFailureMatch = logcatOutput.match( + /FAILURE:\s*(\d+)\s+specs?,\s*(\d+)\s+failures?/ + ); + + if (jasmineSuccessMatch) { + testResults.jasmine.specs = parseInt(jasmineSuccessMatch[1]); + testResults.jasmine.failures = parseInt(jasmineSuccessMatch[2]); + } else if (jasmineFailureMatch) { + testResults.jasmine.specs = parseInt(jasmineFailureMatch[1]); + testResults.jasmine.failures = parseInt(jasmineFailureMatch[2]); + } else { + // Try alternative pattern: "X of Y passed (Z skipped)" + const altJasmineMatch = logcatOutput.match( + /(\d+)\s+of\s+(\d+)\s+passed\s*\((\d+)\s+skipped\)/ + ); + if (altJasmineMatch) { + const passed = parseInt(altJasmineMatch[1]); + const total = parseInt(altJasmineMatch[2]); + const skipped = parseInt(altJasmineMatch[3]); + testResults.jasmine.specs = total; + testResults.jasmine.failures = total - passed - skipped; + } + } + + // Look for manual test patterns (TEST: prefix) + const testLines = logcatOutput.split("\n"); + testLines.forEach((line) => { + if ( + line.includes("CONSOLE LOG") || + line.includes("JS") || + line.includes("{N} Runtime Tests") + ) { + // Handle both JS console logs and Jasmine runtime test logs + const logMatch = line.match( + /(?:CONSOLE LOG|JS|{N} Runtime Tests):\s*(.+)/ + ); + if (logMatch) { + const logContent = logMatch[1]; + + // Count manual tests (those that start with "TEST:") + if (logContent.startsWith("TEST:")) { + testResults.manual.tests.push(logContent); + } + + // Count general pass/fail indicators + if (logContent.includes("✅ PASS") || logContent.includes("PASS:")) { + testResults.general.passes++; + } + if (logContent.includes("❌ FAIL") || logContent.includes("FAIL:")) { + testResults.general.failures++; + testResults.manual.failures.push(logContent); + } } - - // Jasmine tests - if (testResults.jasmine.specs > 0) { - if (testResults.jasmine.failures === 0) { - console.log(`✅ Jasmine Tests: ${testResults.jasmine.specs} specs, 0 failures`); - } else { - console.log(`❌ Jasmine Tests: ${testResults.jasmine.specs} specs, ${testResults.jasmine.failures} failures`); - } - } else { - console.log('ℹ️ Jasmine Tests: No Jasmine output detected (may use different execution path)'); + } + }); + + // Report results + console.log("📊 Test Results Summary:"); + console.log("=".repeat(50)); + + // ES Module tests (prefer failure if both markers present) + if ( + testResults.esModules.total > 0 || + testResults.esModules.status !== "unknown" + ) { + const esmCounts = testResults.esModules.total + ? ` (${testResults.esModules.passedCount}/${testResults.esModules.total} passed, ${testResults.esModules.failedCount} failed)` + : ""; + if (testResults.esModules.status === "failed") { + console.log(`❌ ES Module Tests: FAILED${esmCounts}`); + } else if (testResults.esModules.status === "passed") { + console.log(`✅ ES Module Tests: PASSED${esmCounts}`); + } else { + console.log(`ℹ️ ES Module Tests: Status unknown${esmCounts}`); + } + } else { + console.log("ES Module Tests: No clear results found"); + } + + // Jasmine tests + if (testResults.jasmine.specs > 0) { + if (testResults.jasmine.failures === 0) { + console.log( + `✅ Jasmine Tests: ${testResults.jasmine.specs} specs, 0 failures` + ); + } else { + console.log( + `❌ Jasmine Tests: ${testResults.jasmine.specs} specs, ${testResults.jasmine.failures} failures` + ); + } + } else { + console.log( + "ℹ️ Jasmine Tests: No Jasmine output detected (may use different execution path)" + ); + } + + // Manual tests + if (testResults.manual.tests.length > 0) { + console.log( + `📝 Manual Tests: ${testResults.manual.tests.length} tests executed` + ); + if (testResults.manual.failures.length > 0) { + console.log( + `❌ Manual Test Failures: ${testResults.manual.failures.length}` + ); + } + } + + // General pass/fail counts + if (testResults.general.passes > 0 || testResults.general.failures > 0) { + console.log( + `📈 General Results: ${testResults.general.passes} passes, ${testResults.general.failures} failures` + ); + } + + // Totals across detected test suites + const totalDetectedTests = + (testResults.jasmine.specs || 0) + (testResults.esModules.total || 0); + if (totalDetectedTests > 0) { + const totalFailures = + (testResults.jasmine.failures || 0) + + (testResults.esModules.failedCount || 0); + console.log(`—`); + console.log( + `🧮 Total Detected Tests: ${totalDetectedTests} (Jasmine: ${ + testResults.jasmine.specs || 0 + }, ES Modules: ${testResults.esModules.total || 0})` + ); + console.log(` Total Failures: ${totalFailures}`); + } + console.log("=".repeat(50)); + + // Show recent test output for debugging + const recentTestLines = testLines + .filter((line) => { + const logMatch = line.match( + /(?:CONSOLE LOG|JS|{N} Runtime Tests):\s*(.+)/ + ); + if (!logMatch) return false; + + const logContent = logMatch[1]; + + // Skip unhelpful messages + if ( + logContent === "Passed" || + logContent === "Failed" || + logContent === "Skipped" + ) { + return false; } - - // Manual tests - if (testResults.manual.tests.length > 0) { - console.log(`📝 Manual Tests: ${testResults.manual.tests.length} tests executed`); - if (testResults.manual.failures.length > 0) { - console.log(`❌ Manual Test Failures: ${testResults.manual.failures.length}`); - } + + return ( + logContent.includes("✅") || + logContent.includes("❌") || + logContent.includes("TEST:") || + logContent.includes("PASS") || + logContent.includes("FAIL") || + logContent.includes("specs") || + logContent.includes("failures") || + logContent.includes("SUCCESS:") || + logContent.includes("FAILURE:") || + logContent.includes("ES MODULE") || + logContent.includes("Total tests:") || + logContent.includes("Tests passed:") || + logContent.includes("Tests failed:") || + logContent.includes("FAILED TEST:") || + logContent.includes("Suite:") || + logContent.includes("File:") || + logContent.includes("Error:") + ); + }) + .slice(-5); // Show only last 5 relevant test lines to avoid duplicates + + // Remove consecutive duplicate lines + const uniqueTestLines = []; + let lastLine = ""; + recentTestLines.forEach((line) => { + const logMatch = line.match( + /(?:CONSOLE LOG|JS|{N} Runtime Tests):\s*(.+)/ + ); + if (logMatch) { + const currentContent = logMatch[1]; + if (currentContent !== lastLine) { + uniqueTestLines.push(line); + lastLine = currentContent; } - - // General pass/fail counts - if (testResults.general.passes > 0 || testResults.general.failures > 0) { - console.log(`📈 General Results: ${testResults.general.passes} passes, ${testResults.general.failures} failures`); + } + }); + + if (uniqueTestLines.length > 0) { + console.log("\n📋 Recent Test Output:"); + uniqueTestLines.forEach((line) => { + const logMatch = line.match( + /(?:CONSOLE LOG|JS|{N} Runtime Tests):\s*(.+)/ + ); + if (logMatch) { + console.log(` ${logMatch[1]}`); } - - // Totals across detected test suites - const totalDetectedTests = (testResults.jasmine.specs || 0) + (testResults.esModules.total || 0); - if (totalDetectedTests > 0) { - const totalFailures = (testResults.jasmine.failures || 0) + (testResults.esModules.failedCount || 0); - console.log(`—`); - console.log(`🧮 Total Detected Tests: ${totalDetectedTests} (Jasmine: ${testResults.jasmine.specs || 0}, ES Modules: ${testResults.esModules.total || 0})`); - console.log(` Total Failures: ${totalFailures}`); + }); + } + + // Determine overall result + const hasFailures = + testResults.esModules.status === "failed" || + testResults.jasmine.failures > 0 || + testResults.manual.failures.length > 0 || + testResults.general.failures > 0; + + const hasSuccesses = + testResults.esModules.status === "passed" || + testResults.jasmine.specs > 0 || + testResults.manual.tests.length > 0 || + testResults.general.passes > 0; + + console.log("\n" + "=".repeat(50)); + + if (hasFailures) { + console.error("OVERALL RESULT: TESTS FAILED"); + console.log("\nFailure Details:"); + if (testResults.esModules.status === "failed") { + console.log(" - ES Module tests failed"); + if (testResults.esModules.detailMessages.length > 0) { + testResults.esModules.detailMessages.slice(0, 5).forEach((detail) => { + console.log(` • ${detail}`); + }); + if (testResults.esModules.detailMessages.length > 5) { + console.log( + ` ... and ${ + testResults.esModules.detailMessages.length - 5 + } more` + ); + } } - console.log('='.repeat(50)); - - // Show recent test output for debugging - const recentTestLines = testLines - .filter(line => { - const logMatch = line.match(/(?:CONSOLE LOG|JS|{N} Runtime Tests):\s*(.+)/); - if (!logMatch) return false; - - const logContent = logMatch[1]; - - // Skip unhelpful messages - if (logContent === 'Passed' || logContent === 'Failed' || logContent === 'Skipped') { - return false; - } - - return ( - logContent.includes('✅') || - logContent.includes('❌') || - logContent.includes('TEST:') || - logContent.includes('PASS') || - logContent.includes('FAIL') || - logContent.includes('specs') || - logContent.includes('failures') || - logContent.includes('SUCCESS:') || - logContent.includes('FAILURE:') || - logContent.includes('ES MODULE') || - logContent.includes('Total tests:') || - logContent.includes('Tests passed:') || - logContent.includes('Tests failed:') || - logContent.includes('FAILED TEST:') || - logContent.includes('Suite:') || - logContent.includes('File:') || - logContent.includes('Error:') - ); - }) - .slice(-5); // Show only last 5 relevant test lines to avoid duplicates - - // Remove consecutive duplicate lines - const uniqueTestLines = []; - let lastLine = ''; - recentTestLines.forEach(line => { - const logMatch = line.match(/(?:CONSOLE LOG|JS|{N} Runtime Tests):\s*(.+)/); - if (logMatch) { - const currentContent = logMatch[1]; - if (currentContent !== lastLine) { - uniqueTestLines.push(line); - lastLine = currentContent; - } - } + } + if (testResults.jasmine.failures > 0) { + console.log( + ` - ${testResults.jasmine.failures} Jasmine test failures` + ); + } + if (testResults.manual.failures.length > 0) { + console.log( + ` - ${testResults.manual.failures.length} manual test failures` + ); + testResults.manual.failures.slice(0, 5).forEach((failure) => { + console.log(` • ${failure}`); }); - - if (uniqueTestLines.length > 0) { - console.log('\n📋 Recent Test Output:'); - uniqueTestLines.forEach(line => { - const logMatch = line.match(/(?:CONSOLE LOG|JS|{N} Runtime Tests):\s*(.+)/); - if (logMatch) { - console.log(` ${logMatch[1]}`); - } - }); + if (testResults.manual.failures.length > 5) { + console.log( + ` ... and ${testResults.manual.failures.length - 5} more` + ); } - - // Determine overall result - const hasFailures = testResults.esModules.failed || - testResults.jasmine.failures > 0 || - testResults.manual.failures.length > 0 || - testResults.general.failures > 0; - - const hasSuccesses = testResults.esModules.passed || - testResults.jasmine.specs > 0 || - testResults.manual.tests.length > 0 || - testResults.general.passes > 0; - - console.log('\n' + '='.repeat(50)); - - if (hasFailures) { - console.error('OVERALL RESULT: TESTS FAILED'); - console.log('\nFailure Details:'); - if (testResults.esModules.failed) { - console.log(' - ES Module tests failed'); - } - if (testResults.jasmine.failures > 0) { - console.log(` - ${testResults.jasmine.failures} Jasmine test failures`); - } - if (testResults.manual.failures.length > 0) { - console.log(` - ${testResults.manual.failures.length} manual test failures`); - testResults.manual.failures.slice(0, 5).forEach(failure => { - console.log(` • ${failure}`); - }); - if (testResults.manual.failures.length > 5) { - console.log(` ... and ${testResults.manual.failures.length - 5} more`); - } - } - - // Show detailed failure information from logs - console.log('\n📋 Detailed Failure Information:'); - - // Debug: Let's see what's actually in the logcat output - console.log('\n🔍 DEBUG: All recent logcat lines containing "fail" or "error":'); - const debugLines = logcatOutput.split('\n').filter(line => - line.toLowerCase().includes('fail') || - line.toLowerCase().includes('error') || - line.toLowerCase().includes('expected') || - line.toLowerCase().includes('debug:') - ).slice(-20); // Show more lines to catch failure details - - debugLines.forEach((line, index) => { - console.log(` ${index + 1}: ${line}`); - }); - - const failureLines = logcatOutput.split('\n').filter(line => - (line.includes('CONSOLE LOG') || line.includes('JS') || line.includes('{N} Runtime Tests')) && - (line.toLowerCase().includes('failed test:') || - line.toLowerCase().includes('suite:') || - line.toLowerCase().includes('file:') || - line.toLowerCase().includes('error:') || - line.includes('JASMINE FAILURE:') || - line.includes('JASMINE SUITE:') || - line.includes('JASMINE FILE:') || - line.includes('JASMINE ERROR:') || - line.includes('JASMINE STACK:') || - line.includes('Expected') || - line.includes('Actual') || - line.includes('at ')) - ).slice(-15); // Last 15 failure-related lines for more context - - if (failureLines.length > 0) { - console.log('\nFormatted Failure Information:'); - failureLines.forEach(line => { - const logMatch = line.match(/(?:CONSOLE LOG|JS|{N} Runtime Tests):\s*(.+)/); - if (logMatch) { - console.log(` ${logMatch[1]}`); - } - }); - } else { - console.log(' No detailed failure information found in formatted logs'); - } - - process.exit(1); - } else if (hasSuccesses) { - console.log('OVERALL RESULT: ALL DETECTED TESTS PASSED'); - console.log('Note: Some tests may use different execution paths or output methods'); - } else { - console.log('OVERALL RESULT: NO TEST RESULTS DETECTED'); - console.log('This might indicate tests did not run or complete properly.'); - process.exit(1); - } - - console.log('\n=== Test verification completed successfully ==='); - - } catch (error) { - console.error(`Error checking test results: ${error.message}`); - process.exit(1); + } + + // Show detailed failure information from logs + console.log("\n📋 Detailed Failure Information:"); + + // Debug: Let's see what's actually in the logcat output + console.log( + '\n🔍 DEBUG: All recent logcat lines containing "fail" or "error":' + ); + const debugLines = logcatOutput + .split("\n") + .filter( + (line) => + line.toLowerCase().includes("fail") || + line.toLowerCase().includes("error") || + line.toLowerCase().includes("expected") || + line.toLowerCase().includes("debug:") + ) + .slice(-20); // Show more lines to catch failure details + + debugLines.forEach((line, index) => { + console.log(` ${index + 1}: ${line}`); + }); + + const failureLines = logcatOutput + .split("\n") + .filter( + (line) => + (line.includes("CONSOLE LOG") || + line.includes("JS") || + line.includes("{N} Runtime Tests")) && + (line.toLowerCase().includes("failed test:") || + line.toLowerCase().includes("suite:") || + line.toLowerCase().includes("file:") || + line.toLowerCase().includes("error:") || + line.includes("JASMINE FAILURE:") || + line.includes("JASMINE SUITE:") || + line.includes("JASMINE FILE:") || + line.includes("JASMINE ERROR:") || + line.includes("JASMINE STACK:") || + line.includes("Expected") || + line.includes("Actual") || + line.includes("at ")) + ) + .slice(-15); // Last 15 failure-related lines for more context + + if (failureLines.length > 0) { + console.log("\nFormatted Failure Information:"); + failureLines.forEach((line) => { + const logMatch = line.match( + /(?:CONSOLE LOG|JS|{N} Runtime Tests):\s*(.+)/ + ); + if (logMatch) { + console.log(` ${logMatch[1]}`); + } + }); + } else { + console.log( + " No detailed failure information found in formatted logs" + ); + } + + process.exit(1); + } else if (hasSuccesses) { + console.log("OVERALL RESULT: ALL DETECTED TESTS PASSED"); + console.log( + "Note: Some tests may use different execution paths or output methods" + ); + } else { + console.log("OVERALL RESULT: NO TEST RESULTS DETECTED"); + console.log( + "This might indicate tests did not run or complete properly." + ); + process.exit(1); } + + console.log("\n=== Test verification completed successfully ==="); + } catch (error) { + console.error(`Error checking test results: ${error.message}`); + process.exit(1); + } } // Run the check