test(compose): Robolectric JVM tests for onboarding & lock screens (#377) #396

Merged
JMR-dev merged 2 commits from test-377-robolectric-onboarding-lock into main 2026-07-07 02:25:50 +00:00
6 changed files with 634 additions and 6 deletions
+3 -6
View File
@@ -319,15 +319,12 @@ val jacocoNonJvmTestableSurface = listOf(
// ColorSwatch(Row), FontPicker, FontSizePicker & ParagraphAlignmentControl converted to
// Robolectric JVM Compose tests (#376) — now JVM-covered.
"**/DraftsScreen*",
"**/LockScreen*",
// LockScreen converted to a Robolectric JVM Compose test (#377) — now JVM-covered.
"**/AppLockGateHost*",
"**/FolderDrawer*",
"**/MailboxScreen*",
// AddAnotherAccountScreen converted to a Robolectric JVM Compose test (#373) — now JVM-covered.
"**/BatteryOptimizationScreen*",
"**/ContactsAccessScreen*",
"**/LicenseScreen*",
"**/OnboardingWelcomeScreen*",
// AddAnotherAccountScreen (#373) plus the onboarding welcome/license and contacts/battery steps
// (#377) converted to Robolectric JVM Compose tests — now JVM-covered.
"**/OutboxScreen*",
"**/ReaderScreen*",
"**/ProblemReportsScreen*",
@@ -0,0 +1,82 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package org.libremail.ui.lock
import android.content.Context
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.libremail.R
import org.libremail.ui.theme.LibreMailTheme
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode
/**
* Robolectric JVM Compose test (#377, umbrella #373) for the app-lock gate. Mirrors the instrumented
* [LockScreenTest] on the JVM via the v2 `createComposeRule()` under [RobolectricTestRunner], so
* [LockScreen]'s render + interaction paths become JaCoCo JVM-testable (this file is dropped from
* `jacocoNonJvmTestableSurface`). The instrumented `androidTest` [LockScreenTest] stays.
*
* [LockScreen] is presentational — its biometric prompt is driven by the caller via [onUnlock] — so
* it is exercised in isolation: the locked title/body always show, an optional error string appears
* only when non-null, and the unlock button reports back.
*/
@RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(sdk = [36])
class LockScreenJvmTest {
@get:Rule
val composeTestRule = createComposeRule()
private val context: Context get() = RuntimeEnvironment.getApplication()
private fun string(resId: Int): String = context.getString(resId)
private fun setContent(error: String? = null, onUnlock: () -> Unit = {}) {
composeTestRule.setContent {
LibreMailTheme(darkTheme = false, dynamicColor = false) {
LockScreen(error = error, onUnlock = onUnlock)
}
}
}
@Test
fun showsLockedTitleBody_andUnlockButton() {
setContent()
composeTestRule.onNodeWithText(string(R.string.app_lock_title)).assertIsDisplayed()
composeTestRule.onNodeWithText(string(R.string.app_lock_locked_body)).assertIsDisplayed()
composeTestRule.onNodeWithText(string(R.string.app_lock_unlock)).assertIsDisplayed()
}
@Test
fun noError_hidesErrorText() {
setContent(error = null)
composeTestRule.onNodeWithText(string(R.string.app_lock_unlock_failed)).assertDoesNotExist()
}
@Test
fun error_isDisplayed() {
setContent(error = string(R.string.app_lock_unlock_failed))
composeTestRule.onNodeWithText(string(R.string.app_lock_unlock_failed)).assertIsDisplayed()
}
@Test
fun tappingUnlock_invokesOnUnlock() {
var unlocked = false
setContent(onUnlock = { unlocked = true })
composeTestRule.onNodeWithText(string(R.string.app_lock_unlock)).performClick()
assertTrue(unlocked)
}
}
@@ -0,0 +1,122 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package org.libremail.ui.onboarding
import android.content.Context
import android.content.Intent
import android.provider.Settings
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.compose.LocalLifecycleOwner
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.libremail.R
import org.libremail.ui.theme.LibreMailTheme
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode
/**
* Robolectric JVM Compose test (#377, umbrella #373) for the onboarding battery opt-in step (#49).
* The whole screen is the [OnboardingViewModel] wrapper, so it is driven with a mocked view model
* exposing the `batteryUnrestricted` StateFlow: the "already unrestricted → done" state and the
* "offered" state (guidance + Take me there + Not now) are both rendered, the Take-me-there path is
* asserted to mark the prompt handled and resolve the settings intent, and the ON_RESUME re-read is
* verified. Mirrors the instrumented `BatteryOptimizationStepTest` (which covers the same paths
* through the real view model + NavHost). This file is dropped from `jacocoNonJvmTestableSurface`;
* the instrumented test stays.
*/
// A tall display (`+h2000dp`, merged onto the default device config) so the centered, non-scrolling
// column fits its full "offered" state — guidance + Take me there + Not now — without the lower
// controls clipping below Robolectric's small default viewport (which would make performClick miss).
@RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(sdk = [36], qualifiers = "+w411dp-h2000dp")
class BatteryOptimizationScreenJvmTest {
@get:Rule
val composeTestRule = createComposeRule()
private val context: Context get() = RuntimeEnvironment.getApplication()
private fun string(resId: Int): String = context.getString(resId)
/** RESUMED owner so `collectAsStateWithLifecycle` collects and the ON_RESUME effect fires. */
private val resumedOwner = object : LifecycleOwner {
private val registry =
LifecycleRegistry.createUnsafe(this).apply { currentState = Lifecycle.State.RESUMED }
override val lifecycle: Lifecycle get() = registry
}
private fun setContent(viewModel: OnboardingViewModel, onFinish: () -> Unit = {}) {
composeTestRule.setContent {
CompositionLocalProvider(LocalLifecycleOwner provides resumedOwner) {
LibreMailTheme(darkTheme = false, dynamicColor = false) {
BatteryOptimizationScreen(viewModel = viewModel, onFinish = onFinish)
}
}
}
}
@Test
fun unrestricted_showsDoneState_andContinues() {
val vm = mockk<OnboardingViewModel>(relaxed = true)
every { vm.batteryUnrestricted } returns MutableStateFlow(true)
var finished = false
setContent(vm, onFinish = { finished = true })
composeTestRule.onNodeWithText(string(R.string.onboarding_battery_done_title)).assertIsDisplayed()
// The request/skip affordances are gone in the "done" state.
composeTestRule.onNodeWithText(string(R.string.onboarding_battery_take_me)).assertDoesNotExist()
composeTestRule.onNodeWithText(string(R.string.onboarding_battery_not_now)).assertDoesNotExist()
composeTestRule.onNodeWithText(string(R.string.onboarding_battery_continue)).performClick()
assertTrue(finished)
// The ON_RESUME effect re-reads the live status.
verify { vm.refreshBatteryStatus() }
}
@Test
fun notUnrestricted_takeMeThere_marksHandledAndResolvesSettingsIntent() {
val vm = mockk<OnboardingViewModel>(relaxed = true)
every { vm.batteryUnrestricted } returns MutableStateFlow(false)
every { vm.batterySettingsIntent() } returns Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
var finished = false
setContent(vm, onFinish = { finished = true })
composeTestRule.onNodeWithText(string(R.string.onboarding_battery_title)).assertIsDisplayed()
composeTestRule.onNodeWithText(string(R.string.onboarding_battery_guidance)).assertIsDisplayed()
composeTestRule.onNodeWithText(string(R.string.onboarding_battery_take_me)).performClick()
// Take me there marks the prompt handled up front, then launches the resolved settings intent.
verify { vm.markBatteryPromptHandled() }
verify { vm.batterySettingsIntent() }
// Leaving for Settings does not itself finish onboarding.
assertFalse(finished)
}
@Test
fun notUnrestricted_notNow_finishes() {
val vm = mockk<OnboardingViewModel>(relaxed = true)
every { vm.batteryUnrestricted } returns MutableStateFlow(false)
var finished = false
setContent(vm, onFinish = { finished = true })
composeTestRule.onNodeWithText(string(R.string.onboarding_battery_not_now)).performClick()
assertTrue(finished)
}
}
@@ -0,0 +1,204 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package org.libremail.ui.onboarding
import android.content.Context
import androidx.activity.compose.LocalActivityResultRegistryOwner
import androidx.activity.result.ActivityResultRegistry
import androidx.activity.result.ActivityResultRegistryOwner
import androidx.activity.result.contract.ActivityResultContract
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.core.app.ActivityOptionsCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.compose.LocalLifecycleOwner
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.libremail.R
import org.libremail.ui.theme.LibreMailTheme
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode
/**
* Robolectric JVM Compose test (#377, umbrella #373) for the onboarding contacts-access step (#127,
* #128). Two layers:
* - the stateless [ContactsAccessContent] driven with explicit signals for its three paths — skip,
* grant (the "done" state), and request (with the re-ask rationale) — mirroring the instrumented
* `ContactsAccessStepTest`;
* - the [ContactsAccessScreen] wrapper driven with a mocked [OnboardingViewModel] exposing the
* `contactsGranted` StateFlow, so the wrapper's own wiring (the allow → mark-requested → launch
* path, skip/continue, and the ON_RESUME re-read) gains JVM coverage without a live permission
* dialog.
*
* This file is dropped from `jacocoNonJvmTestableSurface`; the instrumented test stays.
*/
// A tall display (`+h2000dp`, merged onto the default device config) so the centered, non-scrolling
// column fits its full state — including the re-ask rationale plus Allow and Not now — without the
// lower controls clipping below Robolectric's small default viewport (which would make performClick
// miss).
@RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(sdk = [36], qualifiers = "+w411dp-h2000dp")
class ContactsAccessScreenJvmTest {
@get:Rule
val composeTestRule = createComposeRule()
private val context: Context get() = RuntimeEnvironment.getApplication()
private fun string(resId: Int): String = context.getString(resId)
/** RESUMED owner so `collectAsStateWithLifecycle` collects and the ON_RESUME effect fires. */
private val resumedOwner = object : LifecycleOwner {
private val registry =
LifecycleRegistry.createUnsafe(this).apply { currentState = Lifecycle.State.RESUMED }
override val lifecycle: Lifecycle get() = registry
}
/** A no-op registry so the wrapper's READ_CONTACTS launcher never surfaces a system dialog. */
private val noopRegistryOwner = object : ActivityResultRegistryOwner {
override val activityResultRegistry = object : ActivityResultRegistry() {
override fun <I, O> onLaunch(
requestCode: Int,
contract: ActivityResultContract<I, O>,
input: I,
options: ActivityOptionsCompat?,
) {
// Intentionally never dispatch a result: the launch is a no-op in this JVM test.
}
}
}
// --- Stateless ContactsAccessContent -------------------------------------------------------
private fun setContent(
granted: Boolean,
showRationale: Boolean = false,
onAllow: () -> Unit = {},
onSkip: () -> Unit = {},
onContinue: () -> Unit = {},
) {
composeTestRule.setContent {
LibreMailTheme(darkTheme = false, dynamicColor = false) {
ContactsAccessContent(
granted = granted,
showRationale = showRationale,
onAllow = onAllow,
onSkip = onSkip,
onContinue = onContinue,
)
}
}
}
@Test
fun notGranted_notNow_skipsTheStep() {
var skipped = false
var allowed = false
setContent(granted = false, onAllow = { allowed = true }, onSkip = { skipped = true })
composeTestRule.onNodeWithText(string(R.string.onboarding_contacts_title)).assertIsDisplayed()
composeTestRule.onNodeWithText(string(R.string.onboarding_contacts_not_now)).performClick()
assertTrue("Not now must invoke the skip callback", skipped)
assertFalse("Skipping must not request the permission", allowed)
}
@Test
fun notGranted_allow_triggersTheRequest() {
var allowed = false
setContent(granted = false, onAllow = { allowed = true })
composeTestRule.onNodeWithText(string(R.string.onboarding_contacts_allow)).performClick()
assertTrue("Allow must trigger the permission request", allowed)
}
@Test
fun granted_showsDoneState_andContinues() {
var continued = false
setContent(granted = true, onContinue = { continued = true })
composeTestRule.onNodeWithText(string(R.string.onboarding_contacts_done_title)).assertIsDisplayed()
composeTestRule.onNodeWithText(string(R.string.onboarding_contacts_allow)).assertDoesNotExist()
composeTestRule.onNodeWithText(string(R.string.onboarding_contacts_not_now)).assertDoesNotExist()
composeTestRule.onNodeWithText(string(R.string.onboarding_contacts_continue)).performClick()
assertTrue("Continue must invoke the continue callback", continued)
}
@Test
fun reRequest_showsRationale() {
setContent(granted = false, showRationale = true)
composeTestRule.onNodeWithText(string(R.string.onboarding_contacts_rationale)).assertIsDisplayed()
composeTestRule.onNodeWithText(string(R.string.onboarding_contacts_allow)).assertIsDisplayed()
}
// --- ContactsAccessScreen wrapper (VM-driven) ----------------------------------------------
private fun setWrapper(viewModel: OnboardingViewModel, onFinish: () -> Unit = {}) {
composeTestRule.setContent {
CompositionLocalProvider(
LocalLifecycleOwner provides resumedOwner,
LocalActivityResultRegistryOwner provides noopRegistryOwner,
) {
LibreMailTheme(darkTheme = false, dynamicColor = false) {
ContactsAccessScreen(viewModel = viewModel, onFinish = onFinish)
}
}
}
}
@Test
fun wrapper_granted_showsDone_continuesAndRereadsOnResume() {
val vm = mockk<OnboardingViewModel>(relaxed = true)
every { vm.contactsGranted } returns MutableStateFlow(true)
var finished = false
setWrapper(vm, onFinish = { finished = true })
composeTestRule.onNodeWithText(string(R.string.onboarding_contacts_done_title)).assertIsDisplayed()
composeTestRule.onNodeWithText(string(R.string.onboarding_contacts_continue)).performClick()
assertTrue(finished)
// The ON_RESUME effect re-reads the live grant when the step comes to the fore.
verify { vm.refreshContactsStatus() }
}
@Test
fun wrapper_notGranted_allow_marksRequestedAndLaunches() {
val vm = mockk<OnboardingViewModel>(relaxed = true)
every { vm.contactsGranted } returns MutableStateFlow(false)
setWrapper(vm)
composeTestRule.onNodeWithText(string(R.string.onboarding_contacts_allow)).performClick()
// Allow persists "the dialog was shown" up front, then launches the system request.
verify { vm.markContactsPermissionRequested() }
}
@Test
fun wrapper_notGranted_notNow_finishes() {
val vm = mockk<OnboardingViewModel>(relaxed = true)
every { vm.contactsGranted } returns MutableStateFlow(false)
var finished = false
setWrapper(vm, onFinish = { finished = true })
composeTestRule.onNodeWithText(string(R.string.onboarding_contacts_not_now)).performClick()
assertTrue(finished)
}
}
@@ -0,0 +1,113 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package org.libremail.ui.onboarding
import android.content.Context
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.OnBackPressedDispatcherOwner
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.compose.LocalLifecycleOwner
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.libremail.R
import org.libremail.ui.theme.LibreMailTheme
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode
/**
* Robolectric JVM Compose test (#377, umbrella #373) for [LicenseScreen] (#172). Mirrors the
* instrumented `LicenseScreenTest` on the JVM: Agree is gated on having scrolled to the end, Decline
* always works, and the real bundled GPL-3.0 text (`res/raw/license.txt`) actually renders — proving
* the merged Android resources loaded on the JVM. This file is dropped from
* `jacocoNonJvmTestableSurface`; the instrumented test stays (and remains the coverage for the system
* back press, which the JVM rule cannot drive — see [owner]).
*/
@RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(sdk = [36])
class LicenseScreenJvmTest {
@get:Rule
val composeTestRule = createComposeRule()
private val context: Context get() = RuntimeEnvironment.getApplication()
private fun string(resId: Int): String = context.getString(resId)
// LicenseScreen's BackHandler requires both a lifecycle owner and an OnBackPressedDispatcherOwner
// in the composition, so a resumed owner is provided for it to compose against. The actual
// back-press → onDecline behaviour is covered by the instrumented LicenseScreenTest (kept per
// #377); here the BackHandler registration is exercised by rendering and the onDecline path by the
// Decline button, so this JVM test deliberately does not re-drive a system back press (which the
// v2 Robolectric compose rule does not surface a reliable dispatch hook for).
private val owner = object : OnBackPressedDispatcherOwner {
private val registry =
LifecycleRegistry.createUnsafe(this).apply { currentState = Lifecycle.State.RESUMED }
private val dispatcher = OnBackPressedDispatcher()
override val lifecycle: Lifecycle get() = registry
override val onBackPressedDispatcher: OnBackPressedDispatcher get() = dispatcher
}
private fun setContent(onAgree: () -> Unit = {}, onDecline: () -> Unit = {}) {
composeTestRule.setContent {
CompositionLocalProvider(
LocalLifecycleOwner provides owner,
LocalOnBackPressedDispatcherOwner provides owner,
) {
LibreMailTheme(darkTheme = false, dynamicColor = false) {
LicenseScreen(onAgree = onAgree, onDecline = onDecline)
}
}
}
}
@Test
fun licenseText_rendersTheRealBundledGplText() {
setContent()
// Proves res/raw/license.txt actually loaded, not just that some placeholder text exists.
composeTestRule.onNodeWithText("GNU GENERAL PUBLIC LICENSE", substring = true).assertExists()
}
@Test
fun agreeButton_isDisabledUntilScrolledToTheEnd() {
setContent()
composeTestRule.onNodeWithText(string(R.string.license_agree)).assertIsNotEnabled()
}
@Test
fun agreeButton_scrolledToEnd_becomesEnabledAndInvokesOnAgree() {
var agreed = false
setContent(onAgree = { agreed = true })
composeTestRule.onNodeWithTag(LICENSE_SCROLL_END_TAG).performScrollTo()
composeTestRule.onNodeWithText(string(R.string.license_agree)).assertIsEnabled().performClick()
assertTrue(agreed)
}
@Test
fun declineButton_worksWithoutScrolling_andInvokesOnDecline() {
var declined = false
setContent(onDecline = { declined = true })
// No scrolling first: Decline must never be gated the way Agree is.
composeTestRule.onNodeWithText(string(R.string.license_decline)).assertIsEnabled().performClick()
assertTrue(declined)
}
}
@@ -0,0 +1,110 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package org.libremail.ui.onboarding
import android.content.Context
import androidx.activity.compose.LocalActivityResultRegistryOwner
import androidx.activity.result.ActivityResultRegistry
import androidx.activity.result.ActivityResultRegistryOwner
import androidx.activity.result.contract.ActivityResultContract
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.core.app.ActivityOptionsCompat
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.libremail.R
import org.libremail.ui.theme.LibreMailTheme
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode
/**
* Robolectric JVM Compose test (#377, umbrella #373) for the first-run welcome. Renders
* [WelcomeContent] directly for the render + "add account" interaction — as the instrumented
* `OnboardingWelcomeScreenTest` does — to avoid the real POST_NOTIFICATIONS permission dialog that
* [OnboardingWelcomeScreen]'s `NotificationPermissionEffect` would otherwise fire on a device.
*
* A separate case does drive the full [OnboardingWelcomeScreen] wrapper (Scaffold + the notification
* effect), because on the JVM the effect's launcher can be wired to a no-op [ActivityResultRegistry]
* so it never surfaces a system dialog — bringing the wrapper + effect into JVM coverage too. This
* file is dropped from `jacocoNonJvmTestableSurface`; the instrumented test stays.
*/
@RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(sdk = [36])
class OnboardingWelcomeScreenJvmTest {
@get:Rule
val composeTestRule = createComposeRule()
private val context: Context get() = RuntimeEnvironment.getApplication()
private fun string(resId: Int): String = context.getString(resId)
/**
* A no-op [ActivityResultRegistry] so [OnboardingWelcomeScreen]'s notification-permission launcher
* can register and fire on the JVM without ever surfacing a real system permission dialog.
*/
private val noopRegistryOwner = object : ActivityResultRegistryOwner {
override val activityResultRegistry = object : ActivityResultRegistry() {
override fun <I, O> onLaunch(
requestCode: Int,
contract: ActivityResultContract<I, O>,
input: I,
options: ActivityOptionsCompat?,
) {
// Intentionally never dispatch a result: the launch is a no-op in this JVM test.
}
}
}
@Test
fun welcomeContent_rendersTitleSubtitleAndAddAccount() {
composeTestRule.setContent {
LibreMailTheme(darkTheme = false, dynamicColor = false) {
WelcomeContent(onAddAccount = {})
}
}
composeTestRule.onNodeWithText(string(R.string.onboarding_welcome_title)).assertIsDisplayed()
composeTestRule.onNodeWithText(string(R.string.onboarding_welcome_subtitle)).assertIsDisplayed()
composeTestRule.onNodeWithText(string(R.string.onboarding_add_account)).assertIsDisplayed()
}
@Test
fun welcomeContent_tappingAddAccount_invokesCallback() {
var added = false
composeTestRule.setContent {
LibreMailTheme(darkTheme = false, dynamicColor = false) {
WelcomeContent(onAddAccount = { added = true })
}
}
composeTestRule.onNodeWithText(string(R.string.onboarding_add_account)).performClick()
assertTrue(added)
}
@Test
fun onboardingWelcomeScreen_rendersWelcome_andAddAccountReachesCallback() {
var added = false
composeTestRule.setContent {
CompositionLocalProvider(LocalActivityResultRegistryOwner provides noopRegistryOwner) {
LibreMailTheme(darkTheme = false, dynamicColor = false) {
OnboardingWelcomeScreen(onAddAccount = { added = true })
}
}
}
// The full screen (Scaffold + NotificationPermissionEffect) composes and shows the welcome copy.
composeTestRule.onNodeWithText(string(R.string.onboarding_welcome_title)).assertIsDisplayed()
composeTestRule.onNodeWithText(string(R.string.onboarding_add_account)).performClick()
assertTrue(added)
}
}