Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
54ac002
Launch Codename One app via simctl before iOS UI screenshots
shai-almog Oct 21, 2025
264b3fe
Apply locale arguments to simctl launch
shai-almog Oct 22, 2025
4c30112
Port iOS UI test harness to Objective-C
shai-almog Oct 22, 2025
58f6363
Link UIKit into Objective-C UI test harness
shai-almog Oct 22, 2025
f3242f8
Fix UIKit framework reference for UITests
shai-almog Oct 22, 2025
4963e4b
Stabilize iOS UI harness and log Codename One startup
shai-almog Oct 22, 2025
5b140ba
Stabilize iOS UITest rendering detection
shai-almog Oct 22, 2025
cfe1bd1
Fix UIKit framework source tree configuration
shai-almog Oct 22, 2025
06a4374
Fix UIKit framework metadata in build ios script
shai-almog Oct 22, 2025
a95f3d7
Link CoreGraphics for iOS UITest harness
shai-almog Oct 22, 2025
145fb1f
Tighten iOS screenshot readiness detection and surface CN1 lifecycle …
shai-almog Oct 22, 2025
9dea24f
Align iOS UITest launch configuration
shai-almog Oct 23, 2025
a70aeb1
Ensure UITest defaults to Codename One bundle
shai-almog Oct 23, 2025
530fb37
Set deterministic iOS bundle id for UITests
shai-almog Oct 23, 2025
6955741
Adjust iOS UITest bundle selection
shai-almog Oct 23, 2025
66c654d
Fallback to simctl screenshots when XCUI captures are blank
shai-almog Oct 23, 2025
25dedf8
Revert "Fallback to simctl screenshots when XCUI captures are blank"
shai-almog Oct 24, 2025
b959135
Revert "Adjust iOS UITest bundle selection"
shai-almog Oct 24, 2025
df21dd3
Revert "Set deterministic iOS bundle id for UITests"
shai-almog Oct 24, 2025
6bdd4a5
Revert "Ensure UITest defaults to Codename One bundle"
shai-almog Oct 24, 2025
50c4691
Revert "Align iOS UITest launch configuration"
shai-almog Oct 24, 2025
356080b
Revert "Tighten iOS screenshot readiness detection and surface CN1 li…
shai-almog Oct 24, 2025
82c9b56
Merge branch 'master' into codex/fix-blank-screenshots-for-ios-jc5pqm
shai-almog Oct 24, 2025
80ff295
Another attempt at fixing the workflow
shai-almog Oct 24, 2025
eeef12a
Fixed python code
shai-almog Oct 24, 2025
9ef5dfc
Fixed syntax errors
shai-almog Oct 24, 2025
4af56ef
Missing fi
shai-almog Oct 24, 2025
a6b24e6
Dunno
shai-almog Oct 24, 2025
8be844a
Script issues
shai-almog Oct 24, 2025
6557193
Forcefully luanching Codename One in the CI test
shai-almog Oct 25, 2025
c0b969c
Fixed to use the stub
shai-almog Oct 25, 2025
e1f3997
Let's see...
shai-almog Oct 25, 2025
4b4970f
Increased timeout for debugging logic
shai-almog Oct 25, 2025
efaf442
Fixes
shai-almog Oct 25, 2025
be96aec
Fing timeouts
shai-almog Oct 25, 2025
eb41b8c
Merge branch 'master' into codex/fix-blank-screenshots-for-ios-jc5pqm
shai-almog Oct 25, 2025
b14f558
No idea
shai-almog Oct 25, 2025
1a7feb5
Ugh
shai-almog Oct 25, 2025
b5276a3
Fingers crossed
shai-almog Oct 25, 2025
576f7d5
Again
shai-almog Oct 25, 2025
c6d9a20
Fixed argument
shai-almog Oct 25, 2025
ff8c443
Comeon...
shai-almog Oct 25, 2025
eebd7f2
Ugh
shai-almog Oct 26, 2025
5b00dba
Minor fix
shai-almog Oct 26, 2025
f15f604
Fixed c&p code
shai-almog Oct 26, 2025
ff3749e
Removed asserts
shai-almog Oct 26, 2025
b1e0f94
Close?
shai-almog Oct 26, 2025
8b4ff93
Again
shai-almog Oct 26, 2025
7395b9c
Ugh
shai-almog Oct 26, 2025
621107c
OK...
shai-almog Oct 26, 2025
93522ea
Again
shai-almog Oct 26, 2025
b7d2f8f
Is it close?
shai-almog Oct 26, 2025
feeb972
Merge branch 'master' into codex/fix-blank-screenshots-for-ios-jc5pqm
shai-almog Oct 27, 2025
4659ab3
It wasn't
shai-almog Oct 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/scripts-ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
pull-requests: write
issues: write
runs-on: macos-15 # pinning macos-15 avoids surprises during the cutover window
timeout-minutes: 60 # allow enough time for dependency installs and full build
timeout-minutes: 100 # allow enough time for dependency installs and full build
concurrency: # ensure only one mac build runs at once
group: mac-ci-${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: true
Expand Down Expand Up @@ -93,7 +93,7 @@ jobs:
- name: Build sample iOS app and compile workspace
id: build-ios-app
run: ./scripts/build-ios-app.sh -q -DskipTests
timeout-minutes: 30
timeout-minutes: 40

- name: Run iOS UI screenshot tests
env:
Expand All @@ -107,9 +107,9 @@ jobs:

./scripts/run-ios-ui-tests.sh \
"${{ steps.build-ios-app.outputs.workspace }}" \
"" \
"${{ steps.build-ios-app.outputs.app_bundle }}" \
"${{ steps.build-ios-app.outputs.scheme }}"
timeout-minutes: 30
timeout-minutes: 60

- name: Upload iOS artifacts
if: always()
Expand Down
8 changes: 7 additions & 1 deletion scripts/android/tests/PostPrComment.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -324,7 +325,12 @@ private static Map<String, String> 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;
Expand Down
42 changes: 33 additions & 9 deletions scripts/build-ios-app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,13 @@ fi
bia_log "Found generated iOS project at $PROJECT_DIR"

# --- Ensure a real UITest source file exists on disk ---
UITEST_TEMPLATE="$SCRIPT_DIR/ios/tests/HelloCodenameOneUITests.swift.tmpl"
UITEST_TEMPLATE="$SCRIPT_DIR/ios/tests/HelloCodenameOneUITests.m.tmpl"
UITEST_DIR="$PROJECT_DIR/HelloCodenameOneUITests"
UITEST_SWIFT="$UITEST_DIR/HelloCodenameOneUITests.swift"
UITEST_SOURCE="$UITEST_DIR/HelloCodenameOneUITests.m"
if [ -f "$UITEST_TEMPLATE" ]; then
mkdir -p "$UITEST_DIR"
cp -f "$UITEST_TEMPLATE" "$UITEST_SWIFT"
bia_log "Installed UITest source: $UITEST_SWIFT"
cp -f "$UITEST_TEMPLATE" "$UITEST_SOURCE"
bia_log "Installed UITest source: $UITEST_SOURCE"
else
bia_log "UITest template missing at $UITEST_TEMPLATE"; exit 1
fi
Expand Down Expand Up @@ -233,13 +233,37 @@ end
# Ensure a group and file reference exist, then add to the UITest target
proj_dir = File.dirname(proj_path)
ui_dir = File.join(proj_dir, ui_name)
ui_file = File.join(ui_dir, "#{ui_name}.swift")
ui_file = File.join(ui_dir, "#{ui_name}.m")
ui_group = proj.main_group.find_subpath(ui_name, true)
ui_group.set_source_tree("<group>")
file_ref = ui_group.files.find { |f| File.expand_path(f.path, proj_dir) == ui_file }
file_ref ||= ui_group.new_file(ui_file)
ui_target.add_file_references([file_ref]) unless ui_target.source_build_phase.files_references.include?(file_ref)

# Ensure required system frameworks (e.g. UIKit for UIImage helpers) are linked
frameworks_group = proj.frameworks_group || proj.main_group.find_subpath("Frameworks", true)
frameworks_group.set_source_tree("<group>") if frameworks_group.respond_to?(:set_source_tree)
{
"UIKit.framework" => "System/Library/Frameworks/UIKit.framework",
"CoreGraphics.framework" => "System/Library/Frameworks/CoreGraphics.framework"
}.each do |name, path|
ref = frameworks_group.files.find do |f|
f.path == name || f.path == path ||
(f.respond_to?(:real_path) && File.expand_path(f.real_path.to_s) == File.expand_path(path, "/"))
end
unless ref
ref = frameworks_group.new_reference(path)
end
ref.name = name if ref.respond_to?(:name=)
ref.set_source_tree("SDKROOT") if ref.respond_to?(:set_source_tree)
ref.path = path if ref.respond_to?(:path=)
ref.last_known_file_type = "wrapper.framework" if ref.respond_to?(:last_known_file_type=)
phase = ui_target.frameworks_build_phase
unless phase.files_references.include?(ref)
phase.add_file_reference(ref)
end
end

#
# Required settings so Xcode creates a non-empty .xctest and a proper "-Runner.app"
# PRODUCT_NAME feeds the bundle name; TEST_TARGET_NAME feeds the runner name.
Expand All @@ -249,15 +273,13 @@ ui_target.add_file_references([file_ref]) unless ui_target.source_build_phase.fi
xc = ui_target.build_configuration_list[cfg]
next unless xc
bs = xc.build_settings
bs["SWIFT_VERSION"] = "5.0"
bs["GENERATE_INFOPLIST_FILE"] = "YES"
bs["CODE_SIGNING_ALLOWED"] = "NO"
bs["CODE_SIGNING_REQUIRED"] = "NO"
bs["PRODUCT_BUNDLE_IDENTIFIER"] ||= "com.codenameone.examples.uitests"
bs["PRODUCT_NAME"] ||= ui_name
bs["TEST_TARGET_NAME"] ||= app_target&.name || "HelloCodenameOne"
# Optional but harmless on simulators; avoids other edge cases:
bs["ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES"] = "YES"
bs["TARGETED_DEVICE_FAMILY"] ||= "1,2"
end

Expand Down Expand Up @@ -345,7 +367,9 @@ bia_log "Found xcworkspace: $WORKSPACE"

SCHEME="${MAIN_NAME}-CI"

# Make these visible to the next GH Actions step
# Best-effort: locate a simulator .app under the generated project (may be empty here,
# because the runner does the actual build-for-testing).
APP_BUNDLE_PATH="$(/bin/ls -1d "$PROJECT_DIR"/**/Build/Products/Debug-iphonesimulator/*.app 2>/dev/null | head -n1 || true)"
if [ -n "${GITHUB_OUTPUT:-}" ]; then
{
echo "workspace=$WORKSPACE"
Expand All @@ -360,4 +384,4 @@ ARTIFACTS_DIR="${ARTIFACTS_DIR:-$REPO_ROOT/artifacts}"
mkdir -p "$ARTIFACTS_DIR"
xcodebuild -workspace "$WORKSPACE" -list > "$ARTIFACTS_DIR/xcodebuild-list.txt" 2>&1 || true

exit 0
exit 0
195 changes: 195 additions & 0 deletions scripts/ios/tests/HelloCodenameOneUITests.m.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// HelloCodenameOneUITests.m.tmpl
// Objective-C, no modules required

#import <XCTest/XCTest.h>
#import <UIKit/UIKit.h>

@interface HelloCodenameOneUITests : XCTestCase
@end

@implementation HelloCodenameOneUITests {
XCUIApplication *_app;
}

#pragma mark - Setup/Teardown (non-failable)

- (void)setUp {
[super setUp];
self.continueAfterFailure = YES;

NSDictionary *env = NSProcessInfo.processInfo.environment;
NSLog(@"CN1SS:INFO:env=%@", env);

NSString *bundleID = env[@"CN1_AUT_BUNDLE_ID"];
if (bundleID.length == 0) {
// Last resort: your normal AUT id
bundleID = @"com.codenameone.examples";
}
NSLog(@"CN1SS:INFO:ui_test_target_bundle_id=%@", bundleID);
_app = [[XCUIApplication alloc] initWithBundleIdentifier:bundleID];

// Keep iOS from restoring previous state and force English locale.
_app.launchArguments = @[
@"-AppleLocale", @"en_US",
@"-AppleLanguages", @"(en)",
@"-ApplePersistenceIgnoreState", @"YES",
@"--cn1-test-mode", @"1"
];

// Pre-launch snapshot (may be SpringBoard)
[self cn1_saveScreenPreferApp:@"pre_launch"];

NSLog(@"CN1SS:INFO:launch:start args=%@", _app.launchArguments);
[_app launch];
[_app activate];
[self cn1_waitForeground:_app timeout:45.0 step:0.5 label:@"post_launch"];

NSLog(@"CN1SS:INFO:state_after_activation=%ld exists=%d",
(long)_app.state, _app.exists ? 1 : 0);

// As a last resort, terminate and relaunch once (no SpringBoard)
if (_app.state != XCUIApplicationStateRunningForeground) {
@try { [_app terminate]; } @catch (__unused NSException *e) {}
[self cn1_saveScreenPreferApp:@"pre_relaunch"];
[_app launch];
[_app activate];
[self cn1_waitForeground:_app timeout:45.0 step:0.5 label:@"post_relaunch"];
// One extra nudge if still not foreground
if (_app.state != XCUIApplicationStateRunningForeground) {
@try { [_app terminate]; } @catch (__unused NSException *e) {}
[_app launch];
[_app activate];
[self cn1_waitForeground:_app timeout:20.0 step:0.5 label:@"post_second_relaunch"];
}
}
}

- (void)tearDown {
@try { [_app terminate]; } @catch (__unused NSException *e) {}
_app = nil;
[super tearDown];
}

#pragma mark - Single smoke test (no assertions)

- (void)testSmokeLaunchAndScreenshot {
@try {
[self cn1_emitScreenshotNamed:@"MainActivity" app:_app];
NSLog(@"CN1SS:INFO:final_app_state=%ld exists=%d",
(long)_app.state, _app.exists ? 1 : 0);
} @catch (__unused NSException *e) {
NSLog(@"CN1SS:WARN:testSmokeLaunchAndScreenshot caught exception; continuing");
}
// No asserts — we only emit CN1SS output for the Java side to parse.
}

#pragma mark - CN1SS emission

- (void)cn1_emitScreenshotNamed:(NSString *)name app:(XCUIApplication *)app {
// SAFETY: avoid failable app.screenshot if app isn't ready
XCUIScreenshot *shot = XCUIScreen.mainScreen.screenshot;
if (!shot && app && app.exists && app.state == XCUIApplicationStateRunningForeground) {
@try { shot = app.screenshot; } @catch (__unused NSException *e) { shot = nil; }
}
if (!shot) { NSLog(@"CN1SS:WARN:test=%@ no_screenshot", name); return; }

NSData *png = shot.PNGRepresentation;
if (png.length == 0) { NSLog(@"CN1SS:WARN:test=%@ empty_png", name); return; }

[self cn1ssEmitChannel:@"" name:name bytes:png];

UIImage *img = [UIImage imageWithData:png];
if (img) {
NSData *jpeg = UIImageJPEGRepresentation(img, 0.12);
if (jpeg.length > 0) [self cn1ssEmitChannel:@"PREVIEW" name:name bytes:jpeg];
}

@try {
XCTAttachment *att = [XCTAttachment attachmentWithUniformTypeIdentifier:@"public.png"
name:name
payload:png
userInfo:nil];
att.lifetime = XCTAttachmentLifetimeKeepAlways;
[self addAttachment:att];
} @catch (__unused NSException *e) {}

[self cn1_saveScreenPreferApp:[NSString stringWithFormat:@"attach_%@", name]];
}

- (void)cn1ssEmitChannel:(NSString *)channel name:(NSString *)name bytes:(NSData *)bytes {
if (bytes.length == 0) return;
NSString *prefix = channel.length ? [@"CN1SS" stringByAppendingString:channel] : @"CN1SS";
NSString *b64 = [bytes base64EncodedStringWithOptions:0];

const NSUInteger chunk = 2000;
NSUInteger pos = 0, chunks = 0;
while (pos < b64.length) {
@autoreleasepool {
NSUInteger len = MIN(chunk, b64.length - pos);
NSString *seg = [b64 substringWithRange:NSMakeRange(pos, len)];
printf("%s:%s:%06lu:%s\n",
prefix.UTF8String, name.UTF8String,
(unsigned long)pos, seg.UTF8String);
pos += len; chunks += 1;
}
}
printf("CN1SS:END:%s\n", name.UTF8String);
printf("CN1SS:INFO:test=%s chunks=%lu total_b64_len=%lu\n",
name.UTF8String, (unsigned long)chunks, (unsigned long)b64.length);
}

#pragma mark - Snapshots & waits (prefer app image)

- (void)cn1_saveScreenPreferApp:(NSString *)name {
XCUIScreenshot *shot = XCUIScreen.mainScreen.screenshot;
if (!shot && _app && _app.exists && _app.state == XCUIApplicationStateRunningForeground) {
@try { shot = _app.screenshot; } @catch (__unused NSException *e) { shot = nil; }
}
if (!shot) return;

NSData *png = shot.PNGRepresentation;
if (png.length == 0) return;

NSString *tmp = NSTemporaryDirectory(); if (tmp.length == 0) tmp = @"/tmp";
NSString *dir = [tmp stringByAppendingPathComponent:@"cn1screens"];
[[NSFileManager defaultManager] createDirectoryAtPath:dir
withIntermediateDirectories:YES
attributes:nil
error:nil];
NSString *path = [dir stringByAppendingPathComponent:[name stringByAppendingString:@".png"]];
[png writeToFile:path atomically:NO];
NSLog(@"CN1SS:INFO:saved_screenshot name=%@ path=%@", name, path);
}

- (void)cn1_waitForeground:(XCUIApplication *)app
timeout:(NSTimeInterval)timeout
step:(NSTimeInterval)step
label:(NSString *)label
{
NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:timeout];
NSUInteger attempt = 0;

while ([[NSDate date] compare:deadline] == NSOrderedAscending) {
attempt++;
XCUIApplicationState state = app.state;
NSLog(@"CN1SS:INFO:launch_state attempt=%lu state=%ld",
(unsigned long)attempt, (long)state);

if (state == XCUIApplicationStateRunningForeground) {
[self cn1_saveScreenPreferApp:[NSString stringWithFormat:@"%@_foreground_%lu",
label, (unsigned long)attempt]];
return;
} else {
[self cn1_saveScreenPreferApp:[NSString stringWithFormat:@"%@_state_%lu",
label, (unsigned long)attempt]];
}

// Nudge: re-activate periodically in case of transient backgrounding
if ((attempt % 8) == 0) { @try { [app activate]; } @catch (__unused NSException *e) {} }

[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:step]];
}
NSLog(@"CN1SS:WARN:%@_timeout state=%ld", label, (long)app.state);
}

@end
Loading
Loading