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:
@@ -6,6 +6,7 @@ dependencies {
|
||||
compileOnly(libs.android.gradle.plugin)
|
||||
compileOnly(libs.kotlin.gradle.plugin)
|
||||
compileOnly(libs.compose.compiler.gradle.plugin)
|
||||
compileOnly(libs.roborazzi.gradle.plugin)
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
@@ -30,6 +31,10 @@ gradlePlugin {
|
||||
id = "kvitt.android.application"
|
||||
implementationClass = "se.hagstedt.kvitt.buildlogic.AndroidApplicationPlugin"
|
||||
}
|
||||
register("kvittAndroidScreenshots") {
|
||||
id = "kvitt.android.screenshots"
|
||||
implementationClass = "se.hagstedt.kvitt.buildlogic.AndroidScreenshotsPlugin"
|
||||
}
|
||||
register("kvittAndroidHilt") {
|
||||
id = "kvitt.android.hilt"
|
||||
implementationClass = "se.hagstedt.kvitt.buildlogic.HiltPlugin"
|
||||
|
||||
+4
-2
@@ -6,7 +6,8 @@ import org.gradle.api.Project
|
||||
/**
|
||||
* Feature module (SPEC §6.2, convention in android/README.md): Compose screens against the UI contract only.
|
||||
* Features see `:core:model` and `:core:design`; `:core:fakes` only for debug previews and tests, so fakes never reach
|
||||
* a release build. ViewModels are Hilt ViewModels obtained with `hiltViewModel()`.
|
||||
* a release build; `:core:design`'s test fixtures and the screenshot tests (`kvitt.android.screenshots`) for tests. ViewModels are
|
||||
* Hilt ViewModels obtained with `hiltViewModel()`.
|
||||
*/
|
||||
class AndroidFeaturePlugin : Plugin<Project> {
|
||||
override fun apply(target: Project) {
|
||||
@@ -14,13 +15,14 @@ class AndroidFeaturePlugin : Plugin<Project> {
|
||||
pluginManager.apply("kvitt.android.library")
|
||||
pluginManager.apply("kvitt.android.compose")
|
||||
pluginManager.apply("kvitt.android.hilt")
|
||||
pluginManager.apply("kvitt.android.screenshots")
|
||||
val catalog = libs
|
||||
dependencies.add("implementation", dependencies.project(mapOf("path" to ":core:model")))
|
||||
dependencies.add("implementation", dependencies.project(mapOf("path" to ":core:design")))
|
||||
dependencies.add("debugImplementation", dependencies.project(mapOf("path" to ":core:fakes")))
|
||||
dependencies.add("testImplementation", dependencies.project(mapOf("path" to ":core:fakes")))
|
||||
dependencies.add("testImplementation", dependencies.testFixtures(dependencies.project(mapOf("path" to ":core:design"))))
|
||||
dependencies.add("implementation", catalog.library("androidx-hilt-lifecycle-viewmodel-compose"))
|
||||
dependencies.add("implementation", catalog.library("androidx-lifecycle-viewmodel-compose"))
|
||||
dependencies.add("implementation", catalog.library("androidx-lifecycle-runtime-compose"))
|
||||
dependencies.add("implementation", catalog.library("kotlinx-coroutines-core"))
|
||||
}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package se.hagstedt.kvitt.buildlogic
|
||||
|
||||
import io.github.takahirom.roborazzi.RoborazziExtension
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
|
||||
/**
|
||||
* Roborazzi screenshot tests on Robolectric for an Android module with Compose (SPEC §6.2 "Tests", D-010): goldens in
|
||||
* `src/test/screenshots/`, verified by every unit-test run (`roborazzi.test.verify=true` in gradle.properties), and the
|
||||
* test libraries every screenshot test uses. Shared harness: `:core:design`'s test fixtures
|
||||
* (`se.hagstedt.kvitt.design.testing`).
|
||||
*/
|
||||
class AndroidScreenshotsPlugin : Plugin<Project> {
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
pluginManager.apply("io.github.takahirom.roborazzi")
|
||||
extensions.configure(RoborazziExtension::class.java) { outputDir.set(file("src/test/screenshots")) }
|
||||
val catalog = libs
|
||||
dependencies.add("debugImplementation", catalog.library("androidx-compose-ui-test-manifest"))
|
||||
dependencies.add("testImplementation", catalog.library("androidx-compose-ui-test-junit4"))
|
||||
dependencies.add("testImplementation", catalog.library("robolectric"))
|
||||
dependencies.add("testImplementation", catalog.library("roborazzi"))
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
-6
@@ -15,7 +15,8 @@ import org.gradle.api.tasks.PathSensitivity
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
|
||||
/**
|
||||
* Fails the build on a dependency edge outside SPEC §6.2, on `:core:fakes` in a release configuration, and on
|
||||
* Fails the build on a dependency edge outside SPEC §6.2, on `:core:fakes` in a release configuration, on
|
||||
* test fixtures outside a test configuration, and on
|
||||
* imports or fully qualified references outside kotlin.*, kotlinx.* and the project's packages in a pure module.
|
||||
*/
|
||||
@CacheableTask
|
||||
@@ -23,7 +24,7 @@ abstract class ArchitectureCheckTask : DefaultTask() {
|
||||
@get:Input
|
||||
abstract val modulePath: Property<String>
|
||||
|
||||
/** Declared project dependencies as `configuration|:path`. */
|
||||
/** Declared project dependencies as `configuration|:path`, `configuration|:path+testFixtures` for test fixtures. */
|
||||
@get:Input
|
||||
abstract val projectDependencies: ListProperty<String>
|
||||
|
||||
@@ -51,7 +52,9 @@ abstract class ArchitectureCheckTask : DefaultTask() {
|
||||
}
|
||||
val allowed = ArchitectureRules.allowed[path]
|
||||
for (entry in projectDependencies.get()) {
|
||||
val (configuration, target) = entry.split('|', limit = 2)
|
||||
val (configuration, dependency) = entry.split('|', limit = 2)
|
||||
val fixtures = dependency.endsWith(TEST_FIXTURES)
|
||||
val target = dependency.removeSuffix(TEST_FIXTURES)
|
||||
if (target == path) continue
|
||||
if (allowed != null && target !in allowed) {
|
||||
violations += "$path -> $target ($configuration) is not an allowed edge (SPEC §6.2)"
|
||||
@@ -59,6 +62,9 @@ abstract class ArchitectureCheckTask : DefaultTask() {
|
||||
if (target == ArchitectureRules.FAKES && !ArchitectureRules.isTestOrDebugConfiguration(configuration)) {
|
||||
violations += "$path -> $target in '$configuration': fakes only in test or debug configurations"
|
||||
}
|
||||
if (fixtures && !ArchitectureRules.isTestConfiguration(configuration)) {
|
||||
violations += "$path -> $target's test fixtures in '$configuration': test fixtures only in test configurations"
|
||||
}
|
||||
}
|
||||
if (path in ArchitectureRules.pureModules) {
|
||||
if (androidPluginApplied.get()) violations += "$path is pure Kotlin/JVM and must not apply an Android plugin"
|
||||
@@ -90,8 +96,11 @@ abstract class ArchitectureCheckTask : DefaultTask() {
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val IMPORT = Regex("""^import\s+([\w.]+)""")
|
||||
val QUALIFIED = Regex("""(?<![\w.])(java|javax|android|androidx|dalvik|sun)\.[a-z]\w*""")
|
||||
companion object {
|
||||
/** Marks a project dependency on the target's test fixtures in [projectDependencies]. */
|
||||
const val TEST_FIXTURES = "+testFixtures"
|
||||
|
||||
private val IMPORT = Regex("""^import\s+([\w.]+)""")
|
||||
private val QUALIFIED = Regex("""(?<![\w.])(java|javax|android|androidx|dalvik|sun)\.[a-z]\w*""")
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -20,7 +20,10 @@ class ArchitecturePlugin : Plugin<Project> {
|
||||
modulePath.set(projectPath)
|
||||
projectDependencies.set(provider {
|
||||
configurations.flatMap { c ->
|
||||
c.dependencies.withType(ProjectDependency::class.java).map { "${c.name}|${it.path}" }
|
||||
c.dependencies.withType(ProjectDependency::class.java).map { dependency ->
|
||||
val fixtures = dependency.capabilitySelectors.any { it.displayName.contains("test-fixtures") }
|
||||
"${c.name}|${dependency.path}" + if (fixtures) ArchitectureCheckTask.TEST_FIXTURES else ""
|
||||
}
|
||||
}.distinct().sorted()
|
||||
})
|
||||
externalDependencies.set(provider {
|
||||
|
||||
+4
-1
@@ -36,5 +36,8 @@ object ArchitectureRules {
|
||||
|
||||
/** `:core:fakes` may appear only in test and debug configurations (fakes never reach a release build). */
|
||||
fun isTestOrDebugConfiguration(name: String): Boolean =
|
||||
name.contains("test", ignoreCase = true) || name.contains("debug", ignoreCase = true)
|
||||
isTestConfiguration(name) || name.contains("debug", ignoreCase = true)
|
||||
|
||||
/** A module's test fixtures (`testFixtures(project(…))`) may appear only in test configurations. */
|
||||
fun isTestConfiguration(name: String): Boolean = name.contains("test", ignoreCase = true)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user