From 31c84fb377f27f98cc0e4affed058394812a32a5 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 20 Oct 2025 20:25:52 +0300 Subject: [PATCH 01/12] Delay iOS screenshots to allow UI to render --- scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl b/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl index 75a90b9d10..d9a88b3838 100644 --- a/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl +++ b/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl @@ -37,6 +37,8 @@ final class HelloCodenameOneUITests: XCTestCase { } private func captureScreenshot(named name: String) throws { + // Allow Codename One an extra moment to render before grabbing the frame. + RunLoop.current.run(until: Date(timeIntervalSinceNow: 1.0)) let shot = XCUIScreen.main.screenshot() // Save into sandbox tmp (optional – mainly for local debugging) From f53a8431c757734a67d709c8cffa7283cc4fbec7 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 20 Oct 2025 21:13:17 +0300 Subject: [PATCH 02/12] Stabilize iOS screenshot captures and preview publishing --- scripts/android/tests/PostPrComment.java | 8 +- .../tests/HelloCodenameOneUITests.swift.tmpl | 84 ++++++++++++++++++- 2 files changed, 88 insertions(+), 4 deletions(-) diff --git a/scripts/android/tests/PostPrComment.java b/scripts/android/tests/PostPrComment.java index 943e50bebb..19c0e2453a 100644 --- a/scripts/android/tests/PostPrComment.java +++ b/scripts/android/tests/PostPrComment.java @@ -14,6 +14,7 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -324,7 +325,12 @@ private static Map publishPreviewsToBranch(Path previewDir, Stri try (var stream = Files.list(dest)) { stream.filter(Files::isRegularFile) .sorted() - .forEach(path -> urls.put(path.getFileName().toString(), rawBase + "/" + path.getFileName())); + .forEach(path -> { + String fileName = path.getFileName().toString(); + String url = rawBase + "/" + fileName; + urls.put(fileName, url); + urls.put(fileName.toLowerCase(Locale.ROOT), url); + }); } deleteRecursively(worktree); return urls; diff --git a/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl b/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl index d9a88b3838..fb163f2df9 100644 --- a/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl +++ b/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl @@ -1,5 +1,6 @@ import XCTest import UIKit +import CoreGraphics final class HelloCodenameOneUITests: XCTestCase { private var app: XCUIApplication! @@ -37,9 +38,7 @@ final class HelloCodenameOneUITests: XCTestCase { } private func captureScreenshot(named name: String) throws { - // Allow Codename One an extra moment to render before grabbing the frame. - RunLoop.current.run(until: Date(timeIntervalSinceNow: 1.0)) - let shot = XCUIScreen.main.screenshot() + let shot = waitForRenderedScreenshot(label: name) // Save into sandbox tmp (optional – mainly for local debugging) let pngURL = outputDirectory.appendingPathComponent("\(name).png") @@ -81,6 +80,85 @@ final class HelloCodenameOneUITests: XCTestCase { try captureScreenshot(named: "BrowserComponent") } + private func waitForRenderedScreenshot(label: String, timeout: TimeInterval = 10, poll: TimeInterval = 0.4) -> XCUIScreenshot { + let deadline = Date(timeIntervalSinceNow: timeout) + RunLoop.current.run(until: Date(timeIntervalSinceNow: 1.0)) + var attempt = 0 + var screenshot = XCUIScreen.main.screenshot() + while Date() < deadline { + if screenshotHasRenderableContent(screenshot) { + return screenshot + } + attempt += 1 + print("CN1SS:INFO:test=\(label) waiting_for_rendered_frame attempt=\(attempt)") + RunLoop.current.run(until: Date(timeIntervalSinceNow: poll)) + screenshot = XCUIScreen.main.screenshot() + } + return screenshot + } + + private func screenshotHasRenderableContent(_ screenshot: XCUIScreenshot) -> Bool { + guard let cgImage = screenshot.image.cgImage else { + return true + } + let width = cgImage.width + let height = cgImage.height + guard width > 0, height > 0 else { + return false + } + + let insetX = max(0, width / 8) + let insetY = max(0, height / 8) + let cropRect = CGRect( + x: insetX, + y: insetY, + width: max(1, width - insetX * 2), + height: max(1, height - insetY * 2) + ).integral + guard let cropped = cgImage.cropping(to: cropRect) else { + return true + } + + let sampleWidth = 80 + let sampleHeight = 80 + let bytesPerPixel = 4 + let bytesPerRow = sampleWidth * bytesPerPixel + guard let context = CGContext( + data: nil, + width: sampleWidth, + height: sampleHeight, + bitsPerComponent: 8, + bytesPerRow: bytesPerRow, + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue + ) else { + return true + } + context.interpolationQuality = .high + context.draw(cropped, in: CGRect(x: 0, y: 0, width: sampleWidth, height: sampleHeight)) + guard let data = context.data else { + return true + } + + let buffer = data.bindMemory(to: UInt8.self, capacity: sampleHeight * bytesPerRow) + var minLuma = 255 + var maxLuma = 0 + for y in 0.. maxLuma { maxLuma = luma } + } + } + + return maxLuma - minLuma > 12 + } + private func sanitizeTestName(_ name: String) -> String { let allowed = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.-") let underscore: UnicodeScalar = "_" From 56a9e734a9c822af58c8c606dae0687d23e2f60a Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 21 Oct 2025 04:10:57 +0300 Subject: [PATCH 03/12] Ensure iOS sample renders before screenshots Ensure iOS UI test waits for Codename One screens Improve iOS UI readiness detection before screenshots Restore CN1 GL screenshots and log startup Assert Codename One readiness in iOS UI tests Fix display logging in sample template Simplify template density scale formatting Relax iOS surface requirement in UI tests --- .../tests/HelloCodenameOneUITests.swift.tmpl | 142 +++++++++++++++--- scripts/templates/HelloCodenameOne.java.tmpl | 105 ++++++++++++- 2 files changed, 219 insertions(+), 28 deletions(-) diff --git a/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl b/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl index fb163f2df9..515384f5e4 100644 --- a/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl +++ b/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl @@ -5,6 +5,9 @@ import CoreGraphics final class HelloCodenameOneUITests: XCTestCase { private var app: XCUIApplication! private var outputDirectory: URL! + private var targetBundleIdentifier: String? + private var resolvedBundleIdentifier: String? + private let codenameOneSurfaceIdentifier = "cn1.glview" private let chunkSize = 2000 private let previewChannel = "PREVIEW" private let previewQualities: [CGFloat] = [0.60, 0.50, 0.40, 0.35, 0.30, 0.25, 0.20, 0.18, 0.16, 0.14, 0.12, 0.10, 0.08, 0.06, 0.05, 0.04, 0.03, 0.02, 0.01] @@ -12,7 +15,13 @@ final class HelloCodenameOneUITests: XCTestCase { override func setUpWithError() throws { continueAfterFailure = false - app = XCUIApplication() + if let bundleID = ProcessInfo.processInfo.environment["CN1_AUT_BUNDLE_ID"], !bundleID.isEmpty { + targetBundleIdentifier = bundleID + app = XCUIApplication(bundleIdentifier: bundleID) + } else { + app = XCUIApplication() + targetBundleIdentifier = nil + } // Locale for determinism app.launchArguments += ["-AppleLocale", "en_US", "-AppleLanguages", "(en)"] @@ -29,7 +38,26 @@ final class HelloCodenameOneUITests: XCTestCase { try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true) app.launch() + let reportedBundleId = targetBundleIdentifier ?? "(scheme-default)" + print("CN1SS:INFO:ui_test_target_bundle_id=\(reportedBundleId)") + print("CN1SS:INFO:ui_test_launch_arguments=\(app.launchArguments.joined(separator: " "))") + if let resolved = resolveBundleIdentifier() { + resolvedBundleIdentifier = resolved + print("CN1SS:INFO:ui_test_resolved_bundle_id=\(resolved)") + } else { + print("CN1SS:WARN:ui_test_resolved_bundle_id_unavailable=true") + } + + let launchSurface = waitForCodenameOneSurface(timeout: 40, context: "post_launch") + logSurfaceMetrics(launchSurface, context: "post_launch") waitForStableFrame() + + let launchProbe = pollForRenderableContent(label: "launch_probe", timeout: 25, poll: 0.75) + if launchProbe.hasRenderableContent { + print("CN1SS:INFO:test=launch_probe rendered_content_detected=true attempts=\(launchProbe.attempts)") + } else { + print("CN1SS:WARN:test=launch_probe rendered_content_detected=false attempts=\(launchProbe.attempts)") + } } override func tearDownWithError() throws { @@ -38,7 +66,19 @@ final class HelloCodenameOneUITests: XCTestCase { } private func captureScreenshot(named name: String) throws { - let shot = waitForRenderedScreenshot(label: name) + let surface = waitForCodenameOneSurface(timeout: 20, context: "\(name)_pre_capture") + logSurfaceMetrics(surface, context: "\(name)_pre_capture") + + let result = pollForRenderableContent(label: name, timeout: 25, poll: 0.5) + let shot = result.screenshot + if !result.hasRenderableContent { + print("CN1SS:WARN:test=\(name) rendered_content_not_detected_after_timeout=true attempts=\(result.attempts)") + print("CN1SS:ERROR:test=\(name) codenameone_render_assertion_failed attempts=\(result.attempts)") + attachDebugDescription(name: "\(name)_ui_tree") + XCTFail("Codename One UI did not render for test \(name) after \(result.attempts) attempt(s)") + } + + logSurfaceMetrics(surface, context: "\(name)_post_capture") // Save into sandbox tmp (optional – mainly for local debugging) let pngURL = outputDirectory.appendingPathComponent("\(name).png") @@ -75,36 +115,78 @@ final class HelloCodenameOneUITests: XCTestCase { func testBrowserComponentScreenshot() throws { waitForStableFrame() tapNormalized(0.5, 0.70) - // tiny retry to allow BrowserComponent to render + print("CN1SS:INFO:navigation_tap=browser_screen normalized_x=0.50 normalized_y=0.70") RunLoop.current.run(until: Date(timeIntervalSinceNow: 2.0)) try captureScreenshot(named: "BrowserComponent") } - private func waitForRenderedScreenshot(label: String, timeout: TimeInterval = 10, poll: TimeInterval = 0.4) -> XCUIScreenshot { + private func pollForRenderableContent(label: String, timeout: TimeInterval, poll: TimeInterval) -> (screenshot: XCUIScreenshot, hasRenderableContent: Bool, attempts: Int) { let deadline = Date(timeIntervalSinceNow: timeout) - RunLoop.current.run(until: Date(timeIntervalSinceNow: 1.0)) - var attempt = 0 - var screenshot = XCUIScreen.main.screenshot() - while Date() < deadline { - if screenshotHasRenderableContent(screenshot) { - return screenshot + var attempts = 0 + while true { + attempts += 1 + let screenshot = app.screenshot() + let analysis = analyzeScreenshot(screenshot) + let hasContent = analysis.hasRenderableContent + if hasContent { + print("CN1SS:INFO:test=\(label) rendered_frame_detected_attempt=\(attempts)") + return (screenshot, true, attempts) } - attempt += 1 - print("CN1SS:INFO:test=\(label) waiting_for_rendered_frame attempt=\(attempt)") - RunLoop.current.run(until: Date(timeIntervalSinceNow: poll)) - screenshot = XCUIScreen.main.screenshot() + + let now = Date() + if now >= deadline { + print("CN1SS:INFO:test=\(label) rendered_frame_luma_variance=\(analysis.lumaVariance)") + return (screenshot, false, attempts) + } + + print("CN1SS:INFO:test=\(label) waiting_for_rendered_frame attempt=\(attempts) luma_variance=\(analysis.lumaVariance)") + let nextInterval = min(poll, deadline.timeIntervalSince(now)) + RunLoop.current.run(until: Date(timeIntervalSinceNow: nextInterval)) } - return screenshot } - private func screenshotHasRenderableContent(_ screenshot: XCUIScreenshot) -> Bool { + private func waitForCodenameOneSurface(timeout: TimeInterval, context: String) -> XCUIElement? { + let surface = app.otherElements[codenameOneSurfaceIdentifier] + let exists = surface.waitForExistence(timeout: timeout) + print("CN1SS:INFO:codenameone_surface_wait context=\(context) identifier=\(codenameOneSurfaceIdentifier) timeout=\(timeout) exists=\(exists)") + if exists { + return surface + } + let fallback = app.screenshot() + let screenshotAttachment = XCTAttachment(screenshot: fallback) + screenshotAttachment.name = "\(context)_missing_surface_screen" + screenshotAttachment.lifetime = .keepAlways + add(screenshotAttachment) + attachDebugDescription(name: "\(context)_missing_surface") + print("CN1SS:WARN:codenameone_surface_missing context=\(context)") + return nil + } + + private func logSurfaceMetrics(_ surface: XCUIElement?, context: String) { + guard let surface = surface else { + print("CN1SS:INFO:codenameone_surface_metrics context=\(context) frame=absent hittable=false") + return + } + let frame = surface.frame + let formatted = String(format: "x=%.1f y=%.1f width=%.1f height=%.1f", frame.origin.x, frame.origin.y, frame.size.width, frame.size.height) + print("CN1SS:INFO:codenameone_surface_metrics context=\(context) frame=\(formatted) hittable=\(surface.isHittable)") + } + + private func attachDebugDescription(name: String) { + let attachment = XCTAttachment(string: app.debugDescription) + attachment.name = name + attachment.lifetime = .keepAlways + add(attachment) + } + + private func analyzeScreenshot(_ screenshot: XCUIScreenshot) -> (hasRenderableContent: Bool, lumaVariance: Int) { guard let cgImage = screenshot.image.cgImage else { - return true + return (true, 255) } let width = cgImage.width let height = cgImage.height guard width > 0, height > 0 else { - return false + return (false, 0) } let insetX = max(0, width / 8) @@ -116,7 +198,7 @@ final class HelloCodenameOneUITests: XCTestCase { height: max(1, height - insetY * 2) ).integral guard let cropped = cgImage.cropping(to: cropRect) else { - return true + return (true, 255) } let sampleWidth = 80 @@ -132,12 +214,12 @@ final class HelloCodenameOneUITests: XCTestCase { space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue ) else { - return true + return (true, 255) } context.interpolationQuality = .high context.draw(cropped, in: CGRect(x: 0, y: 0, width: sampleWidth, height: sampleHeight)) guard let data = context.data else { - return true + return (true, 255) } let buffer = data.bindMemory(to: UInt8.self, capacity: sampleHeight * bytesPerRow) @@ -156,7 +238,23 @@ final class HelloCodenameOneUITests: XCTestCase { } } - return maxLuma - minLuma > 12 + let variance = maxLuma - minLuma + return (variance > 12, variance) + } + + private func resolveBundleIdentifier() -> String? { + if let explicit = targetBundleIdentifier, !explicit.isEmpty { + return explicit + } + do { + let value = try app.value(forKey: "bundleID") + if let actual = value as? String, !actual.isEmpty { + return actual + } + } catch { + print("CN1SS:WARN:ui_test_bundle_resolution_failed error=\(error)") + } + return nil } private func sanitizeTestName(_ name: String) -> String { diff --git a/scripts/templates/HelloCodenameOne.java.tmpl b/scripts/templates/HelloCodenameOne.java.tmpl index 32656416ee..cfcfbd1afc 100644 --- a/scripts/templates/HelloCodenameOne.java.tmpl +++ b/scripts/templates/HelloCodenameOne.java.tmpl @@ -13,29 +13,61 @@ import com.codename1.ui.layouts.BoxLayout; public class @MAIN_NAME@ { private Form current; private Form mainForm; + private boolean initBootstrapScheduled; public void init(Object context) { - // No special initialization required for this sample + System.out.println("CN1SS:INFO:codenameone_init context=" + (context == null ? "null" : context.getClass().getName())); + System.out.println("CN1SS:INFO:codenameone_init_display_initialized=" + Display.isInitialized()); + if (!initBootstrapScheduled) { + initBootstrapScheduled = true; + Display.getInstance().callSerially(() -> { + System.out.println("CN1SS:INFO:codenameone_init_serial_bootstrap begin=true"); + ensureMainFormVisible("init_serial"); + }); + } } public void start() { + System.out.println("CN1SS:INFO:codenameone_start current_exists=" + (current != null)); + Display display = Display.getInstance(); + int density = display.getDeviceDensity(); + String densityBucket; + try { + densityBucket = display.getDensityStr(); + } catch (IllegalStateException ex) { + densityBucket = "unknown"; + } + double densityScale = density > 0 ? ((double) density) / Display.DENSITY_MEDIUM : 0.0; + String densityScaleStr = formatScale(densityScale); + System.out.println("CN1SS:INFO:codenameone_start_display_initialized=" + display.isInitialized() + + " display_size=" + display.getDisplayWidth() + "x" + display.getDisplayHeight() + + " device_density=" + density + + " density_bucket=" + densityBucket + + " density_scale=" + densityScaleStr); if (current != null) { + System.out.println("CN1SS:INFO:codenameone_restore_previous_form title=" + current.getTitle()); current.show(); return; } - showMainForm(); + ensureMainFormVisible("start"); } public void stop() { + System.out.println("CN1SS:INFO:codenameone_stop capturing_current_form=true"); current = Display.getInstance().getCurrent(); + if (current != null) { + System.out.println("CN1SS:INFO:codenameone_stop_form title=" + current.getTitle()); + } } public void destroy() { + System.out.println("CN1SS:INFO:codenameone_destroy invoked=true"); // Nothing to clean up for this sample } - private void showMainForm() { + private void buildMainFormIfNeeded() { if (mainForm == null) { + System.out.println("CN1SS:INFO:codenameone_build_main_form start=true"); mainForm = new Form("Main Screen", new BorderLayout()); Container content = new Container(BoxLayout.y()); @@ -52,32 +84,93 @@ public class @MAIN_NAME@ { body.getAllStyles().setFgColor(0xf9fafb); Button openBrowser = new Button("Open Browser Screen"); - openBrowser.addActionListener(evt -> showBrowserForm()); + openBrowser.addActionListener(evt -> { + System.out.println("CN1SS:INFO:codenameone_open_browser_action triggered=true"); + showBrowserForm(); + }); content.add(heading); content.add(body); content.add(openBrowser); mainForm.add(BorderLayout.CENTER, content); + System.out.println("CN1SS:INFO:codenameone_build_main_form complete=true"); } + } + + private void ensureMainFormVisible(String reason) { + buildMainFormIfNeeded(); + Display display = Display.getInstance(); + Form visible = display.getCurrent(); + if (visible != mainForm) { + current = mainForm; + mainForm.show(); + System.out.println("CN1SS:INFO:codenameone_main_form_presented reason=" + reason); + } else { + current = mainForm; + System.out.println("CN1SS:INFO:codenameone_main_form_already_visible reason=" + reason); + } + Display.getInstance().callSerially(() -> logFormMetrics("codenameone_main_form_ready", Display.getInstance().getCurrent())); + } + + private void showMainForm() { + buildMainFormIfNeeded(); current = mainForm; mainForm.show(); + System.out.println("CN1SS:INFO:codenameone_main_form_shown title=" + mainForm.getTitle()); + Display.getInstance().callSerially(() -> logFormMetrics("codenameone_main_form_ready", Display.getInstance().getCurrent())); } private void showBrowserForm() { + System.out.println("CN1SS:INFO:codenameone_build_browser_form start=true"); Form browserForm = new Form("Browser Screen", new BorderLayout()); BrowserComponent browser = new BrowserComponent(); - browser.setPage(buildBrowserHtml(), null); + String html = buildBrowserHtml(); + System.out.println("CN1SS:INFO:codenameone_browser_html_length=" + html.length()); + browser.setPage(html, null); browserForm.add(BorderLayout.CENTER, browser); browserForm.getToolbar().addMaterialCommandToLeftBar( "Back", FontImage.MATERIAL_ARROW_BACK, - evt -> showMainForm() + evt -> { + System.out.println("CN1SS:INFO:codenameone_browser_back_action triggered=true"); + ensureMainFormVisible("browser_back"); + } ); current = browserForm; browserForm.show(); + System.out.println("CN1SS:INFO:codenameone_browser_form_shown title=" + browserForm.getTitle()); + Display.getInstance().callSerially(() -> logFormMetrics("codenameone_browser_form_ready", Display.getInstance().getCurrent())); + } + + private void logFormMetrics(String label, Form form) { + if (form == null) { + System.out.println("CN1SS:WARN:" + label + " form=null"); + return; + } + System.out.println("CN1SS:INFO:" + label + + " title=" + form.getTitle() + + " width=" + form.getWidth() + + " height=" + form.getHeight() + + " component_count=" + form.getComponentCount()); + } + + private String formatScale(double scale) { + if (scale <= 0) { + return "0"; + } + int rounded = (int) (scale * 100 + 0.5); + int integerPart = rounded / 100; + int fractionPart = Math.abs(rounded % 100); + if (fractionPart == 0) { + return Integer.toString(integerPart); + } + if (fractionPart < 10) { + return integerPart + ".0" + fractionPart; + } + return integerPart + "." + fractionPart; } private String buildBrowserHtml() { From ce3db3f268b18ba8d820adb9c604673e601c8de0 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:30:15 +0300 Subject: [PATCH 04/12] Restore sample app template and relaunch UI test via Springboard --- .../tests/HelloCodenameOneUITests.swift.tmpl | 200 ++++++++++-------- scripts/templates/HelloCodenameOne.java.tmpl | 105 +-------- 2 files changed, 119 insertions(+), 186 deletions(-) diff --git a/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl b/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl index 515384f5e4..32591b96b9 100644 --- a/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl +++ b/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl @@ -6,8 +6,7 @@ final class HelloCodenameOneUITests: XCTestCase { private var app: XCUIApplication! private var outputDirectory: URL! private var targetBundleIdentifier: String? - private var resolvedBundleIdentifier: String? - private let codenameOneSurfaceIdentifier = "cn1.glview" + private var candidateDisplayNames: [String] = [] private let chunkSize = 2000 private let previewChannel = "PREVIEW" private let previewQualities: [CGFloat] = [0.60, 0.50, 0.40, 0.35, 0.30, 0.25, 0.20, 0.18, 0.16, 0.14, 0.12, 0.10, 0.08, 0.06, 0.05, 0.04, 0.03, 0.02, 0.01] @@ -15,49 +14,34 @@ final class HelloCodenameOneUITests: XCTestCase { override func setUpWithError() throws { continueAfterFailure = false - if let bundleID = ProcessInfo.processInfo.environment["CN1_AUT_BUNDLE_ID"], !bundleID.isEmpty { + let env = ProcessInfo.processInfo.environment + + if let bundleID = env["CN1_AUT_BUNDLE_ID"], !bundleID.isEmpty { targetBundleIdentifier = bundleID app = XCUIApplication(bundleIdentifier: bundleID) } else { app = XCUIApplication() - targetBundleIdentifier = nil } + candidateDisplayNames = buildCandidateDisplayNames(from: env) + // Locale for determinism app.launchArguments += ["-AppleLocale", "en_US", "-AppleLanguages", "(en)"] - // Tip: force light mode or content size if you need pixel-stable shots - // app.launchArguments += ["-uiuserInterfaceStyle", "Light"] // IMPORTANT: write to the app's sandbox, not a host path let tmp = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) - if let tag = ProcessInfo.processInfo.environment["CN1SS_OUTPUT_DIR"], !tag.isEmpty { + if let tag = env["CN1SS_OUTPUT_DIR"], !tag.isEmpty { outputDirectory = tmp.appendingPathComponent(tag, isDirectory: true) } else { outputDirectory = tmp.appendingPathComponent("cn1screens", isDirectory: true) } try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true) - app.launch() - let reportedBundleId = targetBundleIdentifier ?? "(scheme-default)" - print("CN1SS:INFO:ui_test_target_bundle_id=\(reportedBundleId)") + print("CN1SS:INFO:ui_test_target_bundle_id=\(targetBundleIdentifier ?? "(scheme-default)")") print("CN1SS:INFO:ui_test_launch_arguments=\(app.launchArguments.joined(separator: " "))") - if let resolved = resolveBundleIdentifier() { - resolvedBundleIdentifier = resolved - print("CN1SS:INFO:ui_test_resolved_bundle_id=\(resolved)") - } else { - print("CN1SS:WARN:ui_test_resolved_bundle_id_unavailable=true") - } - let launchSurface = waitForCodenameOneSurface(timeout: 40, context: "post_launch") - logSurfaceMetrics(launchSurface, context: "post_launch") + ensureAppLaunched() waitForStableFrame() - - let launchProbe = pollForRenderableContent(label: "launch_probe", timeout: 25, poll: 0.75) - if launchProbe.hasRenderableContent { - print("CN1SS:INFO:test=launch_probe rendered_content_detected=true attempts=\(launchProbe.attempts)") - } else { - print("CN1SS:WARN:test=launch_probe rendered_content_detected=false attempts=\(launchProbe.attempts)") - } } override func tearDownWithError() throws { @@ -65,30 +49,104 @@ final class HelloCodenameOneUITests: XCTestCase { app = nil } - private func captureScreenshot(named name: String) throws { - let surface = waitForCodenameOneSurface(timeout: 20, context: "\(name)_pre_capture") - logSurfaceMetrics(surface, context: "\(name)_pre_capture") + private func ensureAppLaunched(timeout: TimeInterval = 45) { + if app.state == .runningForeground { + logLaunchState(label: "already_running") + return + } + + app.launch() + if app.state == .runningForeground { + logLaunchState(label: "launch") + return + } + + if activateViaSpringboard(deadline: Date().addingTimeInterval(timeout)) { + logLaunchState(label: "springboard") + return + } + + app.activate() + logLaunchState(label: "activate") + } + + private func activateViaSpringboard(deadline: Date) -> Bool { + let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") + springboard.activate() + + for name in candidateDisplayNames where Date() < deadline { + let icon = springboard.icons[name] + if icon.waitForExistence(timeout: 3) { + print("CN1SS:INFO:springboard_icon_tap name=\(name)") + icon.tap() + if app.wait(for: .runningForeground, timeout: 5) { + return true + } + } + } + + if Date() < deadline { + let predicate = NSPredicate(format: "label CONTAINS[c] %@", "Codename") + let fallbackIcon = springboard.icons.matching(predicate).firstMatch + if fallbackIcon.waitForExistence(timeout: 3) { + print("CN1SS:INFO:springboard_icon_fallback label=\(fallbackIcon.label)") + fallbackIcon.tap() + if app.wait(for: .runningForeground, timeout: 5) { + return true + } + } + } + return app.state == .runningForeground + } + + private func buildCandidateDisplayNames(from env: [String: String]) -> [String] { + var names: [String] = [] + if let explicit = env["CN1_AUT_APP_NAME"], !explicit.isEmpty { + names.append(explicit) + } + names.append("HelloCodenameOne") + names.append("Hello Codename One") + if let bundle = env["CN1_AUT_BUNDLE_ID"], !bundle.isEmpty { + if let suffix = bundle.split(separator: ".").last, !suffix.isEmpty { + names.append(String(suffix)) + } + } + return Array(Set(names)).sorted() + } + + private func logLaunchState(label: String) { + let state: String + switch app.state { + case .runningForeground: state = "running_foreground" + case .runningBackground: state = "running_background" + case .runningBackgroundSuspended: state = "running_background_suspended" + case .notRunning: state = "not_running" + @unknown default: state = "unknown" + } + print("CN1SS:INFO:launch_state label=\(label) state=\(state)") + if let resolved = resolveBundleIdentifier() { + print("CN1SS:INFO:ui_test_resolved_bundle_id=\(resolved)") + } else { + print("CN1SS:WARN:ui_test_resolved_bundle_id_unavailable=true") + } + } - let result = pollForRenderableContent(label: name, timeout: 25, poll: 0.5) + private func captureScreenshot(named name: String) throws { + ensureAppLaunched() + waitForStableFrame() + let result = pollForRenderableContent(label: name, timeout: 30, poll: 0.6) let shot = result.screenshot if !result.hasRenderableContent { - print("CN1SS:WARN:test=\(name) rendered_content_not_detected_after_timeout=true attempts=\(result.attempts)") - print("CN1SS:ERROR:test=\(name) codenameone_render_assertion_failed attempts=\(result.attempts)") - attachDebugDescription(name: "\(name)_ui_tree") - XCTFail("Codename One UI did not render for test \(name) after \(result.attempts) attempt(s)") + print("CN1SS:WARN:test=\(name) rendered_content_not_detected attempts=\(result.attempts) luma_variance=\(result.lumaVariance)") } - logSurfaceMetrics(surface, context: "\(name)_post_capture") - - // Save into sandbox tmp (optional – mainly for local debugging) let pngURL = outputDirectory.appendingPathComponent("\(name).png") do { try shot.pngRepresentation.write(to: pngURL) } catch { /* ignore */ } - // ALWAYS attach so we can export from the .xcresult - let att = XCTAttachment(screenshot: shot) - att.name = name - att.lifetime = .keepAlways - add(att) + let attachment = XCTAttachment(screenshot: shot) + attachment.name = name + attachment.lifetime = .keepAlways + add(attachment) emitScreenshotPayloads(for: shot, name: name) } @@ -101,84 +159,52 @@ final class HelloCodenameOneUITests: XCTestCase { /// Tap using normalized coordinates (0...1) private func tapNormalized(_ dx: CGFloat, _ dy: CGFloat) { + let frame = app.frame + guard frame.width > 0, frame.height > 0 else { + return + } let origin = app.coordinate(withNormalizedOffset: .zero) - let target = origin.withOffset(.init(dx: app.frame.size.width * dx, - dy: app.frame.size.height * dy)) + let target = origin.withOffset(.init(dx: frame.width * dx, dy: frame.height * dy)) target.tap() } func testMainScreenScreenshot() throws { - waitForStableFrame() try captureScreenshot(named: "MainActivity") } func testBrowserComponentScreenshot() throws { - waitForStableFrame() tapNormalized(0.5, 0.70) print("CN1SS:INFO:navigation_tap=browser_screen normalized_x=0.50 normalized_y=0.70") RunLoop.current.run(until: Date(timeIntervalSinceNow: 2.0)) try captureScreenshot(named: "BrowserComponent") } - private func pollForRenderableContent(label: String, timeout: TimeInterval, poll: TimeInterval) -> (screenshot: XCUIScreenshot, hasRenderableContent: Bool, attempts: Int) { + private func pollForRenderableContent(label: String, timeout: TimeInterval, poll: TimeInterval) -> (screenshot: XCUIScreenshot, hasRenderableContent: Bool, attempts: Int, lumaVariance: Int) { let deadline = Date(timeIntervalSinceNow: timeout) var attempts = 0 + var latestVariance = 0 while true { attempts += 1 let screenshot = app.screenshot() let analysis = analyzeScreenshot(screenshot) - let hasContent = analysis.hasRenderableContent - if hasContent { - print("CN1SS:INFO:test=\(label) rendered_frame_detected_attempt=\(attempts)") - return (screenshot, true, attempts) + latestVariance = analysis.lumaVariance + if analysis.hasRenderableContent { + print("CN1SS:INFO:test=\(label) rendered_frame_detected attempt=\(attempts) luma_variance=\(analysis.lumaVariance)") + return (screenshot, true, attempts, analysis.lumaVariance) } + print("CN1SS:INFO:test=\(label) waiting_for_rendered_frame attempt=\(attempts) luma_variance=\(analysis.lumaVariance)") let now = Date() if now >= deadline { - print("CN1SS:INFO:test=\(label) rendered_frame_luma_variance=\(analysis.lumaVariance)") - return (screenshot, false, attempts) + print("CN1SS:WARN:test=\(label) rendered_content_timeout attempts=\(attempts) final_luma_variance=\(analysis.lumaVariance)") + return (screenshot, false, attempts, analysis.lumaVariance) } - print("CN1SS:INFO:test=\(label) waiting_for_rendered_frame attempt=\(attempts) luma_variance=\(analysis.lumaVariance)") let nextInterval = min(poll, deadline.timeIntervalSince(now)) RunLoop.current.run(until: Date(timeIntervalSinceNow: nextInterval)) } } - private func waitForCodenameOneSurface(timeout: TimeInterval, context: String) -> XCUIElement? { - let surface = app.otherElements[codenameOneSurfaceIdentifier] - let exists = surface.waitForExistence(timeout: timeout) - print("CN1SS:INFO:codenameone_surface_wait context=\(context) identifier=\(codenameOneSurfaceIdentifier) timeout=\(timeout) exists=\(exists)") - if exists { - return surface - } - let fallback = app.screenshot() - let screenshotAttachment = XCTAttachment(screenshot: fallback) - screenshotAttachment.name = "\(context)_missing_surface_screen" - screenshotAttachment.lifetime = .keepAlways - add(screenshotAttachment) - attachDebugDescription(name: "\(context)_missing_surface") - print("CN1SS:WARN:codenameone_surface_missing context=\(context)") - return nil - } - - private func logSurfaceMetrics(_ surface: XCUIElement?, context: String) { - guard let surface = surface else { - print("CN1SS:INFO:codenameone_surface_metrics context=\(context) frame=absent hittable=false") - return - } - let frame = surface.frame - let formatted = String(format: "x=%.1f y=%.1f width=%.1f height=%.1f", frame.origin.x, frame.origin.y, frame.size.width, frame.size.height) - print("CN1SS:INFO:codenameone_surface_metrics context=\(context) frame=\(formatted) hittable=\(surface.isHittable)") - } - - private func attachDebugDescription(name: String) { - let attachment = XCTAttachment(string: app.debugDescription) - attachment.name = name - attachment.lifetime = .keepAlways - add(attachment) - } - private func analyzeScreenshot(_ screenshot: XCUIScreenshot) -> (hasRenderableContent: Bool, lumaVariance: Int) { guard let cgImage = screenshot.image.cgImage else { return (true, 255) diff --git a/scripts/templates/HelloCodenameOne.java.tmpl b/scripts/templates/HelloCodenameOne.java.tmpl index cfcfbd1afc..32656416ee 100644 --- a/scripts/templates/HelloCodenameOne.java.tmpl +++ b/scripts/templates/HelloCodenameOne.java.tmpl @@ -13,61 +13,29 @@ import com.codename1.ui.layouts.BoxLayout; public class @MAIN_NAME@ { private Form current; private Form mainForm; - private boolean initBootstrapScheduled; public void init(Object context) { - System.out.println("CN1SS:INFO:codenameone_init context=" + (context == null ? "null" : context.getClass().getName())); - System.out.println("CN1SS:INFO:codenameone_init_display_initialized=" + Display.isInitialized()); - if (!initBootstrapScheduled) { - initBootstrapScheduled = true; - Display.getInstance().callSerially(() -> { - System.out.println("CN1SS:INFO:codenameone_init_serial_bootstrap begin=true"); - ensureMainFormVisible("init_serial"); - }); - } + // No special initialization required for this sample } public void start() { - System.out.println("CN1SS:INFO:codenameone_start current_exists=" + (current != null)); - Display display = Display.getInstance(); - int density = display.getDeviceDensity(); - String densityBucket; - try { - densityBucket = display.getDensityStr(); - } catch (IllegalStateException ex) { - densityBucket = "unknown"; - } - double densityScale = density > 0 ? ((double) density) / Display.DENSITY_MEDIUM : 0.0; - String densityScaleStr = formatScale(densityScale); - System.out.println("CN1SS:INFO:codenameone_start_display_initialized=" + display.isInitialized() - + " display_size=" + display.getDisplayWidth() + "x" + display.getDisplayHeight() - + " device_density=" + density - + " density_bucket=" + densityBucket - + " density_scale=" + densityScaleStr); if (current != null) { - System.out.println("CN1SS:INFO:codenameone_restore_previous_form title=" + current.getTitle()); current.show(); return; } - ensureMainFormVisible("start"); + showMainForm(); } public void stop() { - System.out.println("CN1SS:INFO:codenameone_stop capturing_current_form=true"); current = Display.getInstance().getCurrent(); - if (current != null) { - System.out.println("CN1SS:INFO:codenameone_stop_form title=" + current.getTitle()); - } } public void destroy() { - System.out.println("CN1SS:INFO:codenameone_destroy invoked=true"); // Nothing to clean up for this sample } - private void buildMainFormIfNeeded() { + private void showMainForm() { if (mainForm == null) { - System.out.println("CN1SS:INFO:codenameone_build_main_form start=true"); mainForm = new Form("Main Screen", new BorderLayout()); Container content = new Container(BoxLayout.y()); @@ -84,93 +52,32 @@ public class @MAIN_NAME@ { body.getAllStyles().setFgColor(0xf9fafb); Button openBrowser = new Button("Open Browser Screen"); - openBrowser.addActionListener(evt -> { - System.out.println("CN1SS:INFO:codenameone_open_browser_action triggered=true"); - showBrowserForm(); - }); + openBrowser.addActionListener(evt -> showBrowserForm()); content.add(heading); content.add(body); content.add(openBrowser); mainForm.add(BorderLayout.CENTER, content); - System.out.println("CN1SS:INFO:codenameone_build_main_form complete=true"); } - } - - private void ensureMainFormVisible(String reason) { - buildMainFormIfNeeded(); - Display display = Display.getInstance(); - Form visible = display.getCurrent(); - if (visible != mainForm) { - current = mainForm; - mainForm.show(); - System.out.println("CN1SS:INFO:codenameone_main_form_presented reason=" + reason); - } else { - current = mainForm; - System.out.println("CN1SS:INFO:codenameone_main_form_already_visible reason=" + reason); - } - Display.getInstance().callSerially(() -> logFormMetrics("codenameone_main_form_ready", Display.getInstance().getCurrent())); - } - - private void showMainForm() { - buildMainFormIfNeeded(); current = mainForm; mainForm.show(); - System.out.println("CN1SS:INFO:codenameone_main_form_shown title=" + mainForm.getTitle()); - Display.getInstance().callSerially(() -> logFormMetrics("codenameone_main_form_ready", Display.getInstance().getCurrent())); } private void showBrowserForm() { - System.out.println("CN1SS:INFO:codenameone_build_browser_form start=true"); Form browserForm = new Form("Browser Screen", new BorderLayout()); BrowserComponent browser = new BrowserComponent(); - String html = buildBrowserHtml(); - System.out.println("CN1SS:INFO:codenameone_browser_html_length=" + html.length()); - browser.setPage(html, null); + browser.setPage(buildBrowserHtml(), null); browserForm.add(BorderLayout.CENTER, browser); browserForm.getToolbar().addMaterialCommandToLeftBar( "Back", FontImage.MATERIAL_ARROW_BACK, - evt -> { - System.out.println("CN1SS:INFO:codenameone_browser_back_action triggered=true"); - ensureMainFormVisible("browser_back"); - } + evt -> showMainForm() ); current = browserForm; browserForm.show(); - System.out.println("CN1SS:INFO:codenameone_browser_form_shown title=" + browserForm.getTitle()); - Display.getInstance().callSerially(() -> logFormMetrics("codenameone_browser_form_ready", Display.getInstance().getCurrent())); - } - - private void logFormMetrics(String label, Form form) { - if (form == null) { - System.out.println("CN1SS:WARN:" + label + " form=null"); - return; - } - System.out.println("CN1SS:INFO:" + label - + " title=" + form.getTitle() - + " width=" + form.getWidth() - + " height=" + form.getHeight() - + " component_count=" + form.getComponentCount()); - } - - private String formatScale(double scale) { - if (scale <= 0) { - return "0"; - } - int rounded = (int) (scale * 100 + 0.5); - int integerPart = rounded / 100; - int fractionPart = Math.abs(rounded % 100); - if (fractionPart == 0) { - return Integer.toString(integerPart); - } - if (fractionPart < 10) { - return integerPart + ".0" + fractionPart; - } - return integerPart + "." + fractionPart; } private String buildBrowserHtml() { From fe905c224abea5a510bc4ce465145c4deaa2a670 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:30:23 +0300 Subject: [PATCH 05/12] Trigger Codename One main from iOS UI tests --- .../tests/HelloCodenameOneUITests.swift.tmpl | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl b/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl index 32591b96b9..f97085fa1b 100644 --- a/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl +++ b/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl @@ -1,6 +1,7 @@ import XCTest import UIKit import CoreGraphics +import Darwin final class HelloCodenameOneUITests: XCTestCase { private var app: XCUIApplication! @@ -41,6 +42,7 @@ final class HelloCodenameOneUITests: XCTestCase { print("CN1SS:INFO:ui_test_launch_arguments=\(app.launchArguments.joined(separator: " "))") ensureAppLaunched() + triggerCodenameOneMainIfPossible() waitForStableFrame() } @@ -133,6 +135,7 @@ final class HelloCodenameOneUITests: XCTestCase { private func captureScreenshot(named name: String) throws { ensureAppLaunched() + triggerCodenameOneMainIfPossible() waitForStableFrame() let result = pollForRenderableContent(label: name, timeout: 30, poll: 0.6) let shot = result.screenshot @@ -283,6 +286,14 @@ final class HelloCodenameOneUITests: XCTestCase { return nil } + private func triggerCodenameOneMainIfPossible() { + guard let bundleID = resolveBundleIdentifier(), !bundleID.isEmpty else { + print("CN1SS:WARN:codenameone_main_skipped reason=no_bundle_identifier") + return + } + CodenameOneMainInvoker.shared.invokeIfNeeded(app: app, bundleIdentifier: bundleID) + } + private func sanitizeTestName(_ name: String) -> String { let allowed = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.-") let underscore: UnicodeScalar = "_" @@ -361,3 +372,134 @@ final class HelloCodenameOneUITests: XCTestCase { print("\(prefix):END:\(name)") } } + +private final class CodenameOneMainInvoker { + static let shared = CodenameOneMainInvoker() + + private let queue = DispatchQueue(label: "codenameone.main.invoker") + private var invokedBundles: Set = [] + private var handles: [String: UnsafeMutableRawPointer] = [:] + + private init() {} + + func invokeIfNeeded(app: XCUIApplication, bundleIdentifier: String) { + var alreadyInvoked = false + queue.sync { + alreadyInvoked = invokedBundles.contains(bundleIdentifier) + } + if alreadyInvoked { + return + } + + guard let context = prepareInvocation(app: app, bundleIdentifier: bundleIdentifier) else { + return + } + + context.invoke() + + queue.sync { + invokedBundles.insert(bundleIdentifier) + handles[bundleIdentifier] = context.handle + } + print("CN1SS:INFO:codenameone_main_invoked bundle=\(bundleIdentifier)") + } + + private func prepareInvocation(app: XCUIApplication, bundleIdentifier: String) -> InvocationContext? { + guard let container = locateAppContainer(app: app, bundleIdentifier: bundleIdentifier) else { + print("CN1SS:WARN:codenameone_main_skipped reason=container_missing bundle=\(bundleIdentifier)") + return nil + } + + guard let executable = readExecutableName(appContainer: container) else { + print("CN1SS:WARN:codenameone_main_skipped reason=executable_missing bundle=\(bundleIdentifier)") + return nil + } + + let binaryPath = (container as NSString).appendingPathComponent(executable) + guard let handle = dlopen(binaryPath, RTLD_NOW | RTLD_GLOBAL) else { + if let error = dlerror() { + print("CN1SS:WARN:codenameone_main_skipped reason=dlopen_failed bundle=\(bundleIdentifier) error=\(String(cString: error))") + } else { + print("CN1SS:WARN:codenameone_main_skipped reason=dlopen_failed bundle=\(bundleIdentifier)") + } + return nil + } + + guard let initPtr = dlsym(handle, "initConstantPool") else { + print("CN1SS:WARN:codenameone_main_skipped reason=missing_initConstantPool bundle=\(bundleIdentifier)") + return nil + } + + guard let threadPtr = dlsym(handle, "getThreadLocalData") else { + print("CN1SS:WARN:codenameone_main_skipped reason=missing_getThreadLocalData bundle=\(bundleIdentifier)") + return nil + } + + let mainSymbol = "com_codenameone_examples_HelloCodenameOne_main___java_lang_String_1ARRAY" + guard let mainPtr = dlsym(handle, mainSymbol) else { + print("CN1SS:WARN:codenameone_main_skipped reason=missing_main_symbol bundle=\(bundleIdentifier) symbol=\(mainSymbol)") + return nil + } + + let initFn = unsafeBitCast(initPtr, to: InvocationContext.InitConstantPoolFn.self) + let threadFn = unsafeBitCast(threadPtr, to: InvocationContext.GetThreadLocalDataFn.self) + let mainFn = unsafeBitCast(mainPtr, to: InvocationContext.CodenameOneMainFn.self) + + return InvocationContext(handle: handle, initConstantPool: initFn, getThreadLocalData: threadFn, mainFunction: mainFn) + } + + private func locateAppContainer(app: XCUIApplication, bundleIdentifier: String) -> String? { + do { + if let bundleURL = try app.value(forKey: "bundleURL") as? URL { + return bundleURL.path + } + } catch { + print("CN1SS:WARN:codenameone_main_kvc_failed key=bundleURL bundle=\(bundleIdentifier) error=\(error)") + } + + do { + if let bundlePath = try app.value(forKey: "bundlePath") as? String { + return bundlePath + } + } catch { + print("CN1SS:WARN:codenameone_main_kvc_failed key=bundlePath bundle=\(bundleIdentifier) error=\(error)") + } + + if let fallback = Bundle.main.infoDictionary?["CFBundleExecutable"] as? String { + print("CN1SS:WARN:codenameone_main_skipped reason=bundle_path_unavailable bundle=\(bundleIdentifier) fallbackExecutable=\(fallback)") + } else { + print("CN1SS:WARN:codenameone_main_skipped reason=bundle_path_unavailable bundle=\(bundleIdentifier)") + } + return nil + } + + private func readExecutableName(appContainer: String) -> String? { + let infoPath = (appContainer as NSString).appendingPathComponent("Info.plist") + guard let info = NSDictionary(contentsOfFile: infoPath) as? [String: Any] else { + print("CN1SS:WARN:codenameone_main_skipped reason=info_plist_unreadable path=\(infoPath)") + return nil + } + guard let executable = info["CFBundleExecutable"] as? String, !executable.isEmpty else { + print("CN1SS:WARN:codenameone_main_skipped reason=cfbundleexecutablenotfound path=\(infoPath)") + return nil + } + return executable + } + + private struct InvocationContext { + typealias InitConstantPoolFn = @convention(c) () -> Void + typealias GetThreadLocalDataFn = @convention(c) () -> UnsafeMutableRawPointer? + typealias CodenameOneMainFn = @convention(c) (UnsafeMutableRawPointer?, UnsafeMutableRawPointer?) -> Void + + let handle: UnsafeMutableRawPointer + let initConstantPool: InitConstantPoolFn + let getThreadLocalData: GetThreadLocalDataFn + let mainFunction: CodenameOneMainFn + + func invoke() { + initConstantPool() + let threadState = getThreadLocalData() + mainFunction(threadState, nil) + } + } +} From c2e399590565c84024df5f7a3a14f35c02d746cf Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 21 Oct 2025 18:11:58 +0300 Subject: [PATCH 06/12] Improve Codename One UI test bundle discovery --- .../tests/HelloCodenameOneUITests.swift.tmpl | 95 +++++++++++++++---- 1 file changed, 77 insertions(+), 18 deletions(-) diff --git a/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl b/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl index f97085fa1b..e4e197a024 100644 --- a/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl +++ b/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl @@ -2,6 +2,7 @@ import XCTest import UIKit import CoreGraphics import Darwin +import Foundation final class HelloCodenameOneUITests: XCTestCase { private var app: XCUIApplication! @@ -275,13 +276,10 @@ final class HelloCodenameOneUITests: XCTestCase { if let explicit = targetBundleIdentifier, !explicit.isEmpty { return explicit } - do { - let value = try app.value(forKey: "bundleID") - if let actual = value as? String, !actual.isEmpty { - return actual + if let bundle: String = dynamicAppValue("bundleID") { + if !bundle.isEmpty { + return bundle } - } catch { - print("CN1SS:WARN:ui_test_bundle_resolution_failed error=\(error)") } return nil } @@ -449,20 +447,16 @@ private final class CodenameOneMainInvoker { } private func locateAppContainer(app: XCUIApplication, bundleIdentifier: String) -> String? { - do { - if let bundleURL = try app.value(forKey: "bundleURL") as? URL { - return bundleURL.path - } - } catch { - print("CN1SS:WARN:codenameone_main_kvc_failed key=bundleURL bundle=\(bundleIdentifier) error=\(error)") + if let bundleURL: URL = dynamicAppValue("bundleURL"), !bundleURL.path.isEmpty { + return bundleURL.path } - do { - if let bundlePath = try app.value(forKey: "bundlePath") as? String { - return bundlePath - } - } catch { - print("CN1SS:WARN:codenameone_main_kvc_failed key=bundlePath bundle=\(bundleIdentifier) error=\(error)") + if let bundlePath: String = dynamicAppValue("bundlePath"), !bundlePath.isEmpty { + return bundlePath + } + + if let container = locateViaSimctl(bundleIdentifier: bundleIdentifier) { + return container } if let fallback = Bundle.main.infoDictionary?["CFBundleExecutable"] as? String { @@ -486,6 +480,71 @@ private final class CodenameOneMainInvoker { return executable } + private func locateViaSimctl(bundleIdentifier: String) -> String? { + let env = ProcessInfo.processInfo.environment + guard let udid = env["SIMULATOR_UDID"], !udid.isEmpty else { + return nil + } + + let task = Process() + task.executableURL = URL(fileURLWithPath: "/usr/bin/xcrun") + task.arguments = ["simctl", "get_app_container", udid, bundleIdentifier] + + let stdoutPipe = Pipe() + let stderrPipe = Pipe() + task.standardOutput = stdoutPipe + task.standardError = stderrPipe + + do { + try task.run() + } catch { + print("CN1SS:WARN:codenameone_main_simctl_failed bundle=\(bundleIdentifier) error=\(error)") + return nil + } + + task.waitUntilExit() + if task.terminationStatus != 0 { + let data = stderrPipe.fileHandleForReading.readDataToEndOfFile() + if let message = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines), !message.isEmpty { + print("CN1SS:WARN:codenameone_main_simctl_failed bundle=\(bundleIdentifier) status=\(task.terminationStatus) stderr=\(message)") + } else { + print("CN1SS:WARN:codenameone_main_simctl_failed bundle=\(bundleIdentifier) status=\(task.terminationStatus)") + } + return nil + } + + let data = stdoutPipe.fileHandleForReading.readDataToEndOfFile() + guard let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines), !output.isEmpty else { + print("CN1SS:WARN:codenameone_main_simctl_empty bundle=\(bundleIdentifier)") + return nil + } + + return output + } + + private func dynamicAppValue(_ selectorName: String) -> T? { + let selector = NSSelectorFromString(selectorName) + guard app.responds(to: selector) else { + return nil + } + guard let unmanaged = app.perform(selector) else { + return nil + } + let value = unmanaged.takeUnretainedValue() + switch value { + case let typed as T: + return typed + case let number as NSNumber where T.self == Bool.self: + return (number.boolValue as? T) + case let string as NSString where T.self == String.self: + return (string as String) as? T + case let url as NSURL where T.self == URL.self: + return (url as URL) as? T + default: + return nil + } + } + private struct InvocationContext { typealias InitConstantPoolFn = @convention(c) () -> Void typealias GetThreadLocalDataFn = @convention(c) () -> UnsafeMutableRawPointer? From f292b4cde4549cfb2718e5ef041e2c1e58188814 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 21 Oct 2025 19:35:03 +0300 Subject: [PATCH 07/12] Improve Codename One iOS test bootstrap logging --- .../tests/HelloCodenameOneUITests.swift.tmpl | 64 +++++++++++-------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl b/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl index e4e197a024..bbee32f6fd 100644 --- a/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl +++ b/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl @@ -16,6 +16,7 @@ final class HelloCodenameOneUITests: XCTestCase { override func setUpWithError() throws { continueAfterFailure = false + print("CN1SS:INFO:setup_begin test_class=HelloCodenameOneUITests") let env = ProcessInfo.processInfo.environment if let bundleID = env["CN1_AUT_BUNDLE_ID"], !bundleID.isEmpty { @@ -37,7 +38,11 @@ final class HelloCodenameOneUITests: XCTestCase { } else { outputDirectory = tmp.appendingPathComponent("cn1screens", isDirectory: true) } - try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true) + do { + try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true) + } catch { + print("CN1SS:WARN:output_directory_create_failed path=\(outputDirectory.path) error=\(error)") + } print("CN1SS:INFO:ui_test_target_bundle_id=\(targetBundleIdentifier ?? "(scheme-default)")") print("CN1SS:INFO:ui_test_launch_arguments=\(app.launchArguments.joined(separator: " "))") @@ -276,7 +281,7 @@ final class HelloCodenameOneUITests: XCTestCase { if let explicit = targetBundleIdentifier, !explicit.isEmpty { return explicit } - if let bundle: String = dynamicAppValue("bundleID") { + if let bundle: String = codenameApplicationValue("bundleID", for: app) { if !bundle.isEmpty { return bundle } @@ -371,6 +376,29 @@ final class HelloCodenameOneUITests: XCTestCase { } } +private func codenameApplicationValue(_ selectorName: String, for application: XCUIApplication) -> T? { + let selector = NSSelectorFromString(selectorName) + guard application.responds(to: selector) else { + return nil + } + guard let unmanaged = application.perform(selector) else { + return nil + } + let value = unmanaged.takeUnretainedValue() + switch value { + case let typed as T: + return typed + case let number as NSNumber where T.self == Bool.self: + return (number.boolValue as? T) + case let string as NSString where T.self == String.self: + return (string as String) as? T + case let url as NSURL where T.self == URL.self: + return (url as URL) as? T + default: + return nil + } +} + private final class CodenameOneMainInvoker { static let shared = CodenameOneMainInvoker() @@ -393,7 +421,9 @@ private final class CodenameOneMainInvoker { return } + print("CN1SS:INFO:codenameone_main_invoking bundle=\(bundleIdentifier)") context.invoke() + print("CN1SS:INFO:codenameone_main_invocation_complete bundle=\(bundleIdentifier)") queue.sync { invokedBundles.insert(bundleIdentifier) @@ -407,11 +437,13 @@ private final class CodenameOneMainInvoker { print("CN1SS:WARN:codenameone_main_skipped reason=container_missing bundle=\(bundleIdentifier)") return nil } + print("CN1SS:INFO:codenameone_main_container bundle=\(bundleIdentifier) path=\(container)") guard let executable = readExecutableName(appContainer: container) else { print("CN1SS:WARN:codenameone_main_skipped reason=executable_missing bundle=\(bundleIdentifier)") return nil } + print("CN1SS:INFO:codenameone_main_executable bundle=\(bundleIdentifier) name=\(executable)") let binaryPath = (container as NSString).appendingPathComponent(executable) guard let handle = dlopen(binaryPath, RTLD_NOW | RTLD_GLOBAL) else { @@ -422,6 +454,7 @@ private final class CodenameOneMainInvoker { } return nil } + print("CN1SS:INFO:codenameone_main_dlopen_success bundle=\(bundleIdentifier) binary=\(binaryPath)") guard let initPtr = dlsym(handle, "initConstantPool") else { print("CN1SS:WARN:codenameone_main_skipped reason=missing_initConstantPool bundle=\(bundleIdentifier)") @@ -447,11 +480,11 @@ private final class CodenameOneMainInvoker { } private func locateAppContainer(app: XCUIApplication, bundleIdentifier: String) -> String? { - if let bundleURL: URL = dynamicAppValue("bundleURL"), !bundleURL.path.isEmpty { + if let bundleURL: URL = codenameApplicationValue("bundleURL", for: app), !bundleURL.path.isEmpty { return bundleURL.path } - if let bundlePath: String = dynamicAppValue("bundlePath"), !bundlePath.isEmpty { + if let bundlePath: String = codenameApplicationValue("bundlePath", for: app), !bundlePath.isEmpty { return bundlePath } @@ -522,29 +555,6 @@ private final class CodenameOneMainInvoker { return output } - private func dynamicAppValue(_ selectorName: String) -> T? { - let selector = NSSelectorFromString(selectorName) - guard app.responds(to: selector) else { - return nil - } - guard let unmanaged = app.perform(selector) else { - return nil - } - let value = unmanaged.takeUnretainedValue() - switch value { - case let typed as T: - return typed - case let number as NSNumber where T.self == Bool.self: - return (number.boolValue as? T) - case let string as NSString where T.self == String.self: - return (string as String) as? T - case let url as NSURL where T.self == URL.self: - return (url as URL) as? T - default: - return nil - } - } - private struct InvocationContext { typealias InitConstantPoolFn = @convention(c) () -> Void typealias GetThreadLocalDataFn = @convention(c) () -> UnsafeMutableRawPointer? From 38f5e323181d03105e569f8a174d0f5d54439ef3 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 21 Oct 2025 20:26:49 +0300 Subject: [PATCH 08/12] Revert "Improve Codename One iOS test bootstrap logging" This reverts commit f292b4cde4549cfb2718e5ef041e2c1e58188814. --- .../tests/HelloCodenameOneUITests.swift.tmpl | 64 ++++++++----------- 1 file changed, 27 insertions(+), 37 deletions(-) diff --git a/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl b/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl index bbee32f6fd..e4e197a024 100644 --- a/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl +++ b/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl @@ -16,7 +16,6 @@ final class HelloCodenameOneUITests: XCTestCase { override func setUpWithError() throws { continueAfterFailure = false - print("CN1SS:INFO:setup_begin test_class=HelloCodenameOneUITests") let env = ProcessInfo.processInfo.environment if let bundleID = env["CN1_AUT_BUNDLE_ID"], !bundleID.isEmpty { @@ -38,11 +37,7 @@ final class HelloCodenameOneUITests: XCTestCase { } else { outputDirectory = tmp.appendingPathComponent("cn1screens", isDirectory: true) } - do { - try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true) - } catch { - print("CN1SS:WARN:output_directory_create_failed path=\(outputDirectory.path) error=\(error)") - } + try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true) print("CN1SS:INFO:ui_test_target_bundle_id=\(targetBundleIdentifier ?? "(scheme-default)")") print("CN1SS:INFO:ui_test_launch_arguments=\(app.launchArguments.joined(separator: " "))") @@ -281,7 +276,7 @@ final class HelloCodenameOneUITests: XCTestCase { if let explicit = targetBundleIdentifier, !explicit.isEmpty { return explicit } - if let bundle: String = codenameApplicationValue("bundleID", for: app) { + if let bundle: String = dynamicAppValue("bundleID") { if !bundle.isEmpty { return bundle } @@ -376,29 +371,6 @@ final class HelloCodenameOneUITests: XCTestCase { } } -private func codenameApplicationValue(_ selectorName: String, for application: XCUIApplication) -> T? { - let selector = NSSelectorFromString(selectorName) - guard application.responds(to: selector) else { - return nil - } - guard let unmanaged = application.perform(selector) else { - return nil - } - let value = unmanaged.takeUnretainedValue() - switch value { - case let typed as T: - return typed - case let number as NSNumber where T.self == Bool.self: - return (number.boolValue as? T) - case let string as NSString where T.self == String.self: - return (string as String) as? T - case let url as NSURL where T.self == URL.self: - return (url as URL) as? T - default: - return nil - } -} - private final class CodenameOneMainInvoker { static let shared = CodenameOneMainInvoker() @@ -421,9 +393,7 @@ private final class CodenameOneMainInvoker { return } - print("CN1SS:INFO:codenameone_main_invoking bundle=\(bundleIdentifier)") context.invoke() - print("CN1SS:INFO:codenameone_main_invocation_complete bundle=\(bundleIdentifier)") queue.sync { invokedBundles.insert(bundleIdentifier) @@ -437,13 +407,11 @@ private final class CodenameOneMainInvoker { print("CN1SS:WARN:codenameone_main_skipped reason=container_missing bundle=\(bundleIdentifier)") return nil } - print("CN1SS:INFO:codenameone_main_container bundle=\(bundleIdentifier) path=\(container)") guard let executable = readExecutableName(appContainer: container) else { print("CN1SS:WARN:codenameone_main_skipped reason=executable_missing bundle=\(bundleIdentifier)") return nil } - print("CN1SS:INFO:codenameone_main_executable bundle=\(bundleIdentifier) name=\(executable)") let binaryPath = (container as NSString).appendingPathComponent(executable) guard let handle = dlopen(binaryPath, RTLD_NOW | RTLD_GLOBAL) else { @@ -454,7 +422,6 @@ private final class CodenameOneMainInvoker { } return nil } - print("CN1SS:INFO:codenameone_main_dlopen_success bundle=\(bundleIdentifier) binary=\(binaryPath)") guard let initPtr = dlsym(handle, "initConstantPool") else { print("CN1SS:WARN:codenameone_main_skipped reason=missing_initConstantPool bundle=\(bundleIdentifier)") @@ -480,11 +447,11 @@ private final class CodenameOneMainInvoker { } private func locateAppContainer(app: XCUIApplication, bundleIdentifier: String) -> String? { - if let bundleURL: URL = codenameApplicationValue("bundleURL", for: app), !bundleURL.path.isEmpty { + if let bundleURL: URL = dynamicAppValue("bundleURL"), !bundleURL.path.isEmpty { return bundleURL.path } - if let bundlePath: String = codenameApplicationValue("bundlePath", for: app), !bundlePath.isEmpty { + if let bundlePath: String = dynamicAppValue("bundlePath"), !bundlePath.isEmpty { return bundlePath } @@ -555,6 +522,29 @@ private final class CodenameOneMainInvoker { return output } + private func dynamicAppValue(_ selectorName: String) -> T? { + let selector = NSSelectorFromString(selectorName) + guard app.responds(to: selector) else { + return nil + } + guard let unmanaged = app.perform(selector) else { + return nil + } + let value = unmanaged.takeUnretainedValue() + switch value { + case let typed as T: + return typed + case let number as NSNumber where T.self == Bool.self: + return (number.boolValue as? T) + case let string as NSString where T.self == String.self: + return (string as String) as? T + case let url as NSURL where T.self == URL.self: + return (url as URL) as? T + default: + return nil + } + } + private struct InvocationContext { typealias InitConstantPoolFn = @convention(c) () -> Void typealias GetThreadLocalDataFn = @convention(c) () -> UnsafeMutableRawPointer? From 06b2532a78834545fb90cd9464beaa9d75ccd3ad Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 21 Oct 2025 20:26:53 +0300 Subject: [PATCH 09/12] Revert "Improve Codename One UI test bundle discovery" This reverts commit c2e399590565c84024df5f7a3a14f35c02d746cf. --- .../tests/HelloCodenameOneUITests.swift.tmpl | 95 ++++--------------- 1 file changed, 18 insertions(+), 77 deletions(-) diff --git a/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl b/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl index e4e197a024..f97085fa1b 100644 --- a/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl +++ b/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl @@ -2,7 +2,6 @@ import XCTest import UIKit import CoreGraphics import Darwin -import Foundation final class HelloCodenameOneUITests: XCTestCase { private var app: XCUIApplication! @@ -276,10 +275,13 @@ final class HelloCodenameOneUITests: XCTestCase { if let explicit = targetBundleIdentifier, !explicit.isEmpty { return explicit } - if let bundle: String = dynamicAppValue("bundleID") { - if !bundle.isEmpty { - return bundle + do { + let value = try app.value(forKey: "bundleID") + if let actual = value as? String, !actual.isEmpty { + return actual } + } catch { + print("CN1SS:WARN:ui_test_bundle_resolution_failed error=\(error)") } return nil } @@ -447,16 +449,20 @@ private final class CodenameOneMainInvoker { } private func locateAppContainer(app: XCUIApplication, bundleIdentifier: String) -> String? { - if let bundleURL: URL = dynamicAppValue("bundleURL"), !bundleURL.path.isEmpty { - return bundleURL.path - } - - if let bundlePath: String = dynamicAppValue("bundlePath"), !bundlePath.isEmpty { - return bundlePath + do { + if let bundleURL = try app.value(forKey: "bundleURL") as? URL { + return bundleURL.path + } + } catch { + print("CN1SS:WARN:codenameone_main_kvc_failed key=bundleURL bundle=\(bundleIdentifier) error=\(error)") } - if let container = locateViaSimctl(bundleIdentifier: bundleIdentifier) { - return container + do { + if let bundlePath = try app.value(forKey: "bundlePath") as? String { + return bundlePath + } + } catch { + print("CN1SS:WARN:codenameone_main_kvc_failed key=bundlePath bundle=\(bundleIdentifier) error=\(error)") } if let fallback = Bundle.main.infoDictionary?["CFBundleExecutable"] as? String { @@ -480,71 +486,6 @@ private final class CodenameOneMainInvoker { return executable } - private func locateViaSimctl(bundleIdentifier: String) -> String? { - let env = ProcessInfo.processInfo.environment - guard let udid = env["SIMULATOR_UDID"], !udid.isEmpty else { - return nil - } - - let task = Process() - task.executableURL = URL(fileURLWithPath: "/usr/bin/xcrun") - task.arguments = ["simctl", "get_app_container", udid, bundleIdentifier] - - let stdoutPipe = Pipe() - let stderrPipe = Pipe() - task.standardOutput = stdoutPipe - task.standardError = stderrPipe - - do { - try task.run() - } catch { - print("CN1SS:WARN:codenameone_main_simctl_failed bundle=\(bundleIdentifier) error=\(error)") - return nil - } - - task.waitUntilExit() - if task.terminationStatus != 0 { - let data = stderrPipe.fileHandleForReading.readDataToEndOfFile() - if let message = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines), !message.isEmpty { - print("CN1SS:WARN:codenameone_main_simctl_failed bundle=\(bundleIdentifier) status=\(task.terminationStatus) stderr=\(message)") - } else { - print("CN1SS:WARN:codenameone_main_simctl_failed bundle=\(bundleIdentifier) status=\(task.terminationStatus)") - } - return nil - } - - let data = stdoutPipe.fileHandleForReading.readDataToEndOfFile() - guard let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines), !output.isEmpty else { - print("CN1SS:WARN:codenameone_main_simctl_empty bundle=\(bundleIdentifier)") - return nil - } - - return output - } - - private func dynamicAppValue(_ selectorName: String) -> T? { - let selector = NSSelectorFromString(selectorName) - guard app.responds(to: selector) else { - return nil - } - guard let unmanaged = app.perform(selector) else { - return nil - } - let value = unmanaged.takeUnretainedValue() - switch value { - case let typed as T: - return typed - case let number as NSNumber where T.self == Bool.self: - return (number.boolValue as? T) - case let string as NSString where T.self == String.self: - return (string as String) as? T - case let url as NSURL where T.self == URL.self: - return (url as URL) as? T - default: - return nil - } - } - private struct InvocationContext { typealias InitConstantPoolFn = @convention(c) () -> Void typealias GetThreadLocalDataFn = @convention(c) () -> UnsafeMutableRawPointer? From 32579585f58799819a63850adaf4a8d545f20075 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 21 Oct 2025 20:26:56 +0300 Subject: [PATCH 10/12] Revert "Trigger Codename One main from iOS UI tests" This reverts commit fe905c224abea5a510bc4ce465145c4deaa2a670. --- .../tests/HelloCodenameOneUITests.swift.tmpl | 142 ------------------ 1 file changed, 142 deletions(-) diff --git a/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl b/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl index f97085fa1b..32591b96b9 100644 --- a/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl +++ b/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl @@ -1,7 +1,6 @@ import XCTest import UIKit import CoreGraphics -import Darwin final class HelloCodenameOneUITests: XCTestCase { private var app: XCUIApplication! @@ -42,7 +41,6 @@ final class HelloCodenameOneUITests: XCTestCase { print("CN1SS:INFO:ui_test_launch_arguments=\(app.launchArguments.joined(separator: " "))") ensureAppLaunched() - triggerCodenameOneMainIfPossible() waitForStableFrame() } @@ -135,7 +133,6 @@ final class HelloCodenameOneUITests: XCTestCase { private func captureScreenshot(named name: String) throws { ensureAppLaunched() - triggerCodenameOneMainIfPossible() waitForStableFrame() let result = pollForRenderableContent(label: name, timeout: 30, poll: 0.6) let shot = result.screenshot @@ -286,14 +283,6 @@ final class HelloCodenameOneUITests: XCTestCase { return nil } - private func triggerCodenameOneMainIfPossible() { - guard let bundleID = resolveBundleIdentifier(), !bundleID.isEmpty else { - print("CN1SS:WARN:codenameone_main_skipped reason=no_bundle_identifier") - return - } - CodenameOneMainInvoker.shared.invokeIfNeeded(app: app, bundleIdentifier: bundleID) - } - private func sanitizeTestName(_ name: String) -> String { let allowed = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.-") let underscore: UnicodeScalar = "_" @@ -372,134 +361,3 @@ final class HelloCodenameOneUITests: XCTestCase { print("\(prefix):END:\(name)") } } - -private final class CodenameOneMainInvoker { - static let shared = CodenameOneMainInvoker() - - private let queue = DispatchQueue(label: "codenameone.main.invoker") - private var invokedBundles: Set = [] - private var handles: [String: UnsafeMutableRawPointer] = [:] - - private init() {} - - func invokeIfNeeded(app: XCUIApplication, bundleIdentifier: String) { - var alreadyInvoked = false - queue.sync { - alreadyInvoked = invokedBundles.contains(bundleIdentifier) - } - if alreadyInvoked { - return - } - - guard let context = prepareInvocation(app: app, bundleIdentifier: bundleIdentifier) else { - return - } - - context.invoke() - - queue.sync { - invokedBundles.insert(bundleIdentifier) - handles[bundleIdentifier] = context.handle - } - print("CN1SS:INFO:codenameone_main_invoked bundle=\(bundleIdentifier)") - } - - private func prepareInvocation(app: XCUIApplication, bundleIdentifier: String) -> InvocationContext? { - guard let container = locateAppContainer(app: app, bundleIdentifier: bundleIdentifier) else { - print("CN1SS:WARN:codenameone_main_skipped reason=container_missing bundle=\(bundleIdentifier)") - return nil - } - - guard let executable = readExecutableName(appContainer: container) else { - print("CN1SS:WARN:codenameone_main_skipped reason=executable_missing bundle=\(bundleIdentifier)") - return nil - } - - let binaryPath = (container as NSString).appendingPathComponent(executable) - guard let handle = dlopen(binaryPath, RTLD_NOW | RTLD_GLOBAL) else { - if let error = dlerror() { - print("CN1SS:WARN:codenameone_main_skipped reason=dlopen_failed bundle=\(bundleIdentifier) error=\(String(cString: error))") - } else { - print("CN1SS:WARN:codenameone_main_skipped reason=dlopen_failed bundle=\(bundleIdentifier)") - } - return nil - } - - guard let initPtr = dlsym(handle, "initConstantPool") else { - print("CN1SS:WARN:codenameone_main_skipped reason=missing_initConstantPool bundle=\(bundleIdentifier)") - return nil - } - - guard let threadPtr = dlsym(handle, "getThreadLocalData") else { - print("CN1SS:WARN:codenameone_main_skipped reason=missing_getThreadLocalData bundle=\(bundleIdentifier)") - return nil - } - - let mainSymbol = "com_codenameone_examples_HelloCodenameOne_main___java_lang_String_1ARRAY" - guard let mainPtr = dlsym(handle, mainSymbol) else { - print("CN1SS:WARN:codenameone_main_skipped reason=missing_main_symbol bundle=\(bundleIdentifier) symbol=\(mainSymbol)") - return nil - } - - let initFn = unsafeBitCast(initPtr, to: InvocationContext.InitConstantPoolFn.self) - let threadFn = unsafeBitCast(threadPtr, to: InvocationContext.GetThreadLocalDataFn.self) - let mainFn = unsafeBitCast(mainPtr, to: InvocationContext.CodenameOneMainFn.self) - - return InvocationContext(handle: handle, initConstantPool: initFn, getThreadLocalData: threadFn, mainFunction: mainFn) - } - - private func locateAppContainer(app: XCUIApplication, bundleIdentifier: String) -> String? { - do { - if let bundleURL = try app.value(forKey: "bundleURL") as? URL { - return bundleURL.path - } - } catch { - print("CN1SS:WARN:codenameone_main_kvc_failed key=bundleURL bundle=\(bundleIdentifier) error=\(error)") - } - - do { - if let bundlePath = try app.value(forKey: "bundlePath") as? String { - return bundlePath - } - } catch { - print("CN1SS:WARN:codenameone_main_kvc_failed key=bundlePath bundle=\(bundleIdentifier) error=\(error)") - } - - if let fallback = Bundle.main.infoDictionary?["CFBundleExecutable"] as? String { - print("CN1SS:WARN:codenameone_main_skipped reason=bundle_path_unavailable bundle=\(bundleIdentifier) fallbackExecutable=\(fallback)") - } else { - print("CN1SS:WARN:codenameone_main_skipped reason=bundle_path_unavailable bundle=\(bundleIdentifier)") - } - return nil - } - - private func readExecutableName(appContainer: String) -> String? { - let infoPath = (appContainer as NSString).appendingPathComponent("Info.plist") - guard let info = NSDictionary(contentsOfFile: infoPath) as? [String: Any] else { - print("CN1SS:WARN:codenameone_main_skipped reason=info_plist_unreadable path=\(infoPath)") - return nil - } - guard let executable = info["CFBundleExecutable"] as? String, !executable.isEmpty else { - print("CN1SS:WARN:codenameone_main_skipped reason=cfbundleexecutablenotfound path=\(infoPath)") - return nil - } - return executable - } - - private struct InvocationContext { - typealias InitConstantPoolFn = @convention(c) () -> Void - typealias GetThreadLocalDataFn = @convention(c) () -> UnsafeMutableRawPointer? - typealias CodenameOneMainFn = @convention(c) (UnsafeMutableRawPointer?, UnsafeMutableRawPointer?) -> Void - - let handle: UnsafeMutableRawPointer - let initConstantPool: InitConstantPoolFn - let getThreadLocalData: GetThreadLocalDataFn - let mainFunction: CodenameOneMainFn - - func invoke() { - initConstantPool() - let threadState = getThreadLocalData() - mainFunction(threadState, nil) - } - } -} From bf34c31fae19f63babf09ca9ea3d0665c1266f4b Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 21 Oct 2025 20:27:02 +0300 Subject: [PATCH 11/12] Revert "Restore sample app template and relaunch UI test via Springboard" This reverts commit ce3db3f268b18ba8d820adb9c604673e601c8de0. --- .../tests/HelloCodenameOneUITests.swift.tmpl | 200 ++++++++---------- scripts/templates/HelloCodenameOne.java.tmpl | 105 ++++++++- 2 files changed, 186 insertions(+), 119 deletions(-) diff --git a/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl b/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl index 32591b96b9..515384f5e4 100644 --- a/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl +++ b/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl @@ -6,7 +6,8 @@ final class HelloCodenameOneUITests: XCTestCase { private var app: XCUIApplication! private var outputDirectory: URL! private var targetBundleIdentifier: String? - private var candidateDisplayNames: [String] = [] + private var resolvedBundleIdentifier: String? + private let codenameOneSurfaceIdentifier = "cn1.glview" private let chunkSize = 2000 private let previewChannel = "PREVIEW" private let previewQualities: [CGFloat] = [0.60, 0.50, 0.40, 0.35, 0.30, 0.25, 0.20, 0.18, 0.16, 0.14, 0.12, 0.10, 0.08, 0.06, 0.05, 0.04, 0.03, 0.02, 0.01] @@ -14,34 +15,49 @@ final class HelloCodenameOneUITests: XCTestCase { override func setUpWithError() throws { continueAfterFailure = false - let env = ProcessInfo.processInfo.environment - - if let bundleID = env["CN1_AUT_BUNDLE_ID"], !bundleID.isEmpty { + if let bundleID = ProcessInfo.processInfo.environment["CN1_AUT_BUNDLE_ID"], !bundleID.isEmpty { targetBundleIdentifier = bundleID app = XCUIApplication(bundleIdentifier: bundleID) } else { app = XCUIApplication() + targetBundleIdentifier = nil } - candidateDisplayNames = buildCandidateDisplayNames(from: env) - // Locale for determinism app.launchArguments += ["-AppleLocale", "en_US", "-AppleLanguages", "(en)"] + // Tip: force light mode or content size if you need pixel-stable shots + // app.launchArguments += ["-uiuserInterfaceStyle", "Light"] // IMPORTANT: write to the app's sandbox, not a host path let tmp = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) - if let tag = env["CN1SS_OUTPUT_DIR"], !tag.isEmpty { + if let tag = ProcessInfo.processInfo.environment["CN1SS_OUTPUT_DIR"], !tag.isEmpty { outputDirectory = tmp.appendingPathComponent(tag, isDirectory: true) } else { outputDirectory = tmp.appendingPathComponent("cn1screens", isDirectory: true) } try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true) - print("CN1SS:INFO:ui_test_target_bundle_id=\(targetBundleIdentifier ?? "(scheme-default)")") + app.launch() + let reportedBundleId = targetBundleIdentifier ?? "(scheme-default)" + print("CN1SS:INFO:ui_test_target_bundle_id=\(reportedBundleId)") print("CN1SS:INFO:ui_test_launch_arguments=\(app.launchArguments.joined(separator: " "))") + if let resolved = resolveBundleIdentifier() { + resolvedBundleIdentifier = resolved + print("CN1SS:INFO:ui_test_resolved_bundle_id=\(resolved)") + } else { + print("CN1SS:WARN:ui_test_resolved_bundle_id_unavailable=true") + } - ensureAppLaunched() + let launchSurface = waitForCodenameOneSurface(timeout: 40, context: "post_launch") + logSurfaceMetrics(launchSurface, context: "post_launch") waitForStableFrame() + + let launchProbe = pollForRenderableContent(label: "launch_probe", timeout: 25, poll: 0.75) + if launchProbe.hasRenderableContent { + print("CN1SS:INFO:test=launch_probe rendered_content_detected=true attempts=\(launchProbe.attempts)") + } else { + print("CN1SS:WARN:test=launch_probe rendered_content_detected=false attempts=\(launchProbe.attempts)") + } } override func tearDownWithError() throws { @@ -49,104 +65,30 @@ final class HelloCodenameOneUITests: XCTestCase { app = nil } - private func ensureAppLaunched(timeout: TimeInterval = 45) { - if app.state == .runningForeground { - logLaunchState(label: "already_running") - return - } - - app.launch() - if app.state == .runningForeground { - logLaunchState(label: "launch") - return - } - - if activateViaSpringboard(deadline: Date().addingTimeInterval(timeout)) { - logLaunchState(label: "springboard") - return - } - - app.activate() - logLaunchState(label: "activate") - } - - private func activateViaSpringboard(deadline: Date) -> Bool { - let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") - springboard.activate() - - for name in candidateDisplayNames where Date() < deadline { - let icon = springboard.icons[name] - if icon.waitForExistence(timeout: 3) { - print("CN1SS:INFO:springboard_icon_tap name=\(name)") - icon.tap() - if app.wait(for: .runningForeground, timeout: 5) { - return true - } - } - } - - if Date() < deadline { - let predicate = NSPredicate(format: "label CONTAINS[c] %@", "Codename") - let fallbackIcon = springboard.icons.matching(predicate).firstMatch - if fallbackIcon.waitForExistence(timeout: 3) { - print("CN1SS:INFO:springboard_icon_fallback label=\(fallbackIcon.label)") - fallbackIcon.tap() - if app.wait(for: .runningForeground, timeout: 5) { - return true - } - } - } - return app.state == .runningForeground - } - - private func buildCandidateDisplayNames(from env: [String: String]) -> [String] { - var names: [String] = [] - if let explicit = env["CN1_AUT_APP_NAME"], !explicit.isEmpty { - names.append(explicit) - } - names.append("HelloCodenameOne") - names.append("Hello Codename One") - if let bundle = env["CN1_AUT_BUNDLE_ID"], !bundle.isEmpty { - if let suffix = bundle.split(separator: ".").last, !suffix.isEmpty { - names.append(String(suffix)) - } - } - return Array(Set(names)).sorted() - } - - private func logLaunchState(label: String) { - let state: String - switch app.state { - case .runningForeground: state = "running_foreground" - case .runningBackground: state = "running_background" - case .runningBackgroundSuspended: state = "running_background_suspended" - case .notRunning: state = "not_running" - @unknown default: state = "unknown" - } - print("CN1SS:INFO:launch_state label=\(label) state=\(state)") - if let resolved = resolveBundleIdentifier() { - print("CN1SS:INFO:ui_test_resolved_bundle_id=\(resolved)") - } else { - print("CN1SS:WARN:ui_test_resolved_bundle_id_unavailable=true") - } - } - private func captureScreenshot(named name: String) throws { - ensureAppLaunched() - waitForStableFrame() - let result = pollForRenderableContent(label: name, timeout: 30, poll: 0.6) + let surface = waitForCodenameOneSurface(timeout: 20, context: "\(name)_pre_capture") + logSurfaceMetrics(surface, context: "\(name)_pre_capture") + + let result = pollForRenderableContent(label: name, timeout: 25, poll: 0.5) let shot = result.screenshot if !result.hasRenderableContent { - print("CN1SS:WARN:test=\(name) rendered_content_not_detected attempts=\(result.attempts) luma_variance=\(result.lumaVariance)") + print("CN1SS:WARN:test=\(name) rendered_content_not_detected_after_timeout=true attempts=\(result.attempts)") + print("CN1SS:ERROR:test=\(name) codenameone_render_assertion_failed attempts=\(result.attempts)") + attachDebugDescription(name: "\(name)_ui_tree") + XCTFail("Codename One UI did not render for test \(name) after \(result.attempts) attempt(s)") } + logSurfaceMetrics(surface, context: "\(name)_post_capture") + + // Save into sandbox tmp (optional – mainly for local debugging) let pngURL = outputDirectory.appendingPathComponent("\(name).png") do { try shot.pngRepresentation.write(to: pngURL) } catch { /* ignore */ } - let attachment = XCTAttachment(screenshot: shot) - attachment.name = name - attachment.lifetime = .keepAlways - add(attachment) + // ALWAYS attach so we can export from the .xcresult + let att = XCTAttachment(screenshot: shot) + att.name = name + att.lifetime = .keepAlways + add(att) emitScreenshotPayloads(for: shot, name: name) } @@ -159,52 +101,84 @@ final class HelloCodenameOneUITests: XCTestCase { /// Tap using normalized coordinates (0...1) private func tapNormalized(_ dx: CGFloat, _ dy: CGFloat) { - let frame = app.frame - guard frame.width > 0, frame.height > 0 else { - return - } let origin = app.coordinate(withNormalizedOffset: .zero) - let target = origin.withOffset(.init(dx: frame.width * dx, dy: frame.height * dy)) + let target = origin.withOffset(.init(dx: app.frame.size.width * dx, + dy: app.frame.size.height * dy)) target.tap() } func testMainScreenScreenshot() throws { + waitForStableFrame() try captureScreenshot(named: "MainActivity") } func testBrowserComponentScreenshot() throws { + waitForStableFrame() tapNormalized(0.5, 0.70) print("CN1SS:INFO:navigation_tap=browser_screen normalized_x=0.50 normalized_y=0.70") RunLoop.current.run(until: Date(timeIntervalSinceNow: 2.0)) try captureScreenshot(named: "BrowserComponent") } - private func pollForRenderableContent(label: String, timeout: TimeInterval, poll: TimeInterval) -> (screenshot: XCUIScreenshot, hasRenderableContent: Bool, attempts: Int, lumaVariance: Int) { + private func pollForRenderableContent(label: String, timeout: TimeInterval, poll: TimeInterval) -> (screenshot: XCUIScreenshot, hasRenderableContent: Bool, attempts: Int) { let deadline = Date(timeIntervalSinceNow: timeout) var attempts = 0 - var latestVariance = 0 while true { attempts += 1 let screenshot = app.screenshot() let analysis = analyzeScreenshot(screenshot) - latestVariance = analysis.lumaVariance - if analysis.hasRenderableContent { - print("CN1SS:INFO:test=\(label) rendered_frame_detected attempt=\(attempts) luma_variance=\(analysis.lumaVariance)") - return (screenshot, true, attempts, analysis.lumaVariance) + let hasContent = analysis.hasRenderableContent + if hasContent { + print("CN1SS:INFO:test=\(label) rendered_frame_detected_attempt=\(attempts)") + return (screenshot, true, attempts) } - print("CN1SS:INFO:test=\(label) waiting_for_rendered_frame attempt=\(attempts) luma_variance=\(analysis.lumaVariance)") let now = Date() if now >= deadline { - print("CN1SS:WARN:test=\(label) rendered_content_timeout attempts=\(attempts) final_luma_variance=\(analysis.lumaVariance)") - return (screenshot, false, attempts, analysis.lumaVariance) + print("CN1SS:INFO:test=\(label) rendered_frame_luma_variance=\(analysis.lumaVariance)") + return (screenshot, false, attempts) } + print("CN1SS:INFO:test=\(label) waiting_for_rendered_frame attempt=\(attempts) luma_variance=\(analysis.lumaVariance)") let nextInterval = min(poll, deadline.timeIntervalSince(now)) RunLoop.current.run(until: Date(timeIntervalSinceNow: nextInterval)) } } + private func waitForCodenameOneSurface(timeout: TimeInterval, context: String) -> XCUIElement? { + let surface = app.otherElements[codenameOneSurfaceIdentifier] + let exists = surface.waitForExistence(timeout: timeout) + print("CN1SS:INFO:codenameone_surface_wait context=\(context) identifier=\(codenameOneSurfaceIdentifier) timeout=\(timeout) exists=\(exists)") + if exists { + return surface + } + let fallback = app.screenshot() + let screenshotAttachment = XCTAttachment(screenshot: fallback) + screenshotAttachment.name = "\(context)_missing_surface_screen" + screenshotAttachment.lifetime = .keepAlways + add(screenshotAttachment) + attachDebugDescription(name: "\(context)_missing_surface") + print("CN1SS:WARN:codenameone_surface_missing context=\(context)") + return nil + } + + private func logSurfaceMetrics(_ surface: XCUIElement?, context: String) { + guard let surface = surface else { + print("CN1SS:INFO:codenameone_surface_metrics context=\(context) frame=absent hittable=false") + return + } + let frame = surface.frame + let formatted = String(format: "x=%.1f y=%.1f width=%.1f height=%.1f", frame.origin.x, frame.origin.y, frame.size.width, frame.size.height) + print("CN1SS:INFO:codenameone_surface_metrics context=\(context) frame=\(formatted) hittable=\(surface.isHittable)") + } + + private func attachDebugDescription(name: String) { + let attachment = XCTAttachment(string: app.debugDescription) + attachment.name = name + attachment.lifetime = .keepAlways + add(attachment) + } + private func analyzeScreenshot(_ screenshot: XCUIScreenshot) -> (hasRenderableContent: Bool, lumaVariance: Int) { guard let cgImage = screenshot.image.cgImage else { return (true, 255) diff --git a/scripts/templates/HelloCodenameOne.java.tmpl b/scripts/templates/HelloCodenameOne.java.tmpl index 32656416ee..cfcfbd1afc 100644 --- a/scripts/templates/HelloCodenameOne.java.tmpl +++ b/scripts/templates/HelloCodenameOne.java.tmpl @@ -13,29 +13,61 @@ import com.codename1.ui.layouts.BoxLayout; public class @MAIN_NAME@ { private Form current; private Form mainForm; + private boolean initBootstrapScheduled; public void init(Object context) { - // No special initialization required for this sample + System.out.println("CN1SS:INFO:codenameone_init context=" + (context == null ? "null" : context.getClass().getName())); + System.out.println("CN1SS:INFO:codenameone_init_display_initialized=" + Display.isInitialized()); + if (!initBootstrapScheduled) { + initBootstrapScheduled = true; + Display.getInstance().callSerially(() -> { + System.out.println("CN1SS:INFO:codenameone_init_serial_bootstrap begin=true"); + ensureMainFormVisible("init_serial"); + }); + } } public void start() { + System.out.println("CN1SS:INFO:codenameone_start current_exists=" + (current != null)); + Display display = Display.getInstance(); + int density = display.getDeviceDensity(); + String densityBucket; + try { + densityBucket = display.getDensityStr(); + } catch (IllegalStateException ex) { + densityBucket = "unknown"; + } + double densityScale = density > 0 ? ((double) density) / Display.DENSITY_MEDIUM : 0.0; + String densityScaleStr = formatScale(densityScale); + System.out.println("CN1SS:INFO:codenameone_start_display_initialized=" + display.isInitialized() + + " display_size=" + display.getDisplayWidth() + "x" + display.getDisplayHeight() + + " device_density=" + density + + " density_bucket=" + densityBucket + + " density_scale=" + densityScaleStr); if (current != null) { + System.out.println("CN1SS:INFO:codenameone_restore_previous_form title=" + current.getTitle()); current.show(); return; } - showMainForm(); + ensureMainFormVisible("start"); } public void stop() { + System.out.println("CN1SS:INFO:codenameone_stop capturing_current_form=true"); current = Display.getInstance().getCurrent(); + if (current != null) { + System.out.println("CN1SS:INFO:codenameone_stop_form title=" + current.getTitle()); + } } public void destroy() { + System.out.println("CN1SS:INFO:codenameone_destroy invoked=true"); // Nothing to clean up for this sample } - private void showMainForm() { + private void buildMainFormIfNeeded() { if (mainForm == null) { + System.out.println("CN1SS:INFO:codenameone_build_main_form start=true"); mainForm = new Form("Main Screen", new BorderLayout()); Container content = new Container(BoxLayout.y()); @@ -52,32 +84,93 @@ public class @MAIN_NAME@ { body.getAllStyles().setFgColor(0xf9fafb); Button openBrowser = new Button("Open Browser Screen"); - openBrowser.addActionListener(evt -> showBrowserForm()); + openBrowser.addActionListener(evt -> { + System.out.println("CN1SS:INFO:codenameone_open_browser_action triggered=true"); + showBrowserForm(); + }); content.add(heading); content.add(body); content.add(openBrowser); mainForm.add(BorderLayout.CENTER, content); + System.out.println("CN1SS:INFO:codenameone_build_main_form complete=true"); } + } + + private void ensureMainFormVisible(String reason) { + buildMainFormIfNeeded(); + Display display = Display.getInstance(); + Form visible = display.getCurrent(); + if (visible != mainForm) { + current = mainForm; + mainForm.show(); + System.out.println("CN1SS:INFO:codenameone_main_form_presented reason=" + reason); + } else { + current = mainForm; + System.out.println("CN1SS:INFO:codenameone_main_form_already_visible reason=" + reason); + } + Display.getInstance().callSerially(() -> logFormMetrics("codenameone_main_form_ready", Display.getInstance().getCurrent())); + } + + private void showMainForm() { + buildMainFormIfNeeded(); current = mainForm; mainForm.show(); + System.out.println("CN1SS:INFO:codenameone_main_form_shown title=" + mainForm.getTitle()); + Display.getInstance().callSerially(() -> logFormMetrics("codenameone_main_form_ready", Display.getInstance().getCurrent())); } private void showBrowserForm() { + System.out.println("CN1SS:INFO:codenameone_build_browser_form start=true"); Form browserForm = new Form("Browser Screen", new BorderLayout()); BrowserComponent browser = new BrowserComponent(); - browser.setPage(buildBrowserHtml(), null); + String html = buildBrowserHtml(); + System.out.println("CN1SS:INFO:codenameone_browser_html_length=" + html.length()); + browser.setPage(html, null); browserForm.add(BorderLayout.CENTER, browser); browserForm.getToolbar().addMaterialCommandToLeftBar( "Back", FontImage.MATERIAL_ARROW_BACK, - evt -> showMainForm() + evt -> { + System.out.println("CN1SS:INFO:codenameone_browser_back_action triggered=true"); + ensureMainFormVisible("browser_back"); + } ); current = browserForm; browserForm.show(); + System.out.println("CN1SS:INFO:codenameone_browser_form_shown title=" + browserForm.getTitle()); + Display.getInstance().callSerially(() -> logFormMetrics("codenameone_browser_form_ready", Display.getInstance().getCurrent())); + } + + private void logFormMetrics(String label, Form form) { + if (form == null) { + System.out.println("CN1SS:WARN:" + label + " form=null"); + return; + } + System.out.println("CN1SS:INFO:" + label + + " title=" + form.getTitle() + + " width=" + form.getWidth() + + " height=" + form.getHeight() + + " component_count=" + form.getComponentCount()); + } + + private String formatScale(double scale) { + if (scale <= 0) { + return "0"; + } + int rounded = (int) (scale * 100 + 0.5); + int integerPart = rounded / 100; + int fractionPart = Math.abs(rounded % 100); + if (fractionPart == 0) { + return Integer.toString(integerPart); + } + if (fractionPart < 10) { + return integerPart + ".0" + fractionPart; + } + return integerPart + "." + fractionPart; } private String buildBrowserHtml() { From 8989d09150cb763d9b763f5b5d0ea0b9ef22d36b Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 21 Oct 2025 20:27:07 +0300 Subject: [PATCH 12/12] Revert "Ensure iOS sample renders before screenshots" This reverts commit 56a9e734a9c822af58c8c606dae0687d23e2f60a. --- .../tests/HelloCodenameOneUITests.swift.tmpl | 142 +++--------------- scripts/templates/HelloCodenameOne.java.tmpl | 105 +------------ 2 files changed, 28 insertions(+), 219 deletions(-) diff --git a/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl b/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl index 515384f5e4..fb163f2df9 100644 --- a/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl +++ b/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl @@ -5,9 +5,6 @@ import CoreGraphics final class HelloCodenameOneUITests: XCTestCase { private var app: XCUIApplication! private var outputDirectory: URL! - private var targetBundleIdentifier: String? - private var resolvedBundleIdentifier: String? - private let codenameOneSurfaceIdentifier = "cn1.glview" private let chunkSize = 2000 private let previewChannel = "PREVIEW" private let previewQualities: [CGFloat] = [0.60, 0.50, 0.40, 0.35, 0.30, 0.25, 0.20, 0.18, 0.16, 0.14, 0.12, 0.10, 0.08, 0.06, 0.05, 0.04, 0.03, 0.02, 0.01] @@ -15,13 +12,7 @@ final class HelloCodenameOneUITests: XCTestCase { override func setUpWithError() throws { continueAfterFailure = false - if let bundleID = ProcessInfo.processInfo.environment["CN1_AUT_BUNDLE_ID"], !bundleID.isEmpty { - targetBundleIdentifier = bundleID - app = XCUIApplication(bundleIdentifier: bundleID) - } else { - app = XCUIApplication() - targetBundleIdentifier = nil - } + app = XCUIApplication() // Locale for determinism app.launchArguments += ["-AppleLocale", "en_US", "-AppleLanguages", "(en)"] @@ -38,26 +29,7 @@ final class HelloCodenameOneUITests: XCTestCase { try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true) app.launch() - let reportedBundleId = targetBundleIdentifier ?? "(scheme-default)" - print("CN1SS:INFO:ui_test_target_bundle_id=\(reportedBundleId)") - print("CN1SS:INFO:ui_test_launch_arguments=\(app.launchArguments.joined(separator: " "))") - if let resolved = resolveBundleIdentifier() { - resolvedBundleIdentifier = resolved - print("CN1SS:INFO:ui_test_resolved_bundle_id=\(resolved)") - } else { - print("CN1SS:WARN:ui_test_resolved_bundle_id_unavailable=true") - } - - let launchSurface = waitForCodenameOneSurface(timeout: 40, context: "post_launch") - logSurfaceMetrics(launchSurface, context: "post_launch") waitForStableFrame() - - let launchProbe = pollForRenderableContent(label: "launch_probe", timeout: 25, poll: 0.75) - if launchProbe.hasRenderableContent { - print("CN1SS:INFO:test=launch_probe rendered_content_detected=true attempts=\(launchProbe.attempts)") - } else { - print("CN1SS:WARN:test=launch_probe rendered_content_detected=false attempts=\(launchProbe.attempts)") - } } override func tearDownWithError() throws { @@ -66,19 +38,7 @@ final class HelloCodenameOneUITests: XCTestCase { } private func captureScreenshot(named name: String) throws { - let surface = waitForCodenameOneSurface(timeout: 20, context: "\(name)_pre_capture") - logSurfaceMetrics(surface, context: "\(name)_pre_capture") - - let result = pollForRenderableContent(label: name, timeout: 25, poll: 0.5) - let shot = result.screenshot - if !result.hasRenderableContent { - print("CN1SS:WARN:test=\(name) rendered_content_not_detected_after_timeout=true attempts=\(result.attempts)") - print("CN1SS:ERROR:test=\(name) codenameone_render_assertion_failed attempts=\(result.attempts)") - attachDebugDescription(name: "\(name)_ui_tree") - XCTFail("Codename One UI did not render for test \(name) after \(result.attempts) attempt(s)") - } - - logSurfaceMetrics(surface, context: "\(name)_post_capture") + let shot = waitForRenderedScreenshot(label: name) // Save into sandbox tmp (optional – mainly for local debugging) let pngURL = outputDirectory.appendingPathComponent("\(name).png") @@ -115,78 +75,36 @@ final class HelloCodenameOneUITests: XCTestCase { func testBrowserComponentScreenshot() throws { waitForStableFrame() tapNormalized(0.5, 0.70) - print("CN1SS:INFO:navigation_tap=browser_screen normalized_x=0.50 normalized_y=0.70") + // tiny retry to allow BrowserComponent to render RunLoop.current.run(until: Date(timeIntervalSinceNow: 2.0)) try captureScreenshot(named: "BrowserComponent") } - private func pollForRenderableContent(label: String, timeout: TimeInterval, poll: TimeInterval) -> (screenshot: XCUIScreenshot, hasRenderableContent: Bool, attempts: Int) { + private func waitForRenderedScreenshot(label: String, timeout: TimeInterval = 10, poll: TimeInterval = 0.4) -> XCUIScreenshot { let deadline = Date(timeIntervalSinceNow: timeout) - var attempts = 0 - while true { - attempts += 1 - let screenshot = app.screenshot() - let analysis = analyzeScreenshot(screenshot) - let hasContent = analysis.hasRenderableContent - if hasContent { - print("CN1SS:INFO:test=\(label) rendered_frame_detected_attempt=\(attempts)") - return (screenshot, true, attempts) + RunLoop.current.run(until: Date(timeIntervalSinceNow: 1.0)) + var attempt = 0 + var screenshot = XCUIScreen.main.screenshot() + while Date() < deadline { + if screenshotHasRenderableContent(screenshot) { + return screenshot } - - let now = Date() - if now >= deadline { - print("CN1SS:INFO:test=\(label) rendered_frame_luma_variance=\(analysis.lumaVariance)") - return (screenshot, false, attempts) - } - - print("CN1SS:INFO:test=\(label) waiting_for_rendered_frame attempt=\(attempts) luma_variance=\(analysis.lumaVariance)") - let nextInterval = min(poll, deadline.timeIntervalSince(now)) - RunLoop.current.run(until: Date(timeIntervalSinceNow: nextInterval)) + attempt += 1 + print("CN1SS:INFO:test=\(label) waiting_for_rendered_frame attempt=\(attempt)") + RunLoop.current.run(until: Date(timeIntervalSinceNow: poll)) + screenshot = XCUIScreen.main.screenshot() } + return screenshot } - private func waitForCodenameOneSurface(timeout: TimeInterval, context: String) -> XCUIElement? { - let surface = app.otherElements[codenameOneSurfaceIdentifier] - let exists = surface.waitForExistence(timeout: timeout) - print("CN1SS:INFO:codenameone_surface_wait context=\(context) identifier=\(codenameOneSurfaceIdentifier) timeout=\(timeout) exists=\(exists)") - if exists { - return surface - } - let fallback = app.screenshot() - let screenshotAttachment = XCTAttachment(screenshot: fallback) - screenshotAttachment.name = "\(context)_missing_surface_screen" - screenshotAttachment.lifetime = .keepAlways - add(screenshotAttachment) - attachDebugDescription(name: "\(context)_missing_surface") - print("CN1SS:WARN:codenameone_surface_missing context=\(context)") - return nil - } - - private func logSurfaceMetrics(_ surface: XCUIElement?, context: String) { - guard let surface = surface else { - print("CN1SS:INFO:codenameone_surface_metrics context=\(context) frame=absent hittable=false") - return - } - let frame = surface.frame - let formatted = String(format: "x=%.1f y=%.1f width=%.1f height=%.1f", frame.origin.x, frame.origin.y, frame.size.width, frame.size.height) - print("CN1SS:INFO:codenameone_surface_metrics context=\(context) frame=\(formatted) hittable=\(surface.isHittable)") - } - - private func attachDebugDescription(name: String) { - let attachment = XCTAttachment(string: app.debugDescription) - attachment.name = name - attachment.lifetime = .keepAlways - add(attachment) - } - - private func analyzeScreenshot(_ screenshot: XCUIScreenshot) -> (hasRenderableContent: Bool, lumaVariance: Int) { + private func screenshotHasRenderableContent(_ screenshot: XCUIScreenshot) -> Bool { guard let cgImage = screenshot.image.cgImage else { - return (true, 255) + return true } let width = cgImage.width let height = cgImage.height guard width > 0, height > 0 else { - return (false, 0) + return false } let insetX = max(0, width / 8) @@ -198,7 +116,7 @@ final class HelloCodenameOneUITests: XCTestCase { height: max(1, height - insetY * 2) ).integral guard let cropped = cgImage.cropping(to: cropRect) else { - return (true, 255) + return true } let sampleWidth = 80 @@ -214,12 +132,12 @@ final class HelloCodenameOneUITests: XCTestCase { space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue ) else { - return (true, 255) + return true } context.interpolationQuality = .high context.draw(cropped, in: CGRect(x: 0, y: 0, width: sampleWidth, height: sampleHeight)) guard let data = context.data else { - return (true, 255) + return true } let buffer = data.bindMemory(to: UInt8.self, capacity: sampleHeight * bytesPerRow) @@ -238,23 +156,7 @@ final class HelloCodenameOneUITests: XCTestCase { } } - let variance = maxLuma - minLuma - return (variance > 12, variance) - } - - private func resolveBundleIdentifier() -> String? { - if let explicit = targetBundleIdentifier, !explicit.isEmpty { - return explicit - } - do { - let value = try app.value(forKey: "bundleID") - if let actual = value as? String, !actual.isEmpty { - return actual - } - } catch { - print("CN1SS:WARN:ui_test_bundle_resolution_failed error=\(error)") - } - return nil + return maxLuma - minLuma > 12 } private func sanitizeTestName(_ name: String) -> String { diff --git a/scripts/templates/HelloCodenameOne.java.tmpl b/scripts/templates/HelloCodenameOne.java.tmpl index cfcfbd1afc..32656416ee 100644 --- a/scripts/templates/HelloCodenameOne.java.tmpl +++ b/scripts/templates/HelloCodenameOne.java.tmpl @@ -13,61 +13,29 @@ import com.codename1.ui.layouts.BoxLayout; public class @MAIN_NAME@ { private Form current; private Form mainForm; - private boolean initBootstrapScheduled; public void init(Object context) { - System.out.println("CN1SS:INFO:codenameone_init context=" + (context == null ? "null" : context.getClass().getName())); - System.out.println("CN1SS:INFO:codenameone_init_display_initialized=" + Display.isInitialized()); - if (!initBootstrapScheduled) { - initBootstrapScheduled = true; - Display.getInstance().callSerially(() -> { - System.out.println("CN1SS:INFO:codenameone_init_serial_bootstrap begin=true"); - ensureMainFormVisible("init_serial"); - }); - } + // No special initialization required for this sample } public void start() { - System.out.println("CN1SS:INFO:codenameone_start current_exists=" + (current != null)); - Display display = Display.getInstance(); - int density = display.getDeviceDensity(); - String densityBucket; - try { - densityBucket = display.getDensityStr(); - } catch (IllegalStateException ex) { - densityBucket = "unknown"; - } - double densityScale = density > 0 ? ((double) density) / Display.DENSITY_MEDIUM : 0.0; - String densityScaleStr = formatScale(densityScale); - System.out.println("CN1SS:INFO:codenameone_start_display_initialized=" + display.isInitialized() - + " display_size=" + display.getDisplayWidth() + "x" + display.getDisplayHeight() - + " device_density=" + density - + " density_bucket=" + densityBucket - + " density_scale=" + densityScaleStr); if (current != null) { - System.out.println("CN1SS:INFO:codenameone_restore_previous_form title=" + current.getTitle()); current.show(); return; } - ensureMainFormVisible("start"); + showMainForm(); } public void stop() { - System.out.println("CN1SS:INFO:codenameone_stop capturing_current_form=true"); current = Display.getInstance().getCurrent(); - if (current != null) { - System.out.println("CN1SS:INFO:codenameone_stop_form title=" + current.getTitle()); - } } public void destroy() { - System.out.println("CN1SS:INFO:codenameone_destroy invoked=true"); // Nothing to clean up for this sample } - private void buildMainFormIfNeeded() { + private void showMainForm() { if (mainForm == null) { - System.out.println("CN1SS:INFO:codenameone_build_main_form start=true"); mainForm = new Form("Main Screen", new BorderLayout()); Container content = new Container(BoxLayout.y()); @@ -84,93 +52,32 @@ public class @MAIN_NAME@ { body.getAllStyles().setFgColor(0xf9fafb); Button openBrowser = new Button("Open Browser Screen"); - openBrowser.addActionListener(evt -> { - System.out.println("CN1SS:INFO:codenameone_open_browser_action triggered=true"); - showBrowserForm(); - }); + openBrowser.addActionListener(evt -> showBrowserForm()); content.add(heading); content.add(body); content.add(openBrowser); mainForm.add(BorderLayout.CENTER, content); - System.out.println("CN1SS:INFO:codenameone_build_main_form complete=true"); } - } - - private void ensureMainFormVisible(String reason) { - buildMainFormIfNeeded(); - Display display = Display.getInstance(); - Form visible = display.getCurrent(); - if (visible != mainForm) { - current = mainForm; - mainForm.show(); - System.out.println("CN1SS:INFO:codenameone_main_form_presented reason=" + reason); - } else { - current = mainForm; - System.out.println("CN1SS:INFO:codenameone_main_form_already_visible reason=" + reason); - } - Display.getInstance().callSerially(() -> logFormMetrics("codenameone_main_form_ready", Display.getInstance().getCurrent())); - } - - private void showMainForm() { - buildMainFormIfNeeded(); current = mainForm; mainForm.show(); - System.out.println("CN1SS:INFO:codenameone_main_form_shown title=" + mainForm.getTitle()); - Display.getInstance().callSerially(() -> logFormMetrics("codenameone_main_form_ready", Display.getInstance().getCurrent())); } private void showBrowserForm() { - System.out.println("CN1SS:INFO:codenameone_build_browser_form start=true"); Form browserForm = new Form("Browser Screen", new BorderLayout()); BrowserComponent browser = new BrowserComponent(); - String html = buildBrowserHtml(); - System.out.println("CN1SS:INFO:codenameone_browser_html_length=" + html.length()); - browser.setPage(html, null); + browser.setPage(buildBrowserHtml(), null); browserForm.add(BorderLayout.CENTER, browser); browserForm.getToolbar().addMaterialCommandToLeftBar( "Back", FontImage.MATERIAL_ARROW_BACK, - evt -> { - System.out.println("CN1SS:INFO:codenameone_browser_back_action triggered=true"); - ensureMainFormVisible("browser_back"); - } + evt -> showMainForm() ); current = browserForm; browserForm.show(); - System.out.println("CN1SS:INFO:codenameone_browser_form_shown title=" + browserForm.getTitle()); - Display.getInstance().callSerially(() -> logFormMetrics("codenameone_browser_form_ready", Display.getInstance().getCurrent())); - } - - private void logFormMetrics(String label, Form form) { - if (form == null) { - System.out.println("CN1SS:WARN:" + label + " form=null"); - return; - } - System.out.println("CN1SS:INFO:" + label - + " title=" + form.getTitle() - + " width=" + form.getWidth() - + " height=" + form.getHeight() - + " component_count=" + form.getComponentCount()); - } - - private String formatScale(double scale) { - if (scale <= 0) { - return "0"; - } - int rounded = (int) (scale * 100 + 0.5); - int integerPart = rounded / 100; - int fractionPart = Math.abs(rounded % 100); - if (fractionPart == 0) { - return Integer.toString(integerPart); - } - if (fractionPart < 10) { - return integerPart + ".0" + fractionPart; - } - return integerPart + "." + fractionPart; } private String buildBrowserHtml() {