Skip to content

Commit ed9b30f

Browse files
authored
feat(ci): ios integration tests (#1)
Use actions to run ios integration tests on iphone simulators from MacOS's Xcode
1 parent 8bfe3a6 commit ed9b30f

14 files changed

Lines changed: 890 additions & 33 deletions

File tree

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
name: iOS Platform Integration
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
8+
9+
permissions:
10+
contents: read
11+
12+
concurrency:
13+
group: ${{ github.workflow }}-${{ github.ref }}
14+
cancel-in-progress: true
15+
16+
jobs:
17+
ios:
18+
name: iOS
19+
runs-on: macos-latest
20+
env:
21+
MISE_EXEC_AUTO_INSTALL: "false"
22+
MISE_NOT_FOUND_AUTO_INSTALL: "false"
23+
24+
steps:
25+
- name: Checkout
26+
uses: actions/checkout@v6
27+
28+
- name: Install Node
29+
uses: jdx/mise-action@v3
30+
with:
31+
install: true
32+
install_args: node
33+
cache: true
34+
35+
- name: Setup Godot
36+
uses: chickensoft-games/setup-godot@v2
37+
with:
38+
version: 4.6.2
39+
use-dotnet: true
40+
include-templates: true
41+
42+
- name: Setup Xcode version
43+
uses: maxim-lobanov/setup-xcode@v1
44+
with:
45+
xcode-version: latest-stable
46+
47+
- name: Install XcodeGen, Task, SCons and MoltenVK
48+
run: brew install xcodegen task scons molten-vk
49+
50+
- name: Download and Prepare Godot Source Headers
51+
run: |
52+
curl -LO https://github.com/godotengine/godot/archive/refs/tags/4.6.2-stable.tar.gz
53+
tar -xzf 4.6.2-stable.tar.gz
54+
mv godot-4.6.2-stable godot
55+
56+
cd godot
57+
58+
MOLTENVK_PATH="$(brew --prefix molten-vk)"
59+
echo "Using MoltenVK from: ${MOLTENVK_PATH}"
60+
61+
scons platform=macos target=editor \
62+
vulkan_sdk_path="${MOLTENVK_PATH}" \
63+
core/version_generated.gen.h \
64+
core/extension/gdextension_interface.gen.h
65+
66+
- name: Enable Corepack
67+
run: mise x -- corepack enable pnpm
68+
69+
- name: Get pnpm store path
70+
id: pnpm-store
71+
run: echo "path=$(mise x -- corepack pnpm store path --silent)" >> "$GITHUB_OUTPUT"
72+
73+
- name: Cache pnpm store
74+
uses: actions/cache@v5
75+
with:
76+
path: ${{ steps.pnpm-store.outputs.path }}
77+
key: ${{ runner.os }}-${{ runner.arch }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
78+
restore-keys: |
79+
${{ runner.os }}-${{ runner.arch }}-pnpm-store-
80+
81+
- name: Install pnpm dependencies
82+
run: mise x -- corepack pnpm install --frozen-lockfile
83+
84+
- name: Build integration App
85+
run: mise x -- bash -c 'GODOT_BIN="${GODOT:-godot}" scripts/build_integration_ios.sh'
86+
87+
- name: Start iOS Simulator
88+
run: |
89+
DEVICE_ID=$(xcrun simctl list devices available | grep -m 1 "iPhone 16" | awk -F '[()]' '{print $2}')
90+
91+
echo "Booting device ID: $DEVICE_ID"
92+
xcrun simctl boot "$DEVICE_ID"
93+
xcrun simctl bootstatus "$DEVICE_ID"
94+
echo "SIMULATOR_ID=$DEVICE_ID" >> $GITHUB_ENV
95+
96+
- name: Run integration tests
97+
run: |
98+
xcrun simctl install $SIMULATOR_ID dist/integration/ios_debug.app
99+
100+
scripts/run_integration_ios_test.sh ipc_round_trip_probe
101+
scripts/run_integration_ios_test.sh webview_lifecycle_probe
102+
scripts/run_integration_ios_test.sh res_asset_loading_probe

docs/platform-integration-tests.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,10 @@ contract directly:
135135
For tests that do not care about asset loading, the GDScript case reads
136136
`probe.html` and injects it with `load_html_string(...)`. This keeps the test
137137
case in Godot while preserving HTML/JavaScript syntax highlighting in the
138-
fixture file.
138+
fixture file. HTML-string tests pass the probe name through an injected
139+
`globalThis.__kirieProbeName` value instead of deriving it from
140+
`location.search`, because WebKit's `loadHTMLString(_:baseURL:)` treats
141+
`baseURL` as the URL for resolving relative references.
139142

140143
For tests that do care about exported resources, the case loads
141144
`res://web/probe.html?...` with `load_url(...)`.

packages/kirie/native/ios/Kirie/Sources/Kirie/KirieManager.swift

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ private extension Notification.Name {
1212
final class KirieManager: NSObject {
1313
static let shared = KirieManager()
1414

15+
private static let hostWindowRetryDelay: TimeInterval = 0.1
16+
private static let maxHostWindowResolveAttempts = 50
17+
1518
private let notificationCenter = NotificationCenter.default
1619
private let sessionID = UUID().uuidString.lowercased()
1720
private let resourceURLSchemeHandler = KirieResourceURLSchemeHandler()
@@ -24,19 +27,45 @@ final class KirieManager: NSObject {
2427
}
2528

2629
func createWebView(initialURL: String?) {
27-
logInfo("createWebView initialURL=\(initialURL ?? "<nil>")")
30+
createWebView(initialURL: initialURL, remainingHostWindowAttempts: Self.maxHostWindowResolveAttempts)
31+
}
32+
33+
private func createWebView(initialURL: String?, remainingHostWindowAttempts: Int) {
34+
logInfo(
35+
"createWebView initialURL=\(initialURL ?? "<nil>") "
36+
+ "remainingHostWindowAttempts=\(remainingHostWindowAttempts)"
37+
)
2838

2939
guard let hostWindow = resolveHostWindow() else {
40+
if remainingHostWindowAttempts > 0 {
41+
logInfo("No active host window yet; retrying WebView creation")
42+
DispatchQueue.main.asyncAfter(deadline: .now() + Self.hostWindowRetryDelay) { [weak self] in
43+
self?.createWebView(
44+
initialURL: initialURL,
45+
remainingHostWindowAttempts: remainingHostWindowAttempts - 1
46+
)
47+
}
48+
return
49+
}
50+
3051
emitIpcError("Cannot create WebView because no host window was found")
3152
return
3253
}
3354

3455
let containerView = ensureContainerView(attachedTo: hostWindow)
3556
let webView = ensureWebView(attachedTo: containerView)
36-
emitWebViewReady()
57+
hostWindow.layoutIfNeeded()
58+
59+
DispatchQueue.main.async { [weak self, weak webView] in
60+
guard let self, let webView, webView === self.webView else {
61+
return
62+
}
3763

38-
if let initialURL, !initialURL.isEmpty {
39-
load(initialURL, in: webView)
64+
self.emitWebViewReady()
65+
66+
if let initialURL, !initialURL.isEmpty {
67+
self.load(initialURL, in: webView)
68+
}
4069
}
4170
}
4271

@@ -200,9 +229,7 @@ final class KirieManager: NSObject {
200229
private func resolveHostWindow() -> UIWindow? {
201230
let activeScenes = UIApplication.shared.connectedScenes
202231
.compactMap { $0 as? UIWindowScene }
203-
.filter { scene in
204-
scene.activationState == .foregroundActive || scene.activationState == .foregroundInactive
205-
}
232+
.filter { $0.activationState == .foregroundActive }
206233

207234
for scene in activeScenes {
208235
if let keyWindow = scene.windows.first(where: \.isKeyWindow) {

packages/kirie/native/ios/Kirie/Sources/Kirie/KiriePlugin.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class KiriePlugin : public Object {
2424
void loadUrl(String url);
2525
void loadHtmlString(String html, String base_url);
2626
void sendIpcMessage(String message_json);
27+
String getLaunchOption(String key);
2728

2829
virtual Variant callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override;
2930

packages/kirie/native/ios/Kirie/Sources/Kirie/KiriePlugin.mm

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,32 @@ static void call_callback(const Callable &callback, const String &value) {
8585
kirie_swift_send_ipc_message(encoded_message_json.get_data());
8686
}
8787

88+
String KiriePlugin::getLaunchOption(String key) {
89+
CharString encoded_key = key.utf8();
90+
NSString *underscore_key = [NSString stringWithUTF8String:encoded_key.get_data()];
91+
NSString *dash_key = [underscore_key stringByReplacingOccurrencesOfString:@"_" withString:@"-"];
92+
NSArray<NSString *> *arguments = [[NSProcessInfo processInfo] arguments];
93+
94+
for (NSUInteger index = 0; index < arguments.count; index++) {
95+
NSString *argument = arguments[index];
96+
NSArray<NSString *> *option_names = @[ underscore_key, dash_key ];
97+
98+
for (NSString *option_name in option_names) {
99+
NSString *prefix = [NSString stringWithFormat:@"--%@=", option_name];
100+
if ([argument hasPrefix:prefix]) {
101+
return to_godot_string([argument substringFromIndex:prefix.length]);
102+
}
103+
104+
if ([argument isEqualToString:[NSString stringWithFormat:@"--%@", option_name]]
105+
&& index + 1 < arguments.count) {
106+
return to_godot_string(arguments[index + 1]);
107+
}
108+
}
109+
}
110+
111+
return String();
112+
}
113+
88114
KiriePlugin *KiriePlugin::get_singleton() {
89115
return singleton;
90116
}
@@ -146,6 +172,14 @@ static void call_callback(const Callable &callback, const String &value) {
146172
return Variant();
147173
}
148174

175+
if (p_method == StringName("getLaunchOption")) {
176+
if (!require_arg_count(r_error, p_argcount, 1)) {
177+
return Variant();
178+
}
179+
180+
return getLaunchOption(String(*p_args[0]));
181+
}
182+
149183
return Object::callp(p_method, p_args, p_argcount, r_error);
150184
}
151185

packages/kirie/native/ios/Kirie/project.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@ targets:
3030
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD: NO
3131
CLANG_CXX_LANGUAGE_STANDARD: gnu++17
3232
HEADER_SEARCH_PATHS:
33+
- "$(inherited)"
3334
- "$(GODOT_SOURCE_ROOT)"
35+
- "$(GODOT_SOURCE_ROOT)/core"
3436
- "$(GODOT_SOURCE_ROOT)/platform/ios"
35-
- "$(GODOT_SOURCE_ROOT)/bin/obj/platform/ios"
37+
- "$(SRCROOT)/../../../../../godot"
3638
dependencies:
3739
- sdk: Foundation.framework
3840
- sdk: UIKit.framework

0 commit comments

Comments
 (0)