test(upgrade): Assert the splice honoured the template, not just its output

The locale sweep checked format specifiers with a regex that does not know
%<s, which reuses the previous argument and so emits the qualifier twice.
That damages the template, triggers the fallback, and still satisfies every
output assertion because a fallback title also carries one correctly styled
qualifier. Both checks now run against the formatter's own output.

Also pins the highlight colour: it is a parameter because the toolbar tints
FOSS and Pro differently, so hardcoding it back would have stayed green.
This commit is contained in:
darken
2026-08-07 16:51:44 +02:00
committed by Matthias Urhahn
parent 5a635b424b
commit 908acc4ea0
2 changed files with 51 additions and 13 deletions
@@ -2,6 +2,7 @@ package eu.darken.capod.common.upgrade.ui
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.test.core.app.ApplicationProvider
import eu.darken.capod.R
@@ -74,6 +75,31 @@ class BrandTitleTest : BaseComposeRobolectricTest() {
result.text.substring(span.start, span.end) shouldBe qualifier
}
@Test
fun `the highlight defaults to the upgraded brand color`() {
val result = capture { brandTitle(includeQualifier = true, highlightQualifier = true) }
result.spanStyles.single().item.color shouldBe Color(context.getColor(R.color.brand_tertiary))
}
// The toolbar tints by flavor — FOSS on brand_secondary, Pro on brand_tertiary — so the color
// is a parameter rather than a constant. Without this, hardcoding the default back into
// brandTitle would keep every other assertion green while the FOSS toolbar lost its tint.
@Test
fun `a caller-supplied highlight color is the one applied`() {
val custom = Color(context.getColor(R.color.brand_secondary))
val result = capture {
brandTitle(includeQualifier = true, highlightQualifier = true, highlightColor = custom)
}
result.spanStyles.single().item.color shouldBe custom
result.text.substring(
result.spanStyles.single().start,
result.spanStyles.single().end,
) shouldBe qualifier
}
// The markers are injected as format arguments, so a template or formatter that mangled them
// would leak U+FFFC / U+FFF9 into the toolbar.
@Test
@@ -63,16 +63,26 @@ class BrandTitleTemplateLocalesTest {
locales shouldHaveAtLeastSize 60
}
// Asserted against the FORMATTER'S OUTPUT rather than against the template text, because the
// format grammar is bigger than it looks: `%1$s %2$s %<s` reuses the previous argument and so
// emits the qualifier twice, which no reasonable "does it contain %1$s and %2$s" check spots.
// Each marker landing exactly once is precisely the condition spliceTitleTemplate requires, so
// checking it here is both stronger and simpler than modelling the grammar.
@Test
fun `every locale template declares exactly the two title placeholders`() {
fun `every locale template places each slot exactly once when formatted`() {
val offenders = locales.mapNotNull { tag ->
val template = localized(tag).getString(R.string.app_name_upgraded_template)
val specifiers = FORMAT_SPECIFIER
.findAll(template.replace("%%", ""))
.map { it.value }
.sorted()
.toList()
if (specifiers == listOf("%1\$s", "%2\$s")) null else "$tag -> $template"
val formatted = localized(tag).getString(
R.string.app_name_upgraded_template,
BRAND_TITLE_MARKER,
BRAND_QUALIFIER_MARKER,
)
val names = formatted.split(BRAND_TITLE_MARKER).size - 1
val qualifiers = formatted.split(BRAND_QUALIFIER_MARKER).size - 1
if (names == 1 && qualifiers == 1) {
null
} else {
"$tag -> name x$names, qualifier x$qualifiers in '$formatted'"
}
}
offenders shouldBe emptyList()
@@ -89,11 +99,17 @@ class BrandTitleTemplateLocalesTest {
val result = compose(ctx)
val span = result.spanStyles.singleOrNull()
// What Android itself produces for this template. Comparing against it is what proves
// the splice REPRODUCED the translator's arrangement rather than quietly discarding a
// damaged template and rebuilding the default — a fallback renders a plausible title
// with exactly one correctly-styled qualifier, so every other assertion here passes
// straight through it.
val expected = ctx.getString(R.string.app_name_upgraded_template, name, qualifier)
when {
name.isBlank() || qualifier.isBlank() -> "$tag -> blank part"
result.text.contains(BRAND_TITLE_MARKER) -> "$tag -> name marker leaked"
result.text.contains(BRAND_QUALIFIER_MARKER) -> "$tag -> qualifier marker leaked"
!result.text.contains(name) -> "$tag -> name missing from '${result.text}'"
result.text != expected -> "$tag -> rendered '${result.text}', formatter says '$expected'"
span == null -> "$tag -> expected one span, got ${result.spanStyles.size}"
result.text.substring(span.start, span.end) != qualifier ->
"$tag -> span covers '${result.text.substring(span.start, span.end)}', want '$qualifier'"
@@ -104,8 +120,4 @@ class BrandTitleTemplateLocalesTest {
offenders shouldBe emptyList()
}
companion object {
private val FORMAT_SPECIFIER = Regex("""%(\d+\$)?[a-zA-Z]""")
}
}