Merge branch 'main' into fix-reader-inline-images

This commit is contained in:
Jason Ross
2026-07-02 11:25:41 -05:00
committed by GitHub
41 changed files with 1827 additions and 157 deletions
@@ -67,8 +67,9 @@ class ComposeScreenTest {
@Before
fun grantContactsPermission() {
// ComposeScreen requests READ_CONTACTS on first composition; pre-grant it (before the test
// calls setContent) so no system permission dialog appears to block the headless run.
// ComposeScreen no longer requests READ_CONTACTS (the request moved to onboarding/#127); it
// only reads the current grant on resume. Pre-grant it (before setContent) so contactsAllowed
// resolves true and the autocomplete path stays exercised — no system dialog is ever shown.
val instrumentation = InstrumentationRegistry.getInstrumentation()
instrumentation.uiAutomation.grantRuntimePermission(
instrumentation.targetContext.packageName,
@@ -30,6 +30,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.libremail.R
import org.libremail.contacts.ContactsPermissionManager
import org.libremail.data.settings.SettingsRepository
import org.libremail.push.BatteryOptimizationManager
import org.libremail.ui.navigation.Routes
@@ -70,7 +71,11 @@ class BatteryOptimizationStepTest {
val context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
settingsRepository = SettingsRepository(context)
runBlocking { settingsRepository.setBatteryPromptHandled(handled) }
onboarding = OnboardingViewModel(BatteryOptimizationManager(context), settingsRepository)
onboarding = OnboardingViewModel(
BatteryOptimizationManager(context),
ContactsPermissionManager(context),
settingsRepository,
)
onboarding.onAccountAdded(FIRST_ACCOUNT_ID)
composeTestRule.setContent {
@@ -0,0 +1,97 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package org.libremail.ui.onboarding
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
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
/**
* UI tests for the onboarding contacts-access step (#127, #128). They drive the presentational
* [ContactsAccessContent] with explicit signals so the three paths — skip, grant (the "done" state),
* and request (with the re-ask rationale) — run deterministically without a live system permission
* dialog (whose grant state would otherwise leak across the shared instrumentation process).
*/
@RunWith(AndroidJUnit4::class)
class ContactsAccessStepTest {
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
private fun string(resId: Int) = composeTestRule.activity.getString(resId)
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 })
// The "done" copy is shown and the request/skip buttons are gone.
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)
// A re-request explains itself (shouldShowRequestPermissionRationale handling, #128).
composeTestRule.onNodeWithText(string(R.string.onboarding_contacts_rationale)).assertIsDisplayed()
composeTestRule.onNodeWithText(string(R.string.onboarding_contacts_allow)).assertIsDisplayed()
}
}
@@ -23,6 +23,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.libremail.R
import org.libremail.auth.OutlookAuthManager
import org.libremail.contacts.ContactsPermissionManager
import org.libremail.data.settings.SettingsRepository
import org.libremail.domain.model.Message
import org.libremail.push.BatteryOptimizationManager
@@ -85,6 +86,7 @@ class OnboardingFlowTest {
val appContext = composeTestRule.activity.applicationContext
val onboarding = OnboardingViewModel(
BatteryOptimizationManager(appContext),
ContactsPermissionManager(appContext),
SettingsRepository(appContext),
)
composeTestRule.setContent {
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package org.libremail.ui.settings
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
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.contacts.ContactPermissionState
import org.libremail.ui.theme.LibreMailTheme
/**
* UI tests for the Settings contacts-autocomplete row (#129). The row is presentational, so each of
* its three states — on / off / blocked-in-settings — is driven directly and asserted deterministically,
* independent of the process's real `READ_CONTACTS` grant.
*/
@RunWith(AndroidJUnit4::class)
class ContactAutocompleteRowTest {
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
private fun string(resId: Int) = composeTestRule.activity.getString(resId)
private fun setContent(state: ContactPermissionState, onClick: () -> Unit = {}) {
composeTestRule.setContent {
LibreMailTheme(darkTheme = false, dynamicColor = false) {
ContactAutocompleteRow(state = state, onClick = onClick)
}
}
}
@Test
fun granted_showsOnSubtitle_andIsClickable() {
var clicked = false
setContent(ContactPermissionState.GRANTED) { clicked = true }
composeTestRule.onNodeWithText(string(R.string.settings_contacts_autocomplete)).assertIsDisplayed()
composeTestRule.onNodeWithText(string(R.string.settings_contacts_autocomplete_on)).assertIsDisplayed()
composeTestRule.onNodeWithText(string(R.string.settings_contacts_autocomplete_on)).performClick()
assertTrue("Tapping the row must invoke onClick", clicked)
}
@Test
fun denied_showsOffSubtitle() {
setContent(ContactPermissionState.DENIED)
composeTestRule.onNodeWithText(string(R.string.settings_contacts_autocomplete_off)).assertIsDisplayed()
}
@Test
fun blocked_showsBlockedSubtitle() {
setContent(ContactPermissionState.BLOCKED)
composeTestRule.onNodeWithText(string(R.string.settings_contacts_autocomplete_blocked)).assertIsDisplayed()
}
}
@@ -2,6 +2,7 @@
package org.libremail.ui.settings
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithText
@@ -15,6 +16,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.libremail.R
import org.libremail.contacts.ContactsPermissionManager
import org.libremail.data.security.AppLockManager
import org.libremail.data.security.DatabaseKeyCipher
import org.libremail.data.security.DatabaseKeyStore
@@ -57,6 +59,7 @@ class SettingsScreenTest {
insecureDevice,
keyStore,
BatteryOptimizationManager(context),
ContactsPermissionManager(context),
SyncScheduler(Provider { WorkManager.getInstance(context) }),
)
}
@@ -88,6 +91,16 @@ class SettingsScreenTest {
}
}
@Test
fun contactsAutocompleteRow_isShown() {
// The contacts entry (#129) is wired into the real screen; it reflects the live permission
// state, so we assert only that the row is present (state-specific rendering is covered by
// ContactAutocompleteRowTest).
setContent(settingsViewModel(SettingsRepository(context)))
composeTestRule.onNodeWithText(string(R.string.settings_contacts_autocomplete))
.performScrollTo().assertIsDisplayed()
}
@Test
fun enablingAppLockWithoutSecureDevice_showsRejectionSnackbar() {
val settingsRepository = SettingsRepository(context)