Skip to content

Commit 56a9e73

Browse files
committed
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
1 parent f53a843 commit 56a9e73

File tree

2 files changed

+219
-28
lines changed

2 files changed

+219
-28
lines changed

scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl

Lines changed: 120 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,23 @@ import CoreGraphics
55
final 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 {

scripts/templates/HelloCodenameOne.java.tmpl

Lines changed: 99 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,29 +13,61 @@ import com.codename1.ui.layouts.BoxLayout;
1313
public class @MAIN_NAME@ {
1414
private Form current;
1515
private Form mainForm;
16+
private boolean initBootstrapScheduled;
1617

1718
public void init(Object context) {
18-
// No special initialization required for this sample
19+
System.out.println("CN1SS:INFO:codenameone_init context=" + (context == null ? "null" : context.getClass().getName()));
20+
System.out.println("CN1SS:INFO:codenameone_init_display_initialized=" + Display.isInitialized());
21+
if (!initBootstrapScheduled) {
22+
initBootstrapScheduled = true;
23+
Display.getInstance().callSerially(() -> {
24+
System.out.println("CN1SS:INFO:codenameone_init_serial_bootstrap begin=true");
25+
ensureMainFormVisible("init_serial");
26+
});
27+
}
1928
}
2029

2130
public void start() {
31+
System.out.println("CN1SS:INFO:codenameone_start current_exists=" + (current != null));
32+
Display display = Display.getInstance();
33+
int density = display.getDeviceDensity();
34+
String densityBucket;
35+
try {
36+
densityBucket = display.getDensityStr();
37+
} catch (IllegalStateException ex) {
38+
densityBucket = "unknown";
39+
}
40+
double densityScale = density > 0 ? ((double) density) / Display.DENSITY_MEDIUM : 0.0;
41+
String densityScaleStr = formatScale(densityScale);
42+
System.out.println("CN1SS:INFO:codenameone_start_display_initialized=" + display.isInitialized()
43+
+ " display_size=" + display.getDisplayWidth() + "x" + display.getDisplayHeight()
44+
+ " device_density=" + density
45+
+ " density_bucket=" + densityBucket
46+
+ " density_scale=" + densityScaleStr);
2247
if (current != null) {
48+
System.out.println("CN1SS:INFO:codenameone_restore_previous_form title=" + current.getTitle());
2349
current.show();
2450
return;
2551
}
26-
showMainForm();
52+
ensureMainFormVisible("start");
2753
}
2854

2955
public void stop() {
56+
System.out.println("CN1SS:INFO:codenameone_stop capturing_current_form=true");
3057
current = Display.getInstance().getCurrent();
58+
if (current != null) {
59+
System.out.println("CN1SS:INFO:codenameone_stop_form title=" + current.getTitle());
60+
}
3161
}
3262

3363
public void destroy() {
64+
System.out.println("CN1SS:INFO:codenameone_destroy invoked=true");
3465
// Nothing to clean up for this sample
3566
}
3667

37-
private void showMainForm() {
68+
private void buildMainFormIfNeeded() {
3869
if (mainForm == null) {
70+
System.out.println("CN1SS:INFO:codenameone_build_main_form start=true");
3971
mainForm = new Form("Main Screen", new BorderLayout());
4072

4173
Container content = new Container(BoxLayout.y());
@@ -52,32 +84,93 @@ public class @MAIN_NAME@ {
5284
body.getAllStyles().setFgColor(0xf9fafb);
5385

5486
Button openBrowser = new Button("Open Browser Screen");
55-
openBrowser.addActionListener(evt -> showBrowserForm());
87+
openBrowser.addActionListener(evt -> {
88+
System.out.println("CN1SS:INFO:codenameone_open_browser_action triggered=true");
89+
showBrowserForm();
90+
});
5691

5792
content.add(heading);
5893
content.add(body);
5994
content.add(openBrowser);
6095

6196
mainForm.add(BorderLayout.CENTER, content);
97+
System.out.println("CN1SS:INFO:codenameone_build_main_form complete=true");
6298
}
99+
}
100+
101+
private void ensureMainFormVisible(String reason) {
102+
buildMainFormIfNeeded();
103+
Display display = Display.getInstance();
104+
Form visible = display.getCurrent();
105+
if (visible != mainForm) {
106+
current = mainForm;
107+
mainForm.show();
108+
System.out.println("CN1SS:INFO:codenameone_main_form_presented reason=" + reason);
109+
} else {
110+
current = mainForm;
111+
System.out.println("CN1SS:INFO:codenameone_main_form_already_visible reason=" + reason);
112+
}
113+
Display.getInstance().callSerially(() -> logFormMetrics("codenameone_main_form_ready", Display.getInstance().getCurrent()));
114+
}
115+
116+
private void showMainForm() {
117+
buildMainFormIfNeeded();
63118
current = mainForm;
64119
mainForm.show();
120+
System.out.println("CN1SS:INFO:codenameone_main_form_shown title=" + mainForm.getTitle());
121+
Display.getInstance().callSerially(() -> logFormMetrics("codenameone_main_form_ready", Display.getInstance().getCurrent()));
65122
}
66123

67124
private void showBrowserForm() {
125+
System.out.println("CN1SS:INFO:codenameone_build_browser_form start=true");
68126
Form browserForm = new Form("Browser Screen", new BorderLayout());
69127

70128
BrowserComponent browser = new BrowserComponent();
71-
browser.setPage(buildBrowserHtml(), null);
129+
String html = buildBrowserHtml();
130+
System.out.println("CN1SS:INFO:codenameone_browser_html_length=" + html.length());
131+
browser.setPage(html, null);
72132
browserForm.add(BorderLayout.CENTER, browser);
73133
browserForm.getToolbar().addMaterialCommandToLeftBar(
74134
"Back",
75135
FontImage.MATERIAL_ARROW_BACK,
76-
evt -> showMainForm()
136+
evt -> {
137+
System.out.println("CN1SS:INFO:codenameone_browser_back_action triggered=true");
138+
ensureMainFormVisible("browser_back");
139+
}
77140
);
78141

79142
current = browserForm;
80143
browserForm.show();
144+
System.out.println("CN1SS:INFO:codenameone_browser_form_shown title=" + browserForm.getTitle());
145+
Display.getInstance().callSerially(() -> logFormMetrics("codenameone_browser_form_ready", Display.getInstance().getCurrent()));
146+
}
147+
148+
private void logFormMetrics(String label, Form form) {
149+
if (form == null) {
150+
System.out.println("CN1SS:WARN:" + label + " form=null");
151+
return;
152+
}
153+
System.out.println("CN1SS:INFO:" + label
154+
+ " title=" + form.getTitle()
155+
+ " width=" + form.getWidth()
156+
+ " height=" + form.getHeight()
157+
+ " component_count=" + form.getComponentCount());
158+
}
159+
160+
private String formatScale(double scale) {
161+
if (scale <= 0) {
162+
return "0";
163+
}
164+
int rounded = (int) (scale * 100 + 0.5);
165+
int integerPart = rounded / 100;
166+
int fractionPart = Math.abs(rounded % 100);
167+
if (fractionPart == 0) {
168+
return Integer.toString(integerPart);
169+
}
170+
if (fractionPart < 10) {
171+
return integerPart + ".0" + fractionPart;
172+
}
173+
return integerPart + "." + fractionPart;
81174
}
82175

83176
private String buildBrowserHtml() {

0 commit comments

Comments
 (0)