|
26 | 26 | if (typeof global.ArrayBuffer !== "undefined" && value instanceof global.ArrayBuffer) { |
27 | 27 | return "com_codename1_html5_js_typedarrays_ArrayBuffer"; |
28 | 28 | } |
| 29 | + if (typeof global.Uint8ClampedArray !== "undefined" && value instanceof global.Uint8ClampedArray) { |
| 30 | + return "com_codename1_html5_js_typedarrays_Uint8ClampedArray"; |
| 31 | + } |
29 | 32 | if (typeof global.Uint8Array !== "undefined" && value instanceof global.Uint8Array) { |
30 | 33 | return "com_codename1_html5_js_typedarrays_Uint8Array"; |
31 | 34 | } |
@@ -380,10 +383,30 @@ function emitDisplayInitDiag(marker) { |
380 | 383 | emitDiagLine("PARPAR:DIAG:" + marker + ":displayClassExists=" + (state.displayClassExists ? "1" : "0")+ ":instance=" + (state.instance ? "present" : "null")+ ":edt=" + (state.edt ? "present" : "null") + (state.edtThreadName ? ":edtThreadName=" + state.edtThreadName : "")); |
381 | 384 | } |
382 | 385 |
|
| 386 | +// Enable forwarding System.out.println output to the main thread via postMessage. |
| 387 | +// This is only needed in the browser JS port where Playwright cannot reliably |
| 388 | +// capture Worker console.log. Detect the browser Worker context by checking |
| 389 | +// for the native importScripts function (not the polyfill used in Node.js |
| 390 | +// worker_threads test harnesses which uses vm.runInThisContext). |
| 391 | +global.__cn1ForwardConsoleToMain = (typeof WorkerGlobalScope !== "undefined" |
| 392 | + || (typeof self !== "undefined" && typeof self.importScripts === "function" && typeof process === "undefined")); |
| 393 | + |
383 | 394 | function emitDiagLine(line) { |
384 | 395 | if (global.console && typeof global.console.log === "function") { |
385 | 396 | global.console.log(line); |
386 | 397 | } |
| 398 | + // Forward to main thread so Playwright (page.on('console')) can capture |
| 399 | + // CN1SS output from the worker. Worker console.log is not always |
| 400 | + // observable from the page context. |
| 401 | + if (typeof global.postMessage === "function") { |
| 402 | + try { |
| 403 | + global.postMessage({ type: "log", message: String(line) }); |
| 404 | + } catch (postErr) { |
| 405 | + if (global.console && typeof global.console.warn === "function") { |
| 406 | + global.console.warn("emitDiagLine:postMessage failed: " + String(postErr && postErr.message ? postErr.message : postErr)); |
| 407 | + } |
| 408 | + } |
| 409 | + } |
387 | 410 | } |
388 | 411 |
|
389 | 412 | const cn1ssBootstrapChunkBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO5WZ8kAAAAASUVORK5CYII="; |
@@ -971,6 +994,36 @@ bindNative([ |
971 | 994 | return jvm.wrapJsObject(new global.Uint8Array(jvm.unwrapJsValue(buffer), offset | 0, length | 0), "com_codename1_html5_js_typedarrays_Uint8Array"); |
972 | 995 | }); |
973 | 996 |
|
| 997 | +// Uint8ClampedArray factory methods – needed by createImageData() in |
| 998 | +// HTML5Implementation which converts ARGB int[] pixels into canvas ImageData. |
| 999 | +bindNative([ |
| 1000 | + "cn1_com_codename1_html5_js_typedarrays_Uint8ClampedArray_create_int_R_com_codename1_html5_js_typedarrays_Uint8ClampedArray", |
| 1001 | + "cn1_com_codename1_html5_js_typedarrays_Uint8ClampedArray_create___int_R_com_codename1_html5_js_typedarrays_Uint8ClampedArray" |
| 1002 | +], function*(size) { |
| 1003 | + return jvm.wrapJsObject(new global.Uint8ClampedArray(size | 0), "com_codename1_html5_js_typedarrays_Uint8ClampedArray"); |
| 1004 | +}); |
| 1005 | + |
| 1006 | +bindNative([ |
| 1007 | + "cn1_com_codename1_html5_js_typedarrays_Uint8ClampedArray_create_com_codename1_html5_js_typedarrays_ArrayBuffer_R_com_codename1_html5_js_typedarrays_Uint8ClampedArray", |
| 1008 | + "cn1_com_codename1_html5_js_typedarrays_Uint8ClampedArray_create___com_codename1_html5_js_typedarrays_ArrayBuffer_R_com_codename1_html5_js_typedarrays_Uint8ClampedArray" |
| 1009 | +], function*(buffer) { |
| 1010 | + return jvm.wrapJsObject(new global.Uint8ClampedArray(jvm.unwrapJsValue(buffer)), "com_codename1_html5_js_typedarrays_Uint8ClampedArray"); |
| 1011 | +}); |
| 1012 | + |
| 1013 | +bindNative([ |
| 1014 | + "cn1_com_codename1_html5_js_typedarrays_Uint8ClampedArray_create_com_codename1_html5_js_typedarrays_ArrayBuffer_int_R_com_codename1_html5_js_typedarrays_Uint8ClampedArray", |
| 1015 | + "cn1_com_codename1_html5_js_typedarrays_Uint8ClampedArray_create___com_codename1_html5_js_typedarrays_ArrayBuffer_int_R_com_codename1_html5_js_typedarrays_Uint8ClampedArray" |
| 1016 | +], function*(buffer, offset) { |
| 1017 | + return jvm.wrapJsObject(new global.Uint8ClampedArray(jvm.unwrapJsValue(buffer), offset | 0), "com_codename1_html5_js_typedarrays_Uint8ClampedArray"); |
| 1018 | +}); |
| 1019 | + |
| 1020 | +bindNative([ |
| 1021 | + "cn1_com_codename1_html5_js_typedarrays_Uint8ClampedArray_create_com_codename1_html5_js_typedarrays_ArrayBuffer_int_int_R_com_codename1_html5_js_typedarrays_Uint8ClampedArray", |
| 1022 | + "cn1_com_codename1_html5_js_typedarrays_Uint8ClampedArray_create___com_codename1_html5_js_typedarrays_ArrayBuffer_int_int_R_com_codename1_html5_js_typedarrays_Uint8ClampedArray" |
| 1023 | +], function*(buffer, offset, length) { |
| 1024 | + return jvm.wrapJsObject(new global.Uint8ClampedArray(jvm.unwrapJsValue(buffer), offset | 0, length | 0), "com_codename1_html5_js_typedarrays_Uint8ClampedArray"); |
| 1025 | +}); |
| 1026 | + |
974 | 1027 | bindNative([ |
975 | 1028 | "cn1_com_codename1_impl_html5_HTML5Implementation_createCNOutboxEvent_java_lang_String_int_R_com_codename1_html5_js_dom_Event", |
976 | 1029 | "cn1_com_codename1_impl_html5_HTML5Implementation_createCNOutboxEvent___java_lang_String_int_R_com_codename1_html5_js_dom_Event" |
@@ -1489,9 +1542,18 @@ bindCiFallback("HashMap.computeHashCodeNullKey", [ |
1489 | 1542 | emitDiagLine("PARPAR:DIAG:FALLBACK:hashMapComputeHashCode:nullKey=1"); |
1490 | 1543 | return 0; |
1491 | 1544 | } |
| 1545 | + // Try the original captured at port.js load time first. |
1492 | 1546 | if (typeof hashMapComputeHashCodeOriginal === "function") { |
1493 | 1547 | return yield* hashMapComputeHashCodeOriginal(key); |
1494 | 1548 | } |
| 1549 | + // Original wasn't available yet (translated_app.js loads after port.js). |
| 1550 | + // computeHashCode(key) is just key.hashCode(), so call hashCode directly |
| 1551 | + // via virtual dispatch to avoid recursion back into computeHashCode. |
| 1552 | + var hashCodeMethod = jvm.resolveVirtual(key.__class || "java_lang_Object", |
| 1553 | + "cn1_java_lang_Object_hashCode_R_int"); |
| 1554 | + if (typeof hashCodeMethod === "function") { |
| 1555 | + return yield* hashCodeMethod(key); |
| 1556 | + } |
1495 | 1557 | return 0; |
1496 | 1558 | }); |
1497 | 1559 | if (typeof global[hashMapComputeHashCodeImplMethodId] === "function") { |
@@ -3112,6 +3174,9 @@ function emitCn1ssChunks(base64, testName, channelName) { |
3112 | 3174 | emitDiagLine(prefix + ":" + test + ":" + index + ":"); |
3113 | 3175 | cn1ssChunkIndexByStream[streamKey] = nextIndex + 1; |
3114 | 3176 | } |
| 3177 | + // Emit END marker matching the Java emitChannel convention so the |
| 3178 | + // downstream cn1ss_list_tests / cn1ss_decode helpers can detect the stream. |
| 3179 | + emitDiagLine(prefix + ":END:" + test); |
3115 | 3180 | } |
3116 | 3181 |
|
3117 | 3182 | const cn1ssEmitCurrentFormScreenshotMethodId = "cn1_com_codenameone_examples_hellocodenameone_tests_Cn1ssDeviceRunnerHelper_emitCurrentFormScreenshot_java_lang_String_java_lang_Runnable"; |
@@ -3168,31 +3233,16 @@ bindCiFallback("Cn1ssDeviceRunnerHelper.emitCurrentFormScreenshotDom", [ |
3168 | 3233 | cn1ssEmitCurrentFormScreenshotMethodId + "__impl", |
3169 | 3234 | cn1ssEmitCurrentFormScreenshotMethodId |
3170 | 3235 | ], cn1ssHelperClassName, fallbackSymbol); |
| 3236 | + // In worker mode the translated screenshot path eventually calls |
| 3237 | + // BlobUtil.canvasToBlob() which uses HTMLCanvasElement.toBlob(callback). |
| 3238 | + // That callback is a Java object and cannot be invoked from the host |
| 3239 | + // thread, so the worker hangs forever in a wait-loop. Always use the |
| 3240 | + // DOM-based capture via host bridge calls instead – this avoids async |
| 3241 | + // callbacks entirely and works reliably across the worker boundary. |
3171 | 3242 | if (originalResolved && typeof originalResolved.fn === "function") { |
3172 | | - if (cn1ssEmitCurrentFormScreenshotInvokeDepth > 0) { |
3173 | | - emitDiagLine("PARPAR:DIAG:FALLBACK:cn1ssEmitCurrentFormScreenshotDom:originalReentryBypass=1"); |
3174 | | - shouldUseDomFallback = true; |
3175 | | - } else { |
3176 | | - try { |
3177 | | - cn1ssEmitCurrentFormScreenshotInvokeDepth++; |
3178 | | - emitDiagLine("PARPAR:DIAG:FALLBACK:cn1ssEmitCurrentFormScreenshotDom:originalResolved=" + originalResolved.source); |
3179 | | - yield* originalResolved.fn(testName, completion); |
3180 | | - return null; |
3181 | | - } catch (originalErr) { |
3182 | | - emitDiagLine("PARPAR:DIAG:FALLBACK:cn1ssEmitCurrentFormScreenshotDom:originalInvokeErr=" |
3183 | | - + String(originalErr && originalErr.message ? originalErr.message : originalErr)); |
3184 | | - if (originalErr && originalErr.stack) { |
3185 | | - emitDiagLine("PARPAR:DIAG:FALLBACK:cn1ssEmitCurrentFormScreenshotDom:originalInvokeStack=" |
3186 | | - + String(originalErr.stack).split("\n").slice(0, 2).join(" | ")); |
3187 | | - } |
3188 | | - shouldUseDomFallback = true; |
3189 | | - } finally { |
3190 | | - cn1ssEmitCurrentFormScreenshotInvokeDepth = Math.max(0, cn1ssEmitCurrentFormScreenshotInvokeDepth - 1); |
3191 | | - } |
3192 | | - } |
| 3243 | + emitDiagLine("PARPAR:DIAG:FALLBACK:cn1ssEmitCurrentFormScreenshotDom:skipTranslated=canvasToBlob_hang"); |
3193 | 3244 | } else { |
3194 | 3245 | emitDiagLine("PARPAR:DIAG:FALLBACK:cn1ssEmitCurrentFormScreenshotDom:originalMissing=1"); |
3195 | | - shouldUseDomFallback = true; |
3196 | 3246 | } |
3197 | 3247 | const canvas = global.document && typeof global.document.querySelector === "function" |
3198 | 3248 | ? global.document.querySelector("canvas") |
@@ -3268,9 +3318,34 @@ bindCiFallback("Cn1ssDeviceRunnerHelper.emitChannelFastJs", [ |
3268 | 3318 | cn1ssEmitChannelMethodId, |
3269 | 3319 | cn1ssEmitChannelMethodId + "__impl" |
3270 | 3320 | ], function*(payloadBytes, testName, channelName) { |
3271 | | - const base64 = byteArrayToBase64(payloadBytes); |
3272 | 3321 | const test = resolveCn1ssTestName(toCn1StringValue(testName)); |
3273 | 3322 | const channel = toCn1StringValue(channelName); |
| 3323 | + // For the primary screenshot channel (empty channel name), the Java-side |
| 3324 | + // Display.screenshot() in the worker reads from OffscreenCanvas which |
| 3325 | + // may not reflect the main-thread visible canvas. Replace the payload |
| 3326 | + // with a main-thread canvas capture via the host bridge when available. |
| 3327 | + if (!channel && jvm && typeof jvm.invokeHostNative === "function" && !cn1ssScreenshotEmitted[test]) { |
| 3328 | + try { |
| 3329 | + yield jvm.invokeHostNative("__cn1_wait_for_ui_settle__", [{ |
| 3330 | + reason: "screenshot:" + test, |
| 3331 | + maxFrames: 18, |
| 3332 | + stableFrames: 2 |
| 3333 | + }]); |
| 3334 | + const hostResult = yield jvm.invokeHostNative("__cn1_capture_canvas_png__", []); |
| 3335 | + const capturedDataUrl = hostResult == null ? "" : String(hostResult); |
| 3336 | + if (capturedDataUrl && capturedDataUrl.indexOf("data:image/") === 0) { |
| 3337 | + cn1ssScreenshotEmitted[test] = true; |
| 3338 | + const comma = capturedDataUrl.indexOf(","); |
| 3339 | + const hostBase64 = comma >= 0 ? capturedDataUrl.substring(comma + 1) : ""; |
| 3340 | + emitDiagLine("PARPAR:DIAG:FALLBACK:emitChannelFastJs:hostCapture=1:test=" + test + ":len=" + hostBase64.length); |
| 3341 | + emitCn1ssChunks(hostBase64, test, channel); |
| 3342 | + return null; |
| 3343 | + } |
| 3344 | + } catch (_hostErr) { |
| 3345 | + emitDiagLine("PARPAR:DIAG:FALLBACK:emitChannelFastJs:hostCaptureErr=" + String(_hostErr && _hostErr.message ? _hostErr.message : _hostErr)); |
| 3346 | + } |
| 3347 | + } |
| 3348 | + const base64 = byteArrayToBase64(payloadBytes); |
3274 | 3349 | emitCn1ssChunks(base64, test, channel); |
3275 | 3350 | return null; |
3276 | 3351 | }); |
|
0 commit comments