Ask each join state what it lets the user do next #79

Merged
JMR-dev merged 1 commits from test/r38-7-join-states into main 2026-08-25 00:58:27 +00:00
JMR-dev commented 2026-08-25 00:41:00 +00:00 (Migrated from github.com)

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

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)
Sign in to join this conversation.