Android overhaul: one CopyCategoryKeywords task in build-logic instead of a copy in :data and :core:fakes

The task class and its wiring were duplicated verbatim in both build scripts; bundleCategoryKeywords(resourcePath)
registers it once per module. Resource paths and inputs are unchanged.

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:53:44 +02:00
parent 48fe119ae1
commit e4e6e5d792
11 changed files with 76 additions and 368 deletions
@@ -1,99 +0,0 @@
package se.hagstedt.kvitt.feature.account
import android.content.res.Configuration
import android.os.LocaleList
import android.os.Looper
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.unit.Density
import com.github.takahirom.roborazzi.ExperimentalRoborazziApi
import com.github.takahirom.roborazzi.captureScreenRoboImage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import kotlinx.datetime.TimeZone
import org.robolectric.Shadows.shadowOf
import se.hagstedt.kvitt.design.KvittTheme
import se.hagstedt.kvitt.design.component.KvittClock
import se.hagstedt.kvitt.design.component.LocalKvittClock
import java.util.Locale
import kotlin.time.Instant
/**
* Full-screen goldens of this module's screens, as M6's component snapshots but for whole screens (dialogs and menus
* included): `src/test/screenshots/<state>_<sv_light|sv_dark|en_light>.png`, plus `<state>_sv_light_200.png` at 200 % font
* scale for the long states. The screen is the Galaxy S26 (411 × 891 dp), at mdpi (1x) to keep the goldens small (D-098). The
* clock is fixed at 17 October 2026 14:02 in Stockholm; the frame clock is advanced by hand so animations and
* snackbars render the same every run.
*/
object ScreenSnapshots {
const val QUALIFIERS = "w411dp-h891dp-mdpi"
val clock = KvittClock.fixed(Instant.parse("2026-10-17T12:02:00Z"), TimeZone.of("Europe/Stockholm"))
private const val SETTLE_MILLIS = 1_000L
private data class Variant(val language: String, val dark: Boolean, val fontScale: Float) {
val suffix: String get() = "${language}_${if (dark) "dark" else "light"}" + if (fontScale > 1f) "_200" else ""
}
@OptIn(ExperimentalRoborazziApi::class)
fun snap(compose: ComposeContentTestRule, name: String, largeFont: Boolean, content: @Composable () -> Unit) {
val variants = listOf(Variant("sv", dark = false, fontScale = 1f), Variant("sv", dark = true, fontScale = 1f), Variant("en", dark = false, fontScale = 1f)) +
if (largeFont) listOf(Variant("sv", dark = false, fontScale = 2f)) else emptyList()
var current by mutableStateOf(variants.first())
compose.mainClock.autoAdvance = false
compose.setContent {
val variant = current
val base = LocalContext.current
val baseConfiguration = LocalConfiguration.current
val context = remember(variant) {
val config = Configuration(baseConfiguration).apply {
setLocales(LocaleList(Locale.forLanguageTag(if (variant.language == "sv") "sv-SE" else "en-GB")))
fontScale = variant.fontScale
uiMode = (uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()) or if (variant.dark) Configuration.UI_MODE_NIGHT_YES else Configuration.UI_MODE_NIGHT_NO
}
base.createConfigurationContext(config)
}
CompositionLocalProvider(
LocalContext provides context,
LocalConfiguration provides context.resources.configuration,
LocalResources provides context.resources,
LocalDensity provides Density(context),
LocalKvittClock provides clock,
) {
KvittTheme(darkTheme = variant.dark, content = content)
}
}
variants.forEach { variant ->
current = variant
compose.mainClock.advanceTimeBy(SETTLE_MILLIS)
// The screen (and a dialog window) draw in a later frame; one more frame and an idle main looper, so the
// capture shows this variant and not the previous one.
shadowOf(Looper.getMainLooper()).idle()
compose.mainClock.advanceTimeByFrame()
shadowOf(Looper.getMainLooper()).idle()
captureScreenRoboImage("src/test/screenshots/${name}_${variant.suffix}.png")
}
}
}
/** Runs [block] with `Dispatchers.Main` unconfined, so a ViewModel's state is ready when it returns. */
@OptIn(ExperimentalCoroutinesApi::class)
fun <T> withMain(block: () -> T): T {
Dispatchers.setMain(UnconfinedTestDispatcher())
try {
return block()
} finally {
Dispatchers.resetMain()
}
}
@@ -1,99 +0,0 @@
package se.hagstedt.kvitt.feature.home
import android.content.res.Configuration
import android.os.LocaleList
import android.os.Looper
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.unit.Density
import com.github.takahirom.roborazzi.ExperimentalRoborazziApi
import com.github.takahirom.roborazzi.captureScreenRoboImage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import kotlinx.datetime.TimeZone
import org.robolectric.Shadows.shadowOf
import se.hagstedt.kvitt.design.KvittTheme
import se.hagstedt.kvitt.design.component.KvittClock
import se.hagstedt.kvitt.design.component.LocalKvittClock
import java.util.Locale
import kotlin.time.Instant
/**
* Full-screen goldens of this module's screens, as M6's component snapshots but for whole screens (dialogs and menus
* included): `src/test/screenshots/<state>_<sv_light|sv_dark|en_light>.png`, plus `<state>_sv_light_200.png` at 200 % font
* scale for the long states. The screen is the Galaxy S26 (411 × 891 dp), at mdpi (1x) to keep the goldens small (D-098). The
* clock is fixed at 17 October 2026 14:02 in Stockholm; the frame clock is advanced by hand so animations and
* snackbars render the same every run.
*/
object ScreenSnapshots {
const val QUALIFIERS = "w411dp-h891dp-mdpi"
val clock = KvittClock.fixed(Instant.parse("2026-10-17T12:02:00Z"), TimeZone.of("Europe/Stockholm"))
private const val SETTLE_MILLIS = 1_000L
private data class Variant(val language: String, val dark: Boolean, val fontScale: Float) {
val suffix: String get() = "${language}_${if (dark) "dark" else "light"}" + if (fontScale > 1f) "_200" else ""
}
@OptIn(ExperimentalRoborazziApi::class)
fun snap(compose: ComposeContentTestRule, name: String, largeFont: Boolean, content: @Composable () -> Unit) {
val variants = listOf(Variant("sv", dark = false, fontScale = 1f), Variant("sv", dark = true, fontScale = 1f), Variant("en", dark = false, fontScale = 1f)) +
if (largeFont) listOf(Variant("sv", dark = false, fontScale = 2f)) else emptyList()
var current by mutableStateOf(variants.first())
compose.mainClock.autoAdvance = false
compose.setContent {
val variant = current
val base = LocalContext.current
val baseConfiguration = LocalConfiguration.current
val context = remember(variant) {
val config = Configuration(baseConfiguration).apply {
setLocales(LocaleList(Locale.forLanguageTag(if (variant.language == "sv") "sv-SE" else "en-GB")))
fontScale = variant.fontScale
uiMode = (uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()) or if (variant.dark) Configuration.UI_MODE_NIGHT_YES else Configuration.UI_MODE_NIGHT_NO
}
base.createConfigurationContext(config)
}
CompositionLocalProvider(
LocalContext provides context,
LocalConfiguration provides context.resources.configuration,
LocalResources provides context.resources,
LocalDensity provides Density(context),
LocalKvittClock provides clock,
) {
KvittTheme(darkTheme = variant.dark, content = content)
}
}
variants.forEach { variant ->
current = variant
compose.mainClock.advanceTimeBy(SETTLE_MILLIS)
// The screen (and a dialog window: S-16's lost version, S-17's install dialog) draw in a later frame; one
// more frame and an idle main looper, so the capture shows this variant and not the previous one.
shadowOf(Looper.getMainLooper()).idle()
compose.mainClock.advanceTimeByFrame()
shadowOf(Looper.getMainLooper()).idle()
captureScreenRoboImage("src/test/screenshots/${name}_${variant.suffix}.png")
}
}
}
/** Runs [block] with `Dispatchers.Main` unconfined, so a ViewModel's state is ready when it returns. */
@OptIn(ExperimentalCoroutinesApi::class)
fun <T> withMain(block: () -> T): T {
Dispatchers.setMain(UnconfinedTestDispatcher())
try {
return block()
} finally {
Dispatchers.resetMain()
}
}
@@ -1,76 +0,0 @@
package se.hagstedt.kvitt.feature.onboarding.screenshot
import android.content.res.Configuration
import android.os.LocaleList
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.unit.Density
import com.github.takahirom.roborazzi.captureRoboImage
import kotlinx.datetime.TimeZone
import se.hagstedt.kvitt.design.KvittTheme
import se.hagstedt.kvitt.design.component.KvittClock
import se.hagstedt.kvitt.design.component.LocalKvittClock
import java.util.Locale
import kotlin.time.Instant
/**
* Captures a whole screen in three variants (Swedish light and dark, English light; D-098): `src/test/screenshots/<name>_<sv_light|sv_dark|en_light>.png`, plus
* `<name>_sv_light_200.png` when `largeFont` is set (200 % font scale, Swedish, light), as `:core:design`'s Snapshots
* do for components. The phone is the Galaxy S26 (411 dp wide, SPEC §13.1); the clock is fixed at 17 October 2026
* 14:02 in Stockholm so every render is reproducible.
*/
object ScreenSnapshots {
val clock = KvittClock.fixed(Instant.parse("2026-10-17T12:02:00Z"), TimeZone.of("Europe/Stockholm"))
private data class Variant(val language: String, val dark: Boolean, val fontScale: Float) {
val suffix: String get() = "${language}_${if (dark) "dark" else "light"}" + if (fontScale > 1f) "_200" else ""
}
fun snap(compose: ComposeContentTestRule, name: String, largeFont: Boolean, content: @Composable () -> Unit) {
val variants = listOf(Variant("sv", dark = false, fontScale = 1f), Variant("sv", dark = true, fontScale = 1f), Variant("en", dark = false, fontScale = 1f)) +
if (largeFont) listOf(Variant("sv", dark = false, fontScale = 2f)) else emptyList()
var current by mutableStateOf(variants.first())
compose.setContent {
val variant = current
val base = LocalContext.current
val baseConfiguration = LocalConfiguration.current
val context = remember(variant) {
val config = Configuration(baseConfiguration).apply {
setLocales(LocaleList(Locale.forLanguageTag(if (variant.language == "sv") "sv-SE" else "en-GB")))
fontScale = variant.fontScale
uiMode = (uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()) or if (variant.dark) Configuration.UI_MODE_NIGHT_YES else Configuration.UI_MODE_NIGHT_NO
}
base.createConfigurationContext(config)
}
CompositionLocalProvider(
LocalContext provides context,
LocalConfiguration provides context.resources.configuration,
LocalResources provides context.resources,
LocalDensity provides Density(context),
LocalKvittClock provides clock,
) {
KvittTheme(darkTheme = variant.dark) {
Box(Modifier.fillMaxSize()) { content() }
}
}
}
variants.forEach { variant ->
current = variant
compose.waitForIdle()
compose.onRoot().captureRoboImage("src/test/screenshots/${name}_${variant.suffix}.png")
}
}
}
@@ -1,17 +0,0 @@
package se.hagstedt.kvitt.feature.settings.logic
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.rules.TestWatcher
import org.junit.runner.Description
/** `viewModelScope` runs on an unconfined test dispatcher, so every ViewModel call completes before it returns. */
@OptIn(ExperimentalCoroutinesApi::class)
class MainDispatcherRule : TestWatcher() {
override fun starting(description: Description) = Dispatchers.setMain(UnconfinedTestDispatcher())
override fun finished(description: Description) = Dispatchers.resetMain()
}