R38.7, child 7 of 8 under #52. Thirteen tests against JoinScreenContent, one file, nothing under app/src/main touched and no new entry in TestTags.
What each state is pinned to
state
what the test asks
Idle
the "pick two or more files" prompt renders, and CHOOSE_FILES calls onPickInputs
Ready
one FileRow per input in the order they were picked; "Join 3 files" reads the real count; JOIN calls onJoin; CHOOSE_DIFFERENT_FILES reoffers the picker
Joining
"Joining 2 files…", the bar is indeterminate, CANCEL calls onCancel
Waiting
the whole Paused paragraph, and cancel is still offered
Joined
the size line, both ConcatStrategy branches, and Save/Start over as two different buttons handing back "joined.mp4" and a reset
Saved
the saved name and "Join more", wired to onReset
Failed
the message it carries renders; Start over resets
Callbacks are recorded into one ordered list of what was invoked and with what, so the assertions
are assertEquals(listOf("save:joined.mp4", "reset"), events) rather than "something fired".
The two novel assertions
Order. A join is the one flow where the order of the inputs is the content of the output. The
rows are read back sorted by their position on screen and compared as a list, so a reversal is
visible; JoinLeafTagsTest proves only that a row tags itself with the file it shows, which a
reordered list satisfies equally.
Indeterminate, not merely present. The join bar carries no percentage on purpose — FFmpeg
reports progress against one input's duration, which means nothing across a concatenation — and the
converter's bar is determinate, so assertRangeInfoEquals(ProgressBarRangeInfo.Indeterminate) is
what would notice a fabricated percentage arriving here.
Mutations, each reverted after
1. Text(s.message) -> Text("") in the Failed branch
JoinStateAffordancesTest > a failed join renders the message it carries FAILED
java.lang.AssertionError: Failed: assertExists.
Reason: Expected exactly '1' node but could not find any node that satisfies: (Text + InputText +
EditableText contains 'The second file has no audio track, so joining stopped.' (ignoreCase: false))
2. when (s.strategy) -> when (ConcatStrategy.STREAM_COPY) in the Joined branch
JoinStateAffordancesTest > a re-encoded join says the files differed FAILED
java.lang.AssertionError: Failed: assertExists.
Reason: Expected exactly '1' node but could not find any node that satisfies: (Text + InputText +
EditableText contains 'Files differed in format, so they were re-encoded to match.' (ignoreCase: false))
3. s.inputs.forEach -> s.inputs.reversed().forEach in the Ready branch — the ordering
claim, which nothing in the suite made before:
JoinStateAffordancesTest > the picked inputs are listed in the order they were picked FAILED
java.lang.AssertionError: expected:<[join.fileRow:intro.mp4, join.fileRow:middle.mp4,
join.fileRow:outro.mp4]> but was:<[join.fileRow:outro.mp4, join.fileRow:middle.mp4,
join.fileRow:intro.mp4]>
Each mutation reddened exactly one test and left the other twelve green.
Not covered, deliberately
JoinState.Joined.mimeType is never rendered by this composable — the entry point reads it to
open the save dialog with a matching type. Reachable only through the launcher wiring, which is #64's.
The Failed message's colour.MaterialTheme.colorScheme.error is a theme lookup, not state
logic; asserting it would pin the theme rather than the branch.
is JoinState.Idle -> Unit inside the scrolling branch stays unreachable by construction, as #52 records. Not chased.
No coverage figure is quoted, per the correction on #52: the mutations are the evidence.
Gate
:app:assembleDebug :app:testDebugUnitTest :app:compileDebugAndroidTestKotlin :app:ktlintCheck :app:detekt :app:lintDebug --continue — BUILD SUCCESSFUL. 352 unit tests across 52 classes, 0
failures; JoinScreenContentTest and JoinLeafTagsTest pass unedited.
The typographic characters in the asserted copy (U+2026, U+2014) were compared byte-for-byte against JoinScreen.kt, and the Paused paragraph against ConverterScreen.kt's, rather than retyped.
R38.7, child 7 of 8 under #52. Thirteen tests against `JoinScreenContent`, one file, nothing under
`app/src/main` touched and no new entry in `TestTags`.
### What each state is pinned to
| state | what the test asks |
|---|---|
| `Idle` | the "pick two or more files" prompt renders, and `CHOOSE_FILES` calls `onPickInputs` |
| `Ready` | one `FileRow` per input **in the order they were picked**; `"Join 3 files"` reads the real count; `JOIN` calls `onJoin`; `CHOOSE_DIFFERENT_FILES` reoffers the picker |
| `Joining` | `"Joining 2 files…"`, the bar is **indeterminate**, `CANCEL` calls `onCancel` |
| `Waiting` | the whole Paused paragraph, and cancel is still offered |
| `Joined` | the size line, both `ConcatStrategy` branches, and Save/Start over as two different buttons handing back `"joined.mp4"` and a reset |
| `Saved` | the saved name and `"Join more"`, wired to `onReset` |
| `Failed` | the message it carries renders; Start over resets |
Callbacks are recorded into one ordered list of what was invoked and with what, so the assertions
are `assertEquals(listOf("save:joined.mp4", "reset"), events)` rather than "something fired".
### The two novel assertions
**Order.** A join is the one flow where the order of the inputs is the content of the output. The
rows are read back sorted by their position on screen and compared as a list, so a reversal is
visible; `JoinLeafTagsTest` proves only that a row tags itself with the file it shows, which a
reordered list satisfies equally.
**Indeterminate, not merely present.** The join bar carries no percentage on purpose — FFmpeg
reports progress against one input's duration, which means nothing across a concatenation — and the
converter's bar is determinate, so `assertRangeInfoEquals(ProgressBarRangeInfo.Indeterminate)` is
what would notice a fabricated percentage arriving here.
### Mutations, each reverted after
**1. `Text(s.message)` -> `Text("")` in the `Failed` branch**
```
JoinStateAffordancesTest > a failed join renders the message it carries FAILED
java.lang.AssertionError: Failed: assertExists.
Reason: Expected exactly '1' node but could not find any node that satisfies: (Text + InputText +
EditableText contains 'The second file has no audio track, so joining stopped.' (ignoreCase: false))
```
**2. `when (s.strategy)` -> `when (ConcatStrategy.STREAM_COPY)` in the `Joined` branch**
```
JoinStateAffordancesTest > a re-encoded join says the files differed FAILED
java.lang.AssertionError: Failed: assertExists.
Reason: Expected exactly '1' node but could not find any node that satisfies: (Text + InputText +
EditableText contains 'Files differed in format, so they were re-encoded to match.' (ignoreCase: false))
```
**3. `s.inputs.forEach` -> `s.inputs.reversed().forEach` in the `Ready` branch** — the ordering
claim, which nothing in the suite made before:
```
JoinStateAffordancesTest > the picked inputs are listed in the order they were picked FAILED
java.lang.AssertionError: expected:<[join.fileRow:intro.mp4, join.fileRow:middle.mp4,
join.fileRow:outro.mp4]> but was:<[join.fileRow:outro.mp4, join.fileRow:middle.mp4,
join.fileRow:intro.mp4]>
```
Each mutation reddened exactly one test and left the other twelve green.
### Not covered, deliberately
- **`JoinState.Joined.mimeType`** is never rendered by this composable — the entry point reads it to
open the save dialog with a matching type. Reachable only through the launcher wiring, which is
#64's.
- **The `Failed` message's colour.** `MaterialTheme.colorScheme.error` is a theme lookup, not state
logic; asserting it would pin the theme rather than the branch.
- **`is JoinState.Idle -> Unit`** inside the scrolling branch stays unreachable by construction, as
#52 records. Not chased.
- **No coverage figure is quoted**, per the correction on #52: the mutations are the evidence.
### Gate
`:app:assembleDebug :app:testDebugUnitTest :app:compileDebugAndroidTestKotlin :app:ktlintCheck
:app:detekt :app:lintDebug --continue` — BUILD SUCCESSFUL. 352 unit tests across 52 classes, 0
failures; `JoinScreenContentTest` and `JoinLeafTagsTest` pass unedited.
The typographic characters in the asserted copy (U+2026, U+2014) were compared byte-for-byte against
`JoinScreen.kt`, and the Paused paragraph against `ConverterScreen.kt`'s, rather than retyped.
Closes #63.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
R38.7, child 7 of 8 under #52. Thirteen tests against
JoinScreenContent, one file, nothing underapp/src/maintouched and no new entry inTestTags.What each state is pinned to
IdleCHOOSE_FILEScallsonPickInputsReadyFileRowper input in the order they were picked;"Join 3 files"reads the real count;JOINcallsonJoin;CHOOSE_DIFFERENT_FILESreoffers the pickerJoining"Joining 2 files…", the bar is indeterminate,CANCELcallsonCancelWaitingJoinedConcatStrategybranches, and Save/Start over as two different buttons handing back"joined.mp4"and a resetSaved"Join more", wired toonResetFailedCallbacks are recorded into one ordered list of what was invoked and with what, so the assertions
are
assertEquals(listOf("save:joined.mp4", "reset"), events)rather than "something fired".The two novel assertions
Order. A join is the one flow where the order of the inputs is the content of the output. The
rows are read back sorted by their position on screen and compared as a list, so a reversal is
visible;
JoinLeafTagsTestproves only that a row tags itself with the file it shows, which areordered list satisfies equally.
Indeterminate, not merely present. The join bar carries no percentage on purpose — FFmpeg
reports progress against one input's duration, which means nothing across a concatenation — and the
converter's bar is determinate, so
assertRangeInfoEquals(ProgressBarRangeInfo.Indeterminate)iswhat would notice a fabricated percentage arriving here.
Mutations, each reverted after
1.
Text(s.message)->Text("")in theFailedbranch2.
when (s.strategy)->when (ConcatStrategy.STREAM_COPY)in theJoinedbranch3.
s.inputs.forEach->s.inputs.reversed().forEachin theReadybranch — the orderingclaim, which nothing in the suite made before:
Each mutation reddened exactly one test and left the other twelve green.
Not covered, deliberately
JoinState.Joined.mimeTypeis never rendered by this composable — the entry point reads it toopen the save dialog with a matching type. Reachable only through the launcher wiring, which is
#64's.
Failedmessage's colour.MaterialTheme.colorScheme.erroris a theme lookup, not statelogic; asserting it would pin the theme rather than the branch.
is JoinState.Idle -> Unitinside the scrolling branch stays unreachable by construction, as#52 records. Not chased.
Gate
:app:assembleDebug :app:testDebugUnitTest :app:compileDebugAndroidTestKotlin :app:ktlintCheck :app:detekt :app:lintDebug --continue— BUILD SUCCESSFUL. 352 unit tests across 52 classes, 0failures;
JoinScreenContentTestandJoinLeafTagsTestpass unedited.The typographic characters in the asserted copy (U+2026, U+2014) were compared byte-for-byte against
JoinScreen.kt, and the Paused paragraph againstConverterScreen.kt's, rather than retyped.Closes #63.
🤖 Generated with Claude Code