test(coverage): high-value branch-coverage additions #293

Merged
JMR-dev merged 2 commits from test-289-branch-coverage into main 2026-07-04 06:56:15 +00:00
JMR-dev commented 2026-07-04 06:37:41 +00:00 (Migrated from github.com)

Closes #289

Test-only additions that close real (non-coroutine) branch gaps in pure-JVM logic already ≥95% line-covered. No production changes. Validated with :app:testDebugUnitTest :app:jacocoTestReport :app:ktlintCheck :app:detekt (all green). No emulator needed.

Branch coverage: before → after (per source file, JaCoCo)

Source file Before After Missed closed
richtext/RichTextHtmlParser.kt 75.3% (43 miss) 94.3% (10 miss) −33
richtext/RichTextEditing.kt 76.5% (35 miss) 94.0% (9 miss) −26
ui/compose/ComposeViewModel.kt 79.7% (37 miss) 91.8% (15 miss) −22
mail/HtmlToText.kt 64.7% (12 miss) 94.1% (2 miss) −10
reporting/DiagnosticsCollector.kt 59.4% (13 miss) 90.6% (3 miss) −10
data/SignatureBlock.kt 64.3% (5 miss) 100% (0 miss) −5
reporting/AppLog.kt 50.0% (4 miss) 100% (0 miss) −4
ui/mailbox/MailboxViewModel.kt 69.2% (40 miss) 76.2% (31 miss) −9
data/repository/MailRepositoryImpl.kt 66.0% (81 miss) 68.1% (76 miss) −5
data/repository/AccountRepositoryImpl.kt BR 100% / LN 98.0% BR 100% / LN 100% reorderAccounts line

~124 real branches closed across the batch.

