@@ -5,14 +5,23 @@ import CoreGraphics
55final class HelloCodenameOneUITests: XCTestCase {
66 private var app: XCUIApplication!
77 private var outputDirectory: URL!
8+ private var targetBundleIdentifier: String?
9+ private var resolvedBundleIdentifier: String?
10+ private let codenameOneSurfaceIdentifier = "cn1.glview"
811 private let chunkSize = 2000
912 private let previewChannel = "PREVIEW"
1013 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]
1114 private let maxPreviewBytes = 20 * 1024
1215
1316 override func setUpWithError() throws {
1417 continueAfterFailure = false
15- app = XCUIApplication()
18+ if let bundleID = ProcessInfo.processInfo.environment["CN1_AUT_BUNDLE_ID"], !bundleID.isEmpty {
19+ targetBundleIdentifier = bundleID
20+ app = XCUIApplication(bundleIdentifier: bundleID)
21+ } else {
22+ app = XCUIApplication()
23+ targetBundleIdentifier = nil
24+ }
1625
1726 // Locale for determinism
1827 app.launchArguments += ["-AppleLocale", "en_US", "-AppleLanguages", "(en)"]
@@ -29,7 +38,26 @@ final class HelloCodenameOneUITests: XCTestCase {
2938 try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true)
3039
3140 app.launch()
41+ let reportedBundleId = targetBundleIdentifier ?? "(scheme-default)"
42+ print("CN1SS:INFO:ui_test_target_bundle_id=\(reportedBundleId)")
43+ print("CN1SS:INFO:ui_test_launch_arguments=\(app.launchArguments.joined(separator: " "))")
44+ if let resolved = resolveBundleIdentifier() {
45+ resolvedBundleIdentifier = resolved
46+ print("CN1SS:INFO:ui_test_resolved_bundle_id=\(resolved)")
47+ } else {
48+ print("CN1SS:WARN:ui_test_resolved_bundle_id_unavailable=true")
49+ }
50+
51+ let launchSurface = waitForCodenameOneSurface(timeout: 40, context: "post_launch")
52+ logSurfaceMetrics(launchSurface, context: "post_launch")
3253 waitForStableFrame()
54+
55+ let launchProbe = pollForRenderableContent(label: "launch_probe", timeout: 25, poll: 0.75)
56+ if launchProbe.hasRenderableContent {
57+ print("CN1SS:INFO:test=launch_probe rendered_content_detected=true attempts=\(launchProbe.attempts)")
58+ } else {
59+ print("CN1SS:WARN:test=launch_probe rendered_content_detected=false attempts=\(launchProbe.attempts)")
60+ }
3361 }
3462
3563 override func tearDownWithError() throws {
@@ -38,7 +66,19 @@ final class HelloCodenameOneUITests: XCTestCase {
3866 }
3967
4068 private func captureScreenshot(named name: String) throws {
41- let shot = waitForRenderedScreenshot(label: name)
69+ let surface = waitForCodenameOneSurface(timeout: 20, context: "\(name)_pre_capture")
70+ logSurfaceMetrics(surface, context: "\(name)_pre_capture")
71+
72+ let result = pollForRenderableContent(label: name, timeout: 25, poll: 0.5)
73+ let shot = result.screenshot
74+ if !result.hasRenderableContent {
75+ print("CN1SS:WARN:test=\(name) rendered_content_not_detected_after_timeout=true attempts=\(result.attempts)")
76+ print("CN1SS:ERROR:test=\(name) codenameone_render_assertion_failed attempts=\(result.attempts)")
77+ attachDebugDescription(name: "\(name)_ui_tree")
78+ XCTFail("Codename One UI did not render for test \(name) after \(result.attempts) attempt(s)")
79+ }
80+
81+ logSurfaceMetrics(surface, context: "\(name)_post_capture")
4282
4383 // Save into sandbox tmp (optional – mainly for local debugging)
4484 let pngURL = outputDirectory.appendingPathComponent("\(name).png")
@@ -75,36 +115,78 @@ final class HelloCodenameOneUITests: XCTestCase {
75115 func testBrowserComponentScreenshot() throws {
76116 waitForStableFrame()
77117 tapNormalized(0.5, 0.70)
78- // tiny retry to allow BrowserComponent to render
118+ print("CN1SS:INFO:navigation_tap=browser_screen normalized_x=0.50 normalized_y=0.70")
79119 RunLoop.current.run(until: Date(timeIntervalSinceNow: 2.0))
80120 try captureScreenshot(named: "BrowserComponent")
81121 }
82122
83- private func waitForRenderedScreenshot (label: String, timeout: TimeInterval = 10 , poll: TimeInterval = 0.4 ) -> XCUIScreenshot {
123+ private func pollForRenderableContent (label: String, timeout: TimeInterval, poll: TimeInterval) -> (screenshot: XCUIScreenshot, hasRenderableContent: Bool, attempts: Int) {
84124 let deadline = Date(timeIntervalSinceNow: timeout)
85- RunLoop.current.run(until: Date(timeIntervalSinceNow: 1.0))
86- var attempt = 0
87- var screenshot = XCUIScreen.main.screenshot()
88- while Date() < deadline {
89- if screenshotHasRenderableContent(screenshot) {
90- return screenshot
125+ var attempts = 0
126+ while true {
127+ attempts += 1
128+ let screenshot = app.screenshot()
129+ let analysis = analyzeScreenshot(screenshot)
130+ let hasContent = analysis.hasRenderableContent
131+ if hasContent {
132+ print("CN1SS:INFO:test=\(label) rendered_frame_detected_attempt=\(attempts)")
133+ return (screenshot, true, attempts)
91134 }
92- attempt += 1
93- print("CN1SS:INFO:test=\(label) waiting_for_rendered_frame attempt=\(attempt)")
94- RunLoop.current.run(until: Date(timeIntervalSinceNow: poll))
95- screenshot = XCUIScreen.main.screenshot()
135+
136+ let now = Date()
137+ if now >= deadline {
138+ print("CN1SS:INFO:test=\(label) rendered_frame_luma_variance=\(analysis.lumaVariance)")
139+ return (screenshot, false, attempts)
140+ }
141+
142+ print("CN1SS:INFO:test=\(label) waiting_for_rendered_frame attempt=\(attempts) luma_variance=\(analysis.lumaVariance)")
143+ let nextInterval = min(poll, deadline.timeIntervalSince(now))
144+ RunLoop.current.run(until: Date(timeIntervalSinceNow: nextInterval))
96145 }
97- return screenshot
98146 }
99147
100- private func screenshotHasRenderableContent(_ screenshot: XCUIScreenshot) -> Bool {
148+ private func waitForCodenameOneSurface(timeout: TimeInterval, context: String) -> XCUIElement? {
149+ let surface = app.otherElements[codenameOneSurfaceIdentifier]
150+ let exists = surface.waitForExistence(timeout: timeout)
151+ print("CN1SS:INFO:codenameone_surface_wait context=\(context) identifier=\(codenameOneSurfaceIdentifier) timeout=\(timeout) exists=\(exists)")
152+ if exists {
153+ return surface
154+ }
155+ let fallback = app.screenshot()
156+ let screenshotAttachment = XCTAttachment(screenshot: fallback)
157+ screenshotAttachment.name = "\(context)_missing_surface_screen"
158+ screenshotAttachment.lifetime = .keepAlways
159+ add(screenshotAttachment)
160+ attachDebugDescription(name: "\(context)_missing_surface")
161+ print("CN1SS:WARN:codenameone_surface_missing context=\(context)")
162+ return nil
163+ }
164+
165+ private func logSurfaceMetrics(_ surface: XCUIElement?, context: String) {
166+ guard let surface = surface else {
167+ print("CN1SS:INFO:codenameone_surface_metrics context=\(context) frame=absent hittable=false")
168+ return
169+ }
170+ let frame = surface.frame
171+ let formatted = String(format: "x=%.1f y=%.1f width=%.1f height=%.1f", frame.origin.x, frame.origin.y, frame.size.width, frame.size.height)
172+ print("CN1SS:INFO:codenameone_surface_metrics context=\(context) frame=\(formatted) hittable=\(surface.isHittable)")
173+ }
174+
175+ private func attachDebugDescription(name: String) {
176+ let attachment = XCTAttachment(string: app.debugDescription)
177+ attachment.name = name
178+ attachment.lifetime = .keepAlways
179+ add(attachment)
180+ }
181+
182+ private func analyzeScreenshot(_ screenshot: XCUIScreenshot) -> (hasRenderableContent: Bool, lumaVariance: Int) {
101183 guard let cgImage = screenshot.image.cgImage else {
102- return true
184+ return ( true, 255)
103185 }
104186 let width = cgImage.width
105187 let height = cgImage.height
106188 guard width > 0, height > 0 else {
107- return false
189+ return ( false, 0)
108190 }
109191
110192 let insetX = max(0, width / 8)
@@ -116,7 +198,7 @@ final class HelloCodenameOneUITests: XCTestCase {
116198 height: max(1, height - insetY * 2)
117199 ).integral
118200 guard let cropped = cgImage.cropping(to: cropRect) else {
119- return true
201+ return ( true, 255)
120202 }
121203
122204 let sampleWidth = 80
@@ -132,12 +214,12 @@ final class HelloCodenameOneUITests: XCTestCase {
132214 space: CGColorSpaceCreateDeviceRGB(),
133215 bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
134216 ) else {
135- return true
217+ return ( true, 255)
136218 }
137219 context.interpolationQuality = .high
138220 context.draw(cropped, in: CGRect(x: 0, y: 0, width: sampleWidth, height: sampleHeight))
139221 guard let data = context.data else {
140- return true
222+ return ( true, 255)
141223 }
142224
143225 let buffer = data.bindMemory(to: UInt8.self, capacity: sampleHeight * bytesPerRow)
@@ -156,7 +238,23 @@ final class HelloCodenameOneUITests: XCTestCase {
156238 }
157239 }
158240
159- return maxLuma - minLuma > 12
241+ let variance = maxLuma - minLuma
242+ return (variance > 12, variance)
243+ }
244+
245+ private func resolveBundleIdentifier() -> String? {
246+ if let explicit = targetBundleIdentifier, !explicit.isEmpty {
247+ return explicit
248+ }
249+ do {
250+ let value = try app.value(forKey: "bundleID")
251+ if let actual = value as? String, !actual.isEmpty {
252+ return actual
253+ }
254+ } catch {
255+ print("CN1SS:WARN:ui_test_bundle_resolution_failed error=\(error)")
256+ }
257+ return nil
160258 }
161259
162260 private func sanitizeTestName(_ name: String) -> String {
0 commit comments