Say what each conversion state puts on screen, and what it withholds

The screen's state machine had a seam and no matrix behind it. Every arm of
the `when` returns `Unit`, so an arm can render anything at all and still
compile -- a button offered where it cannot work, a state's own data never
reaching the node meant to show it, an affordance wired to the wrong
callback. The leaf tests cannot see any of that: they compose `FileCard`,
`AdvancedPicker` and the three pickers directly and never hold a
`ConversionState`.

The arm worth guarding most is `Ready`'s `enabled = validation.isValid`. The
Advanced picker deliberately lets an impossible container / codec pair be
selected, so that one expression is all that stands between an invalid spec
and a job that cannot succeed. `enabled = true` compiles, renders an
identical screen apart from one colour, and passed the whole suite before
this.

Callbacks are asserted over the complete log rather than one at a time, so a
case reads "this one fired and nothing else". A bare "the callback ran" check
stays green on an arm that fires the right callback for the wrong reason.

The routing chip needed a tag to be locatable at all: its text comes from the
finished job, so a text matcher would have to name a routing explanation the
screen does not own. That is the only production change here.

Not asserted, deliberately: `Failed`'s error colour, which Compose publishes
nowhere in the semantics tree; and the three absent `FileCard`s, which are
compile-guarded -- `Idle`, `Saved` and `Failed` carry no `input` -- so those
lines state the intent without being what enforces it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-24 19:35:21 -05:00
co-authored by Claude Opus 5
parent 6166763f24
commit 2f3f461cc1
3 changed files with 417 additions and 1 deletions
@@ -289,7 +289,11 @@ internal fun ConverterScreenContent(
// explains why a job was slow, makes the software fallback
// visible, and is how the user learns a remux happened rather
// than a re-encode.
AssistChip(onClick = {}, label = { Text(s.routeReason) })
AssistChip(
onClick = {},
label = { Text(s.routeReason) },
modifier = Modifier.testTag(TestTags.Converter.ROUTE_REASON),
)
}
Button(
onClick = { actions.onSave(s.suggestedName) },
@@ -55,6 +55,15 @@ object TestTags {
/** The determinate bar in `Converting`. It carries no text, so nothing else can find it. */
const val PROGRESS: String = "converter.progress"
/**
* The chip on `Converted` that says which engine ran the job and why.
*
* Conditional on `routeReason` being non-blank, and that condition is what the tag is for:
* its text comes from the finished job, so a text matcher looking for it would have to
* name a routing explanation the screen does not own.
*/
const val ROUTE_REASON: String = "converter.routeReason"
const val FILE_CARD: String = "converter.fileCard"
const val FILE_CARD_NAME: String = "converter.fileCard.name"