+46
-37
@@ -1,10 +1,11 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package org.libremail.ui.accountsetup
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Instrumentation
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.platform.UriHandler
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
@@ -13,14 +14,9 @@ import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasData
|
||||
import androidx.test.espresso.intent.matcher.UriMatchers.hasHost
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import jakarta.mail.AuthenticationFailedException
|
||||
import org.hamcrest.CoreMatchers.allOf
|
||||
import org.hamcrest.CoreMatchers.equalTo
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -35,8 +31,17 @@ import org.libremail.ui.theme.LibreMailTheme
|
||||
* [AppPasswordSetupScreen] + [AppPasswordViewModel] over a [FakeAccountRepository] for the preset
|
||||
* Gmail vendor: the provider-specific chrome renders, entering an email + app password and tapping
|
||||
* "Test & add" persists through the repository and reports the new account id, and tapping the
|
||||
* "create an app password" help link fires the browser intent. That launch is asserted with
|
||||
* Espresso-Intents (mirroring `AccountPickerScreenTest`'s Outlook test), so no real browser opens.
|
||||
* "create an app password" help link opens the provider's help page.
|
||||
*
|
||||
* The outbound help links are verified by injecting a recording [UriHandler] for [LocalUriHandler]
|
||||
* and asserting the URL the screen asked to open — deliberately NOT via Espresso-Intents. The two
|
||||
* approaches verify the same behaviour, but `Intents.intended(...)` runs an `onView(isRoot())` view
|
||||
* assertion whose `RootViewPicker` waits up to 10s for a window-focused root; on the CI emulator the
|
||||
* activity window intermittently reports `has-window-focus=false`, so that assertion flakes with
|
||||
* `RootViewWithoutFocusException` (an infra flake that fails every `intended()`-based E2E test on the
|
||||
* affected leg and forces a costly 9-min retry). Driving the link through a fake [UriHandler] keeps
|
||||
* the whole test on Compose interactions, which do not depend on window focus, so it is deterministic
|
||||
* — while still asserting the exact provider page the tap opens.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AppPasswordSetupScreenTest {
|
||||
@@ -46,6 +51,9 @@ class AppPasswordSetupScreenTest {
|
||||
|
||||
private val provider = MailProvider.GMAIL
|
||||
|
||||
// Captures the URL the screen hands to LocalUriHandler instead of launching a real browser.
|
||||
private val uriHandler = RecordingUriHandler()
|
||||
|
||||
private fun string(resId: Int, vararg args: Any) = composeTestRule.activity.getString(resId, *args)
|
||||
|
||||
private fun setContent(
|
||||
@@ -57,8 +65,10 @@ class AppPasswordSetupScreenTest {
|
||||
repository,
|
||||
)
|
||||
composeTestRule.setContent {
|
||||
LibreMailTheme(darkTheme = false, dynamicColor = false) {
|
||||
AppPasswordSetupScreen(onBack = {}, onAccountAdded = onAccountAdded, viewModel = viewModel)
|
||||
CompositionLocalProvider(LocalUriHandler provides uriHandler) {
|
||||
LibreMailTheme(darkTheme = false, dynamicColor = false) {
|
||||
AppPasswordSetupScreen(onBack = {}, onAccountAdded = onAccountAdded, viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,34 +100,29 @@ class AppPasswordSetupScreenTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Tapping the "create an app password" link opens the provider's help page via
|
||||
* [androidx.compose.ui.platform.UriHandler], which starts an `ACTION_VIEW` intent. Stubbing that
|
||||
* intent both proves the tap launched it and stops a real browser from opening on the device.
|
||||
* Tapping the "create an app password" link opens the provider's help page via [LocalUriHandler].
|
||||
* Asserting the URL captured by [RecordingUriHandler] proves the tap requested the right page
|
||||
* without launching a real browser (and without the window-focus-dependent Espresso-Intents
|
||||
* assertion that flakes on CI — see the class comment).
|
||||
*/
|
||||
@Test
|
||||
fun tappingCreateAppPasswordPage_launchesBrowserIntentToHelpUrl() {
|
||||
setContent()
|
||||
|
||||
Intents.init()
|
||||
try {
|
||||
Intents.intending(hasAction(Intent.ACTION_VIEW))
|
||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_CANCELED, null))
|
||||
composeTestRule.onNodeWithText(string(R.string.app_password_open_page, provider.displayName))
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
composeTestRule.onNodeWithText(string(R.string.app_password_open_page, provider.displayName))
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
Intents.intended(allOf(hasAction(Intent.ACTION_VIEW), hasData(provider.appPasswordHelpUrl)))
|
||||
} finally {
|
||||
Intents.release()
|
||||
}
|
||||
assertEquals(provider.appPasswordHelpUrl, uriHandler.lastUri)
|
||||
}
|
||||
|
||||
/**
|
||||
* When the connection test fails specifically because IMAP is disabled (Gmail's "not enabled for
|
||||
* IMAP use"), the screen surfaces the actionable "turn on IMAP" dialog instead of a generic error,
|
||||
* and its help link opens the provider's enable-IMAP page (#390). Driving the failure through a
|
||||
* [FakeAccountRepository] exercises the real classification + dialog wiring end to end on device.
|
||||
* [FakeAccountRepository] exercises the real classification + dialog wiring end to end on device;
|
||||
* the help link's target is verified through the injected [RecordingUriHandler] (see the class
|
||||
* comment for why not Espresso-Intents).
|
||||
*/
|
||||
@Test
|
||||
fun imapDisabledFailure_showsThePrompt_andHelpLinkOpensTheProviderPage() {
|
||||
@@ -138,16 +143,20 @@ class AppPasswordSetupScreenTest {
|
||||
}
|
||||
composeTestRule.onNodeWithText(string(R.string.imap_disabled_message, provider.displayName)).assertIsDisplayed()
|
||||
|
||||
Intents.init()
|
||||
try {
|
||||
Intents.intending(hasAction(Intent.ACTION_VIEW))
|
||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_CANCELED, null))
|
||||
composeTestRule.onNodeWithText(string(R.string.imap_disabled_help)).performClick()
|
||||
|
||||
composeTestRule.onNodeWithText(string(R.string.imap_disabled_help)).performClick()
|
||||
// Mirrors the previous Espresso hasHost(...) check: the Gmail enable-IMAP page is on Google's
|
||||
// support host. Verifying the exact host keeps the assertion strength without any focus wait.
|
||||
assertEquals("support.google.com", Uri.parse(uriHandler.lastUri).host)
|
||||
}
|
||||
|
||||
Intents.intended(allOf(hasAction(Intent.ACTION_VIEW), hasData(hasHost(equalTo("support.google.com")))))
|
||||
} finally {
|
||||
Intents.release()
|
||||
/** A [UriHandler] that records the last opened URL instead of starting a real `ACTION_VIEW` intent. */
|
||||
private class RecordingUriHandler : UriHandler {
|
||||
var lastUri: String? = null
|
||||
private set
|
||||
|
||||
override fun openUri(uri: String) {
|
||||
lastUri = uri
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+42
-28
@@ -4,23 +4,22 @@ package org.libremail.ui.onboarding
|
||||
import android.app.Activity
|
||||
import android.app.Instrumentation
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.platform.UriHandler
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasData
|
||||
import androidx.test.espresso.intent.matcher.UriMatchers.hasHost
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import net.openid.appauth.AuthorizationManagementActivity
|
||||
import org.hamcrest.CoreMatchers.allOf
|
||||
import org.hamcrest.CoreMatchers.equalTo
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -33,11 +32,18 @@ import org.libremail.ui.theme.LibreMailTheme
|
||||
/**
|
||||
* End-to-end UI test for the pre-auth Outlook IMAP-enablement notice (#411). Drives the real
|
||||
* [OutlookImapNoticeScreen] + [AccountSetupViewModel] over a [FakeAccountRepository]: the IMAP
|
||||
* question and both outbound links render, tapping a help link fires the browser `ACTION_VIEW`
|
||||
* intent, and tapping the bottom "Sign in" button starts the existing Microsoft OAuth (AppAuth)
|
||||
* flow. Both launches are asserted with Espresso-Intents (mirroring `AccountPickerScreenTest`), so no
|
||||
* real browser ever opens; the interstitial → OAuth navigation in the full onboarding graph is
|
||||
* covered by `OnboardingFlowTest`.
|
||||
* question and both outbound links render, tapping the "How to enable IMAP" help link opens
|
||||
* Microsoft's help article, and tapping the bottom "Sign in" button starts the existing Microsoft
|
||||
* OAuth (AppAuth) flow.
|
||||
*
|
||||
* The help link is verified by injecting a recording [UriHandler] for [LocalUriHandler] and asserting
|
||||
* the opened URL — not via Espresso-Intents, whose `intended(...)` runs an `onView(isRoot())`
|
||||
* assertion that waits for a window-focused root and flakes with `RootViewWithoutFocusException` on
|
||||
* the CI emulator (see `AppPasswordSetupScreenTest` for the full write-up). The "Sign in" launch has
|
||||
* no [UriHandler] seam — AppAuth calls `startActivity` directly — so it stays on Espresso-Intents,
|
||||
* matched by AppAuth's [AuthorizationManagementActivity] component and stubbed so no real browser
|
||||
* opens; the interstitial → OAuth navigation in the full onboarding graph is covered by
|
||||
* `OnboardingFlowTest`.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class OutlookImapNoticeScreenTest {
|
||||
@@ -48,13 +54,18 @@ class OutlookImapNoticeScreenTest {
|
||||
private val context: Context =
|
||||
InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
|
||||
|
||||
// Captures the URL the screen hands to LocalUriHandler instead of launching a real browser.
|
||||
private val uriHandler = RecordingUriHandler()
|
||||
|
||||
private fun string(resId: Int) = composeTestRule.activity.getString(resId)
|
||||
|
||||
private fun setContent(onAccountAdded: (String) -> Unit = {}) {
|
||||
val viewModel = AccountSetupViewModel(OutlookAuthManager(context), FakeAccountRepository())
|
||||
composeTestRule.setContent {
|
||||
LibreMailTheme(darkTheme = false, dynamicColor = false) {
|
||||
OutlookImapNoticeScreen(onBack = {}, onAccountAdded = onAccountAdded, viewModel = viewModel)
|
||||
CompositionLocalProvider(LocalUriHandler provides uriHandler) {
|
||||
LibreMailTheme(darkTheme = false, dynamicColor = false) {
|
||||
OutlookImapNoticeScreen(onBack = {}, onAccountAdded = onAccountAdded, viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,27 +81,20 @@ class OutlookImapNoticeScreenTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Tapping the "How to enable IMAP" link opens Microsoft's help article via
|
||||
* [androidx.compose.ui.platform.UriHandler], which starts an `ACTION_VIEW` intent. Stubbing that
|
||||
* intent both proves the tap launched it and stops a real browser from opening on the device.
|
||||
* Tapping the "How to enable IMAP" link opens Microsoft's help article via [LocalUriHandler].
|
||||
* Asserting the URL captured by [RecordingUriHandler] proves the tap requested the right page
|
||||
* without launching a real browser (and without the window-focus-dependent Espresso-Intents
|
||||
* assertion that flakes on CI — see the class comment).
|
||||
*/
|
||||
@Test
|
||||
fun tappingImapHelpLink_opensTheMicrosoftArticle() {
|
||||
setContent()
|
||||
|
||||
Intents.init()
|
||||
try {
|
||||
Intents.intending(hasAction(Intent.ACTION_VIEW))
|
||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_CANCELED, null))
|
||||
composeTestRule.onNodeWithText(string(R.string.outlook_imap_help))
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
composeTestRule.onNodeWithText(string(R.string.outlook_imap_help))
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
Intents.intended(allOf(hasAction(Intent.ACTION_VIEW), hasData(hasHost(equalTo("support.microsoft.com")))))
|
||||
} finally {
|
||||
Intents.release()
|
||||
}
|
||||
assertEquals("support.microsoft.com", Uri.parse(uriHandler.lastUri).host)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,4 +122,14 @@ class OutlookImapNoticeScreenTest {
|
||||
Intents.release()
|
||||
}
|
||||
}
|
||||
|
||||
/** A [UriHandler] that records the last opened URL instead of starting a real `ACTION_VIEW` intent. */
|
||||
private class RecordingUriHandler : UriHandler {
|
||||
var lastUri: String? = null
|
||||
private set
|
||||
|
||||
override fun openUri(uri: String) {
|
||||
lastUri = uri
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user