The instrumented suites can get past sign-up again

Two of the six suites tapped submit without filling the age gate. Submit is
disabled until birth-year holds four digits, and performClick on a disabled
Compose node does not throw: it does nothing. Each run then failed up to two
minutes later looking for a screen the tap never reached, blaming whatever
it had waited for.

A JVM test now holds it: any suite that taps submit must fill birth-year, so
the next one written cannot quietly do the same thing.

android#21 also reports that nav-contacts and nav-account no longer exist
anywhere in the app. They do. They are declared in NavDestinations.tabs and
applied through a variable, so a grep for the literal testTag("nav-" finds
nothing, which reads as the navigation having been rebuilt without them. A
third test pins that too, so the claim cannot be made again from a grep.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-09-09 17:12:40 -04:00
co-authored by Claude Opus 5
parent e3df77272f
commit c893824b62
3 changed files with 77 additions and 0 deletions
@@ -94,6 +94,8 @@ class LiveSignUpFleetTest {
compose.onNodeWithTag("handle").performTextInput(name)
compose.onNodeWithTag("passphrase").performTextInput("a-long-enough-passphrase")
// See SignUpFlowTest: a disabled submit swallows the tap silently.
compose.onNodeWithTag("birth-year").performTextInput("1990")
compose.onNodeWithTag("submit").performClick()
// Generous: a network round trip to two services, then a device
@@ -160,6 +160,11 @@ class SignUpFlowTest {
compose.onNodeWithTag("email").performTextInput("$handle@example.invalid")
compose.onNodeWithTag("handle").performTextInput(handle)
compose.onNodeWithTag("passphrase").performTextInput("a-long-enough-passphrase")
// Submit is disabled until the age gate has four digits, and
// performClick on a disabled node does NOT throw: it does nothing, and
// the run then fails two minutes later looking for a screen the tap
// never reached.
compose.onNodeWithTag("birth-year").performTextInput("1990")
compose.onNodeWithTag("submit").performClick()
// Generous: sign-up is a network round trip plus a device registration
@@ -0,0 +1,70 @@
package io.flamenet.messenger.app
import java.io.File
import kotlin.test.assertTrue
import org.junit.Test
/**
* The on-device suites have to be able to get past sign-up.
*
* Submit is disabled until the age gate holds four digits, and `performClick()`
* on a disabled Compose node does not throw: it does nothing. A suite that
* skips the age gate taps a dead button and then fails up to two minutes later
* looking for a screen it never reached, blaming whatever it waited for.
*
* These run on the JVM, so they hold whether or not anybody has an emulator.
*/
class InstrumentedSuitesCanSignUpTest {
private fun androidTestDir(): File {
var dir = File(System.getProperty("user.dir"))
while (!File(dir, "app/src/androidTest/kotlin").isDirectory && dir.parentFile != null) {
dir = dir.parentFile
}
return File(dir, "app/src/androidTest/kotlin/io/flamenet/messenger/app")
}
private fun suites(): List<File> =
androidTestDir().listFiles { f: File -> f.name.endsWith(".kt") }?.toList() ?: emptyList()
@Test
fun `the suites are where this test expects them`() {
assertTrue(suites().isNotEmpty(), "no instrumented suites found: this test is checking nothing")
}
@Test
fun `every suite that submits the form also fills the age gate`() {
for (f in suites()) {
val body = f.readText()
if (!body.contains("""onNodeWithTag("submit").performClick()""")) continue
assertTrue(
body.contains("""onNodeWithTag("birth-year")"""),
"${f.name} taps submit without filling birth-year. Submit is disabled " +
"until the age gate has four digits and performClick on a disabled " +
"node does nothing, so the run fails later looking for a screen the " +
"tap never reached. See android#21.",
)
}
}
@Test
fun `the tags the suites wait for still exist in the app`() {
var dir = File(System.getProperty("user.dir"))
while (!File(dir, "app/src/main/kotlin").isDirectory && dir.parentFile != null) {
dir = dir.parentFile
}
val main =
File(dir, "app/src/main/kotlin")
.walkTopDown()
.filter { it.extension == "kt" }
.joinToString("\n") { it.readText() }
// Declared in NavDestinations and applied through a variable, so a
// grep for the literal `testTag("nav-` finds nothing and reads as the
// navigation having been rebuilt without them. It has not.
for (tag in listOf("nav-contacts", "nav-account")) {
assertTrue(
main.contains("\"$tag\""),
"the suites wait for $tag and nothing in the app declares it any more",
)
}
}
}