Android overhaul: one screenshot harness in :core:design's test fixtures; kvitt.android.screenshots wires Roborazzi

Eight modules each had a copy of the same configuration-context harness (ScreenSnapshots/Snapshots); M12f had to fix
the same capture bug in two of them. :core:design's test fixtures (src/testFixtures, package
se.hagstedt.kvitt.design.testing) now hold Variant, TestClock, TestEnvironment, ScreenSnapshots with its three
capture modes (Root; Screen, with the hand-advanced frame clock; ScreenWhenIdle), MainDispatcherRule and withMain.
Test fixtures rather than a :core:testing module: :core:design's own tests can use them without a dependency on
itself, no module is added to the §6.2 graph, and lint treats them as test sources. The architecture check rejects
test fixtures outside test configurations. Modules keep only their own frame (analytics household, settle size and
inline overlays, settings avatar pictures). Every golden is unchanged.

kvitt.android.screenshots applies Roborazzi with outputDir src/test/screenshots and adds the screenshot test
libraries; the feature plugin applies it and adds the test fixtures, so the seven feature build scripts lose their
copied blocks. Removed: roborazzi-junit-rule and roborazzi-compose (never imported), the explicit
lifecycle-viewmodel-compose (feature plugin, :app) and hilt-lifecycle-viewmodel-compose in :app (both still arrive
through the features; release and debug runtime classpaths are identical), and the unused catalog entries
androidx-hilt-navigation-compose and androidx-lifecycle-viewmodel-compose. @Before/@After setMain pairs in nine test
classes use MainDispatcherRule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
2026-10-18 22:54:03 +02:00
parent e4e6e5d792
commit 8229ed60ea
55 changed files with 472 additions and 664 deletions
+3 -16
View File
@@ -1,9 +1,8 @@
// :feature:onboarding — Pairing, partner profile step, waiting for partner, device removed (S-1, S-2, S-3, S-18; M7a).
// Convention: android/README.md. :core:model and :core:design come from the feature plugin; :core:fakes only for debug
// and tests.
// :feature:onboarding — Pairing, partner profile step, waiting for partner, device removed (S-1, S-2, S-3, S-18).
// Convention: android/README.md; :core:model, :core:design, :core:fakes (debug and tests), :core:design's test fixtures
// and the screenshot tests come from kvitt.android.feature.
plugins {
id("kvitt.android.feature")
alias(libs.plugins.roborazzi)
}
android {
@@ -11,19 +10,7 @@ android {
buildFeatures { buildConfig = true }
}
roborazzi {
outputDir.set(file("src/test/screenshots"))
}
dependencies {
// The Android photo picker (AC-4.3) through ActivityResultContracts.PickVisualMedia.
implementation(libs.androidx.activity.compose)
debugImplementation(libs.androidx.compose.ui.test.manifest)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.androidx.compose.ui.test.junit4)
testImplementation(libs.robolectric)
testImplementation(libs.roborazzi)
testImplementation(libs.roborazzi.compose)
testImplementation(libs.roborazzi.junit.rule)
}
@@ -1,16 +1,11 @@
package se.hagstedt.kvitt.feature.onboarding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import se.hagstedt.kvitt.design.format.KvittLanguage
import se.hagstedt.kvitt.fakes.FakeRepositories
@@ -22,15 +17,13 @@ import se.hagstedt.kvitt.fakes.onboarding.ScriptedProfileRepository
import se.hagstedt.kvitt.fakes.onboarding.ScriptedSessionRepository
import se.hagstedt.kvitt.model.OnlineResult
import se.hagstedt.kvitt.model.SessionState
import se.hagstedt.kvitt.design.testing.MainDispatcherRule
/** S-2, S-3 and S-18 state holders over the onboarding fake scenarios. */
@OptIn(ExperimentalCoroutinesApi::class)
class OtherViewModelsTest {
@Before
fun main() = Dispatchers.setMain(UnconfinedTestDispatcher())
@After
fun reset() = Dispatchers.resetMain()
@get:Rule
val main = MainDispatcherRule()
private fun profile(scenario: FakeScenario, picture: CallOutcome = CallOutcome.Succeed, name: CallOutcome = CallOutcome.Succeed) =
FakeRepositories(scenario).let { f ->
@@ -1,16 +1,11 @@
package se.hagstedt.kvitt.feature.onboarding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import se.hagstedt.kvitt.fakes.FakeRepositories
import se.hagstedt.kvitt.fakes.FakeScenario
@@ -19,14 +14,12 @@ import se.hagstedt.kvitt.fakes.onboarding.OnboardingScenarios
import se.hagstedt.kvitt.fakes.onboarding.PairCall
import se.hagstedt.kvitt.fakes.onboarding.ScriptedSessionRepository
import se.hagstedt.kvitt.model.InviteKind
import se.hagstedt.kvitt.design.testing.MainDispatcherRule
@OptIn(ExperimentalCoroutinesApi::class)
class PairingViewModelTest {
@Before
fun main() = Dispatchers.setMain(UnconfinedTestDispatcher())
@After
fun reset() = Dispatchers.resetMain()
@get:Rule
val main = MainDispatcherRule()
private fun vm(scenario: FakeScenario = OnboardingScenarios.unpaired, outcome: CallOutcome = CallOutcome.Succeed, allowLanHttp: Boolean = false) =
ScriptedSessionRepository(FakeRepositories(scenario).store, outcome).let { it to PairingViewModel(it, allowLanHttp) }
@@ -1,19 +1,18 @@
package se.hagstedt.kvitt.feature.onboarding.screenshot
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.v2.createComposeRule
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.ParameterizedRobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode
import se.hagstedt.kvitt.design.testing.MainDispatcherRule
import se.hagstedt.kvitt.design.testing.ScreenSnapshots
/**
* One golden per screen state × light/dark × Swedish/English (PLAN-M7a §5, §7), plus 200 % for the states marked so.
@@ -23,22 +22,19 @@ import org.robolectric.annotation.GraphicsMode
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(ParameterizedRobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(sdk = [36], qualifiers = "w411dp-h891dp-mdpi")
@Config(sdk = [36], qualifiers = ScreenSnapshots.QUALIFIERS)
class OnboardingScreenshotTest(private val name: String) {
@get:Rule
val compose = createComposeRule()
@Before
fun mainDispatcher() = Dispatchers.setMain(UnconfinedTestDispatcher())
@After
fun resetMainDispatcher() = Dispatchers.resetMain()
@get:Rule
val main = MainDispatcherRule()
@Test
fun screen() {
val state = OnboardingStates.all.first { it.name == name }
val content = state.render()
ScreenSnapshots.snap(compose, state.name, state.largeFont, content)
ScreenSnapshots.snap(compose, state.name, state.largeFont) { Box(Modifier.fillMaxSize()) { content() } }
}
companion object {