Skip to content

[Devicelab] Add physical device integration tests for iOS Universal Links - #191201

Closed
okorohelijah wants to merge 4 commits into
flutter:masterfrom
okorohelijah:add_integration_test
Closed

okorohelijah wants to merge 4 commits into
flutter:masterfrom
okorohelijah:add_integration_test

Conversation

@okorohelijah

@okorohelijah okorohelijah commented Aug 17, 2026 •

Copy link
Copy Markdown
Contributor

This PR adds a physical device Devicelab integration test to verify iOS Universal Link (Deep Link) routing from the native iOS operating system into the Dart framework.

Currently, there are no integration tests in Flutter CI that verify authentic iOS deep link routing on physical devices. We manually have to test deep link behavior when OS updates roll out (or when the Engine's FlutterSceneDelegate is modified), leaving us highly vulnerable to regressions (such as the one addressed in #185430).

This new test:

  • Compiles a static iOS project (dev/integration_tests/ios_universal_link) in release mode.
  • Installs it onto a physical iOS bot using xcrun devicectl device install app.
  • Force-launches the app with a Universal Link payload using xcrun devicectl device process launch --payload-url.
  • Verifies that the Engine successfully catches the NSUserActivity, extracts the webpageURL, and correctly routes it to Dart via SystemChannels.navigation.

Fixes #189200
Prevents regressions related to #185430

If you had to change anything in the flutter/tests repo, include a link to the migration guide as per the breaking change policy.

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance.

Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the gemini-code-assist bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.

@github-actions github-actions Bot added the team-ios Owned by iOS platform team label Aug 17, 2026
@okorohelijah

Copy link
Copy Markdown
Contributor Author

The Testing Approach

In a real-world scenario, a Universal Link is triggered when a user taps a URL from a host app like iMessage, Notes, or Safari. To test this end-to-end on physical devices in CI, we would traditionally have to write a brittle XCUITest to drive one of those host apps (e.g., scripting the UI to open Notes and tap a link) just to trigger the OS-level handoff. Because driving host apps via UI automation is incredibly flaky and slow, this test utilizes a developer diagnostic tool (xcrun devicectl process launch --payload-url) to bypass the UI entirely.

When devicectl executes with --payload-url, it forces the iOS developer daemon to cold-start the app and manually inject an NSUserActivity directly into the app's SceneDelegate. This simulates the exact payload that a tap in iMessage or Notes would have generated, allowing us to rigorously test the Native-to-Dart bridge (the FlutterSceneLifeCycle engine code) without relying on external network validation or UI automation.

Limitations of this Test

This test treats Apple's OS as a trusted black box. It does not test Apple's routing logic.

It does not test if iOS correctly downloads the apple-app-site-association file.
It does not verify Apple's domain validation.
It strictly tests the Flutter Engine. It guarantees that when iOS delivers an NSUserActivity to a Flutter app, our C++ Engine successfully translates it and pushes it to Dart. It ensures that the native deep-linking bridge remains solid against future regressions.

@okorohelijah
okorohelijah marked this pull request as ready for review August 17, 2026 09:45
@okorohelijah
okorohelijah requested a review from a team as a code owner August 17, 2026 09:45

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new integration test and Devicelab task, ios_universal_link_test, to verify that iOS Universal Links correctly cold-start the Flutter Engine and forward the route to the Dart framework. The changes include the test runner script, CI target configuration, and a complete static iOS integration test project. Feedback on the test runner script suggests optimizing the process execution by handling early exits via process.exitCode and refactoring duplicate stream listening logic for stdout and stderr into a helper function.

Comment thread dev/devicelab/bin/tasks/ios_universal_link_test.dart Outdated
@okorohelijah okorohelijah added the CICD Run CI/CD label Aug 17, 2026
@okorohelijah
okorohelijah requested a review from vashworth August 17, 2026 09:58
@vashworth

vashworth commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

We sync'd offline, but this test should be run on a simulator. It will not pass on a physical device due to codesigning

@okorohelijah

okorohelijah commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor Author

@vashworth We synced offline, and I agree with moving this to a simulator to conserve our limited physical Devicelab capacity. I'll get this updated!

Regarding the code-signing concern: this test is actually structured to pass on our physical CI bots without any provisioning errors. Because our Devicelab physical devices strictly use the standard, wildcard development.mobileprovision, they naturally don't support special capabilities like the Associated Domains entitlement. If we tried to build an app with that entitlement in CI, Xcode would indeed throw a code-signing error.

To work around that limitation, I intentionally stripped CODE_SIGN_ENTITLEMENTS out of this test app's Xcode project. Because it asks for no special capabilities, it compiles perfectly green under the wildcard profile. To actually test the Universal Link routing without the entitlement, the test uses the xcrun devicectl device process launch --payload-url command. This artificially injects the NSUserActivity payload directly into the app, entirely bypassing Apple's OS-level domain verification and entitlement checks.

However, since this is testing pure software routing into the Engine, the simulator provides the exact same coverage. I'll pivot the test runner and .ci.yaml to target the simulator to save physical device resources.

@vashworth
vashworth marked this pull request as draft August 19, 2026 20:01
pull Bot pushed a commit to Budda0ne/flutter that referenced this pull request Sep 4, 2026
This is a pivot of flutter#191201 to run on the iOS Simulator instead of
physical devicelab hardware to conserve capacity.

This PR introduces automated integration tests for iOS deep linking and
Universal Links for standard standalone Flutter apps on the iOS
Simulator.

1. **UIScene Lifecycle (`ios_universal_link`):**
- Tests the standard `UISceneDelegate` lifecycle
(`UIApplicationSceneManifest`) used by default Flutter apps.
2. **Complete Cold and Warm Start Coverage:**
- **HTTPS Universal Links:** Verified on both cold launch and warm
background resume.
- **Custom Schemes (`testscheme://`):** Verified on both cold launch and
warm background resume.
3. **End-to-End Route Delivery:**
- Verifies that the iOS deep link correctly traverses the Flutter Engine
and is delivered to the Dart framework (`pushRouteInformation`),
rendering the destination route on the UI for XCUITest assertion.
4. **Devicelab Automation:**
- Implemented `dev/devicelab/bin/tasks/ios_universal_link_test.dart` to
provision a clean simulator runtime, build the test app, run XCUITests
via `xcodebuild test`, and guarantee simulator teardown.
- Pre-warms simulator caches before assertion loops to prevent
first-frame deadline timeouts.

*(Note: Plugin lifecycle and Add-to-App deep link testing is decoupled
into flutter#192173).*
@vashworth

Copy link
Copy Markdown
Contributor

@okorohelijah Closing this since the others have already landed.

@vashworth vashworth closed this Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CICD Run CI/CD team-ios Owned by iOS platform team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Devicelab] Add physical device integration tests for iOS Universal Links (Deep Linking)

2 participants