Skip to content

Commit c932ff1

Browse files
Fix WebView element access on iOS 26 by including SafariViewService hierarchy (mobile-dev-inc#2872)
* Fix WebView element access on iOS 26 by including SafariViewService hierarchy * Rebuild iOS runners to include the change * Run iOS e2e tests on iOS 26.1 * Add webview tests * Bump runner to macOS 26 No idea if this will solve the test problems. It's a shot in the dark... --------- Co-authored-by: Dan Caseley <dan@mobile.dev>
1 parent 8c58216 commit c932ff1

File tree

7 files changed

+81
-22
lines changed

7 files changed

+81
-22
lines changed

.github/workflows/test-e2e.yaml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -254,10 +254,9 @@ jobs:
254254

255255
test-ios:
256256
name: Test on iOS
257-
runs-on: macos-latest
257+
runs-on: macos-26
258258
needs: build
259259
timeout-minutes: 120
260-
#if: ${{ false }}
261260

262261
env:
263262
MAESTRO_DRIVER_STARTUP_TIMEOUT: 240000 # 240s
@@ -289,7 +288,11 @@ jobs:
289288
maestro --version
290289
291290
- name: Boot Simulator
292-
run: ./.github/scripts/boot_simulator.sh
291+
run: |
292+
xcrun simctl list runtimes
293+
export RUNTIME="iOS26.1"
294+
export DEVICE_TYPE="iPhone 17 Pro"
295+
./.github/scripts/boot_simulator.sh
293296
294297
- name: Download apps
295298
working-directory: ${{ github.workspace }}/e2e
@@ -322,7 +325,7 @@ jobs:
322325
retention-days: 7
323326
include-hidden-files: true
324327

325-
- name: Upload xc test runner logs
328+
- name: Upload xctest runner logs
326329
uses: actions/upload-artifact@v6
327330
if: success() || failure()
328331
with:
@@ -331,7 +334,7 @@ jobs:
331334
retention-days: 7
332335
include-hidden-files: true
333336

334-
- name: Upload screen recording of AVD
337+
- name: Upload screen recording of Simulator
335338
uses: actions/upload-artifact@v6
336339
if: success() || failure()
337340
with:

e2e/manifest.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ https://storage.googleapis.com/mobile.dev/cli_e2e/nowinandroid.apk
44
https://storage.googleapis.com/mobile.dev/cli_e2e/demo_app.apk
55
https://storage.googleapis.com/mobile.dev/cli_e2e/demo_app.zip
66
https://storage.googleapis.com/mobile.dev/cli_e2e/setOrientation.apk
7+
https://storage.googleapis.com/mobile.dev/cli_e2e/SimpleWebViewApp.zip

e2e/run_tests

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,27 +36,31 @@ trap 'rm -f pipe' EXIT
3636

3737
export MAESTRO_EXAMPLE="test-value" # Relied upon in a test
3838

39-
# Run passing tests
4039
for workspace_dir in ./workspaces/*; do
4140

42-
# Skip workspaces with no tests for iOS
43-
case $(basename "$workspace_dir") in
44-
demo_app|nowinandroid|setOrientation) # demo_app has OOM issues on GHA
41+
app_name="$(basename "$workspace_dir")"
42+
43+
case $app_name in
44+
# Skip workspaces with no tests for iOS
45+
# demo_app has OOM issues on GHA
46+
demo_app|nowinandroid|setOrientation)
4547
if [ "$platform" = "ios" ]; then
4648
continue
4749
fi
4850
;;
51+
# Skip workspaces with no tests for Android
52+
simple_web_view)
53+
if [ "$platform" = "android" ]; then
54+
continue
55+
fi
56+
;;
57+
# Skip web-only workspaces for all mobile tests - those are run by run_tests_web
58+
sauce_demo)
59+
continue
60+
;;
4961
esac
5062

5163
WORKSPACE_PASS=true
52-
app_name="$(basename "$workspace_dir")"
53-
54-
# Skip web-only workspaces for mobile e2e tests
55-
case $app_name in
56-
sauce_demo)
57-
continue
58-
;;
59-
esac
6064

6165
_h1 "run tests for app \"$app_name\" on platform \"$platform\""
6266

@@ -81,7 +85,7 @@ for workspace_dir in ./workspaces/*; do
8185
###
8286
# edge case: some workspaces have no failing flows
8387
case $app_name in
84-
wikipedia|setOrientation)
88+
wikipedia|setOrientation|no-app|simple_web_view)
8589
continue
8690
;;
8791
esac
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
appId: com.example.SimpleWebViewApp
2+
tags:
3+
- passing
4+
- ios
5+
---
6+
- launchApp:
7+
clearState: true
8+
9+
- tapOn: Open Login Page
10+
11+
- assertVisible: Login
12+
- assertVisible: Sign In
13+
- assertVisible: Forgot your password?

maestro-ios-xctest-runner/maestro-driver-iosUITests/Routes/Handlers/ViewHierarchyHandler.swift

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,11 @@ struct ViewHierarchyHandler: HTTPHandler {
5252
let statusBars = logger.measure(message: "Fetch status bar hierarchy") {
5353
fullStatusBars(springboardApplication)
5454
} ?? []
55-
55+
56+
// Fetch Safari WebView hierarchy for iOS 26+ (runs in separate SafariViewService process)
57+
let safariWebViewHierarchy = logger.measure(message: "Fetch Safari WebView hierarchy") {
58+
getSafariWebViewHierarchy()
59+
}
5660

5761
let deviceFrame = springboardApplication.frame
5862
let deviceAxFrame = [
@@ -70,7 +74,7 @@ struct ViewHierarchyHandler: HTTPHandler {
7074
let appWidth = appFrame["Width"], appWidth > 0,
7175
let appHeight = appFrame["Height"], appHeight > 0
7276
else {
73-
return AXElement(children: [appHierarchy, AXElement(children: statusBars)].compactMap { $0 })
77+
return AXElement(children: [appHierarchy, AXElement(children: statusBars), safariWebViewHierarchy].compactMap { $0 })
7478
}
7579

7680
let offsetX = deviceWidth - appWidth
@@ -81,9 +85,9 @@ struct ViewHierarchyHandler: HTTPHandler {
8185

8286
let adjustedAppHierarchy = expandElementSizes(appHierarchy, offset: offset)
8387

84-
return AXElement(children: [adjustedAppHierarchy, AXElement(children: statusBars)].compactMap { $0 })
88+
return AXElement(children: [adjustedAppHierarchy, AXElement(children: statusBars), safariWebViewHierarchy].compactMap { $0 })
8589
} else {
86-
return AXElement(children: [appHierarchy, AXElement(children: statusBars)].compactMap { $0 })
90+
return AXElement(children: [appHierarchy, AXElement(children: statusBars), safariWebViewHierarchy].compactMap { $0 })
8791
}
8892
}
8993

@@ -229,6 +233,40 @@ struct ViewHierarchyHandler: HTTPHandler {
229233

230234
return snapshots
231235
}
236+
237+
/// Fetches the Safari WebView hierarchy for iOS 26+ where SFSafariViewController
238+
/// runs in a separate process (com.apple.SafariViewService).
239+
/// Returns nil if not on iOS 26+, Safari service is not running, or no webviews exist.
240+
private func getSafariWebViewHierarchy() -> AXElement? {
241+
let systemVersion = ProcessInfo.processInfo.operatingSystemVersion
242+
guard systemVersion.majorVersion >= 26 else {
243+
return nil
244+
}
245+
246+
let safariWebService = XCUIApplication(bundleIdentifier: "com.apple.SafariViewService")
247+
248+
let isRunning = safariWebService.state == .runningForeground || safariWebService.state == .runningBackground
249+
guard isRunning else {
250+
return nil
251+
}
252+
253+
let webViewCount = safariWebService.webViews.count
254+
guard webViewCount > 0 else {
255+
return nil
256+
}
257+
258+
NSLog("[Start] Fetching Safari WebView hierarchy (\(webViewCount) webview(s) detected)")
259+
260+
do {
261+
AXClientSwizzler.overwriteDefaultParameters["maxDepth"] = snapshotMaxDepth
262+
let safariHierarchy = try elementHierarchy(xcuiElement: safariWebService)
263+
NSLog("[Done] Safari WebView hierarchy fetched successfully")
264+
return safariHierarchy
265+
} catch {
266+
NSLog("[Error] Failed to fetch Safari WebView hierarchy: \(error.localizedDescription)")
267+
return nil
268+
}
269+
}
232270

233271
private func findRecoveryElement(_ element: XCUIElement) throws -> XCUIElement {
234272
if try element.snapshot().children.count > 1 {

0 commit comments

Comments
 (0)