Merge main into feat-160-app-password-disclaimer

This commit is contained in:
Jason Ross
2026-07-02 23:09:17 -05:00
committed by GitHub
15 changed files with 1118 additions and 7 deletions
@@ -0,0 +1,92 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package org.libremail.ui.onboarding
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.junit4.createAndroidComposeRule
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.test.espresso.Espresso
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.ui.theme.LibreMailTheme
/**
* Component test for [LicenseScreen] (#172), in isolation from the real onboarding nav graph (that
* wiring — persisting acceptance, popping the license off the back stack, calling `finish()` on
* Decline — belongs to `onboardingGraph()` in `LibreMailApp.kt`, not this composable). This test only
* owns the screen's own contract: Agree is gated on having scrolled to the end, Decline (and back)
* always works, and the real bundled GPL-3.0 text actually renders.
*/
@RunWith(AndroidJUnit4::class)
class LicenseScreenTest {
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
private fun string(resId: Int) = composeTestRule.activity.getString(resId)
private fun setContent(onAgree: () -> Unit = {}, onDecline: () -> Unit = {}) {
composeTestRule.setContent {
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 —
// an empty or missing resource would make agreeButton_isDisabledUntilScrolledToTheEnd below
// pass for the wrong reason (nothing to scroll).
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)
}
@Test
fun systemBack_invokesOnDeclineJustLikeTheButton() {
var declined = false
setContent(onDecline = { declined = true })
Espresso.pressBack()
assertTrue(declined)
}
}
@@ -99,6 +99,12 @@ class OnboardingFlowTest {
// Real collaborators are cheap here: the manager just wraps PowerManager and the repository
// reads the on-device settings DataStore. This test drives its own nav graph (without the
// battery step), so the onboarding view model's battery decision is inert for this flow.
//
// This hand-rolled graph starts at ONBOARDING_WELCOME directly, deliberately bypassing the
// GPL-3.0 license gate (#172) that precedes it in the real onboardingGraph() in
// LibreMailApp.kt: this test owns the picker → setup → finish tail, not the license screen's
// own contract (scroll-to-agree, decline-exits, hard-gating), which LicenseScreenTest covers
// in isolation instead.
val appContext = composeTestRule.activity.applicationContext
val onboarding = OnboardingViewModel(
BatteryOptimizationManager(appContext),