Merge pull request #396 from JMR-dev/test-377-robolectric-onboarding-lock
test(compose): Robolectric JVM tests for onboarding & lock screens (#377)
This commit was merged in pull request #396.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user