Hold the three pickers to the constant they hand back #67

Merged
JMR-dev merged 1 commits from test/r38-3-pickers into main 2026-08-24 21:43:16 +00:00
JMR-dev commented 2026-08-24 20:59:30 +00:00 (Migrated from github.com)

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

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