Skip to content

Commit fc2d6f4

Browse files
authored
[NT-3070] iOS UI tests in CI (#259)
* [NT-3070] Wire ios-sdk into the pnpm implementation runner with build and run scripts * [NT-3070] Add iOS UI test build and matrix run jobs to the main pipeline * Update runs-on to namespace-profile-macos-apple-silicon-arm64-6-cpu-14-gb for ios ui tests * Update runs-on to namespace-profile-macos-apple-silicon-arm64-6-cpu-14-gb for ios ui tests for e2e root * [NT-3070] Fix iOS UI test runner xctestrun glob and propagate pipe exit code * [NT-3070] Assert iOS xctestrun exists and enable pipefail in CI test step * [NT-3070] Make weak JSContext bindings mutable to satisfy the Swift compiler
1 parent d5663a8 commit fc2d6f4

4 files changed

Lines changed: 179 additions & 2 deletions

File tree

.github/workflows/main-pipeline.yaml

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ jobs:
2626
e2e_web_sdk_react: ${{ steps.filter.outputs.e2e_web_sdk_react }}
2727
e2e_react_web_sdk: ${{ steps.filter.outputs.e2e_react_web_sdk }}
2828
e2e_react_native_android: ${{ steps.filter.outputs.e2e_react_native_android }}
29+
e2e_ios: ${{ steps.filter.outputs.e2e_ios }}
2930
steps:
3031
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
3132

@@ -125,6 +126,14 @@ jobs:
125126
- 'package.json'
126127
- 'pnpm-lock.yaml'
127128
- '.github/workflows/main-pipeline.yaml'
129+
# iOS native implementation E2E coverage scope.
130+
e2e_ios:
131+
- 'implementations/ios-sdk/**'
132+
- 'lib/mocks/**'
133+
- 'packages/ios/**'
134+
- 'package.json'
135+
- 'pnpm-lock.yaml'
136+
- '.github/workflows/main-pipeline.yaml'
128137
129138
setup:
130139
name: 🛠️ pnpm install
@@ -650,6 +659,57 @@ jobs:
650659
if-no-files-found: error
651660
retention-days: 1
652661

662+
e2e-ios-sdk-build:
663+
name: 🍎 Build iOS UI Test Bundles
664+
runs-on: namespace-profile-macos-apple-silicon-arm64-6-cpu-14-gb
665+
timeout-minutes: 30
666+
needs: [setup, changes]
667+
if: needs.changes.outputs.e2e_ios == 'true'
668+
env:
669+
DERIVED_DATA: /tmp/optimization-ios-derived-data
670+
steps:
671+
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
672+
673+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
674+
with:
675+
node-version-file: '.nvmrc'
676+
package-manager-cache: false
677+
678+
- uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3
679+
680+
- name: Install XcodeGen and xcbeautify
681+
run: brew install xcodegen xcbeautify
682+
683+
- name: Set up caches (Namespace)
684+
uses: namespacelabs/nscloud-cache-action@15799a6b54e5765f85b2aac25b3f0df43ed571c0 # v1.4.3
685+
with:
686+
cache: pnpm
687+
path: |
688+
~/Library/Caches/org.swift.swiftpm
689+
690+
- name: Show toolchain
691+
run: |
692+
xcodebuild -version
693+
xcrun simctl list runtimes | head
694+
695+
- run: pnpm install --prefer-offline --frozen-lockfile
696+
697+
- name: Build iOS UI test bundles (SwiftUI + UIKit)
698+
run: pnpm run implementation:ios-sdk -- test:e2e:ios:build:release
699+
700+
- name: Stage Build/Products for artifact
701+
run: |
702+
mkdir -p /tmp/ios-artifact
703+
cp -R "$DERIVED_DATA/Build/Products" /tmp/ios-artifact/Products
704+
ls /tmp/ios-artifact/Products/*.xctestrun
705+
706+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
707+
with:
708+
name: ios-uitest-bundles
709+
path: /tmp/ios-artifact/
710+
if-no-files-found: error
711+
retention-days: 1
712+
653713
e2e-react-native-android:
654714
name: 📱 E2E React Native Android (shard ${{ matrix.shard }}/2)
655715
runs-on: namespace-profile-linux-16-vcpu-32-gb-ram-optimal
@@ -808,3 +868,107 @@ jobs:
808868
implementations/react-native-sdk/.detox/
809869
/tmp/mock-server.log
810870
retention-days: 7
871+
872+
e2e-ios-sdk:
873+
name: 🍎 E2E iOS UI (${{ matrix.scheme }})
874+
runs-on: namespace-profile-macos-apple-silicon-arm64-6-cpu-14-gb
875+
timeout-minutes: 45
876+
needs: [setup, changes, e2e-ios-sdk-build]
877+
if: needs.changes.outputs.e2e_ios == 'true'
878+
strategy:
879+
fail-fast: false
880+
matrix:
881+
include:
882+
- scheme: SwiftUI
883+
- scheme: UIKit
884+
env:
885+
DERIVED_DATA: /tmp/optimization-ios-derived-data
886+
IOS_SCHEME: ${{ matrix.scheme }}
887+
IOS_SIM_NAME: 'iPhone 16'
888+
IOS_SIM_OS: 'latest'
889+
# Smoke mode for the first PR — restrict to one test class per scheme.
890+
# Remove this env var in a follow-up PR to enable the full suite.
891+
IOS_ONLY_TESTING: OptimizationAppUITests${{ matrix.scheme }}/PreviewPanelTests
892+
steps:
893+
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
894+
895+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
896+
with:
897+
node-version-file: '.nvmrc'
898+
package-manager-cache: false
899+
900+
- uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3
901+
902+
- name: Install xcbeautify
903+
run: brew install xcbeautify
904+
905+
- uses: namespacelabs/nscloud-cache-action@15799a6b54e5765f85b2aac25b3f0df43ed571c0 # v1.4.3
906+
with:
907+
cache: pnpm
908+
909+
- run: pnpm install --prefer-offline --frozen-lockfile
910+
911+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
912+
with:
913+
name: ios-uitest-bundles
914+
path: /tmp/ios-artifact/
915+
916+
- name: Reconstruct DerivedData layout at stable path
917+
run: |
918+
mkdir -p "$DERIVED_DATA/Build"
919+
mv /tmp/ios-artifact/Products "$DERIVED_DATA/Build/Products"
920+
ls "$DERIVED_DATA/Build/Products"
921+
922+
- name: Boot iOS Simulator
923+
run: |
924+
DEVICE_UDID=$(xcrun simctl create "ci-${{ matrix.scheme }}" "$IOS_SIM_NAME")
925+
echo "DEVICE_UDID=$DEVICE_UDID" >> "$GITHUB_ENV"
926+
xcrun simctl boot "$DEVICE_UDID"
927+
xcrun simctl bootstatus "$DEVICE_UDID" -b
928+
929+
- name: Start Mock Server
930+
run: |
931+
pnpm --dir lib/mocks serve > /tmp/mock-server.log 2>&1 &
932+
echo $! > /tmp/mock-server.pid
933+
for i in {1..60}; do
934+
if nc -z localhost 8000 2>/dev/null; then
935+
echo "Mock server is ready"
936+
break
937+
fi
938+
echo "Waiting for mock server... ($i/60)"
939+
sleep 1
940+
done
941+
if ! nc -z localhost 8000 2>/dev/null; then
942+
echo "Mock server failed to start:"
943+
cat /tmp/mock-server.log
944+
exit 1
945+
fi
946+
947+
- name: Verify built xctestrun exists for ${{ matrix.scheme }}
948+
shell: 'bash -eo pipefail {0}'
949+
run: |
950+
shopt -s nullglob
951+
matches=("$DERIVED_DATA"/Build/Products/OptimizationApp"$IOS_SCHEME"_*.xctestrun)
952+
if [ ${#matches[@]} -eq 0 ]; then
953+
echo "No xctestrun found for scheme $IOS_SCHEME under $DERIVED_DATA/Build/Products/" >&2
954+
ls -la "$DERIVED_DATA/Build/Products/" || true
955+
exit 1
956+
fi
957+
printf 'Found xctestrun: %s\n' "${matches[@]}"
958+
959+
- name: Run iOS UI tests (${{ matrix.scheme }})
960+
shell: 'bash -eo pipefail {0}'
961+
run: pnpm run implementation:ios-sdk -- test:e2e:ios:run:release
962+
963+
- name: Stop Mock Server
964+
if: always()
965+
run: kill $(cat /tmp/mock-server.pid) 2>/dev/null || true
966+
967+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
968+
if: ${{ !cancelled() }}
969+
with:
970+
name: ci-results-ios-${{ matrix.scheme }}
971+
path: |
972+
/tmp/optimization-ios-derived-data/Test-*.xcresult
973+
/tmp/mock-server.log
974+
retention-days: 7
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "@implementation/ios-sdk",
3+
"version": "0.0.0",
4+
"private": true,
5+
"scripts": {
6+
"xcodegen": "xcodegen generate",
7+
"test:e2e:ios:build:release": "set -o pipefail && pnpm run xcodegen && xcodebuild build-for-testing -project OptimizationApp.xcodeproj -scheme OptimizationAppSwiftUI -configuration Release -destination 'generic/platform=iOS Simulator' -derivedDataPath /tmp/optimization-ios-derived-data CODE_SIGNING_ALLOWED=NO COMPILER_INDEX_STORE_ENABLE=NO | xcbeautify && xcodebuild build-for-testing -project OptimizationApp.xcodeproj -scheme OptimizationAppUIKit -configuration Release -destination 'generic/platform=iOS Simulator' -derivedDataPath /tmp/optimization-ios-derived-data CODE_SIGNING_ALLOWED=NO COMPILER_INDEX_STORE_ENABLE=NO | xcbeautify",
8+
"test:e2e:ios:run:release": "set -o pipefail && xcodebuild test-without-building -xctestrun \"$(ls /tmp/optimization-ios-derived-data/Build/Products/OptimizationApp${IOS_SCHEME}_*.xctestrun | head -1)\" -destination \"platform=iOS Simulator,name=${IOS_SIM_NAME:-iPhone 16},OS=${IOS_SIM_OS:-latest}\" -resultBundlePath /tmp/optimization-ios-derived-data/Test-${IOS_SCHEME}.xcresult ${IOS_ONLY_TESTING:+-only-testing:${IOS_ONLY_TESTING}} | xcbeautify"
9+
}
10+
}

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"format:check": "prettier . --check",
1515
"format:fix": "prettier . --check --write",
1616
"implementation:install": "pnpm run build:pkgs && pnpm run implementation:run -- --all -- implementation:install",
17+
"implementation:ios-sdk": "pnpm run implementation:run -- ios-sdk",
1718
"implementation:node-sdk": "pnpm run implementation:run -- node-sdk",
1819
"implementation:node-sdk+web-sdk": "pnpm run implementation:run -- node-sdk+web-sdk",
1920
"implementation:react-native-sdk": "pnpm run implementation:run -- react-native-sdk",
@@ -38,13 +39,15 @@
3839
"prepare": "husky",
3940
"serve:mocks": "pnpm --dir lib/mocks serve",
4041
"setup:e2e": "pnpm run build:pkgs && pnpm run implementation:run -- --all -- implementation:install && pnpm run playwright:install && pnpm run playwright:install-deps",
42+
"setup:e2e:ios-sdk": "pnpm run implementation:ios-sdk -- xcodegen",
4143
"setup:e2e:node-sdk": "pnpm run build:pkgs && pnpm run implementation:run -- node-sdk implementation:install && pnpm run implementation:run -- node-sdk implementation:setup:e2e",
4244
"setup:e2e:node-sdk+web-sdk": "pnpm run build:pkgs && pnpm run implementation:run -- node-sdk+web-sdk implementation:install && pnpm run implementation:run -- node-sdk+web-sdk implementation:setup:e2e",
4345
"setup:e2e:react-native-sdk": "pnpm run build:pkgs && pnpm run implementation:run -- react-native-sdk implementation:install && pnpm run implementation:run -- react-native-sdk implementation:setup:e2e",
4446
"setup:e2e:react-web-sdk": "pnpm run build:pkgs && pnpm run implementation:run -- react-web-sdk implementation:install && pnpm run implementation:run -- react-web-sdk implementation:setup:e2e",
4547
"setup:e2e:web-sdk_react": "pnpm run build:pkgs && pnpm run implementation:run -- web-sdk_react implementation:install && pnpm run implementation:run -- web-sdk_react implementation:setup:e2e",
4648
"setup:e2e:web-sdk": "pnpm run build:pkgs && pnpm run implementation:run -- web-sdk implementation:install && pnpm run implementation:run -- web-sdk implementation:setup:e2e",
4749
"test:e2e": "pnpm run setup:e2e && pnpm run implementation:run -- --all -- implementation:test:e2e:run",
50+
"test:e2e:ios-sdk": "pnpm run setup:e2e:ios-sdk && pnpm run implementation:ios-sdk -- test:e2e:ios:build:release && IOS_SCHEME=SwiftUI pnpm run implementation:ios-sdk -- test:e2e:ios:run:release && IOS_SCHEME=UIKit pnpm run implementation:ios-sdk -- test:e2e:ios:run:release",
4851
"test:e2e:node-sdk": "pnpm run setup:e2e:node-sdk && pnpm run implementation:run -- node-sdk implementation:test:e2e:run",
4952
"test:e2e:node-sdk+web-sdk": "pnpm run setup:e2e:node-sdk+web-sdk && pnpm run implementation:run -- node-sdk+web-sdk implementation:test:e2e:run",
5053
"test:e2e:react-native-sdk": "pnpm run setup:e2e:react-native-sdk && pnpm run implementation:run -- react-native-sdk implementation:test:e2e:run",

packages/ios/ContentfulOptimization/Sources/ContentfulOptimization/Polyfills/NativePolyfills.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ enum NativePolyfills {
7676
}
7777

7878
private static func registerNativeSetTimeout(in context: JSContext, timerStore: TimerStore) {
79-
weak let weakContext = context
79+
weak var weakContext = context
8080
let nativeSetTimeout: @convention(block) (Int, Int) -> Void = { timerId, delayMs in
8181
let workItem = DispatchWorkItem {
8282
guard let ctx = weakContext else { return }
@@ -107,7 +107,7 @@ enum NativePolyfills {
107107
}
108108

109109
private static func registerNativeFetch(in context: JSContext) {
110-
weak let weakContext = context
110+
weak var weakContext = context
111111
let nativeFetch: @convention(block) (String, String, String, JSValue, Int) -> Void = {
112112
urlString, method, headersJSON, bodyValue, callbackId in
113113

0 commit comments

Comments
 (0)