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,13 +1,13 @@
// :app — MainActivity, Application, Hilt wiring (every :core:model interface bound to :data, D-041 and D-085),
// navigation and deep links, push handling and notifications, network security and release signing (SPEC §6.2, M8).
// navigation and deep links, push handling and notifications, network security and release signing (SPEC §6.2).
import java.util.Properties
plugins {
id("kvitt.android.application")
id("kvitt.android.compose")
id("kvitt.android.hilt")
id("kvitt.android.screenshots")
alias(libs.plugins.google.services)
alias(libs.plugins.roborazzi)
}
// SPEC §10.6: release signing from android/local.properties (git-ignored); without the keys the release build is unsigned.
@@ -67,10 +67,6 @@ android {
}
}
roborazzi {
outputDir.set(file("src/test/screenshots"))
}
dependencies {
implementation(project(":core:model"))
implementation(project(":core:design"))
@@ -85,25 +81,16 @@ dependencies {
implementation(project(":feature:analytics"))
implementation(project(":feature:settings"))
testImplementation(project(":core:fakes"))
testImplementation(testFixtures(project(":core:design")))
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.hilt.lifecycle.viewmodel.compose)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.lifecycle.process)
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.datetime)
implementation(libs.coil)
implementation(libs.coil.compose)
debugImplementation(libs.androidx.compose.ui.test.manifest)
testImplementation(libs.androidx.compose.ui.test.junit4)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.robolectric)
testImplementation(libs.roborazzi)
testImplementation(libs.roborazzi.compose)
testImplementation(libs.roborazzi.junit.rule)
}
@@ -1,31 +1,24 @@
package se.hagstedt.kvitt
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
import se.hagstedt.kvitt.fakes.FakeScenarios
import se.hagstedt.kvitt.link.AppLink
import se.hagstedt.kvitt.model.SessionState
import se.hagstedt.kvitt.design.testing.MainDispatcherRule
/** The shell's link and permission rules: AC-2.6 (D-084) and AC-2.9. A plain JVM test over the fakes. */
@OptIn(ExperimentalCoroutinesApi::class)
class AppViewModelTest {
@Before
fun setUp() = Dispatchers.setMain(UnconfinedTestDispatcher())
@After
fun tearDown() = Dispatchers.resetMain()
@get:Rule
val main = MainDispatcherRule()
private fun vm(fakes: FakeRepositories) = AppViewModel(fakes.session, fakes.sync, fakes.accounts)
@@ -11,6 +11,7 @@ import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode
import se.hagstedt.kvitt.design.KvittTheme
import se.hagstedt.kvitt.design.testing.ScreenSnapshots
/**
* Golden screenshots of the empty screen in light and dark (Roborazzi on Robolectric). `check` verifies them;
@@ -19,7 +20,7 @@ import se.hagstedt.kvitt.design.KvittTheme
*/
@RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(sdk = [36], qualifiers = "w411dp-h891dp-mdpi", application = Application::class)
@Config(sdk = [36], qualifiers = ScreenSnapshots.QUALIFIERS, application = Application::class)
class KvittEmptyScreenScreenshotTest {
@get:Rule
val compose = createComposeRule()