test(accountsetup): de-flake URL-open link E2E tests via fake LocalUriHandler #467

Merged
JMR-dev merged 1 commits from fix-apppassword-intent-flake into main 2026-07-09 01:15:18 +00:00
JMR-dev commented 2026-07-09 00:05:22 +00:00 (Migrated from github.com)

Problem

AppPasswordSetupScreenTest.imapDisabledFailure_showsThePrompt_andHelpLinkOpensTheProviderPage (and its sibling URL-open tests) flake on the CI E2E matrix with:

androidx.test.espresso.base.RootViewPicker$RootViewWithoutFocusException:
Waited for the root of the view hierarchy to have window focus ... for 10 seconds
... Root{... has-window-focus=false ...}

A single failure fails the whole ~9-min E2E leg and forces a retry.

Root cause (confirmed from CI run 28980028281, job "E2E (35)")

Intents.intended(...) internally runs onView(isRoot()).check(...), and Espresso's RootViewPicker waits up to 10s for a window-focused root:

at androidx.test.espresso.ViewInteraction.check(ViewInteraction.java:366)
at androidx.test.espresso.intent.Intents.intended(Intents.java:186)
at org.libremail.ui.accountsetup.AppPasswordSetupScreenTest...(AppPasswordSetupScreenTest.kt:110)

On the CI emulator the app window intermittently reports has-window-focus=false, so intended() times out. This is not caused by a missing intent stub — the intending(ACTION_VIEW).respondWith(...) stubs were already present and already on main; no external activity launches. The focus loss is environmental: the same run failed 8 unrelated tests at once, all sharing the Espresso RootViewPicker dependency (Intents.intended, Espresso.pressBack, a focus-dependent clipboard read), while 280+ pure-Compose tests passed in that same run. CI already disables animations and sends input keyevent 82 at boot, and this still recurs post-#454.

Fix

For the tests whose links open a URL through Compose's LocalUriHandler (the ACTION_VIEW / "browser-open" shape), inject a recording UriHandler and assert the exact URL the screen opens — instead of Espresso-Intents. This keeps the entire test on Compose interactions, which do not depend on window focus, so it is deterministic. The assertion is not weakened (it still asserts the provider page URL / host).

Converted:

  • AppPasswordSetupScreenTest.tappingCreateAppPasswordPage_launchesBrowserIntentToHelpUrl -> asserts provider.appPasswordHelpUrl
  • AppPasswordSetupScreenTest.imapDisabledFailure_showsThePrompt_andHelpLinkOpensTheProviderPage -> asserts host support.google.com
  • OutlookImapNoticeScreenTest.tappingImapHelpLink_opensTheMicrosoftArticle -> asserts host support.microsoft.com (sweep of the same pattern)

Not in scope (same root cause, no UriHandler seam)

These fire real system intents / use other RootViewPicker APIs, so they can't use this pattern and still rely on Espresso — they remain exposed to the same emulator window-focus flake and will need a separate fix (likely CI-level window focus, or an injectable launcher seam):

  • AccountPickerScreenTest.tappingOutlook_launchesTheAppAuthBrowserIntent (AppAuth hasComponent)
  • OutlookImapNoticeScreenTest.tappingSignIn_launchesTheAppAuthBrowserIntent (AppAuth hasComponent)
  • BatteryOptimizationStepTest.batteryStep_takeMeThere_opensThisAppsSystemSettings (Settings intent)
  • LicenseScreenTest.systemBack_invokesOnDeclineJustLikeTheButton (Espresso.pressBack)
  • ReportReviewScreenTest.tappingCopy_putsThePayloadOnTheSystemClipboard_andShowsAConfirmation (focus-dependent clipboard read)

Validation

Local fast gate (JDK 21): assembleDebug testDebugUnitTest jacocoTestCoverageVerification compileDebugAndroidTestKotlin lintDebug ktlintCheck detekt — all green. Pure test change, so no app logging added. Local emulator E2E skipped (flaky on the Windows dev box); the CI matrix is the authoritative validation for the flake.

