Skip to content

Commit ce3db3f

Browse files
committed
Restore sample app template and relaunch UI test via Springboard
1 parent 56a9e73 commit ce3db3f

File tree

2 files changed

+119
-186
lines changed

2 files changed

+119
-186
lines changed

scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl

Lines changed: 113 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -6,89 +6,147 @@ final class HelloCodenameOneUITests: XCTestCase {
66
private var app: XCUIApplication!
77
private var outputDirectory: URL!
88
private var targetBundleIdentifier: String?
9-
private var resolvedBundleIdentifier: String?
10-
private let codenameOneSurfaceIdentifier = "cn1.glview"
9+
private var candidateDisplayNames: [String] = []
1110
private let chunkSize = 2000
1211
private let previewChannel = "PREVIEW"
1312
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]
1413
private let maxPreviewBytes = 20 * 1024
1514

1615
override func setUpWithError() throws {
1716
continueAfterFailure = false
18-
if let bundleID = ProcessInfo.processInfo.environment["CN1_AUT_BUNDLE_ID"], !bundleID.isEmpty {
17+
let env = ProcessInfo.processInfo.environment
18+
19+
if let bundleID = env["CN1_AUT_BUNDLE_ID"], !bundleID.isEmpty {
1920
targetBundleIdentifier = bundleID
2021
app = XCUIApplication(bundleIdentifier: bundleID)
2122
} else {
2223
app = XCUIApplication()
23-
targetBundleIdentifier = nil
2424
}
2525

26+
candidateDisplayNames = buildCandidateDisplayNames(from: env)
27+
2628
// Locale for determinism
2729
app.launchArguments += ["-AppleLocale", "en_US", "-AppleLanguages", "(en)"]
28-
// Tip: force light mode or content size if you need pixel-stable shots
29-
// app.launchArguments += ["-uiuserInterfaceStyle", "Light"]
3030

3131
// IMPORTANT: write to the app's sandbox, not a host path
3232
let tmp = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
33-
if let tag = ProcessInfo.processInfo.environment["CN1SS_OUTPUT_DIR"], !tag.isEmpty {
33+
if let tag = env["CN1SS_OUTPUT_DIR"], !tag.isEmpty {
3434
outputDirectory = tmp.appendingPathComponent(tag, isDirectory: true)
3535
} else {
3636
outputDirectory = tmp.appendingPathComponent("cn1screens", isDirectory: true)
3737
}
3838
try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true)
3939

40-
app.launch()
41-
let reportedBundleId = targetBundleIdentifier ?? "(scheme-default)"
42-
print("CN1SS:INFO:ui_test_target_bundle_id=\(reportedBundleId)")
40+
print("CN1SS:INFO:ui_test_target_bundle_id=\(targetBundleIdentifier ?? "(scheme-default)")")
4341
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-
}
5042

51-
let launchSurface = waitForCodenameOneSurface(timeout: 40, context: "post_launch")
52-
logSurfaceMetrics(launchSurface, context: "post_launch")
43+
ensureAppLaunched()
5344
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-
}
6145
}
6246

6347
override func tearDownWithError() throws {
6448
app?.terminate()
6549
app = nil
6650
}
6751

68-
private func captureScreenshot(named name: String) throws {
69-
let surface = waitForCodenameOneSurface(timeout: 20, context: "\(name)_pre_capture")
70-
logSurfaceMetrics(surface, context: "\(name)_pre_capture")
52+
private func ensureAppLaunched(timeout: TimeInterval = 45) {
53+
if app.state == .runningForeground {
54+
logLaunchState(label: "already_running")
55+
return
56+
}
57+
58+
app.launch()
59+
if app.state == .runningForeground {
60+
logLaunchState(label: "launch")
61+
return
62+
}
63+
64+
if activateViaSpringboard(deadline: Date().addingTimeInterval(timeout)) {
65+
logLaunchState(label: "springboard")
66+
return
67+
}
68+
69+
app.activate()
70+
logLaunchState(label: "activate")
71+
}
72+
73+
private func activateViaSpringboard(deadline: Date) -> Bool {
74+
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
75+
springboard.activate()
76+
77+
for name in candidateDisplayNames where Date() < deadline {
78+
let icon = springboard.icons[name]
79+
if icon.waitForExistence(timeout: 3) {
80+
print("CN1SS:INFO:springboard_icon_tap name=\(name)")
81+
icon.tap()
82+
if app.wait(for: .runningForeground, timeout: 5) {
83+
return true
84+
}
85+
}
86+
}
87+
88+
if Date() < deadline {
89+
let predicate = NSPredicate(format: "label CONTAINS[c] %@", "Codename")
90+
let fallbackIcon = springboard.icons.matching(predicate).firstMatch
91+
if fallbackIcon.waitForExistence(timeout: 3) {
92+
print("CN1SS:INFO:springboard_icon_fallback label=\(fallbackIcon.label)")
93+
fallbackIcon.tap()
94+
if app.wait(for: .runningForeground, timeout: 5) {
95+
return true
96+
}
97+
}
98+
}
99+
return app.state == .runningForeground
100+
}
101+
102+
private func buildCandidateDisplayNames(from env: [String: String]) -> [String] {
103+
var names: [String] = []
104+
if let explicit = env["CN1_AUT_APP_NAME"], !explicit.isEmpty {
105+
names.append(explicit)
106+
}
107+
names.append("HelloCodenameOne")
108+
names.append("Hello Codename One")
109+
if let bundle = env["CN1_AUT_BUNDLE_ID"], !bundle.isEmpty {
110+
if let suffix = bundle.split(separator: ".").last, !suffix.isEmpty {
111+
names.append(String(suffix))
112+
}
113+
}
114+
return Array(Set(names)).sorted()
115+
}
116+
117+
private func logLaunchState(label: String) {
118+
let state: String
119+
switch app.state {
120+
case .runningForeground: state = "running_foreground"
121+
case .runningBackground: state = "running_background"
122+
case .runningBackgroundSuspended: state = "running_background_suspended"
123+
case .notRunning: state = "not_running"
124+
@unknown default: state = "unknown"
125+
}
126+
print("CN1SS:INFO:launch_state label=\(label) state=\(state)")
127+
if let resolved = resolveBundleIdentifier() {
128+
print("CN1SS:INFO:ui_test_resolved_bundle_id=\(resolved)")
129+
} else {
130+
print("CN1SS:WARN:ui_test_resolved_bundle_id_unavailable=true")
131+
}
132+
}
71133

72-
let result = pollForRenderableContent(label: name, timeout: 25, poll: 0.5)
134+
private func captureScreenshot(named name: String) throws {
135+
ensureAppLaunched()
136+
waitForStableFrame()
137+
let result = pollForRenderableContent(label: name, timeout: 30, poll: 0.6)
73138
let shot = result.screenshot
74139
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)")
140+
print("CN1SS:WARN:test=\(name) rendered_content_not_detected attempts=\(result.attempts) luma_variance=\(result.lumaVariance)")
79141
}
80142

