mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
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:
@@ -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
|
||||
|
||||
+25
-13
@@ -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]""")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user