## Problem `AppPasswordSetupScreenTest.imapDisabledFailure_showsThePrompt_andHelpLinkOpensTheProviderPage` (and its sibling URL-open tests) flake on the CI E2E matrix with: ``` androidx.test.espresso.base.RootViewPicker$RootViewWithoutFocusException: Waited for the root of the view hierarchy to have window focus ... for 10 seconds ... Root{... has-window-focus=false ...} ``` A single failure fails the whole ~9-min E2E leg and forces a retry. ### Root cause (confirmed from CI run 28980028281, job "E2E (35)") `Intents.intended(...)` internally runs `onView(isRoot()).check(...)`, and Espresso's `RootViewPicker` waits up to 10s for a **window-focused** root: ``` at androidx.test.espresso.ViewInteraction.check(ViewInteraction.java:366) at androidx.test.espresso.intent.Intents.intended(Intents.java:186) at org.libremail.ui.accountsetup.AppPasswordSetupScreenTest...(AppPasswordSetupScreenTest.kt:110) ``` On the CI emulator the app window intermittently reports `has-window-focus=false`, so `intended()` times out. **This is not caused by a missing intent stub** — the `intending(ACTION_VIEW).respondWith(...)` stubs were already present and already on `main`; no external activity launches. The focus loss is environmental: the same run failed **8 unrelated tests at once**, all sharing the Espresso `RootViewPicker` dependency (`Intents.intended`, `Espresso.pressBack`, a focus-dependent clipboard read), while 280+ pure-Compose tests passed in that same run. CI already disables animations and sends `input keyevent 82` at boot, and this still recurs post-#454. ## Fix For the tests whose links open a URL through Compose's `LocalUriHandler` (the `ACTION_VIEW` / "browser-open" shape), inject a recording `UriHandler` and assert the exact URL the screen opens — instead of Espresso-Intents. This keeps the entire test on Compose interactions, which do **not** depend on window focus, so it is deterministic. The assertion is not weakened (it still asserts the provider page URL / host). Converted: - `AppPasswordSetupScreenTest.tappingCreateAppPasswordPage_launchesBrowserIntentToHelpUrl` -> asserts `provider.appPasswordHelpUrl` - `AppPasswordSetupScreenTest.imapDisabledFailure_showsThePrompt_andHelpLinkOpensTheProviderPage` -> asserts host `support.google.com` - `OutlookImapNoticeScreenTest.tappingImapHelpLink_opensTheMicrosoftArticle` -> asserts host `support.microsoft.com` (sweep of the same pattern) ## Not in scope (same root cause, no `UriHandler` seam) These fire real system intents / use other `RootViewPicker` APIs, so they can't use this pattern and still rely on Espresso — they remain exposed to the same emulator window-focus flake and will need a separate fix (likely CI-level window focus, or an injectable launcher seam): - `AccountPickerScreenTest.tappingOutlook_launchesTheAppAuthBrowserIntent` (AppAuth `hasComponent`) - `OutlookImapNoticeScreenTest.tappingSignIn_launchesTheAppAuthBrowserIntent` (AppAuth `hasComponent`) - `BatteryOptimizationStepTest.batteryStep_takeMeThere_opensThisAppsSystemSettings` (Settings intent) - `LicenseScreenTest.systemBack_invokesOnDeclineJustLikeTheButton` (`Espresso.pressBack`) - `ReportReviewScreenTest.tappingCopy_putsThePayloadOnTheSystemClipboard_andShowsAConfirmation` (focus-dependent clipboard read) ## Validation Local fast gate (JDK 21): `assembleDebug testDebugUnitTest jacocoTestCoverageVerification compileDebugAndroidTestKotlin lintDebug ktlintCheck detekt` — all green. Pure test change, so no app logging added. Local emulator E2E skipped (flaky on the Windows dev box); the CI matrix is the authoritative validation for the flake.
mergify[bot] commented 2026-07-09 00:42:55 +00:00 (Migrated from github.com)

Merge Queue Status

This pull request spent 32 minutes 25 seconds in the queue, including 26 minutes 46 seconds running CI.

Required conditions to merge
<!--- DO NOT EDIT -*- Mergify Payload -*- {"version": 1, "state": "merged", "queue_rule_name": "default", "queued_at": "2026-07-09T00:42:53.642289+00:00", "estimated_time_of_merge": null, "speculative_check_pr": null, "required_conditions": []} -*- Mergify Payload End -*- --> # Merge Queue Status - ✅ **Entered queue** — `2026-07-09 00:42 UTC` · Rule: `default` · triggered by merge protections - ✅ **Checks passed** · on draft #473 - ✅ **Merged** — `2026-07-09 01:15 UTC` · at `65eae0f6c76e5e56ff33f6e3c6b32aa60a3f6810` · merge This pull request spent **32 minutes 25 seconds** in the queue, including **26 minutes 46 seconds** running CI. <details> <summary>Required conditions to merge</summary> - `-conflict` - [X] #467 - [X] #469 - `-draft` - [X] #467 - [X] #469 - [X] `base = main` - [X] `check-success = CI passed` - `github-review-approved` [🛡 GitHub repository ruleset rule `main`] - [X] #467 - [X] #469 - `label != broken` - [X] #467 - [X] #469 - [X] any of [🛡 GitHub branch protection]: - [X] `check-success = Debug build` - [ ] `check-neutral = Debug build` - [ ] `check-skipped = Debug build` - [X] any of [🛡 GitHub branch protection]: - [X] `check-success = Unit tests` - [ ] `check-neutral = Unit tests` - [ ] `check-skipped = Unit tests` - [X] any of [🛡 GitHub branch protection]: - [X] `check-success = CI passed` - [ ] `check-neutral = CI passed` - [ ] `check-skipped = CI passed` - [X] any of [🛡 GitHub repository ruleset rule `main`]: - [X] `check-success = @github-actions/CI passed` - [ ] `check-neutral = @github-actions/CI passed` - [ ] `check-skipped = @github-actions/CI passed` </details>
JMR-dev commented 2026-07-09 01:16:04 +00:00 (Migrated from github.com)

@Mergifyio refresh

@Mergifyio refresh
mergify[bot] commented 2026-07-09 01:16:09 +00:00 (Migrated from github.com)

refresh

✅ Pull request refreshed

> refresh #### ✅ Pull request refreshed
Sign in to join this conversation.