From 2df5bc901bd576cfac42d6485240f4cf1d27c511 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sun, 19 Jul 2026 04:19:19 +0300 Subject: [PATCH 1/5] Fix JavaScript port status reliability --- .../charts/views/CombinedXYChart.java | 52 +- .../accessibility/AccessibilityManager.java | 36 +- .../ui/accessibility/AccessibilityNode.java | 27 + .../impl/html5/HTML5BrowserComponent.java | 73 +- .../codename1/impl/html5/HTML5CameraImpl.java | 41 +- .../codename1/impl/html5/HTML5VideoIO.java | 9 +- Ports/JavaScriptPort/src/main/webapp/port.js | 700 ++++++++++++------ .../charts/views/CombinedXYChartTest.java | 27 + .../build-javascript-port-hellocodenameone.sh | 10 +- .../tests/BrowserComponentScreenshotTest.java | 23 +- .../hellocodenameone/tests/CameraApiTest.java | 59 +- .../tests/Cn1ssDeviceRunner.java | 115 +-- .../tests/OrientationLockScreenshotTest.java | 51 +- scripts/run-javascript-browser-tests.sh | 15 +- scripts/run-javascript-headless-browser.mjs | 7 + scripts/run-javascript-screenshot-tests.sh | 7 + .../tools/translator/ByteCodeClass.java | 11 + .../translator/JavascriptMethodGenerator.java | 7 +- .../translator/JavascriptReachability.java | 56 +- .../codename1/tools/translator/Parser.java | 6 +- .../src/javascript/browser_bridge.js | 594 +++++++++++++++ .../src/javascript/parparvm_runtime.js | 109 ++- vm/JavaAPI/src/java/io/PrintStream.java | 10 +- .../JavascriptRuntimeSemanticsTest.java | 25 +- .../tools/translator/JsFloatingFormatApp.java | 61 ++ .../tools/translator/JsLocaleTimeZoneApp.java | 9 + .../translator/JsRtaResurrectedClassApp.java | 21 + 27 files changed, 1620 insertions(+), 541 deletions(-) create mode 100644 vm/tests/src/test/resources/com/codename1/tools/translator/JsFloatingFormatApp.java create mode 100644 vm/tests/src/test/resources/com/codename1/tools/translator/JsRtaResurrectedClassApp.java diff --git a/CodenameOne/src/com/codename1/charts/views/CombinedXYChart.java b/CodenameOne/src/com/codename1/charts/views/CombinedXYChart.java index 2a4c2950782..c883dc12e51 100644 --- a/CodenameOne/src/com/codename1/charts/views/CombinedXYChart.java +++ b/CodenameOne/src/com/codename1/charts/views/CombinedXYChart.java @@ -21,8 +21,6 @@ import com.codename1.charts.renderers.XYMultipleSeriesRenderer; import com.codename1.charts.renderers.XYMultipleSeriesRenderer.Orientation; import com.codename1.charts.renderers.XYSeriesRenderer; -import com.codename1.io.Log; - import java.util.List; @@ -42,11 +40,6 @@ public class CombinedXYChart extends XYChart { /// The embedded XY charts. private final XYChart[] mCharts; - /// The supported charts for being combined. - private final Class[] xyChartTypes = new Class[]{TimeChart.class, LineChart.class, - CubicLineChart.class, BarChart.class, BubbleChart.class, ScatterChart.class, - RangeBarChart.class, RangeStackedBarChart.class}; - /// Builds a new combined XY chart instance. /// /// #### Parameters @@ -63,11 +56,7 @@ public CombinedXYChart(XYMultipleSeriesDataset dataset, XYMultipleSeriesRenderer int length = chartDefinitions.length; mCharts = new XYChart[length]; for (int i = 0; i < length; i++) { - try { - mCharts[i] = getXYChart(chartDefinitions[i].getType()); - } catch (Exception e) { // PMD Fix: EmptyCatchBlock log exception - Log.e(e); - } + mCharts[i] = createXYChart(chartDefinitions[i].getType()); if (mCharts[i] == null) { throw new IllegalArgumentException("Unknown chart type " + chartDefinitions[i].getType()); } else { @@ -95,21 +84,32 @@ public CombinedXYChart(XYMultipleSeriesDataset dataset, XYMultipleSeriesRenderer /// /// an instance of a chart implementation /// - /// #### Throws - /// - /// - `IllegalAccessException` - /// - /// - `InstantiationException` - private XYChart getXYChart(String type) throws IllegalAccessException, InstantiationException { - XYChart chart = null; - int length = xyChartTypes.length; - for (int i = 0; i < length && chart == null; i++) { - XYChart newChart = (XYChart) xyChartTypes[i].newInstance(); - if (type.equals(newChart.getChartType())) { - chart = newChart; - } + static XYChart createXYChart(String type) { + if (TimeChart.TYPE.equals(type)) { + return new TimeChart(); + } + if (LineChart.TYPE.equals(type)) { + return new LineChart(); + } + if (CubicLineChart.TYPE.equals(type)) { + return new CubicLineChart(); + } + if (BarChart.TYPE.equals(type)) { + return new BarChart(); + } + if (BubbleChart.TYPE.equals(type)) { + return new BubbleChart(); + } + if (ScatterChart.TYPE.equals(type)) { + return new ScatterChart(); + } + if (RangeBarChart.TYPE.equals(type)) { + return new RangeBarChart(); + } + if (RangeStackedBarChart.TYPE.equals(type)) { + return new RangeStackedBarChart(); } - return chart; + return null; } /// The graphical representation of a series. diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityManager.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityManager.java index 9a58315a0d1..d358a204179 100644 --- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityManager.java +++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityManager.java @@ -37,7 +37,6 @@ import com.codename1.ui.TextField; import com.codename1.ui.geom.Rectangle; import com.codename1.ui.table.Table; -import com.codename1.ui.util.WeakHashMap; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -63,9 +62,6 @@ public final class AccessibilityManager { public static final int CHANGE_ALL = 0x7fffffff; private static final AccessibilityManager INSTANCE = new AccessibilityManager(); - private final Map componentIds = new WeakHashMap(); - private final Map> virtualIds = - new WeakHashMap>(); private long nextId = 1; private long generation; private boolean dirty = true; @@ -142,8 +138,11 @@ public synchronized AccessibilityTreeSnapshot getSnapshot(Form form) { return snapshot; } if (form == null) { - snapshot = new AccessibilityTreeSnapshot(++generation, Collections.emptyList(), - Collections.emptyMap()); + generation++; + AccessibilityTreeSnapshot emptySnapshot = new AccessibilityTreeSnapshot( + generation, Collections.emptyList(), + Collections.emptyMap()); + snapshot = emptySnapshot; snapshotForm = null; dirty = false; pendingChanges = 0; @@ -156,7 +155,9 @@ public synchronized AccessibilityTreeSnapshot getSnapshot(Form form) { List rootIds = new ArrayList(); LinkedHashMap nodes = new LinkedHashMap(); freeze(roots, -1, rootIds, nodes); - snapshot = new AccessibilityTreeSnapshot(++generation, rootIds, nodes); + generation++; + AccessibilityTreeSnapshot updatedSnapshot = new AccessibilityTreeSnapshot(generation, rootIds, nodes); + snapshot = updatedSnapshot; snapshotForm = form; dirty = false; pendingChanges = 0; @@ -188,24 +189,21 @@ public void run() { } private long idFor(Component component) { - Long id = componentIds.get(component); - if (id == null) { - id = Long.valueOf(nextId++); - componentIds.put(component, id); + AccessibilityNode semantics = component.getSemantics(); + long id = semantics.getInternalId(); + if (id == 0) { + id = nextId++; + semantics.setInternalId(id); } - return id.longValue(); + return id; } private long idForVirtual(Component host, String path) { - Map hostIds = virtualIds.get(host); - if (hostIds == null) { - hostIds = new LinkedHashMap(); - virtualIds.put(host, hostIds); - } - Long id = hostIds.get(path); + AccessibilityNode semantics = host.getSemantics(); + Long id = semantics.getInternalVirtualId(path); if (id == null) { id = Long.valueOf(nextId++); - hostIds.put(path, id); + semantics.putInternalVirtualId(path, id.longValue()); } return id.longValue(); } diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNode.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNode.java index 3668f0b799b..6d3f6802b3d 100644 --- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNode.java +++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNode.java @@ -26,7 +26,9 @@ import com.codename1.ui.geom.Rectangle; import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; /// Mutable semantic configuration for a lightweight component or virtual child. /// @@ -35,6 +37,12 @@ /// virtual nodes must have a stable {@link #setVirtualKey(java.lang.String) virtual key}. public class AccessibilityNode { private Component owner; + // Stable runtime IDs belong to the component-owned semantic node. Keeping + // them here gives them exactly the component's lifetime without relying on + // the weak-value UI cache, whose boxed Long values may be collected while + // the component is still alive. + private long internalId; + private Map internalVirtualIds; private String virtualKey; private String identifier; private String label; @@ -94,6 +102,25 @@ private AccessibilityNode changed(int type) { public Component getOwner() { return owner; } + + long getInternalId() { + return internalId; + } + + void setInternalId(long internalId) { + this.internalId = internalId; + } + + Long getInternalVirtualId(String path) { + return internalVirtualIds == null ? null : internalVirtualIds.get(path); + } + + void putInternalVirtualId(String path, long id) { + if (internalVirtualIds == null) { + internalVirtualIds = new LinkedHashMap(); + } + internalVirtualIds.put(path, Long.valueOf(id)); + } public String getVirtualKey() { return virtualKey; } diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5BrowserComponent.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5BrowserComponent.java index 9a9f334b42a..8f66a78d500 100644 --- a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5BrowserComponent.java +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5BrowserComponent.java @@ -319,6 +319,32 @@ private static boolean isCORSRestricted(HTMLIFrameElement iframe) { private boolean listenersInstalled; private List frameListeners; + private ShouldLoadURLCallback navigationCallback; + + private void installNavigationCallback() { + if (navigationCallback == null) { + navigationCallback = new ShouldLoadURLCallback() { + + @Override + public boolean shouldLoadURL(final String url) { + // The host bridge already invokes this callback + // asynchronously on the worker. BrowserComponent will + // marshal the registered result callback onto the EDT. + parent.fireBrowserNavigationCallbacks(url); + + // The callback URL is only a transport from Javascript to Java. + // It must never become a real iframe navigation. + return false; + } + }; + } + if (iframe == null) { + installShouldLoadURLCallbackShared(navigationCallback); + } else { + installShouldLoadURLCallback(iframe, navigationCallback); + } + } + private void installFrameListeners() { if (listenersInstalled) { return; @@ -362,23 +388,7 @@ public void handleEvent(Event evt) { HTML5Implementation._log("Failed to add event handlers to iframe, probably due to a CORS error"); } - installShouldLoadURLCallback(iframe, new ShouldLoadURLCallback() { - - @Override - public boolean shouldLoadURL(final JSObject url) { - new Thread() { - public void run() { - - parent.fireBrowserNavigationCallbacks(JS.unwrapString(url)); - } - }.start(); - - // We always return false since this will only be used for - // the javascript bridge and we don't want it to try to - // do a window.location load. - return false; - } - }); + installNavigationCallback(); } else { HTML5Implementation._log("Is cors restricted"); } @@ -446,23 +456,7 @@ public HTML5BrowserComponent(HTMLElement el, Object p) { parent = (BrowserComponent)p; parent.fireWebEvent("onStart", new ActionEvent(CN.getProperty("browser.window.location.href", ""))); parent.fireWebEvent("onLoad", new ActionEvent(CN.getProperty("browser.window.location.href", ""))); - installShouldLoadURLCallbackShared(new ShouldLoadURLCallback() { - - @Override - public boolean shouldLoadURL(final JSObject url) { - new Thread() { - public void run() { - - parent.fireBrowserNavigationCallbacks(JS.unwrapString(url)); - } - }.start(); - - // We always return false since this will only be used for - // the javascript bridge and we don't want it to try to - // do a window.location load. - return false; - } - }); + installNavigationCallback(); } return; @@ -682,6 +676,7 @@ public void execute(String javascript){ } WindowExt win = iframe == null ? ((WindowExt)Window.current()) : (WindowExt)iframe.getContentWindow(); + installNavigationCallback(); win.eval(javascript); //Window win = iframe.getContentWindow(); //win.getLocation().assign("javascript:"+javascript); @@ -697,6 +692,7 @@ public String executeAndReturnString(String javascript){ } //WindowExt win = (WindowExt)iframe.getContentWindow(); Window win = iframe == null ? Window.current() : iframe.getContentWindow(); + installNavigationCallback(); return evalStr(win, javascript); //return win.eval(javascript); } @@ -935,13 +931,16 @@ static interface LoadPageCallback extends JSObject { @JSFunctor interface ShouldLoadURLCallback extends JSObject { - boolean shouldLoadURL(JSObject url); + boolean shouldLoadURL(String url); } - @JSBody(params={"iframe","callback"}, script="try {var win=iframe.contentWindow||iframe; win.cn1application = win.cn1application || {}; win.cn1application.shouldNavigate=callback;} catch (e) { console.log('Failed to install shouldNavigate in iframe for browser component.');}") + // Implemented by port.js through a main-thread host call. This must not be + // an @JSBody: ParparVM executes @JSBody code in the worker, where iframe is + // only a host-ref proxy and has no live contentWindow. native static void installShouldLoadURLCallback(HTMLIFrameElement el, ShouldLoadURLCallback callback); - @JSBody(params={"callback"}, script="try {var win=window; win.cn1application = win.cn1application || {}; win.cn1application.shouldNavigate=callback;} catch (e) { console.log('Failed to install shouldNavigate in iframe for browser component.');}") + // See installShouldLoadURLCallback(). The shared-window variant uses the + // same host bridge and installs on the real browser window. native static void installShouldLoadURLCallbackShared(ShouldLoadURLCallback callback); } diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5CameraImpl.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5CameraImpl.java index dbdefdf2cd0..e5b526905da 100644 --- a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5CameraImpl.java +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5CameraImpl.java @@ -38,7 +38,6 @@ import com.codename1.impl.CameraImpl; import com.codename1.io.FileSystemStorage; import com.codename1.io.Log; -import com.codename1.ui.Display; import com.codename1.ui.PeerComponent; import com.codename1.ui.geom.Dimension; import com.codename1.util.AsyncResource; @@ -132,25 +131,23 @@ public void takePhoto(PhotoCaptureOptions opts, AsyncResource res return; } final PhotoCaptureOptions o = opts == null ? new PhotoCaptureOptions() : opts; - try { - CapturedPhoto cp = grab(o.getWidth(), o.getHeight(), o.getJpegQuality(), o.getFilePath()); - final CapturedPhoto fcp = cp; - final AsyncResource r = result; - Display.getInstance().callSerially(new Runnable() { - @Override public void run() { r.complete(fcp); } - }); - } catch (Throwable t) { - final Throwable e = t; - final AsyncResource r = result; - Display.getInstance().callSerially(new Runnable() { - @Override public void run() { r.error(e); } - }); - } + final AsyncResource out = result; + new Thread(new Runnable() { + @Override public void run() { + try { + out.complete(grab(o.getWidth(), o.getHeight(), o.getJpegQuality(), + o.getFilePath(), true)); + } catch (Throwable t) { + out.error(t); + } + } + }, "cn1-html5-camera-photo").start(); } /// Grab a single JPEG still from the live video on the main thread. Returns a /// CapturedPhoto (and optionally persists it to {@code filePath}). - private CapturedPhoto grab(int reqW, int reqH, int quality, String filePath) throws IOException { + private CapturedPhoto grab(int reqW, int reqH, int quality, String filePath, + boolean persist) throws IOException { String packed = nativeCameraGrab(video, reqW, reqH, (quality > 0 ? quality : 90) / 100.0); if (packed == null) { @@ -164,8 +161,11 @@ private CapturedPhoto grab(int reqW, int reqH, int quality, String filePath) thr int w = parseInt(packed.substring(0, c1), DEFAULT_W); int h = parseInt(packed.substring(c1 + 1, c2), DEFAULT_H); byte[] jpeg = Base64.decode(packed.substring(c2 + 1).getBytes()); - String path = filePath != null ? filePath : tempPath(); - writeBytes(path, jpeg); + String path = filePath; + if (persist) { + path = path != null ? path : tempPath(); + writeBytes(path, jpeg); + } return new CapturedPhoto(jpeg, path, w, h); } @@ -220,7 +220,10 @@ private void deliverFrame() { return; } try { - CapturedPhoto cp = grab(0, 0, 80, null); + // Preview frames are transient. Persisting every interval frame + // filled browser storage and made camera delivery progressively + // slower; only takePhoto() writes a file. + CapturedPhoto cp = grab(0, 0, 80, null, false); l.onFrame(new CameraFrame(cp.getJpegBytes(), null, cp.getWidth(), cp.getHeight(), 0, System.nanoTime(), frameFormat)); } catch (Throwable t) { diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5VideoIO.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5VideoIO.java index ed1e5367c95..b718e57a4ab 100644 --- a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5VideoIO.java +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5VideoIO.java @@ -64,8 +64,10 @@ public VideoCodec[] getAvailableEncoders() { List out = new ArrayList(); out.add(new VideoCodec(CODEC_H264, "H.264 (WebCodecs)", "video/avc", true, true, false, true, -1, -1, new String[]{CONTAINER_MP4})); out.add(new VideoCodec(CODEC_VP9, "VP9 (WebCodecs)", "video/vp9", true, true, false, true, -1, -1, new String[]{CONTAINER_WEBM})); - out.add(new VideoCodec(CODEC_AAC, "AAC (WebCodecs)", "audio/mp4a-latm", false, true, false, false, -1, -1, new String[]{CONTAINER_MP4})); - out.add(new VideoCodec(CODEC_OPUS, "Opus (WebCodecs)", "audio/opus", false, true, false, false, -1, -1, new String[]{CONTAINER_WEBM})); + if (cn1AudioWebCodecsAvailable()) { + out.add(new VideoCodec(CODEC_AAC, "AAC (WebCodecs)", "audio/mp4a-latm", false, true, false, false, -1, -1, new String[]{CONTAINER_MP4})); + out.add(new VideoCodec(CODEC_OPUS, "Opus (WebCodecs)", "audio/opus", false, true, false, false, -1, -1, new String[]{CONTAINER_WEBM})); + } return out.toArray(new VideoCodec[out.size()]); } @@ -336,6 +338,9 @@ private static String safeError(int peer) { @JSBody(params = {}, script = "return (typeof VideoEncoder !== 'undefined' && typeof VideoDecoder !== 'undefined');") private static native boolean cn1WebCodecsAvailable(); + @JSBody(params = {}, script = "return (typeof AudioEncoder !== 'undefined' && typeof AudioData !== 'undefined');") + private static native boolean cn1AudioWebCodecsAvailable(); + // ============================================================= JS: decoder @JSBody(params = {"url"}, script = diff --git a/Ports/JavaScriptPort/src/main/webapp/port.js b/Ports/JavaScriptPort/src/main/webapp/port.js index 728531d6a6b..0b37c4c844d 100644 --- a/Ports/JavaScriptPort/src/main/webapp/port.js +++ b/Ports/JavaScriptPort/src/main/webapp/port.js @@ -1081,6 +1081,186 @@ bindNative([ return jvm.wrapJsObject(u8, "com_codename1_html5_js_typedarrays_Uint8Array"); }); +function javaRegexReplacement(matchArgs, replacement) { + let out = ""; + for (let i = 0; i < replacement.length; i++) { + const ch = replacement.charAt(i); + if (ch === "\\" && i + 1 < replacement.length) { + out += replacement.charAt(++i); + continue; + } + if (ch === "$" && i + 1 < replacement.length) { + let end = i + 1; + while (end < replacement.length && /[0-9]/.test(replacement.charAt(end))) { + end++; + } + if (end > i + 1) { + const group = parseInt(replacement.substring(i + 1, end), 10); + if (group < matchArgs.length - 2) { + out += matchArgs[group] == null ? "" : String(matchArgs[group]); + i = end - 1; + continue; + } + } + } + out += ch; + } + return out; +} + +function replaceJavaRegex(source, regex, replacement, replaceAll) { + const input = jvm.toNativeString(source); + const pattern = jvm.toNativeString(regex); + const replacementText = jvm.toNativeString(replacement); + const compiled = new RegExp(pattern, replaceAll ? "g" : ""); + return jvm.createStringLiteral(input.replace(compiled, function() { + return javaRegexReplacement(arguments, replacementText); + })); +} + +bindNative([ + "cn1_com_codename1_impl_JdkApiRewriteHelper_replaceAll_java_lang_String_java_lang_String_java_lang_String_R_java_lang_String" +], function*(source, regex, replacement) { + return replaceJavaRegex(source, regex, replacement, true); +}); + +bindNative([ + "cn1_com_codename1_impl_JdkApiRewriteHelper_replaceFirst_java_lang_String_java_lang_String_java_lang_String_R_java_lang_String" +], function*(source, regex, replacement) { + return replaceJavaRegex(source, regex, replacement, false); +}); + +function cn1CryptoByteValues(value) { + if (value == null) { + return null; + } + const out = new Array(value.length | 0); + for (let i = 0; i < out.length; i++) { + out[i] = (value[i] | 0) & 0xff; + } + return out; +} + +function cn1CryptoJavaBytes(value) { + if (value == null) { + return null; + } + const out = jvm.newArray(value.length | 0, "JAVA_BYTE", 1); + for (let i = 0; i < out.length; i++) { + const unsigned = value[i] | 0; + out[i] = unsigned > 127 ? unsigned - 256 : unsigned; + } + return out; +} + +function* cn1CryptoHost(request) { + if (typeof jvm.invokeHostNative !== "function") { + throw new Error("Web Crypto host bridge is unavailable"); + } + try { + return yield jvm.invokeHostNative("__cn1_crypto__", [request]); + } catch (err) { + // The public security API deliberately translates port RuntimeExceptions + // into CryptoException. A rejected Web Crypto Promise arrives as a host + // Error, so normalize it into the Java exception hierarchy before it + // crosses back through Cipher/Signature/KeyGenerator. + const ex = jvm.createException("java_lang_RuntimeException"); + if (typeof ex.ctor === "function") { + yield* cn1_ivAdapt(ex.ctor(ex.object)); + } + ex.object.__cn1HostCryptoError = err == null ? "Web Crypto operation failed" : String(err); + throw ex.object; + } +} + +bindNative([ + "cn1_com_codename1_impl_CodenameOneImplementation_secureRandomBytes_byte_1ARRAY_R_void", + "cn1_com_codename1_impl_CodenameOneImplementation_secureRandomBytes_byte_1ARRAY" +], function*(_impl, out) { + const random = yield* cn1CryptoHost({ op: "random", length: out.length | 0 }); + for (let i = 0; i < out.length; i++) { + const unsigned = random[i] | 0; + out[i] = unsigned > 127 ? unsigned - 256 : unsigned; + } + return null; +}); + +function cn1CryptoAesBinding(op) { + return function*(_impl, transformation, key, iv, aad, data) { + const result = yield* cn1CryptoHost({ + op: op, + transformation: jvm.toNativeString(transformation), + key: cn1CryptoByteValues(key), + iv: cn1CryptoByteValues(iv), + aad: cn1CryptoByteValues(aad), + data: cn1CryptoByteValues(data) + }); + return cn1CryptoJavaBytes(result); + }; +} + +bindNative([ + "cn1_com_codename1_impl_CodenameOneImplementation_aesEncrypt_java_lang_String_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY_R_byte_1ARRAY" +], cn1CryptoAesBinding("aesEncrypt")); +bindNative([ + "cn1_com_codename1_impl_CodenameOneImplementation_aesDecrypt_java_lang_String_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY_R_byte_1ARRAY" +], cn1CryptoAesBinding("aesDecrypt")); + +function cn1CryptoRsaBinding(op) { + return function*(_impl, transformation, key, data) { + const result = yield* cn1CryptoHost({ + op: op, + transformation: jvm.toNativeString(transformation), + key: cn1CryptoByteValues(key), + data: cn1CryptoByteValues(data) + }); + return cn1CryptoJavaBytes(result); + }; +} + +bindNative([ + "cn1_com_codename1_impl_CodenameOneImplementation_rsaEncrypt_java_lang_String_byte_1ARRAY_byte_1ARRAY_R_byte_1ARRAY" +], cn1CryptoRsaBinding("rsaEncrypt")); +bindNative([ + "cn1_com_codename1_impl_CodenameOneImplementation_rsaDecrypt_java_lang_String_byte_1ARRAY_byte_1ARRAY_R_byte_1ARRAY" +], cn1CryptoRsaBinding("rsaDecrypt")); + +bindNative([ + "cn1_com_codename1_impl_CodenameOneImplementation_cryptoSign_java_lang_String_java_lang_String_byte_1ARRAY_byte_1ARRAY_R_byte_1ARRAY" +], function*(_impl, algorithm, keyAlgorithm, key, data) { + const result = yield* cn1CryptoHost({ + op: "sign", + algorithm: jvm.toNativeString(algorithm), + keyAlgorithm: jvm.toNativeString(keyAlgorithm), + key: cn1CryptoByteValues(key), + data: cn1CryptoByteValues(data) + }); + return cn1CryptoJavaBytes(result); +}); + +bindNative([ + "cn1_com_codename1_impl_CodenameOneImplementation_cryptoVerify_java_lang_String_java_lang_String_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY_R_boolean" +], function*(_impl, algorithm, keyAlgorithm, key, data, signature) { + return (yield* cn1CryptoHost({ + op: "verify", + algorithm: jvm.toNativeString(algorithm), + keyAlgorithm: jvm.toNativeString(keyAlgorithm), + key: cn1CryptoByteValues(key), + data: cn1CryptoByteValues(data), + signature: cn1CryptoByteValues(signature) + })) ? 1 : 0; +}); + +bindNative([ + "cn1_com_codename1_impl_CodenameOneImplementation_generateRsaKeyPair_int_R_byte_2ARRAY" +], function*(_impl, bits) { + const result = yield* cn1CryptoHost({ op: "generateRsaKeyPair", bits: bits | 0 }); + const pair = jvm.newArray(2, "JAVA_BYTE", 2); + pair[0] = cn1CryptoJavaBytes(result[0]); + pair[1] = cn1CryptoJavaBytes(result[1]); + return pair; +}); + bindNative(["cn1_com_codename1_html5_js_core_JSArray_create_R_com_codename1_html5_js_core_JSArray", "cn1_com_codename1_html5_js_core_JSArray_create___R_com_codename1_html5_js_core_JSArray"], function() { const arr = []; return jvm.wrapJsObject(arr, "com_codename1_html5_js_core_JSArray"); @@ -1124,6 +1304,78 @@ bindNative(["cn1_com_codename1_html5_js_browser_Window_current_R_com_codename1_h return wrapper; }); +// Window's static timer methods are Java stubs so the same API can compile +// without a DOM. On the worker port they must run on the browser host: leaving +// the stubs in place returns timer id 0 and silently drops every callback +// (camera frame delivery was the first assertion test to expose this). +function* cn1SetHostTimer(kind, handler, delay) { + if (typeof jvm.invokeHostNative !== "function") { + return 0; + } + if (!handler || !handler.__class) { + return 0; + } + if (!handler.__cn1NativeTimerCallback) { + handler.__cn1NativeTimerCallback = function() { + try { + spawnVirtualCallback(handler, "cn1_s_onTimer", [], "__cn1TimerCallbackPending"); + } catch (err) { + jvm.fail(err); + } + }; + } + return (yield jvm.invokeHostNative(kind, [ + handler.__cn1NativeTimerCallback, + Math.max(0, delay | 0) + ])) | 0; +} + +bindNative([ + "cn1_com_codename1_html5_js_browser_Window_setTimeout_java_lang_Object_int_R_int", + "cn1_com_codename1_html5_js_browser_Window_setTimeout___java_lang_Object_int_R_int" +], function*(handler, delay) { + return yield* cn1SetHostTimer("__cn1_timer_set_timeout__", handler, delay); +}); + +bindNative([ + "cn1_com_codename1_html5_js_browser_Window_clearTimeout_int", + "cn1_com_codename1_html5_js_browser_Window_clearTimeout___int" +], function*(id) { + if (typeof jvm.invokeHostNative === "function") { + yield jvm.invokeHostNative("__cn1_timer_clear_timeout__", [id | 0]); + } +}); + +bindNative([ + "cn1_com_codename1_html5_js_browser_Window_setInterval_java_lang_Object_int_R_int", + "cn1_com_codename1_html5_js_browser_Window_setInterval___java_lang_Object_int_R_int" +], function*(handler, delay) { + return yield* cn1SetHostTimer("__cn1_timer_set_interval__", handler, delay); +}); + +bindNative([ + "cn1_com_codename1_html5_js_browser_Window_clearInterval_int", + "cn1_com_codename1_html5_js_browser_Window_clearInterval___int" +], function*(id) { + if (typeof jvm.invokeHostNative === "function") { + yield jvm.invokeHostNative("__cn1_timer_clear_interval__", [id | 0]); + } +}); + +bindNative([ + "cn1_com_codename1_impl_html5_HTML5BrowserComponent_installShouldLoadURLCallback_com_codename1_impl_html5_JSOImplementations_HTMLIFrameElement_com_codename1_impl_html5_HTML5BrowserComponent_ShouldLoadURLCallback" +], function*(iframe, callback) { + yield jvm.invokeHostNative("__cn1_install_browser_navigation_callback__", [iframe, callback]); + return null; +}); + +bindNative([ + "cn1_com_codename1_impl_html5_HTML5BrowserComponent_installShouldLoadURLCallbackShared_com_codename1_impl_html5_HTML5BrowserComponent_ShouldLoadURLCallback" +], function*(callback) { + yield jvm.invokeHostNative("__cn1_install_browser_navigation_callback__", [null, callback]); + return null; +}); + bindNative([ "cn1_com_codename1_impl_html5_JSOImplementations_WindowExt_getCn1_R_com_codename1_impl_html5_JSOImplementations_CN1Native", "cn1_com_codename1_impl_html5_JSOImplementations_WindowExt_getCn1___R_com_codename1_impl_html5_JSOImplementations_CN1Native" @@ -2048,6 +2300,180 @@ bindNative([ yield jvm.invokeHostNative("__cn1_camera_close__", [{ video: ref }]); }); +// HTML5VideoIO uses Window-only APIs (document, HTMLVideoElement, WebCodecs, +// and the muxer scripts). Translated Java runs in a Worker, so every one of +// these operations must execute on the browser host. Keeping the complete +// media session on the host also means async script loads and encoder flushes +// have a real Promise completion instead of a worker-side polling race. +function* cn1VideoIoHost(request) { + if (typeof jvm.invokeHostNative !== "function") { + throw new Error("VideoIO host bridge is unavailable"); + } + return yield jvm.invokeHostNative("__cn1_video_io__", [request]); +} + +function cn1VideoIoString(value) { + return value == null ? null : jvm.createStringLiteral(String(value)); +} + +bindNative([ + "cn1_com_codename1_impl_html5_HTML5VideoIO_cn1WebCodecsAvailable_R_boolean" +], function*() { + return (yield* cn1VideoIoHost({ op: "webCodecsAvailable" })) ? 1 : 0; +}); + +bindNative([ + "cn1_com_codename1_impl_html5_HTML5VideoIO_cn1AudioWebCodecsAvailable_R_boolean" +], function*() { + return (yield* cn1VideoIoHost({ op: "audioWebCodecsAvailable" })) ? 1 : 0; +}); + +bindNative([ + "cn1_com_codename1_impl_html5_HTML5VideoIO_cn1VideoOpen_java_lang_String_R_int" +], function*(url) { + return (yield* cn1VideoIoHost({ op: "videoOpen", url: jvm.toNativeString(url) })) | 0; +}); + +bindNative([ + "cn1_com_codename1_impl_html5_HTML5VideoIO_cn1VideoReady_int_R_boolean" +], function*(id) { + return (yield* cn1VideoIoHost({ op: "videoReady", id: id | 0 })) ? 1 : 0; +}); + +bindNative([ + "cn1_com_codename1_impl_html5_HTML5VideoIO_cn1VideoWidth_int_R_int" +], function*(id) { + return (yield* cn1VideoIoHost({ op: "videoWidth", id: id | 0 })) | 0; +}); + +bindNative([ + "cn1_com_codename1_impl_html5_HTML5VideoIO_cn1VideoHeight_int_R_int" +], function*(id) { + return (yield* cn1VideoIoHost({ op: "videoHeight", id: id | 0 })) | 0; +}); + +bindNative([ + "cn1_com_codename1_impl_html5_HTML5VideoIO_cn1VideoDuration_int_R_int" +], function*(id) { + return (yield* cn1VideoIoHost({ op: "videoDuration", id: id | 0 })) | 0; +}); + +bindNative([ + "cn1_com_codename1_impl_html5_HTML5VideoIO_cn1VideoSeek_int_int" +], function*(id, ms) { + yield* cn1VideoIoHost({ op: "videoSeek", id: id | 0, ms: ms | 0 }); +}); + +bindNative([ + "cn1_com_codename1_impl_html5_HTML5VideoIO_cn1VideoSeeked_int_R_boolean" +], function*(id) { + return (yield* cn1VideoIoHost({ op: "videoSeeked", id: id | 0 })) ? 1 : 0; +}); + +bindNative([ + "cn1_com_codename1_impl_html5_HTML5VideoIO_cn1VideoCapture_int_int_int_R_java_lang_String" +], function*(id, w, h) { + return cn1VideoIoString(yield* cn1VideoIoHost({ + op: "videoCapture", id: id | 0, w: w | 0, h: h | 0 + })); +}); + +bindNative([ + "cn1_com_codename1_impl_html5_HTML5VideoIO_cn1VideoClose_int" +], function*(id) { + yield* cn1VideoIoHost({ op: "videoClose", id: id | 0 }); +}); + +bindNative([ + "cn1_com_codename1_impl_html5_HTML5VideoIO_cn1VideoBlobUrl_java_lang_String_java_lang_String_R_java_lang_String" +], function*(b64, mime) { + return cn1VideoIoString(yield* cn1VideoIoHost({ + op: "videoBlobUrl", b64: jvm.toNativeString(b64), mime: jvm.toNativeString(mime) + })); +}); + +bindNative([ + "cn1_com_codename1_impl_html5_HTML5VideoIO_cn1EncEnsureLibs" +], function*() { + yield* cn1VideoIoHost({ op: "encEnsureLibs" }); +}); + +bindNative([ + "cn1_com_codename1_impl_html5_HTML5VideoIO_cn1EncLibsReady_java_lang_String_R_boolean" +], function*(container) { + return (yield* cn1VideoIoHost({ + op: "encLibsReady", container: jvm.toNativeString(container) + })) ? 1 : 0; +}); + +bindNative([ + "cn1_com_codename1_impl_html5_HTML5VideoIO_cn1EncOpen_java_lang_String_java_lang_String_int_int_int_int_boolean_java_lang_String_int_int_int_R_int" +], function*(container, videoCodec, w, h, fps, videoBitRate, hasAudio, + audioCodec, audioBitRate, sampleRate, channels) { + return (yield* cn1VideoIoHost({ + op: "encOpen", + container: jvm.toNativeString(container), + videoCodec: jvm.toNativeString(videoCodec), + w: w | 0, + h: h | 0, + fps: fps | 0, + videoBitRate: videoBitRate | 0, + hasAudio: !!hasAudio, + audioCodec: audioCodec == null ? null : jvm.toNativeString(audioCodec), + audioBitRate: audioBitRate | 0, + sampleRate: sampleRate | 0, + channels: channels | 0 + })) | 0; +}); + +bindNative([ + "cn1_com_codename1_impl_html5_HTML5VideoIO_cn1EncError_int_R_java_lang_String" +], function*(peer) { + return cn1VideoIoString(yield* cn1VideoIoHost({ op: "encError", peer: peer | 0 })); +}); + +bindNative([ + "cn1_com_codename1_impl_html5_HTML5VideoIO_cn1EncFrame_int_java_lang_String_int_int_double" +], function*(peer, b64, w, h, ptsUs) { + yield* cn1VideoIoHost({ + op: "encFrame", peer: peer | 0, b64: jvm.toNativeString(b64), + w: w | 0, h: h | 0, ptsUs: +ptsUs + }); +}); + +bindNative([ + "cn1_com_codename1_impl_html5_HTML5VideoIO_cn1EncAudio_int_java_lang_String_int_int_double" +], function*(peer, b64, sampleRate, channels, ptsUs) { + yield* cn1VideoIoHost({ + op: "encAudio", peer: peer | 0, b64: jvm.toNativeString(b64), + sampleRate: sampleRate | 0, channels: channels | 0, ptsUs: +ptsUs + }); +}); + +bindNative([ + "cn1_com_codename1_impl_html5_HTML5VideoIO_cn1EncFlush_int" +], function*(peer) { + yield* cn1VideoIoHost({ op: "encFlush", peer: peer | 0 }); +}); + +bindNative([ + "cn1_com_codename1_impl_html5_HTML5VideoIO_cn1EncDone_int_R_boolean" +], function*(peer) { + return (yield* cn1VideoIoHost({ op: "encDone", peer: peer | 0 })) ? 1 : 0; +}); + +bindNative([ + "cn1_com_codename1_impl_html5_HTML5VideoIO_cn1EncResult_int_R_java_lang_String" +], function*(peer) { + return cn1VideoIoString(yield* cn1VideoIoHost({ op: "encResult", peer: peer | 0 })); +}); + +bindNative([ + "cn1_com_codename1_impl_html5_HTML5VideoIO_cn1EncClose_int" +], function*(peer) { + yield* cn1VideoIoHost({ op: "encClose", peer: peer | 0 }); +}); + // Fullscreen: document.fullscreen* lives on the main thread. Queries return the // real host state; enter/exit do the host request and then invoke the Java // RequestFullScreenCallback (onComplete(boolean)) back in the worker. @@ -3896,216 +4322,8 @@ const baseTestPrepareMethodId = "cn1_s_prepare"; const baseTestRunTestMethodId = "cn1_s_runTest_R_boolean"; const baseTestFailMethodId = "cn1_s_fail_java_lang_String"; const baseTestDoneMethodId = "cn1_s_done"; -const cn1ssForcedTimeoutTestClasses = Object.freeze({ - // UNSKIP-PHASE2: "com_codenameone_examples_hellocodenameone_tests_MediaPlaybackScreenshotTest": "mediaPlayback", - "com_codenameone_examples_hellocodenameone_tests_BytecodeTranslatorRegressionTest": "bytecodeTranslatorRegression", - // The 3D model test loads a ~6K-triangle glTF model with a decoded JPEG - // base-color texture. The heavy onInit (glTF parse + image decode + a large - // getRGB upload) reliably wedges the headless SwiftShader WebGL path before - // the capture window, and a curved, bilinearly-textured model would not match - // a stored golden across the ARM/x64 SwiftShader rasterizers anyway. It is - // validated on the real-GPU platforms (iOS Metal) and in the simulator - // instead; the geometry path is still covered on JS by Gpu3DCube / - // Gpu3DTexturedCube / Gpu3DAnimation, which capture reliably. - "com_codenameone_examples_hellocodenameone_tests_Gpu3DModelScreenshotTest": "gpu3dModelHeadlessGl", - // BrowserComponent's ``onLoad`` event never reaches the worker side - // — the iframe ``load`` event isn't currently routed through the - // worker-callback transport, so ``loaded = true`` never gets set - // and the test waits on its own ``readyRunnable`` indefinitely. The - // 10s ``cn1ssTestTimeoutMs`` deadline in the lambdaBridge await - // never gets a chance to fire because we're still inside the - // bytecode-emitted dispatch chain. Force-timeout so the rest of - // the screenshot suite can finalize. - "com_codenameone_examples_hellocodenameone_tests_BrowserComponentScreenshotTest": "browserComponentLoadEvent", - // ChatInput/ChatView un-parked: their dark-phase emit no longer spills into the - // next test now that awaitTestCompletion gives DualAppearanceBaseTest its full - // 30s on HTML5 (was clobbered to a flat 10s by the bridge). - // The 14 *ThemeScreenshotTest entries that used to live here were - // unparked when the JS port started bundling the modern native - // theme resources (iOSModernTheme.res / AndroidMaterialTheme.res - // mirrored into webapp/assets/ by scripts/build-native-themes.sh). - // DualAppearanceBaseTest now installs the OS-appropriate modern - // theme via the cn1.modernThemeResource Display property that - // HTML5Implementation publishes during installNativeTheme(); see - // the matching cn1ssForcedTimeoutTestNames map below for the - // short-name list that was un-parked in tandem. - // jsChunkDrop block removed for the graphics grid, KotlinUiTest, - // MainScreenScreenshotTest, the Tabs / ImageViewer / TextArea / - // ToastBar / picker tests, and ChartLine -- the chunk emitter bug - // those entries worked around is fixed on master in #4875 - // (8582151ec). Verified on CI: all 26 ``tests.graphics.*`` cells, - // KotlinUiTest, MainScreenScreenshotTest, ChartLine, and the - // animation / transition grid now produce comparable PNGs. - // - // The chart tests below run AFTER ~60 prior tests have accumulated - // ~420 hostRef-tracked canvases on the page, at which point the - // JS port's Document.createElement bridge starts returning a null - // host receiver and the runtime emits ``VIRTUAL_FAIL ... - // methodId=cn1_s_createElement_..._HTMLElement receiverClass=null``. - // The first failure cascades through every chart that runs later - // -- ChartLine (the first chart in the suite order) succeeds for - // the same reason: it runs before the threshold. This is a - // separate canvas-accumulation / Document-wrapper-staleness bug - // (likely in the ``Window.getDocument`` cache landed in - // 80bfa41de) tracked separately from the chunk-emit fix. Skip - // these charts under a distinct reason so the cause is obvious in - // CI logs. - // Un-parked after wrapJsObject class-wipe fix landed: the previous - // logic at parparvm_runtime.js wrapJsObject unconditionally - // overwrote cached wrapper.__class with whatever inferJsObjectClass - // returned, including null. Re-wrapping the same Document value via - // a host-bridge round-trip without ``__cn1HostClass`` set wiped its - // class, and the next ``cn1_s_createElement`` virtual dispatch - // failed with receiverClass=null, cascading through every chart - // that ran later. The fix preserves the cached class when the new - // resolution is null. - //"com_codenameone_examples_hellocodenameone_tests_charts_ChartDoughnutScreenshotTest": "chartDocumentStaleness", - //"com_codenameone_examples_hellocodenameone_tests_charts_ChartRadarScreenshotTest": "chartDocumentStaleness", - //"com_codenameone_examples_hellocodenameone_tests_charts_ChartTimeChartScreenshotTest": "chartDocumentStaleness", - // ChartCombinedXY re-parked: exact longs did NOT fix it -- it still hangs the - // suite in a non-terminating form-construction/layout loop (runTest never - // returns, so the per-test deadline never arms). Distinct from the chart - // axis-label bug that exact longs DID fix. Needs its own non-termination fix. - "com_codenameone_examples_hellocodenameone_tests_charts_ChartCombinedXYScreenshotTest": "chartCombinedXyHang", - // SVGStatic re-parked: installGlobal() runs and the GeneratedSVGImage is - // registered with correct dimensions (each grid cell is sized), but paintSVG - // renders BLANK on the JS port -- the shapes draw under GeneratedSVGImage's - // getTransform/setTransform round-trip, which is the known JS-port canvas - // transform-leak family. isShapeSupported=true and fillShape/drawShape - // delegate fine, so this is a transform-composition bug, not missing shape - // support. The build-time SVG wiring (SVGRegistry.installGlobal in the JS - // launcher) is correct and stays; only the runtime transform render is the - // gap. Park until the SVG transform render is fixed so the suite stays green - // (a delivering test with no golden fails as "Reference screenshot missing"). - // UNSKIP-PHASE2: "com_codenameone_examples_hellocodenameone_tests_SVGStaticScreenshotTest": "svgTransformBlankRender", - // FileSystemStorageOpenInputStreamMissingTest RE-PARKED: its runTest blocks on - // a flaky LocalForage.getItem host call. The per-test dispatch watchdog DOES - // fire on it, but force-advancing cannot recover a DEGRADED host channel (the - // recovery path runNextTestMethod itself needs the channel), so once the - // channel is bad the suite is dead regardless. Parking removes it as a wedge - // candidate (no golden -> zero parity impact) and restores the known-green tail - // from efabb3e1a. The watchdog stays as a healthy-channel safety net for - // genuine isolated blocks; it is NOT a cure for host-channel degradation. - "com_codenameone_examples_hellocodenameone_tests_FileSystemStorageOpenInputStreamMissingTest": "fileSystemStorageLocalForageHang", - // Transform + Rotated kept UN-parked -- never reached on the prior run - // (CombinedXY hung first); testing whether they render now. - // Two more late-suite tests that hit the canvas-accumulation - // threshold and hang waiting for SCREENSHOT_DONE. On the run that - // didn't get this far they finish cleanly, but the canary-test - // identity drifts between runs depending on cumulative canvas - // pressure -- consistently breaks once an instance of the cascade - // bites. Park them with the chart tail so the suite reliably - // reaches comparison. - // Un-parked: canvasContextWipe root cause fixed at 5dce6a24a - // (defensive __cn1CachedDocWrapper invalidation in getDocument). - // UNSKIP-TRIAGE(surface-id+freeze fix): "com_codenameone_examples_hellocodenameone_tests_ToastBarTopPositionScreenshotTest": "canvasContextWipe", - // Un-parked: canvasContextWipe no-op recovery for save/restore/ - // setTransform/etc at d696fb682 + AbstractAnimationScreenshotTest - // safety net (efc9bdb67) should now keep this from hanging. - //"com_codenameone_examples_hellocodenameone_tests_SheetSlideUpAnimationScreenshotTest": "canvasContextWipe", - // TextAreaAlignmentStates' form renders correctly, but the screenshot - // captures it underneath a leftover Sheet overlay from - // SheetScreenshotTest (which ran ~7 tests earlier). On JS port the - // Sheet teardown doesn't complete before the next test starts so - // a dim/blur layer persists across forms. Separate test-isolation - // bug worth chasing; for now park here so the suite is reliable. - // UNSKIP-TRIAGE(surface-id+freeze fix): "com_codenameone_examples_hellocodenameone_tests_TextAreaAlignmentScreenshotTest": "sheetTearDownLeak", - // LightweightPickerButtons HARD-parks the worker (heartbeat keeps firing - // but the green scheduler goes fully idle: runnable=0, resumes frozen, NOT - // a jso-bridge cross -- RETRIES/HOSTCALL_TIMEOUT both 0). It is the - // lightweight-popup capture deadlock: Picker.setUseLightweightPopup(true) + - // startEditingAsync() opens a popup whose animating date wheels never settle, - // and the nested callSerially -> emitCurrentFormScreenshot -> stopEditing() - // chain waits on a paint that the popup animation starves. This is a - // test/popup-lifecycle deadlock, distinct from the chartDocumentStaleness - // response-cross (now handled by the invokeJsoBridge retry + host-call - // watchdog), so the retry can't rescue it -- park it so the suite reaches - // comparison. ValidatorLightweightPicker, which DID drift here previously, - // now runs clean once the cross is recovered, so it stays un-parked. - //"com_codenameone_examples_hellocodenameone_tests_ValidatorLightweightPickerScreenshotTest": "chartDocumentStaleness", - "com_codenameone_examples_hellocodenameone_tests_LightweightPickerButtonsScreenshotTest": "lightweightPopupCaptureDeadlock", - // CssGradients lands at suite index ~92 -- well past the canvas- - // accumulation threshold that exhausts the JS port's - // Document.createElement host-receiver cache. The failure manifests - // as the same ``cn1_s_createElement ... receiverClass=null`` cascade - // documented in the chartDocumentStaleness comment above, and on - // half of CI runs it halts the entire screenshot suite before the - // FINAL marker fires (taking ~18 trailing tests with it). The test - // has a JS golden so was earlier flipped on, but the underlying - // staleness has to be fixed at the bridge layer before it can run - // reliably -- park here in the meantime so the rest of the suite - // is deterministic. - // Un-parked: canvasContextWipe root cause fixed at 5dce6a24a. - // UNSKIP-TRIAGE(surface-id+freeze fix): "com_codenameone_examples_hellocodenameone_tests_CssGradientsScreenshotTest": "canvasContextWipe", - // Sheet's backdrop blur path produces a cn1_s_save VIRTUAL_FAIL loop - // on the canvas-accumulation tail (same ~suite-position-90 staleness - // as ChartDoughnut etc.). The runtime fix in 618629361 turned the - // first throw from a single suite-halt into a retry loop, which on - // half of CI runs sits at SheetScreenshotTest for the remainder of - // the budget. Park here for deterministic completion. - // Un-parked: canvasContextWipe root cause fixed at 5dce6a24a. - // UNSKIP-TRIAGE(surface-id+freeze fix): "com_codenameone_examples_hellocodenameone_tests_SheetScreenshotTest": "canvasContextWipe", - // graphicsTransform3dCanvasHang: the 3D perspective / camera transform - // tests render into a surface the worker-side screenshot path can't - // resolve (SCREENSHOT_START reports canvasCandidates=0), so the suite - // re-dispatches the same index indefinitely and never reaches the - // per-test deadline. The 2D transform tests (rotation / translation / - // affine) are unaffected. Mirrored in Cn1ssDeviceRunner's Java skip list. - // UNSKIP-TRIAGE(surface-id+freeze fix): "com_codenameone_examples_hellocodenameone_tests_graphics_TransformPerspective": "graphicsTransform3dCanvasHang", - // UNSKIP-TRIAGE(surface-id+freeze fix): "com_codenameone_examples_hellocodenameone_tests_graphics_TransformCamera": "graphicsTransform3dCanvasHang" -}); -const cn1ssForcedTimeoutTestNames = Object.freeze({ - // UNSKIP-PHASE2: "MediaPlaybackScreenshotTest": "mediaPlayback", - "BytecodeTranslatorRegressionTest": "bytecodeTranslatorRegression", - // SimdLargeAllocaTest hung the suite on bk76kkr50 with VIRTUAL_FAILs - // on HTML5Impl methods (cn1_s_paintDirty / cn1_s_flushGraphics) and - // Canvas2D getImageData. The {} receiver propagated into the - // HTML5Implementation instance itself, not just the Canvas2DContext. - // Likely SimdLargeAlloca corrupts shared state via its large-alloca - // pattern; needs its own investigation. - "SimdLargeAllocaTest": "simdLargeAllocaCorrupt", - "BackgroundThreadUiAccessTest": "backgroundThreadUiAccess", - "VPNDetectionAPITest": "vpnDetectionApi", - "CallDetectionAPITest": "callDetectionApi", - "LocalNotificationOverrideTest": "localNotificationOverride", - "Base64NativePerformanceTest": "base64NativePerformance", - "BrowserComponentScreenshotTest": "browserComponentLoadEvent", - "AccessibilityTest": "accessibility", - // ChatInput/ChatView were parked because their dark-phase capture ran past the - // flat 10s deadline the bridge imposed, so the runner force-advanced and the - // pending dark emit captured the NEXT test (ChatInput_dark -> ImageViewer). - // Root cause fixed: awaitTestCompletion now computes the type-aware deadline - // (DualAppearanceBaseTest gets 30s on HTML5) instead of the bridge's flat 10s. - // The 14 *ThemeScreenshotTest short-name entries were un-parked - // alongside the fully-qualified-class entries in - // cn1ssForcedTimeoutTestClasses above when the modern native - // theme resources started shipping in the JS port bundle. - // See the matching comment in cn1ssForcedTimeoutTestClasses above: - // the jsChunkDrop short-name entries (KotlinUiTest, - // MainScreenScreenshotTest, the ImageViewer / Tabs / TextArea / - // ToastBar / picker tests, and the tests.graphics.* grid) have been - // removed because the chunk emitter bug they worked around is fixed - // on master in #4875 (8582151ec). The chart short-names below stay - // until the canvas-accumulation / Document-wrapper-staleness bug - // tracked under "chartDocumentStaleness" is resolved. - //"ChartDoughnutScreenshotTest": "chartDocumentStaleness", - //"ChartRadarScreenshotTest": "chartDocumentStaleness", - //"ChartTimeChartScreenshotTest": "chartDocumentStaleness", - // ChartCombinedXY re-parked (non-terminating layout loop); Transform + Rotated - // kept un-parked -- see note in cn1ssForcedTimeoutTestClasses above. - "ChartCombinedXYScreenshotTest": "chartCombinedXyHang", - // UNSKIP-TRIAGE(surface-id+freeze fix): "ToastBarTopPositionScreenshotTest": "canvasContextWipe", - //"SheetSlideUpAnimationScreenshotTest": "canvasContextWipe", - // UNSKIP-TRIAGE(surface-id+freeze fix): "TextAreaAlignmentScreenshotTest": "sheetTearDownLeak", - //"ValidatorLightweightPickerScreenshotTest": "chartDocumentStaleness", - //"LightweightPickerButtonsScreenshotTest": "chartDocumentStaleness", - // UNSKIP-TRIAGE(surface-id+freeze fix): "CssGradientsScreenshotTest": "canvasContextWipe", - // UNSKIP-TRIAGE(surface-id+freeze fix): "SheetScreenshotTest": "canvasContextWipe", - // graphicsTransform3dCanvasHang -- see matching fully-qualified entries - // in cn1ssForcedTimeoutTestClasses above. - // UNSKIP-TRIAGE(surface-id+freeze fix): "TransformPerspective": "graphicsTransform3dCanvasHang", - // UNSKIP-TRIAGE(surface-id+freeze fix): "TransformCamera": "graphicsTransform3dCanvasHang" -}); +// Every selected port-status test executes normally. Failures are surfaced to +// the harness; there is no forced-timeout or known-bad bypass list. if (jvm && typeof jvm.addVirtualMethod === "function" && jvm.classes && jvm.classes["java_lang_String"]) { const stringMethods = jvm.classes["java_lang_String"].methods || {}; @@ -4294,6 +4512,21 @@ function getCn1ssLambdaCaptureValue(lambdaObject, ordinal) { return null; } +function getCn1ssLambdaCaptureValues(lambdaObject) { + if (!lambdaObject || typeof lambdaObject !== "object") { + return []; + } + const captures = []; + const keys = Object.keys(lambdaObject); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (key.indexOf("Cn1ssDeviceRunner_lambda_") >= 0 && /_arg_\d+$/.test(key)) { + captures.push(lambdaObject[key]); + } + } + return captures; +} + function resolveCn1ssRunnerTranslatedMethod(methodIds, fallbackSymbol) { return resolveCurrentTranslatedMethod(methodIds, cn1ssRunnerClassId, fallbackSymbol); } @@ -4439,29 +4672,6 @@ function* runCn1ssResolvedTest(callTarget, effectiveTestObject, effectiveTestNam // "connecting" with the PNG queued, and onopen's flush never runs -> 0 // delivered even though capture succeeded. cn1ssWsConnect(); - const forcedTimeoutReason = cn1ssForcedTimeoutTestClasses[effectiveTestClassId] - || cn1ssForcedTimeoutTestNames[nativeTestName] - || null; - if (forcedTimeoutReason != null) { - emitDiagLine("PARPAR:DIAG:FALLBACK:key=FALLBACK:Cn1ssDeviceRunner.forcedTimeout:" + forcedTimeoutReason + ":HIT"); - try { - const finalizeMethod = jvm.resolveVirtual(callTarget.__class, cn1ssRunnerFinalizeTestMethodId); - if (typeof finalizeMethod === "function") { - return yield* cn1_ivAdapt(finalizeMethod( - callTarget, - effectiveIndex, - effectiveTestObject, - normalizedTestName, - 1 - )); - } - } catch (_finalizeErr) { - const finalizeErrDetail = yield* stringifyThrowable(_finalizeErr); - emitLambdaBridgeDiag("PARPAR:DIAG:FALLBACK:lambdaBridge:forcedTimeoutFinalizeError=" + finalizeErrDetail); - return yield* forceAdvanceCn1ssRunner(callTarget, effectiveIndex, "forcedTimeoutFinalizeFailed"); - } - return yield* forceAdvanceCn1ssRunner(callTarget, effectiveIndex, "forcedTimeoutFinalizeMissing"); - } // PER-TEST DISPATCH WATCHDOG: a test whose prepare()/runTest() BLOCKS (parked // on a hung host round-trip -- LocalForage.getItem, getContext, etc.) never // returns, so it never reaches the catch (only synchronous THROWS do -- those @@ -4665,10 +4875,23 @@ function* runCn1ssResolvedTest(callTarget, effectiveTestObject, effectiveTestNam bindCiFallback("Cn1ssDeviceRunner.lambda1RunBridge", [ cn1ssRunnerLambda1RunMethodId ], function*(__cn1ThisObject) { - const runner = getCn1ssLambdaCaptureValue(__cn1ThisObject, 1); - const testName = getCn1ssLambdaCaptureValue(__cn1ThisObject, 2); - const index = getCn1ssLambdaCaptureValue(__cn1ThisObject, 3); - const testObject = getCn1ssLambdaCaptureValue(__cn1ThisObject, 4); + const captures = getCn1ssLambdaCaptureValues(__cn1ThisObject); + let runner = null; + let testName = null; + let index = null; + let testObject = null; + for (let i = 0; i < captures.length; i++) { + const value = captures[i]; + if (value && value.__class === cn1ssRunnerClassId) { + runner = value; + } else if (jvm.instanceOf(value, "com_codenameone_examples_hellocodenameone_tests_BaseTest")) { + testObject = value; + } else if (value && value.__class === "java_lang_String") { + testName = value; + } else if (typeof value === "number" || typeof value === "bigint") { + index = Number(value); + } + } if (!runner || runner.__class !== cn1ssRunnerClassId) { emitLambdaBridgeDiag("PARPAR:DIAG:FALLBACK:lambda1RunBridge:missingDispatch=1"); return null; @@ -4677,6 +4900,9 @@ bindCiFallback("Cn1ssDeviceRunner.lambda1RunBridge", [ "PARPAR:DIAG:FALLBACK:lambda1RunBridge:dispatch:index=" + String(index == null ? "null" : (index | 0)) + ":test=" + (testObject && testObject.__class ? testObject.__class : "null") ); + if (!testObject && index != null) { + testObject = resolveCn1ssIndexedTestObject(index | 0); + } return yield* runCn1ssResolvedTest(runner, testObject, testName, index | 0); }); diff --git a/maven/core-unittests/src/test/java/com/codename1/charts/views/CombinedXYChartTest.java b/maven/core-unittests/src/test/java/com/codename1/charts/views/CombinedXYChartTest.java index ac7c5614c20..d3ae56e9222 100644 --- a/maven/core-unittests/src/test/java/com/codename1/charts/views/CombinedXYChartTest.java +++ b/maven/core-unittests/src/test/java/com/codename1/charts/views/CombinedXYChartTest.java @@ -31,4 +31,31 @@ public void testCombinedXYChart() { Assertions.assertEquals("Combined", chart.getChartType()); } + + @FormTest + public void testSupportedChildChartsAreConstructedWithInitializedState() { + String[] types = new String[] { + TimeChart.TYPE, + LineChart.TYPE, + CubicLineChart.TYPE, + BarChart.TYPE, + BubbleChart.TYPE, + ScatterChart.TYPE, + RangeBarChart.TYPE, + RangeStackedBarChart.TYPE + }; + + for (String type : types) { + XYChart chart = CombinedXYChart.createXYChart(type); + Assertions.assertNotNull(chart, "Missing child chart factory for " + type); + Assertions.assertEquals(type, chart.getChartType()); + + double[] range = new double[] {1, 2, 3, 4}; + chart.setCalcRange(range, 0); + Assertions.assertSame(range, chart.getCalcRange(0), + "Child chart constructor did not initialize XYChart state for " + type); + } + + Assertions.assertNull(CombinedXYChart.createXYChart("unsupported")); + } } diff --git a/scripts/build-javascript-port-hellocodenameone.sh b/scripts/build-javascript-port-hellocodenameone.sh index 7ef77a947f0..809c9daea93 100755 --- a/scripts/build-javascript-port-hellocodenameone.sh +++ b/scripts/build-javascript-port-hellocodenameone.sh @@ -96,8 +96,14 @@ COMMON_CLASSES="$COMMON_ROOT/target/classes" COMMON_DEPS_DIR="$COMMON_ROOT/target/parparvm-deps" PARPARVM_JAVA_API="$PARPARVM_ROOT/target/bundle/parparvm-java-api.jar" PARPARVM_COMPILER="$PARPARVM_ROOT/target/bundle/parparvm-compiler.jar" -CN1_CORE_JAR="$(find "$HOME/.m2/repository/com/codenameone/codenameone-core" -path '*/8.0-SNAPSHOT/codenameone-core-8.0-SNAPSHOT.jar' -type f | head -n 1 || true)" -JAVA_RUNTIME_JAR="$(find "$HOME/.m2/repository/com/codenameone/java-runtime" -path '*/8.0-SNAPSHOT/java-runtime-8.0-SNAPSHOT.jar' -type f | head -n 1 || true)" +CN1_CORE_JAR="$REPO_ROOT/maven/core/target/codenameone-core-8.0-SNAPSHOT.jar" +if [ ! -f "$CN1_CORE_JAR" ]; then + CN1_CORE_JAR="$(find "$HOME/.m2/repository/com/codenameone/codenameone-core" -path '*/8.0-SNAPSHOT/codenameone-core-8.0-SNAPSHOT.jar' -type f | head -n 1 || true)" +fi +JAVA_RUNTIME_JAR="$REPO_ROOT/maven/java-runtime/target/java-runtime-8.0-SNAPSHOT.jar" +if [ ! -f "$JAVA_RUNTIME_JAR" ]; then + JAVA_RUNTIME_JAR="$(find "$HOME/.m2/repository/com/codenameone/java-runtime" -path '*/8.0-SNAPSHOT/java-runtime-8.0-SNAPSHOT.jar' -type f | head -n 1 || true)" +fi for required in "$COMMON_CLASSES" "$PARPARVM_JAVA_API" "$PARPARVM_COMPILER" "$CN1_CORE_JAR" "$JAVA_RUNTIME_JAR"; do if [ ! -e "$required" ]; then diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/BrowserComponentScreenshotTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/BrowserComponentScreenshotTest.java index ae91c5e3add..f3d0e4744e2 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/BrowserComponentScreenshotTest.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/BrowserComponentScreenshotTest.java @@ -2,6 +2,7 @@ import com.codename1.ui.BrowserComponent; import com.codename1.ui.CN; +import com.codename1.ui.Display; import com.codename1.ui.Form; import com.codename1.ui.layouts.BorderLayout; import com.codename1.ui.util.UITimer; @@ -35,10 +36,23 @@ public boolean runTest() throws Exception { @Override protected void registerReadyCallback(Form parent, final Runnable run) { - this.readyRunnable = run; + // BrowserComponent is a DOM peer on HTML5, outside the CN1 canvas that + // Display.screenshot() captures. Exercise the real peer and callback + // path there, but finish as an assertion test instead of recording a + // misleading black canvas rectangle as a visual baseline. + this.readyRunnable = isHtml5() ? this::done : run; checkReady(); } + @Override + public boolean shouldTakeScreenshot() { + return !isHtml5(); + } + + private static boolean isHtml5() { + return "HTML5".equals(Display.getInstance().getPlatformName()); + } + private void checkReady() { if (!loaded || readyRunnable == null) { return; @@ -49,6 +63,13 @@ private void checkReady() { // Verify content is actually present in the DOM browser.execute("callback.onSuccess(document.body.innerText)", new SuccessCallback() { public void onSucess(BrowserComponent.JSRef result) { + String text = result == null ? null : result.getValue(); + if (text == null + || text.indexOf("Codename One") < 0 + || text.indexOf("BrowserComponent instrumentation test content.") < 0) { + fail("BrowserComponent DOM content was not available through execute(): " + text); + return; + } jsReady = true; checkReady(); } diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/CameraApiTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/CameraApiTest.java index 239bc72b8f3..0deb9eb3548 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/CameraApiTest.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/CameraApiTest.java @@ -40,16 +40,17 @@ /// End-to-end exercise of the low-level `com.codename1.camera.Camera` API /// against whichever per-port `CameraImpl` is in use. /// -/// On the JavaSE simulator (where `JavaSECameraImpl` synthesises frames and -/// never touches a real webcam) this runs the full assertion chain in CI -/// without triggering any permission prompts. On iOS / Android / JavaScript -/// the camera open call would surface an OS permission dialog that would -/// hang the automated runner, so the test self-skips on those platforms. +/// On the JavaSE simulator (where `JavaSECameraImpl` synthesises frames) and +/// JavaScript (where the Playwright runner supplies Chromium's deterministic +/// fake media device) this runs the full assertion chain in CI without a +/// physical webcam or permission prompt. On iOS / Android the camera open call +/// would still surface an OS permission dialog, so the test self-skips there. /// The native-port code paths are verified separately: /// - iOS: device build sanity + XCTest screenshot suite /// - Android: instrumentation tests against a granted-permissions /// `--grant-runtime-permissions` install -/// - JavaScript: manual browser smoke test +/// - JavaScript: this test, through getUserMedia, MediaStream, video/canvas +/// capture and JPEG encoding /// /// No `Cn1ssDeviceRunnerHelper.emitCurrentFormScreenshot` -- this is an /// assertion test, not a screenshot test. @@ -66,16 +67,16 @@ public boolean shouldTakeScreenshot() { @Override public boolean runTest() { String platform = Display.getInstance().getPlatformName(); - // Only the JavaSE simulator ships a synthetic JavaSECameraImpl, so only it - // can assert the Camera API end to end in CI. Real-camera ports (ios/and) - // need runtime permission, and native desktop ports that do not implement - // host webcam capture (win) honestly report Camera.isSupported() == false - // rather than fabricating frames -- both skip here. - boolean isSimulator = !"ios".equals(platform) + // JavaSE uses its synthetic CameraImpl. The JavaScript Playwright runner + // supplies Chromium's fake media device, which still exercises the real + // HTML5 getUserMedia/video/canvas/JPEG path. Native mobile ports need an + // OS permission dialog, and Windows does not implement host webcam + // capture yet, so those remain outside this cross-port headless test. + boolean isHeadlessCameraSupported = "HTML5".equals(platform) + || (!"ios".equals(platform) && !"and".equals(platform) - && !"win".equals(platform) - && !"HTML5".equals(platform); - if (!isSimulator) { + && !"win".equals(platform)); + if (!isHeadlessCameraSupported) { System.out.println("CN1SS:INFO:test=CameraApiTest status=SKIPPED reason=needs-runtime-permission-on-" + platform); done(); return true; @@ -225,27 +226,15 @@ private static void sleep(long ms) { } private static CapturedPhoto awaitPhoto(AsyncResource resource, long timeoutMs) { - long deadline = System.currentTimeMillis() + timeoutMs; - final Object lock = new Object(); - final CapturedPhoto[] out = new CapturedPhoto[1]; - final Throwable[] err = new Throwable[1]; - resource.ready(new com.codename1.util.SuccessCallback() { - @Override public void onSucess(CapturedPhoto value) { - synchronized (lock) { out[0] = value; lock.notifyAll(); } - } - }); - resource.except(new com.codename1.util.SuccessCallback() { - @Override public void onSucess(Throwable t) { - synchronized (lock) { err[0] = t; lock.notifyAll(); } - } - }); - synchronized (lock) { - while (out[0] == null && err[0] == null - && System.currentTimeMillis() < deadline) { - try { lock.wait(50); } catch (InterruptedException ignored) { } - } + try { + // AsyncResource.get(timeout) uses invokeAndBlock when called on the + // EDT, so completion callbacks can still be dispatched. The old + // hand-written wait registered ready() on the EDT and then blocked + // that same EDT, guaranteeing a false timeout on asynchronous ports. + return resource.get((int) timeoutMs); + } catch (Throwable t) { + return null; } - return out[0]; } } diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java index 55bafc8d732..b152de4eebe 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java @@ -128,9 +128,8 @@ private static int testTimeoutMs(BaseTest testClass) { // Calling Display.getInstance() at static-init time was tripping the iOS // class loader (Cn1ssDeviceRunner failed to load before runSuite could - // log a single starting test=...). Keep the array as a plain literal - - // every test ends up in the jar regardless, and the platform-specific - // skipping is handled at runtime by shouldForceTimeoutInHtml5 below. + // log a single starting test=...). Keep the array as a plain literal so + // every test ends up in the jar without platform-dependent initialization. private static final BaseTest[] DEFAULT_TEST_CLASSES = new BaseTest[]{ new MainScreenScreenshotTest(), // Advertising API: renders a banner + native-ad feed via the @@ -512,11 +511,6 @@ private void runNextTest(int index) { CN.callSerially(() -> { Cn1ssDeviceRunnerHelper.clearTransportFailure(); log("CN1SS:INFO:suite starting test=" + testName); - if (shouldForceTimeoutInHtml5(testName)) { - log("CN1SS:ERR:suite test=" + testName + " forced timeout (HTML5 fallback)"); - finalizeTest(index, testClass, testName, true); - return; - } try { testClass.prepare(); testClass.runTest(); @@ -530,105 +524,6 @@ private void runNextTest(int index) { }); } - private boolean shouldForceTimeoutInHtml5(String testName) { - if (!"HTML5".equals(Display.getInstance().getPlatformName())) { - return false; - } - // This list mirrors the authoritative skip set maintained in - // Ports/JavaScriptPort/src/main/webapp/port.js - // (cn1ssForcedTimeoutTestClasses + cn1ssForcedTimeoutTestNames). - // The Java gate runs first: anything skipped here never reaches the - // port.js bridge, so keep the two lists in sync. When a test gets - // un-parked in port.js (e.g. after a runtime fix lands), remove it - // here too — otherwise the suite stays silent on tests that the - // JS-side comments believe should now run. - // - // The list is intentionally an inline `||` chain rather than a - // static HashSet/Set field. Earlier revisions of this file used a - // static collection initialised via a static method call (or a - // method-call initializer for DEFAULT_TEST_CLASSES); both broke iOS - // class loading - Cn1ssDeviceRunner failed to load before runSuite() - // could even log a single starting test=... entry, leaving the suite - // to time out at the 300s end-marker deadline. Keep all skip lookups - // inline to avoid triggering the same static-init failure path. - return isJsSkippedNativeTest(testName) - || isJsSkippedKnownRuntimeBug(testName); - } - - private static boolean isJsSkippedNativeTest(String testName) { - // Native APIs / platform bridges that the JavaScript port doesn't - // implement. These would surface as exceptions or hangs the moment - // the test starts; coverage stays on iOS/Android/JavaSE. - return "MediaPlaybackScreenshotTest".equals(testName) - || "BytecodeTranslatorRegressionTest".equals(testName) - || "BackgroundThreadUiAccessTest".equals(testName) - || "VPNDetectionAPITest".equals(testName) - || "CallDetectionAPITest".equals(testName) - || "LocalNotificationOverrideTest".equals(testName) - || "Base64NativePerformanceTest".equals(testName) - || "AccessibilityTest".equals(testName) - // CryptoApiTest exercises AES/RSA/Signature/SecureRandom which - // route through CodenameOneImplementation overrides; the - // JavaScript port doesn't yet provide a crypto bridge. - || "CryptoApiTest".equals(testName) - // BrowserComponent's iframe ``load`` event isn't routed - // through the worker-callback transport, so the test waits on - // its own ``readyRunnable`` indefinitely. Tracked under - // ``browserComponentLoadEvent`` in port.js. - || "BrowserComponentScreenshotTest".equals(testName); - } - - private static boolean isJsSkippedKnownRuntimeBug(String testName) { - // Tests parked because of specific JS-port runtime bugs. Each entry - // has a matching comment in port.js explaining the symptom and the - // working-name we track it under. When the underlying bug is fixed - // the matching entry is removed from BOTH lists. - return - // ``simdLargeAllocaCorrupt``: SimdLargeAllocaTest corrupts the - // HTML5Implementation instance state via its large-alloca - // pattern, propagating ``{}`` receivers into subsequent - // canvas / paintDirty calls. Needs its own investigation. - "SimdLargeAllocaTest".equals(testName) - // ``chatInputEmitHijack`` / ``chatViewEmitHijack``: - // emitChannel host-bridges to a capture of the visible - // browser canvas instead of the test-supplied off-screen - // Image, so the dual-appearance dark/light streams contain - // the previous test's pixels. - || "ChatInputScreenshotTest".equals(testName) - || "ChatViewScreenshotTest".equals(testName) - // ``canvasContextWipe``: a leftover Canvas2D state mutation - // from a prior test wipes the test's context once we hit - // the canvas-accumulation tail. The targeted no-op recovery - // covers most subjects but these four still hang their - // SCREENSHOT_DONE wait on half of CI runs. - || "ToastBarTopPositionScreenshotTest".equals(testName) - || "CssGradientsScreenshotTest".equals(testName) - || "SheetScreenshotTest".equals(testName) - // ``sheetTearDownLeak``: TextAreaAlignmentStates' form - // renders correctly, but the screenshot captures it under - // a leftover Sheet overlay because Sheet teardown doesn't - // complete before the next test starts. - || "TextAreaAlignmentScreenshotTest".equals(testName) - // ``chartCombinedXyCapture``: ChartCombinedXY hangs the - // SUITE in canvasToBlob retry loop after ~88 fallback-path - // captures. Transform + Rotated weren't reached on the - // unpark-all run because CombinedXY took down the suite - // first; parked under the same suspicion. - || "ChartCombinedXYScreenshotTest".equals(testName) - || "ChartTransformScreenshotTest".equals(testName) - || "ChartRotatedScreenshotTest".equals(testName) - // ``graphicsTransform3dCanvasHang``: the 3D perspective / - // camera transform tests render into a canvas the worker-side - // screenshot path can't resolve (SCREENSHOT_START reports - // canvasCandidates=0), so the suite re-dispatches the same - // index indefinitely and never reaches the per-test deadline. - // The 2D transform tests (rotation, translation, affine) are - // unaffected and keep running. Tracked in port.js under the - // same name. - || "TransformPerspective".equals(testName) - || "TransformCamera".equals(testName); - } - private void awaitTestCompletion(int index, BaseTest testClass, String testName, long deadline) { if (deadline <= 0L) { // Sentinel from the JS-port bridge (port.js runCn1ssResolvedTest): @@ -720,9 +615,9 @@ private void finalizeTest(int index, BaseTest testClass, String testName, boolea /// A retry is only safe when the timeout was truly silent. Gates: /// - one retry per test (retriedTestIndex); /// - native ports only: on HTML5 the suite advancement is co-driven by - /// port.js (runCn1ssResolvedTest dispatches per index) and known-bad - /// tests are parked via its forced-timeout lists, so a Java-side rerun - /// would fight that machinery; + /// port.js (runCn1ssResolvedTest dispatches per index), and its bridge + /// owns transport recovery and the dispatch watchdog. A Java-side rerun + /// would race that lifecycle and can emit stale pixels into the next test; /// - no failure was reported (a real failure should surface, not retry); /// - no capture was started (an in-flight capture could emit after the /// rerun's form is up and ship the wrong pixels under this test's name); diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/OrientationLockScreenshotTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/OrientationLockScreenshotTest.java index 75ae65cc61a..3805bf4e4c9 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/OrientationLockScreenshotTest.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/OrientationLockScreenshotTest.java @@ -41,34 +41,41 @@ public boolean runTest() { @Override protected void onShowCompleted() { CN.lockOrientation(false); - waitForOrientation(this, false, () -> { - // The simulator rotation animation can land in slightly - // different sub-pixel positions if we capture the form too - // early after CN.isPortrait() flips. Mirror BaseTest's - // 1500ms readiness timer so the screenshot waits for the - // post-rotation layout pass to settle and revalidate the - // form layout once before snapping; otherwise this test - // produces 4-7% AA-only diffs run-over-run. - revalidate(); - UITimer.timer(1500, false, this, () -> { - revalidate(); - markCaptureStarted(); - Cn1ssDeviceRunnerHelper.emitCurrentFormScreenshot("landscape", () -> { - CN.lockOrientation(true); - restorePortrait(this, RESTORE_POLL_ATTEMPTS, () -> { - revalidate(); - UITimer.timer(POST_PORTRAIT_SETTLE_MS, false, this, - OrientationLockScreenshotTest.this::done); - }); - }); - }); - }); + if (CN.canForceOrientation()) { + waitForOrientation(this, false, () -> captureSettledForm(this)); + } else { + // Fixed-orientation platforms cannot ever satisfy the + // requested landscape predicate. Exercise lockOrientation, + // but proceed with the current form instead of burning the + // poll budget and racing the test watchdog. + captureSettledForm(this); + } } }; hi.add(new Label("Testing orientation lock...")); hi.show(); return true; } + + private void captureSettledForm(Form form) { + // The simulator rotation animation can land in slightly different + // sub-pixel positions if we capture the form too early after + // CN.isPortrait() flips. Mirror BaseTest's readiness timer so the + // post-rotation layout pass settles before capture. + form.revalidate(); + UITimer.timer(1500, false, form, () -> { + form.revalidate(); + markCaptureStarted(); + Cn1ssDeviceRunnerHelper.emitCurrentFormScreenshot("landscape", () -> { + CN.lockOrientation(true); + restorePortrait(form, RESTORE_POLL_ATTEMPTS, () -> { + form.revalidate(); + UITimer.timer(POST_PORTRAIT_SETTLE_MS, false, form, + OrientationLockScreenshotTest.this::done); + }); + }); + }); + } private void waitForOrientation(Form form, boolean portrait, Runnable onDone) { waitForOrientation(form, portrait, ORIENTATION_POLL_ATTEMPTS, onDone); diff --git a/scripts/run-javascript-browser-tests.sh b/scripts/run-javascript-browser-tests.sh index ea733a47875..5b18cc7a9ee 100755 --- a/scripts/run-javascript-browser-tests.sh +++ b/scripts/run-javascript-browser-tests.sh @@ -184,17 +184,10 @@ if [ "$PARPAR_DIAG_ENABLED" != "0" ]; then fi fi -# Screenshot baselines were captured against the earlier JS port -# behaviour where worker-side addEventListener calls silently became -# no-ops (functions were dropped at the worker->host boundary). The new -# worker-callback round-trip would legitimately fire events from the -# BrowserComponent iframe, MediaPlayback, etc., but those tests are -# intentionally time-limited and their recorded placeholder frames -# assume no listeners run. Keep them stable by disabling event -# forwarding here; production apps do not set this flag and get real -# input/resize/focus events routed to Java handlers. Set -# ``CN1_JS_ENABLE_EVENT_FORWARDING=1`` to opt a suite run back in. -if [ "${CN1_JS_ENABLE_EVENT_FORWARDING:-0}" != "1" ]; then +# Exercise the same event-forwarding path production applications use. An +# explicit opt-out remains available for bridge diagnostics, but CI must not +# make BrowserComponent and input tests stable by disabling their events. +if [ "${CN1_JS_DISABLE_EVENT_FORWARDING:-0}" = "1" ]; then if [[ "$URL" == *\?* ]]; then URL="${URL}&cn1DisableEventForwarding=1" else diff --git a/scripts/run-javascript-headless-browser.mjs b/scripts/run-javascript-headless-browser.mjs index db60110d6e8..3f9604cb0ad 100755 --- a/scripts/run-javascript-headless-browser.mjs +++ b/scripts/run-javascript-headless-browser.mjs @@ -44,6 +44,13 @@ let finalizeProfile = async () => {}; const launchArgs = [ '--autoplay-policy=no-user-gesture-required', + // Exercise the real browser camera path deterministically in CI. Chromium's + // fake device still flows through getUserMedia, MediaStream,