What was added

  • RichTextHtmlParser (new RichTextHtmlParserTest): drives the CSS/attr helpers directly — parseCssColor (short/long/invalid hex, non-#), parseFontSizePt (pt/px/zero/malformed), parseInlineStyles/parseBaseStyle/parseTextAlign/richAlignFor, extractHref/extractStyleAttr/unescape — plus parser edges: unterminated tag, stray/empty/unclosed tags, <img> without/with non-cid src, <li> outside a list, unknown block tag, clamped-to-trimmed channels.
  • RichTextEditing: removeLink (was 0% covered), applyLink/styleAt/isStyled caret & partial-range edges, quote/ordered marker detect+remove, remap* collapse-to-null branches, insertImage unshifted-prefix branch.
  • ComposeViewModel: blank draft/from nav args, resumed draft with null account, first-non-empty account wait, non-inline attachment retention, contact-search cancellation, single-field autosave detection, empty-new-composition no-op, post-send exit guard, signature-swap HTML rebuild (the "no longer ends with old block" path), remembered-font seed onto an existing HTML body.
  • MailboxViewModel: blank account arg, empty folderUnreadCounts/null currentFolderRole, no-selection no-op, null-message error fallbacks (selection + refresh), onDrawerOpened without a drawer account, non-trash/spam delete, unified-view non-inbox refresh, confirmPending null arm.
  • MailRepositoryImpl: non-selectable same-role folder (excluded → archive fails), cancelOutboxMessage (present + already-gone rows). The role-folder fallbacks (archive/reportSpam/trash missing-folder, resolveRoleFolder cold-cache retry) were already covered by the existing suite.
  • AccountRepositoryImpl: reorderAccounts (#240 path).
  • HtmlToText: out-of-range/surrogate numeric refs (decimal + hex), valid supplementary-plane refs, named dash entities.
  • SignatureBlock: isEmpty field independence; blank-text-but-non-blank-html block.
  • AppLog (new AppLogUninstalledTest): the "no buffer installed yet" path.

Deliberately skipped (with reason)

The remaining misses are not real logic branches:

  • MailRepositoryImpl (76) — dominated by Kotlin coroutine state-machine dispatch (withContext(Dispatchers.IO) / suspend-point resume branches; e.g. openMessage's 11 on one line) that only cover if a mock actually suspends at that exact point, plus defensive empty-group loop branches (a routing group is never empty). Role-folder logic itself is now fully covered.
  • MailboxViewModel (31) / ComposeViewModel (15) — coroutine lambda / combine / fold resume branches whose behaviour is asserted elsewhere.
  • DiagnosticsCollector (3) — Build.VERSION.RELEASE / MANUFACTURER / MODEL ?: ""; the non-null side only occurs on-device (the JVM android.jar stub returns null), and the fields are static final so can't be set on JDK 21.
  • HtmlToText (2) — a toIntOrNull() null branch on a value the regex guarantees is all digits (unreachable).
  • A handful of RichText defensive branches (e.g. lineStartsTouching is never empty, so hasBlock's isNotEmpty() false side is dead).

🤖 Generated with Claude Code

Closes #289 Test-only additions that close **real** (non-coroutine) branch gaps in pure-JVM logic already ≥95% line-covered. No production changes. Validated with `:app:testDebugUnitTest :app:jacocoTestReport :app:ktlintCheck :app:detekt` (all green). No emulator needed. ## Branch coverage: before → after (per source file, JaCoCo) | Source file | Before | After | Missed closed | |---|---|---|---| | `richtext/RichTextHtmlParser.kt` | 75.3% (43 miss) | **94.3%** (10 miss) | −33 | | `richtext/RichTextEditing.kt` | 76.5% (35 miss) | **94.0%** (9 miss) | −26 | | `ui/compose/ComposeViewModel.kt` | 79.7% (37 miss) | **91.8%** (15 miss) | −22 | | `mail/HtmlToText.kt` | 64.7% (12 miss) | **94.1%** (2 miss) | −10 | | `reporting/DiagnosticsCollector.kt` | 59.4% (13 miss) | **90.6%** (3 miss) | −10 | | `data/SignatureBlock.kt` | 64.3% (5 miss) | **100%** (0 miss) | −5 | | `reporting/AppLog.kt` | 50.0% (4 miss) | **100%** (0 miss) | −4 | | `ui/mailbox/MailboxViewModel.kt` | 69.2% (40 miss) | **76.2%** (31 miss) | −9 | | `data/repository/MailRepositoryImpl.kt` | 66.0% (81 miss) | **68.1%** (76 miss) | −5 | | `data/repository/AccountRepositoryImpl.kt` | BR 100% / **LN 98.0%** | BR 100% / **LN 100%** | reorderAccounts line | **~124 real branches closed** across the batch. ## What was added - **RichTextHtmlParser** (new `RichTextHtmlParserTest`): drives the CSS/attr helpers directly — `parseCssColor` (short/long/invalid hex, non-`#`), `parseFontSizePt` (pt/px/zero/malformed), `parseInlineStyles`/`parseBaseStyle`/`parseTextAlign`/`richAlignFor`, `extractHref`/`extractStyleAttr`/`unescape` — plus parser edges: unterminated tag, stray/empty/unclosed tags, `<img>` without/with non-cid src, `<li>` outside a list, unknown block tag, clamped-to-trimmed channels. - **RichTextEditing**: `removeLink` (was 0% covered), `applyLink`/`styleAt`/`isStyled` caret & partial-range edges, quote/ordered marker detect+remove, `remap*` collapse-to-null branches, `insertImage` unshifted-prefix branch. - **ComposeViewModel**: blank draft/from nav args, resumed draft with null account, first-non-empty account wait, non-inline attachment retention, contact-search cancellation, single-field autosave detection, empty-new-composition no-op, post-send exit guard, **signature-swap HTML rebuild** (the "no longer ends with old block" path), remembered-font seed onto an existing HTML body. - **MailboxViewModel**: blank account arg, empty `folderUnreadCounts`/null `currentFolderRole`, no-selection no-op, null-message error fallbacks (selection + refresh), `onDrawerOpened` without a drawer account, non-trash/spam delete, unified-view non-inbox refresh, `confirmPending` null arm. - **MailRepositoryImpl**: non-selectable same-role folder (excluded → archive fails), `cancelOutboxMessage` (present + already-gone rows). The role-folder fallbacks (`archive`/`reportSpam`/`trash` missing-folder, `resolveRoleFolder` cold-cache retry) were already covered by the existing suite. - **AccountRepositoryImpl**: `reorderAccounts` (#240 path). - **HtmlToText**: out-of-range/surrogate numeric refs (decimal + hex), valid supplementary-plane refs, named dash entities. - **SignatureBlock**: `isEmpty` field independence; blank-text-but-non-blank-html block. - **AppLog** (new `AppLogUninstalledTest`): the "no buffer installed yet" path. ## Deliberately skipped (with reason) The remaining misses are **not real logic branches**: - **MailRepositoryImpl (76)** — dominated by Kotlin coroutine state-machine dispatch (`withContext(Dispatchers.IO)` / suspend-point resume branches; e.g. `openMessage`'s 11 on one line) that only cover if a mock actually suspends at that exact point, plus defensive empty-`group` loop branches (a routing group is never empty). Role-folder logic itself is now fully covered. - **MailboxViewModel (31)** / **ComposeViewModel (15)** — coroutine lambda / `combine` / `fold` resume branches whose *behaviour* is asserted elsewhere. - **DiagnosticsCollector (3)** — `Build.VERSION.RELEASE` / `MANUFACTURER` / `MODEL` `?: ""`; the non-null side only occurs on-device (the JVM `android.jar` stub returns null), and the fields are `static final` so can't be set on JDK 21. - **HtmlToText (2)** — a `toIntOrNull()` null branch on a value the regex guarantees is all digits (unreachable). - A handful of RichText defensive branches (e.g. `lineStartsTouching` is never empty, so `hasBlock`'s `isNotEmpty()` false side is dead). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.