Make the screen leaves nameable from a test, and give tests a tag vocabulary #65
Merged
JMR-dev
merged 2 commits from 2026-08-24 20:53:38 +00:00
test/r38-1-screen-test-seam into main
2
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
df2e42a2b3 |
Name the screen leaves, and give the tests a tag vocabulary
Kotlin `private` on a top-level declaration is file-scoped, so every leaf composable in the two screens was invisible even to the JVM test source set, which is a friend of main. The only three declarations src/test could name were ConverterScreen, JoinScreen and AppRoot -- there was nothing to write a test against, which is why #52 could not be started as filed. `internal` is the same choice MainActivity already documents for Destination: the unit tests can name it, and it stays invisible to anything outside the module. Eleven declarations in ConverterScreen and FileRow in JoinScreen. The tag table is the other half. Tests reference a symbol rather than a literal, which is what keeps the five children that follow independent: "Cancel", "Start over" and "Save file" are each rendered by both screens and by more than one state branch, so rewording one would otherwise redden several PRs at once and no diff would explain why. There were zero testTag, semantics or contentDescription calls anywhere in main before this. Every tag is applied inside main. A tag a test hands down as a Modifier proves only that the test set it -- it would survive the affordance losing its own tag entirely, which is the vacuous shape CLAUDE.md records nine of in one review. That is why FileRow and DetailRow derive theirs from data they already hold rather than taking an index from the call site. TestTags is public rather than internal, and R38.8 is the reason. It reads the table from androidTest, and whether that is a friend source set of main under AGP 9 had no in-tree answer -- nothing referenced a main internal from there. Settled by compiling one: it is a friend, so internal would work today. Public anyway, because that friendship is AGP wiring rather than something this project states, and the KDoc now carries the measurement so nobody has to repeat it. Smoke tests cover each leaf: it renders, and its tag resolves to exactly one node. Counting rather than asserting existence is deliberate, since a duplicated tag fails differently depending on which finder a later test happens to use. The state-branch tags -- Convert, Cancel, Save file, Start over, the progress bars -- have no bite yet: reaching a branch needs the state seam R38.5 extracts, and the state matrix is R38.6/R38.7 by design. Five mutations, all red on the named test alone: FORMAT_CHIPS deleted, FILE_CARD_NAME deleted, detailRow's tag no longer derived from its label, FileRow's no longer derived from its name, and JOIN_MORE given JOIN's value -- the last caught only by the uniqueness check, which is what it is for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
64c1a60a97 |
Drain the escaped coroutine error before the next test starts
`ConversionViewModelProbeFailureTest.an OutOfMemoryError is not swallowed` deliberately lets a real error escape `viewModelScope.launch`, which has no exception handler by design -- the ViewModel's KDoc says an OOM raised in the probe should reach the thread's handler and take the process down. On the JVM something else takes it. kotlinx-coroutines-test installs a process-wide collector for uncaught coroutine errors, keeps whatever it catches, and hands the backlog to the next `runTest` that starts, which throws UncaughtExceptionsBeforeTest. Every Compose test is a `runTest`: that is how `createComposeRule` runs a composition. So the error lands on an unrelated test in an unrelated file, and the message names neither the test that caused it nor the error's origin. Nothing has hit it yet only because the sole Compose test in the repo happens to run before the ViewModel one. R38 adds six more Compose classes in exactly the two packages that surround it, and the first two of them made the suite fail in two different files on two consecutive runs of identical, green code -- the throw is on a real Dispatchers.IO thread, delivered after the state assertion that ends the test responsible, so which class catches it is a race. Drain it where a Compose rule is built. A @Before cannot: the rule's `runTest` wraps the statement that calls it, so it has already thrown. @BeforeClass cannot either, because Robolectric runs it outside the sandbox classloader, where the collector is a different object. Constructing the rule is early enough, since JUnit builds a fresh test-class instance -- and every @get:Rule field on it -- before evaluating any rule. This is containment, not the cure. The cure is a seam: give the probe hop an injectable dispatcher the way the constructor already does for cleanupDispatcher, so the error has somewhere to land. That is a production change and deserves its own commit. kotlinx-coroutines-test was already on the unit-test classpath through compose-ui-test-junit4; it is declared now because a file imports it. Pinned, like robolectric and the linters: org.jetbrains.kotlinx is not one of the groups the prerelease guard covers, so a float here would be free to take a milestone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |