Basing on main rather than test/r38-1-screen-test-seam: #65 merged and deleted that branch while this was in flight, so the seam it provides (internal leaves, the TestTags table, createDrainedComposeRule) is already in main's history at df2e42a.
One new file, app/src/test/java/org/libremediaconverter/convert/ConverterPickerSelectionTest.kt. Nothing in main is touched, so this cannot collide with #58 or #60.
What it asserts
Nine tests over FormatPicker, QualityPicker and EnginePicker — chips in, selection out, no state of their own:
picker
selection
callback
prose
FormatPicker
the selected format is lit and all 13 others are not; null lights nothing
pressing all 14 chips records all 14 constants in order
the custom line appears only when nothing is selected
QualityPicker
the selected tier is lit, the other is not
pressing both chips records both tiers
the description under the chips is the selected tier's
EnginePicker
the selected preference is lit, the other two are not
pressing all three chips records all three constants
—
The click tests press every chip and compare the whole recorded list against entries, rather than pressing one and asserting the callback fired. That is what makes the constant load-bearing: a hardcoded onSelect fires exactly as many times as a correct one. The selection tests assert over every chip for the same reason — the chip that should be lit proves nothing alone, because != lights it too when the enum has one entry and lights all its siblings when it has more.
Labels come off OutputFormat.label, QualityTier.label and the screen's own EnginePreference.label(), so a reword cannot redden this file for the wrong reason.
Mutation testing — the acceptance bar
Green is not evidence, so each assertion was reverted against and the failure recorded verbatim.
1. The issue's first named mutation, sharpened. There is no per-chip lambda to no-op inside entries.forEach, so the stronger form is a hardcoded constant — it proves the identity is checked, not just that something fired. onClick = { onSelect(format) } -> onClick = { onSelect(OutputFormat.MP4_H264) }:
ConverterPickerSelectionTest > clicking a format chip reports that format FAILED
java.lang.AssertionError: expected:<[MP4_H264, MP4_H265, WEBM_VP9, MKV_H264, MKV_H265, REMUX_MP4, REMUX_MKV, MP3, M4A_AAC, OPUS, FLAC, WAV, GIF, FRAMES_PNG]> but was:<[MP4_H264, MP4_H264, MP4_H264, MP4_H264, MP4_H264, MP4_H264, MP4_H264, MP4_H264, MP4_H264, MP4_H264, MP4_H264, MP4_H264, MP4_H264, MP4_H264]>
at org.junit.Assert.fail(Assert.java:89)
2. The issue's second named mutation.selected = format == selected -> selected = format != selected — both format selection tests go red:
ConverterPickerSelectionTest > the format picker lights the selected format and no other FAILED
ConverterPickerSelectionTest > the format picker lights nothing when the spec is custom FAILED
java.lang.AssertionError: Failed to assert the following: (Selected = 'false')
Semantics of the node:
Node #184 at (l=2.0, t=6.0, r=46.0, b=42.0)px
Focused = 'false'
Role = 'Checkbox'
Selected = 'true'
Text = '[MP4 (H.264)]'
MergeDescendants = 'true'
Has 13 siblings
Selector used: ((hasAnyAncestorThat(TestTag = 'converter.formatChips')) && (Text + InputText + EditableText contains 'MP4 (H.264)'))
3 and 4. The other two pickers, so no picker is covered vacuously.onClick = { onSelect(preference) } -> onClick = {}, and Text(selected.description, ...) -> Text(QualityTier.FAST.description, ...):
ConverterPickerSelectionTest > clicking an engine chip reports that preference FAILED
java.lang.AssertionError: expected:<[AUTO, PREFER_HARDWARE, FORCE_SOFTWARE]> but was:<[]>
ConverterPickerSelectionTest > the quality picker explains the tier that is selected FAILED
java.lang.AssertionError: Failed: assertExists.
Reason: Expected exactly '1' node but could not find any node that satisfies: (Text + InputText + EditableText contains 'Software encode with CRF. Slower, smaller files.' (ignoreCase: false))
ConverterScreen.kt was restored with git restore after the last run; git status shows the one new file and nothing else.
assertIsSelected works on a Material 3 FilterChip under Robolectric.AppRootRestorationTest only proved it for NavigationBarItem; the chips carry Role = 'Checkbox' and Selected as shown above, so no custom matcher is needed.
Locate a chip by ancestor, not by child.chipIn uses onNode(hasAnyAncestor(hasTestTag(row)) and hasText(label)) rather than onNodeWithTag(row).onChildren(), so it does not assume how many semantics nodes Material 3 puts between a FlowRow and its chips. Also: performClick on all 14 format chips works without scrolling, so performSemanticsAction is not needed.
Not covered, deliberately
The "Output format", "Quality" and "Engine" headings — untagged Text calls with no enum behind them and no behaviour to bite on.
QualityTier.description is asserted for presence and for which tier, not for wording, since the wording is the enum's.
The one display literal here with no symbol behind it, "Custom — set below.", was extracted from ConverterScreen.kt programmatically rather than retyped: it holds a U+2014 that would have failed silently as "no node found".
Gate
./gradlew :app:assembleDebug :app:testDebugUnitTest :app:compileDebugAndroidTestKotlin :app:ktlintCheck :app:detekt :app:lintDebug --continue — BUILD SUCCESSFUL. 45 test classes, 298 tests, 0 failures. detekt stays at 0 findings; ktlintFormat made no changes.
Closes #59. Child 3 of 8 decomposing #52.
Basing on `main` rather than `test/r38-1-screen-test-seam`: #65 merged and deleted that branch while this was in flight, so the seam it provides (`internal` leaves, the `TestTags` table, `createDrainedComposeRule`) is already in `main`'s history at `df2e42a`.
One new file, `app/src/test/java/org/libremediaconverter/convert/ConverterPickerSelectionTest.kt`. Nothing in `main` is touched, so this cannot collide with #58 or #60.
## What it asserts
Nine tests over `FormatPicker`, `QualityPicker` and `EnginePicker` — chips in, selection out, no state of their own:
| picker | selection | callback | prose |
|---|---|---|---|
| `FormatPicker` | the selected format is lit and all 13 others are not; `null` lights nothing | pressing all 14 chips records all 14 constants in order | the custom line appears only when nothing is selected |
| `QualityPicker` | the selected tier is lit, the other is not | pressing both chips records both tiers | the description under the chips is the selected tier's |
| `EnginePicker` | the selected preference is lit, the other two are not | pressing all three chips records all three constants | — |
The click tests press **every** chip and compare the whole recorded list against `entries`, rather than pressing one and asserting the callback fired. That is what makes the constant load-bearing: a hardcoded `onSelect` fires exactly as many times as a correct one. The selection tests assert over every chip for the same reason — the chip that should be lit proves nothing alone, because `!=` lights it too when the enum has one entry and lights all its siblings when it has more.
Labels come off `OutputFormat.label`, `QualityTier.label` and the screen's own `EnginePreference.label()`, so a reword cannot redden this file for the wrong reason.
## Mutation testing — the acceptance bar
Green is not evidence, so each assertion was reverted against and the failure recorded verbatim.
**1. The issue's first named mutation, sharpened.** There is no per-chip lambda to no-op inside `entries.forEach`, so the stronger form is a hardcoded constant — it proves the *identity* is checked, not just that something fired. `onClick = { onSelect(format) }` -> `onClick = { onSelect(OutputFormat.MP4_H264) }`:
```
ConverterPickerSelectionTest > clicking a format chip reports that format FAILED
java.lang.AssertionError: expected:<[MP4_H264, MP4_H265, WEBM_VP9, MKV_H264, MKV_H265, REMUX_MP4, REMUX_MKV, MP3, M4A_AAC, OPUS, FLAC, WAV, GIF, FRAMES_PNG]> but was:<[MP4_H264, MP4_H264, MP4_H264, MP4_H264, MP4_H264, MP4_H264, MP4_H264, MP4_H264, MP4_H264, MP4_H264, MP4_H264, MP4_H264, MP4_H264, MP4_H264]>
at org.junit.Assert.fail(Assert.java:89)
```
**2. The issue's second named mutation.** `selected = format == selected` -> `selected = format != selected` — **both** format selection tests go red:
```
ConverterPickerSelectionTest > the format picker lights the selected format and no other FAILED
ConverterPickerSelectionTest > the format picker lights nothing when the spec is custom FAILED
java.lang.AssertionError: Failed to assert the following: (Selected = 'false')
Semantics of the node:
Node #184 at (l=2.0, t=6.0, r=46.0, b=42.0)px
Focused = 'false'
Role = 'Checkbox'
Selected = 'true'
Text = '[MP4 (H.264)]'
MergeDescendants = 'true'
Has 13 siblings
Selector used: ((hasAnyAncestorThat(TestTag = 'converter.formatChips')) && (Text + InputText + EditableText contains 'MP4 (H.264)'))
```
**3 and 4. The other two pickers, so no picker is covered vacuously.** `onClick = { onSelect(preference) }` -> `onClick = {}`, and `Text(selected.description, ...)` -> `Text(QualityTier.FAST.description, ...)`:
```
ConverterPickerSelectionTest > clicking an engine chip reports that preference FAILED
java.lang.AssertionError: expected:<[AUTO, PREFER_HARDWARE, FORCE_SOFTWARE]> but was:<[]>
ConverterPickerSelectionTest > the quality picker explains the tier that is selected FAILED
java.lang.AssertionError: Failed: assertExists.
Reason: Expected exactly '1' node but could not find any node that satisfies: (Text + InputText + EditableText contains 'Software encode with CRF. Slower, smaller files.' (ignoreCase: false))
```
`ConverterScreen.kt` was restored with `git restore` after the last run; `git status` shows the one new file and nothing else.
## Two notes for the siblings on #58 and #60
- **`assertIsSelected` works on a Material 3 `FilterChip` under Robolectric.** `AppRootRestorationTest` only proved it for `NavigationBarItem`; the chips carry `Role = 'Checkbox'` and `Selected` as shown above, so no custom matcher is needed.
- **Locate a chip by ancestor, not by child.** `chipIn` uses `onNode(hasAnyAncestor(hasTestTag(row)) and hasText(label))` rather than `onNodeWithTag(row).onChildren()`, so it does not assume how many semantics nodes Material 3 puts between a `FlowRow` and its chips. Also: `performClick` on all 14 format chips works without scrolling, so `performSemanticsAction` is not needed.
## Not covered, deliberately
- The `"Output format"`, `"Quality"` and `"Engine"` headings — untagged `Text` calls with no enum behind them and no behaviour to bite on.
- `QualityTier.description` is asserted for presence and for *which* tier, not for wording, since the wording is the enum's.
The one display literal here with no symbol behind it, `"Custom — set below."`, was extracted from `ConverterScreen.kt` programmatically rather than retyped: it holds a U+2014 that would have failed silently as "no node found".
## Gate
`./gradlew :app:assembleDebug :app:testDebugUnitTest :app:compileDebugAndroidTestKotlin :app:ktlintCheck :app:detekt :app:lintDebug --continue` — BUILD SUCCESSFUL. 45 test classes, 298 tests, 0 failures. detekt stays at 0 findings; `ktlintFormat` made no changes.
🤖 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.
Closes #59. Child 3 of 8 decomposing #52.
Basing on
mainrather thantest/r38-1-screen-test-seam: #65 merged and deleted that branch while this was in flight, so the seam it provides (internalleaves, theTestTagstable,createDrainedComposeRule) is already inmain's history atdf2e42a.One new file,
app/src/test/java/org/libremediaconverter/convert/ConverterPickerSelectionTest.kt. Nothing inmainis touched, so this cannot collide with #58 or #60.What it asserts
Nine tests over
FormatPicker,QualityPickerandEnginePicker— chips in, selection out, no state of their own:FormatPickernulllights nothingQualityPickerEnginePickerThe click tests press every chip and compare the whole recorded list against
entries, rather than pressing one and asserting the callback fired. That is what makes the constant load-bearing: a hardcodedonSelectfires exactly as many times as a correct one. The selection tests assert over every chip for the same reason — the chip that should be lit proves nothing alone, because!=lights it too when the enum has one entry and lights all its siblings when it has more.Labels come off
OutputFormat.label,QualityTier.labeland the screen's ownEnginePreference.label(), so a reword cannot redden this file for the wrong reason.Mutation testing — the acceptance bar
Green is not evidence, so each assertion was reverted against and the failure recorded verbatim.
1. The issue's first named mutation, sharpened. There is no per-chip lambda to no-op inside
entries.forEach, so the stronger form is a hardcoded constant — it proves the identity is checked, not just that something fired.onClick = { onSelect(format) }->onClick = { onSelect(OutputFormat.MP4_H264) }:2. The issue's second named mutation.
selected = format == selected->selected = format != selected— both format selection tests go red:3 and 4. The other two pickers, so no picker is covered vacuously.
onClick = { onSelect(preference) }->onClick = {}, andText(selected.description, ...)->Text(QualityTier.FAST.description, ...):ConverterScreen.ktwas restored withgit restoreafter the last run;git statusshows the one new file and nothing else.Two notes for the siblings on #58 and #60
assertIsSelectedworks on a Material 3FilterChipunder Robolectric.AppRootRestorationTestonly proved it forNavigationBarItem; the chips carryRole = 'Checkbox'andSelectedas shown above, so no custom matcher is needed.chipInusesonNode(hasAnyAncestor(hasTestTag(row)) and hasText(label))rather thanonNodeWithTag(row).onChildren(), so it does not assume how many semantics nodes Material 3 puts between aFlowRowand its chips. Also:performClickon all 14 format chips works without scrolling, soperformSemanticsActionis not needed.Not covered, deliberately
"Output format","Quality"and"Engine"headings — untaggedTextcalls with no enum behind them and no behaviour to bite on.QualityTier.descriptionis asserted for presence and for which tier, not for wording, since the wording is the enum's.The one display literal here with no symbol behind it,
"Custom — set below.", was extracted fromConverterScreen.ktprogrammatically rather than retyped: it holds a U+2014 that would have failed silently as "no node found".Gate
./gradlew :app:assembleDebug :app:testDebugUnitTest :app:compileDebugAndroidTestKotlin :app:ktlintCheck :app:detekt :app:lintDebug --continue— BUILD SUCCESSFUL. 45 test classes, 298 tests, 0 failures. detekt stays at 0 findings;ktlintFormatmade no changes.🤖 Generated with Claude Code