Merge pull request #394 from JMR-dev/test-376-robolectric-richtext
test(compose): Robolectric JVM tests for rich-text format controls (#376)
This commit was merged in pull request #394.
This commit is contained in:
+17
-4
@@ -1,4 +1,5 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
import org.gradle.testing.jacoco.plugins.JacocoTaskExtension
|
||||
import org.gradle.testing.jacoco.tasks.JacocoCoverageVerification
|
||||
import org.gradle.testing.jacoco.tasks.JacocoReport
|
||||
import java.util.Properties
|
||||
@@ -212,6 +213,20 @@ jacoco {
|
||||
toolVersion = libs.versions.jacoco.get()
|
||||
}
|
||||
|
||||
// The Robolectric-backed JVM Compose UI tests (#373) load the classes-under-test through
|
||||
// Robolectric's sandbox classloader, which presents them to the JaCoCo agent WITHOUT a code-source
|
||||
// location. JaCoCo skips no-location classes by default, so on-the-fly coverage for every composable
|
||||
// exercised only by a Robolectric test would silently record as zero — the file would be removed
|
||||
// from `jacocoNonJvmTestableSurface` yet contribute nothing but missed lines, dragging the bundle
|
||||
// ratio DOWN instead of up. `isIncludeNoLocationClasses = true` makes the agent keep that coverage;
|
||||
// `jdk.internal.*` is excluded because instrumenting those JDK classes breaks under JDK 17+.
|
||||
tasks.withType<Test>().configureEach {
|
||||
configure<JacocoTaskExtension> {
|
||||
isIncludeNoLocationClasses = true
|
||||
excludes = listOf("jdk.internal.*")
|
||||
}
|
||||
}
|
||||
|
||||
// Unit-test coverage (issue #192). Two tasks share ONE scoping so they can never measure different
|
||||
// surfaces: `jacocoTestReport` (XML+HTML under build/reports/jacoco/jacocoTestReport/) and
|
||||
// `jacocoTestCoverageVerification` (the no-regression gate, further down). Both read the exec data
|
||||
@@ -302,10 +317,8 @@ val jacocoNonJvmTestableSurface = listOf(
|
||||
"**/AppPasswordSetupScreen*",
|
||||
"**/ManualSetupScreen*",
|
||||
"**/ComposeScreen*",
|
||||
"**/ColorSwatch*",
|
||||
"**/FontPicker*",
|
||||
"**/FontSizePicker*",
|
||||
"**/ParagraphAlignmentControl*",
|
||||
// ColorSwatch(Row), FontPicker, FontSizePicker & ParagraphAlignmentControl converted to
|
||||
// Robolectric JVM Compose tests (#376) — now JVM-covered.
|
||||
"**/DraftsScreen*",
|
||||
"**/LockScreen*",
|
||||
"**/AppLockGateHost*",
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package org.libremail.ui.compose.format
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsNotSelected
|
||||
import androidx.compose.ui.test.assertIsSelected
|
||||
import androidx.compose.ui.test.junit4.v2.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.performClick
|
||||
import org.junit.Assert.assertEquals
|
||||
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 port of the instrumented `ColorSwatchRowTest` (#376, umbrella #373): drives the
|
||||
* shared font-color / highlight swatch row on the JVM under [RobolectricTestRunner] via the v2
|
||||
* `createComposeRule()` — no emulator — so [ColorSwatchRow] and its [ColorSwatch] model count toward
|
||||
* JaCoCo's JVM-testable surface. Every swatch is a TalkBack-labeled, selectable button, so nodes are
|
||||
* addressed by their content description. The instrumented `ColorSwatchRowTest` stays as the
|
||||
* on-device E2E. See [org.libremail.ui.onboarding.AddAnotherAccountScreenJvmTest] for the pattern.
|
||||
*/
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||
@Config(sdk = [36])
|
||||
class ColorSwatchRowJvmTest {
|
||||
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
private val context: Context get() = RuntimeEnvironment.getApplication()
|
||||
|
||||
private fun string(resId: Int): String = context.getString(resId)
|
||||
|
||||
private val red = ColorSwatch(argb = 0xFFD32F2F.toInt(), label = "Red")
|
||||
private val blue = ColorSwatch(argb = 0xFF1976D2.toInt(), label = "Blue")
|
||||
|
||||
private fun setContent(selectedArgb: Int?, onSelect: (Int?) -> Unit = {}) {
|
||||
composeTestRule.setContent {
|
||||
LibreMailTheme(darkTheme = false, dynamicColor = false) {
|
||||
ColorSwatchRow(swatches = listOf(red, blue), selectedArgb = selectedArgb, onSelect = onSelect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun showsNoneEntry_andEverySwatch() {
|
||||
setContent(selectedArgb = null)
|
||||
|
||||
composeTestRule.onNodeWithContentDescription(string(R.string.format_color_none)).assertIsDisplayed()
|
||||
composeTestRule.onNodeWithContentDescription(red.label).assertIsDisplayed()
|
||||
composeTestRule.onNodeWithContentDescription(blue.label).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tappingASwatch_reportsItsArgb() {
|
||||
var picked: Int? = -1
|
||||
setContent(selectedArgb = null) { picked = it }
|
||||
|
||||
composeTestRule.onNodeWithContentDescription(blue.label).performClick()
|
||||
|
||||
assertEquals(blue.argb, picked)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tappingNoneEntry_reportsNull() {
|
||||
var picked: Int? = red.argb
|
||||
setContent(selectedArgb = red.argb) { picked = it }
|
||||
|
||||
composeTestRule.onNodeWithContentDescription(string(R.string.format_color_none)).performClick()
|
||||
|
||||
assertEquals(null, picked)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun selectedSwatch_isMarkedSelected_andOthersAreNot() {
|
||||
setContent(selectedArgb = red.argb)
|
||||
|
||||
composeTestRule.onNodeWithContentDescription(red.label).assertIsSelected()
|
||||
composeTestRule.onNodeWithContentDescription(blue.label).assertIsNotSelected()
|
||||
// With a color selected, the leading "no color" entry is not the selected one.
|
||||
composeTestRule.onNodeWithContentDescription(string(R.string.format_color_none)).assertIsNotSelected()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noSelection_marksTheNoneEntrySelected() {
|
||||
setContent(selectedArgb = null)
|
||||
|
||||
composeTestRule.onNodeWithContentDescription(string(R.string.format_color_none)).assertIsSelected()
|
||||
composeTestRule.onNodeWithContentDescription(red.label).assertIsNotSelected()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package org.libremail.ui.compose.format
|
||||
|
||||
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.assertEquals
|
||||
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 port of the instrumented `FontPickerTest` (#376, umbrella #373): drives the
|
||||
* formatting toolbar's font-family dropdown on the JVM under [RobolectricTestRunner] via the v2
|
||||
* `createComposeRule()` — no emulator — so [FontPicker] counts toward JaCoCo's JVM-testable surface.
|
||||
* Exercises the anchor label for the null (Default) and selected states plus opening the menu and
|
||||
* picking a font / the Default entry, which drives the null-vs-CSS-stack `onSelect` branch. The
|
||||
* instrumented `FontPickerTest` stays as the on-device E2E. See
|
||||
* [org.libremail.ui.onboarding.AddAnotherAccountScreenJvmTest] for the pattern.
|
||||
*/
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||
@Config(sdk = [36])
|
||||
class FontPickerJvmTest {
|
||||
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
private val context: Context get() = RuntimeEnvironment.getApplication()
|
||||
|
||||
private fun string(resId: Int): String = context.getString(resId)
|
||||
|
||||
private fun setContent(selectedCss: String?, onSelect: (String?) -> Unit = {}) {
|
||||
composeTestRule.setContent {
|
||||
LibreMailTheme(darkTheme = false, dynamicColor = false) {
|
||||
FontPicker(selectedCss = selectedCss, onSelect = onSelect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noFontSelected_buttonShowsDefaultLabel() {
|
||||
setContent(selectedCss = null)
|
||||
|
||||
composeTestRule.onNodeWithText(string(R.string.format_font_default)).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aFontSelected_buttonShowsItsDisplayName() {
|
||||
val inter = FontRegistry.choices.first { it.name == "Inter" }
|
||||
setContent(selectedCss = inter.css)
|
||||
|
||||
composeTestRule.onNodeWithText("Inter").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tappingTheButton_opensAMenuListingEveryRegistryFont() {
|
||||
setContent(selectedCss = null)
|
||||
|
||||
composeTestRule.onNodeWithText(string(R.string.format_font_default)).performClick()
|
||||
|
||||
FontRegistry.choices.forEach { choice ->
|
||||
composeTestRule.onNodeWithText(choice.name).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pickingAFontFromTheMenu_reportsItsCssStack() {
|
||||
var picked: String? = "unset"
|
||||
val lora = FontRegistry.choices.first { it.name == "Lora" }
|
||||
setContent(selectedCss = null) { picked = it }
|
||||
|
||||
composeTestRule.onNodeWithText(string(R.string.format_font_default)).performClick()
|
||||
composeTestRule.onNodeWithText("Lora").performClick()
|
||||
|
||||
assertEquals(lora.css, picked)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pickingDefaultFromTheMenu_clearsBySelectingNull() {
|
||||
var picked: String? = "unset"
|
||||
val inter = FontRegistry.choices.first { it.name == "Inter" }
|
||||
// The button reads "Inter" here, so the menu's own "Default" entry is the only such match.
|
||||
setContent(selectedCss = inter.css) { picked = it }
|
||||
|
||||
composeTestRule.onNodeWithText("Inter").performClick()
|
||||
composeTestRule.onNodeWithText(string(R.string.format_font_default)).performClick()
|
||||
|
||||
assertEquals(null, picked)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package org.libremail.ui.compose.format
|
||||
|
||||
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.assertEquals
|
||||
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 port of the instrumented `FontSizePickerTest` (#376, umbrella #373): drives the
|
||||
* formatting toolbar's font-size dropdown on the JVM under [RobolectricTestRunner] via the v2
|
||||
* `createComposeRule()` — no emulator — so [FontSizePicker] counts toward JaCoCo's JVM-testable
|
||||
* surface. Exercises the anchor label for the null (Default) and selected states plus opening the
|
||||
* menu and picking a preset / the Default entry, which drives the null-vs-point-size `onSelect`
|
||||
* branch. The instrumented `FontSizePickerTest` stays as the on-device E2E. See
|
||||
* [org.libremail.ui.onboarding.AddAnotherAccountScreenJvmTest] for the pattern.
|
||||
*/
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||
@Config(sdk = [36])
|
||||
class FontSizePickerJvmTest {
|
||||
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
private val context: Context get() = RuntimeEnvironment.getApplication()
|
||||
|
||||
private fun string(resId: Int): String = context.getString(resId)
|
||||
|
||||
private fun string(resId: Int, vararg args: Any): String = context.getString(resId, *args)
|
||||
|
||||
private fun setContent(selectedPt: Int?, onSelect: (Int?) -> Unit = {}) {
|
||||
composeTestRule.setContent {
|
||||
LibreMailTheme(darkTheme = false, dynamicColor = false) {
|
||||
FontSizePicker(selectedPt = selectedPt, onSelect = onSelect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noSizeSelected_buttonShowsDefaultLabel() {
|
||||
setContent(selectedPt = null)
|
||||
|
||||
composeTestRule.onNodeWithText(string(R.string.format_size_default)).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aSizeSelected_buttonShowsItsPointValue() {
|
||||
setContent(selectedPt = 18)
|
||||
|
||||
composeTestRule.onNodeWithText(string(R.string.format_size_pt, 18)).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tappingTheButton_opensAMenuListingDefaultAndEveryPreset() {
|
||||
setContent(selectedPt = null)
|
||||
|
||||
// Before the menu opens, "Default" only labels the anchor button itself - a unique match.
|
||||
composeTestRule.onNodeWithText(string(R.string.format_size_default)).performClick()
|
||||
|
||||
FONT_SIZE_PRESETS_PT.forEach { pt ->
|
||||
composeTestRule.onNodeWithText(string(R.string.format_size_pt, pt)).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pickingAPresetFromTheMenu_reportsItsPointSize() {
|
||||
var picked: Int? = -1
|
||||
setContent(selectedPt = null) { picked = it }
|
||||
|
||||
composeTestRule.onNodeWithText(string(R.string.format_size_default)).performClick()
|
||||
composeTestRule.onNodeWithText(string(R.string.format_size_pt, 14)).performClick()
|
||||
|
||||
assertEquals(14, picked)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pickingDefaultFromTheMenu_clearsBySelectingNull() {
|
||||
var picked: Int? = 12
|
||||
setContent(selectedPt = 12) { picked = it }
|
||||
|
||||
// The button reads "12 pt" here, so the menu's own "Default" entry is the only such match.
|
||||
composeTestRule.onNodeWithText(string(R.string.format_size_pt, 12)).performClick()
|
||||
composeTestRule.onNodeWithText(string(R.string.format_size_default)).performClick()
|
||||
|
||||
assertEquals(null, picked)
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package org.libremail.ui.compose.format
|
||||
|
||||
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.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.libremail.richtext.RichAlign
|
||||
import org.libremail.ui.theme.LibreMailTheme
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.robolectric.annotation.GraphicsMode
|
||||
|
||||
/**
|
||||
* Robolectric JVM port of the instrumented `ParagraphAlignmentControlTest` (#376, umbrella #373):
|
||||
* drives the three-state paragraph-alignment control on the JVM under [RobolectricTestRunner] via
|
||||
* the v2 `createComposeRule()` — no emulator — so [ParagraphAlignmentControl] counts toward JaCoCo's
|
||||
* JVM-testable surface. Each button is a bare glyph, so nodes are addressed by the shared glyph
|
||||
* constants. The instrumented `ParagraphAlignmentControlTest` stays as the on-device E2E. See
|
||||
* [org.libremail.ui.onboarding.AddAnotherAccountScreenJvmTest] for the pattern.
|
||||
*/
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||
@Config(sdk = [36])
|
||||
class ParagraphAlignmentControlJvmTest {
|
||||
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
private fun setContent(selected: RichAlign?, onSelect: (RichAlign) -> Unit = {}) {
|
||||
composeTestRule.setContent {
|
||||
LibreMailTheme(darkTheme = false, dynamicColor = false) {
|
||||
ParagraphAlignmentControl(selected = selected, onSelect = onSelect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun showsAllThreeAlignmentGlyphs() {
|
||||
setContent(selected = RichAlign.START)
|
||||
|
||||
composeTestRule.onNodeWithText(ALIGN_START_GLYPH).assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText(ALIGN_CENTER_GLYPH).assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText(ALIGN_END_GLYPH).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tappingCenter_reportsCenter() {
|
||||
var picked: RichAlign? = null
|
||||
setContent(selected = RichAlign.START) { picked = it }
|
||||
|
||||
composeTestRule.onNodeWithText(ALIGN_CENTER_GLYPH).performClick()
|
||||
|
||||
assertEquals(RichAlign.CENTER, picked)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tappingEnd_reportsEnd() {
|
||||
var picked: RichAlign? = null
|
||||
setContent(selected = RichAlign.START) { picked = it }
|
||||
|
||||
composeTestRule.onNodeWithText(ALIGN_END_GLYPH).performClick()
|
||||
|
||||
assertEquals(RichAlign.END, picked)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tappingStart_reportsStart() {
|
||||
var picked: RichAlign? = null
|
||||
setContent(selected = RichAlign.CENTER) { picked = it }
|
||||
|
||||
composeTestRule.onNodeWithText(ALIGN_START_GLYPH).performClick()
|
||||
|
||||
assertEquals(RichAlign.START, picked)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user