81-
logSurfaceMetrics(surface, context: "\(name)_post_capture")
82-
83-
// Save into sandbox tmp (optional – mainly for local debugging)
84143
let pngURL = outputDirectory.appendingPathComponent("\(name).png")
85144
do { try shot.pngRepresentation.write(to: pngURL) } catch { /* ignore */ }
86145

87-
// ALWAYS attach so we can export from the .xcresult
88-
let att = XCTAttachment(screenshot: shot)
89-
att.name = name
90-
att.lifetime = .keepAlways
91-
add(att)
146+
let attachment = XCTAttachment(screenshot: shot)
147+
attachment.name = name
148+
attachment.lifetime = .keepAlways
149+
add(attachment)
92150

93151
emitScreenshotPayloads(for: shot, name: name)
94152
}
@@ -101,84 +159,52 @@ final class HelloCodenameOneUITests: XCTestCase {
101159

102160
/// Tap using normalized coordinates (0...1)
103161
private func tapNormalized(_ dx: CGFloat, _ dy: CGFloat) {
162+
let frame = app.frame
163+
guard frame.width > 0, frame.height > 0 else {
164+
return
165+
}
104166
let origin = app.coordinate(withNormalizedOffset: .zero)
105-
let target = origin.withOffset(.init(dx: app.frame.size.width * dx,
106-
dy: app.frame.size.height * dy))
167+
let target = origin.withOffset(.init(dx: frame.width * dx, dy: frame.height * dy))
107168
target.tap()
108169
}
109170

110171
func testMainScreenScreenshot() throws {
111-
waitForStableFrame()
112172
try captureScreenshot(named: "MainActivity")
113173
}
114174

115175
func testBrowserComponentScreenshot() throws {
116-
waitForStableFrame()
117176
tapNormalized(0.5, 0.70)
118177
print("CN1SS:INFO:navigation_tap=browser_screen normalized_x=0.50 normalized_y=0.70")
119178
RunLoop.current.run(until: Date(timeIntervalSinceNow: 2.0))
120179
try captureScreenshot(named: "BrowserComponent")
121180
}
122181

123-
private func pollForRenderableContent(label: String, timeout: TimeInterval, poll: TimeInterval) -> (screenshot: XCUIScreenshot, hasRenderableContent: Bool, attempts: Int) {
182+
private func pollForRenderableContent(label: String, timeout: TimeInterval, poll: TimeInterval) -> (screenshot: XCUIScreenshot, hasRenderableContent: Bool, attempts: Int, lumaVariance: Int) {
124183
let deadline = Date(timeIntervalSinceNow: timeout)
125184
var attempts = 0
185+
var latestVariance = 0
126186
while true {
127187
attempts += 1
128188
let screenshot = app.screenshot()
129189
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)
190+
latestVariance = analysis.lumaVariance
191+
if analysis.hasRenderableContent {
192+
print("CN1SS:INFO:test=\(label) rendered_frame_detected attempt=\(attempts) luma_variance=\(analysis.lumaVariance)")
193+
return (screenshot, true, attempts, analysis.lumaVariance)
134194
}
135195

196+
print("CN1SS:INFO:test=\(label) waiting_for_rendered_frame attempt=\(attempts) luma_variance=\(analysis.lumaVariance)")
136197
let now = Date()
137198
if now >= deadline {
138-
print("CN1SS:INFO:test=\(label) rendered_frame_luma_variance=\(analysis.lumaVariance)")
139-
return (screenshot, false, attempts)
199+
print("CN1SS:WARN:test=\(label) rendered_content_timeout attempts=\(attempts) final_luma_variance=\(analysis.lumaVariance)")
200+
return (screenshot, false, attempts, analysis.lumaVariance)
140201
}
141202

142-
print("CN1SS:INFO:test=\(label) waiting_for_rendered_frame attempt=\(attempts) luma_variance=\(analysis.lumaVariance)")
143203
let nextInterval = min(poll, deadline.timeIntervalSince(now))
144204
RunLoop.current.run(until: Date(timeIntervalSinceNow: nextInterval))
145205
}
146206
}
147207

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-
182208
private func analyzeScreenshot(_ screenshot: XCUIScreenshot) -> (hasRenderableContent: Bool, lumaVariance: Int) {
183209
guard let cgImage = screenshot.image.cgImage else {
184210
return (true, 255)

0 commit comments

Comments
 (0)