From ee8465fa34314833d4ec3edf2b1886282452509f Mon Sep 17 00:00:00 2001 From: Paul Hitt Date: Thu, 17 Sep 2026 08:29:24 -0400 Subject: [PATCH] The on-device suites run again, and the alerts inbox has a door The instrumented suites had not had a green run since 2026-08-26. Since then the composer hides send behind a camera until there is text, the status chips moved to the Account screen, contacts became circles with the profile behind a long-press, the stories tab posts through new-story, alerts left the bar, and the app moved inside imePadding() so a keyboard shortens the screen. Every helper that tapped a control by its tag without scrolling to it was tapping outside the window. The suites now scroll before they tap, assert send after typing, look for the status chips and the device id on the Account screen, long-press a circle for the profile, and post a story from the stories tab. Sign-up takes a bare host in instrumented.sh; a URL became https://https:// and every test skipped. One product change. Alerts left the bar on 2026-08-28 with the note that the screen survives for the tap-through from Account, and that tap-through was never built, so the inbox had no way in. It is a row on Account now, counting unread the way the bar badge does. CrudSurfaceTest 4 of 4, FeatureUseTest 4 of 4, ChatFlowTest add-and-send green on the emulator against relay1. The reply case needs something answering as the fixture account and stays open. Co-Authored-By: Claude Fable 5.1 --- .../io/flamenet/messenger/app/ChatFlowTest.kt | 14 +++++- .../messenger/app/CrossPlatformTest.kt | 7 +++ .../flamenet/messenger/app/CrudSurfaceTest.kt | 48 ++++++++++++++++--- .../messenger/app/LiveSignUpFleetTest.kt | 16 ++++++- .../io/flamenet/messenger/app/ChatScreens.kt | 22 ++++++++- scripts/instrumented.sh | 16 +++++++ 6 files changed, 113 insertions(+), 10 deletions(-) diff --git a/app/src/androidTest/kotlin/io/flamenet/messenger/app/ChatFlowTest.kt b/app/src/androidTest/kotlin/io/flamenet/messenger/app/ChatFlowTest.kt index 0c6cc90..6691f24 100644 --- a/app/src/androidTest/kotlin/io/flamenet/messenger/app/ChatFlowTest.kt +++ b/app/src/androidTest/kotlin/io/flamenet/messenger/app/ChatFlowTest.kt @@ -7,6 +7,7 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performTextClearance import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.printToString @@ -150,8 +151,16 @@ class ChatFlowTest { compose.onNodeWithTag("passphrase").performTextInput("a-long-enough-passphrase") // The age gate, which nothing here filled. compose.onNodeWithTag("birth-year").performTextInput("1990") + // Scroll first. The app sits inside imePadding() since 2026-09-15, so an + // open keyboard shortens the viewport rather than covering it, and the + // submit button is laid out below it. A click on a node outside the + // window lands nowhere and nothing says so; the test then waits for a + // screen that never comes. + compose.onNodeWithTag("submit").performScrollTo() compose.onNodeWithTag("submit").performClick() - awaitTag("device-id", 120_000) + // Signed in means the contacts screen, whose bar carries this tag. The + // device id is on the Account screen and is not what sign-up lands on. + awaitTag("nav-contacts", 120_000) } /** @@ -169,6 +178,9 @@ class ChatFlowTest { if (nodesWithTag("nav-account").isEmpty()) return compose.onNodeWithTag("nav-account").performClick() awaitTag("sign-out", 20_000) + // The Account screen is long and this button is at its foot. Scroll, or the + // tap lands outside the window and the app stays signed in. + compose.onNodeWithTag("sign-out").performScrollTo() compose.onNodeWithTag("sign-out").performClick() awaitTag("relay", 30_000) } diff --git a/app/src/androidTest/kotlin/io/flamenet/messenger/app/CrossPlatformTest.kt b/app/src/androidTest/kotlin/io/flamenet/messenger/app/CrossPlatformTest.kt index 84b92bf..d42df42 100644 --- a/app/src/androidTest/kotlin/io/flamenet/messenger/app/CrossPlatformTest.kt +++ b/app/src/androidTest/kotlin/io/flamenet/messenger/app/CrossPlatformTest.kt @@ -7,6 +7,7 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performTextClearance import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.printToString @@ -51,6 +52,12 @@ class CrossPlatformTest { compose.onNodeWithTag("passphrase").performTextInput("a-long-enough-passphrase") // The age gate, which nothing here filled. compose.onNodeWithTag("birth-year").performTextInput("1990") + // Scroll first. The app sits inside imePadding() since 2026-09-15, so an + // open keyboard shortens the viewport rather than covering it, and the + // submit button is laid out below it. A click on a node outside the + // window lands nowhere and nothing says so; the test then waits for a + // screen that never comes. + compose.onNodeWithTag("submit").performScrollTo() compose.onNodeWithTag("submit").performClick() awaitTag("nav-contacts", 150_000) android.util.Log.i("fnxplat", "ANDROID_HANDLE=$handle") diff --git a/app/src/androidTest/kotlin/io/flamenet/messenger/app/CrudSurfaceTest.kt b/app/src/androidTest/kotlin/io/flamenet/messenger/app/CrudSurfaceTest.kt index 8ddd0a1..0ad283b 100644 --- a/app/src/androidTest/kotlin/io/flamenet/messenger/app/CrudSurfaceTest.kt +++ b/app/src/androidTest/kotlin/io/flamenet/messenger/app/CrudSurfaceTest.kt @@ -7,6 +7,9 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.longClick +import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performTextClearance import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.printToString @@ -91,10 +94,16 @@ class CrudSurfaceTest { assumeTrue("the relay is unreachable - skipped, and a skip is not a pass", relayReachable()) signUp() - // Presence is a claim about you, so you can set it. + // Presence is a claim about you, so you can set it. It lives on the + // Account screen, under "Your status", below the fold. + compose.onNodeWithTag("nav-account").performClick() + awaitTag("presence-online", 20_000) listOf("online", "away", "busy", "offline").forEach { + compose.onNodeWithTag("presence-$it").performScrollTo() compose.onNodeWithTag("presence-$it").assertExists() } + compose.onNodeWithTag("nav-contacts").performClick() + awaitTag("add-contact", 20_000) compose.onNodeWithTag("add-contact").performClick() compose.onNodeWithTag("new-contact").performTextInput(peerHandle) @@ -106,8 +115,10 @@ class CrudSurfaceTest { awaitNode(hasText(peerHandle, substring = true), 60_000) // Read the id the row was given, then open the contact screen. + // Since the 2026-08-28 Home redesign a contact is a circle, and the + // profile with everything you can do to them is behind a long-press. val peerId = peerIdFromTags() - compose.onNodeWithTag("contact-menu-$peerId").performClick() + compose.onNodeWithTag("buddy-circle-$peerId").performTouchInput { longClick() } compose.onNodeWithTag("contact-message").assertExists() compose.onNodeWithTag("contact-call").assertExists() compose.onNodeWithTag("contact-block").assertExists() @@ -125,11 +136,17 @@ class CrudSurfaceTest { // Stories are reachable from the contact list, and say what they are // when there are none, an empty strip with no explanation is a feature // nobody discovers. - compose.onNodeWithTag("post-story").assertExists() + compose.onNodeWithTag("nav-stories").performClick() + awaitTag("new-story", 20_000) + compose.onNodeWithTag("new-story").assertExists() - compose.onNodeWithTag("nav-alerts").performClick() - awaitTag("mark-all-read", 20_000) - compose.onNodeWithTag("mark-all-read").assertExists() + // Alerts left the bar on 2026-08-28; the door is a row on Account. + compose.onNodeWithTag("nav-account").performClick() + awaitTag("alerts-inbox", 20_000) + compose.onNodeWithTag("alerts-inbox").performScrollTo() + compose.onNodeWithTag("alerts-inbox").performClick() + awaitTag("alerts-heading", 20_000) + compose.onNodeWithTag("nav-contacts").performClick() compose.onNodeWithTag("nav-account").performClick() awaitTag("display-name", 20_000) @@ -165,7 +182,15 @@ class CrudSurfaceTest { awaitTag("draft", 20_000) compose.onNodeWithTag("draft").assertExists() + // An empty draft offers the camera; the send button takes its place + // once there is something to send. Both halves, in that order. + compose.onNodeWithTag("camera").assertExists() + compose.onNodeWithTag("draft").performTextInput("a line to send") compose.onNodeWithTag("send").assertExists() + // The safety number exists once the peer's keys are known, and the + // first message is what fetches them. + compose.onNodeWithTag("send").performClick() + awaitNode(hasText("a line to send", substring = true), 60_000) compose.onNodeWithTag("attach").assertExists() compose.onNodeWithTag("back").assertExists() @@ -187,7 +212,7 @@ class CrudSurfaceTest { private fun peerIdFromTags(): Long { val dump = visibleText() val match = - Regex("contact-menu-(\\d+)").find(dump) + Regex("buddy-circle-(\\d+)").find(dump) ?: throw AssertionError("no contact row found. On screen: $dump") return match.groupValues[1].toLong() } @@ -201,6 +226,12 @@ class CrudSurfaceTest { compose.onNodeWithTag("passphrase").performTextInput("a-long-enough-passphrase") // The age gate, which nothing here filled. compose.onNodeWithTag("birth-year").performTextInput("1990") + // Scroll first. The app sits inside imePadding() since 2026-09-15, so an + // open keyboard shortens the viewport rather than covering it, and the + // submit button is laid out below it. A click on a node outside the + // window lands nowhere and nothing says so; the test then waits for a + // screen that never comes. + compose.onNodeWithTag("submit").performScrollTo() compose.onNodeWithTag("submit").performClick() awaitTag("nav-contacts", 120_000) } @@ -220,6 +251,9 @@ class CrudSurfaceTest { if (nodesWithTag("nav-account").isEmpty()) return compose.onNodeWithTag("nav-account").performClick() awaitTag("sign-out", 20_000) + // The Account screen is long and this button is at its foot. Scroll, or the + // tap lands outside the window and the app stays signed in. + compose.onNodeWithTag("sign-out").performScrollTo() compose.onNodeWithTag("sign-out").performClick() runCatching { awaitTag("relay", 30_000) }.onFailure { throw AssertionError("could not get back to signed out. On screen: " + visibleText()) diff --git a/app/src/androidTest/kotlin/io/flamenet/messenger/app/LiveSignUpFleetTest.kt b/app/src/androidTest/kotlin/io/flamenet/messenger/app/LiveSignUpFleetTest.kt index 4348c9c..2613543 100644 --- a/app/src/androidTest/kotlin/io/flamenet/messenger/app/LiveSignUpFleetTest.kt +++ b/app/src/androidTest/kotlin/io/flamenet/messenger/app/LiveSignUpFleetTest.kt @@ -6,6 +6,7 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performTextClearance import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.printToString @@ -96,11 +97,21 @@ class LiveSignUpFleetTest { compose.onNodeWithTag("passphrase").performTextInput("a-long-enough-passphrase") // See SignUpFlowTest: a disabled submit swallows the tap silently. compose.onNodeWithTag("birth-year").performTextInput("1990") + // Scroll first. The app sits inside imePadding() since 2026-09-15, so an + // open keyboard shortens the viewport rather than covering it, and the + // submit button is laid out below it. A click on a node outside the + // window lands nowhere and nothing says so; the test then waits for a + // screen that never comes. + compose.onNodeWithTag("submit").performScrollTo() compose.onNodeWithTag("submit").performClick() // Generous: a network round trip to two services, then a device // registration publishing twenty prekeys, on an emulator. - awaitTag("device-id", 150_000) + awaitTag("nav-contacts", 150_000) + // The device id is on the Account screen, which sign-up does not land on. + compose.onNodeWithTag("nav-account").performClick() + awaitTag("device-id", 30_000) + compose.onNodeWithTag("device-id").performScrollTo() compose.onNodeWithTag("device-id").assertIsDisplayed() android.util.Log.i("fnlive", "ANDROID_SIGNED_UP=$name relay=$relay") } @@ -119,6 +130,9 @@ class LiveSignUpFleetTest { if (nodesWithTag("nav-account").isEmpty()) return compose.onNodeWithTag("nav-account").performClick() awaitTag("sign-out", 20_000) + // The Account screen is long and this button is at its foot. Scroll, or the + // tap lands outside the window and the app stays signed in. + compose.onNodeWithTag("sign-out").performScrollTo() compose.onNodeWithTag("sign-out").performClick() awaitTag("relay", 30_000) } diff --git a/app/src/main/kotlin/io/flamenet/messenger/app/ChatScreens.kt b/app/src/main/kotlin/io/flamenet/messenger/app/ChatScreens.kt index 2284533..2141658 100644 --- a/app/src/main/kotlin/io/flamenet/messenger/app/ChatScreens.kt +++ b/app/src/main/kotlin/io/flamenet/messenger/app/ChatScreens.kt @@ -588,7 +588,11 @@ fun ColumnScope.NotificationsScreen( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - Text("Alerts", style = MaterialTheme.typography.headlineMedium) + Text( + "Alerts", + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.testTag("alerts-heading"), + ) // Only when there is something to mark. if (unread > 0) { KitChip( @@ -728,6 +732,22 @@ fun ColumnScope.AccountScreen( SettingsSection { DisplaySection() } SectionBand() + // The door to the alerts inbox. Alerts left the bar on 2026-08-28 with the + // note that the screen "survives for the badge tap-through from Account", + // and this is that tap-through; without it the screen had no way in. + SettingsSection { + SettingsRow( + label = "Alerts", + description = + if (phase.notifications.any { !it.read }) { + "${phase.notifications.count { !it.read }} unread" + } else { + "Mentions, replies to your stories, and requests." + }, + tag = "alerts-inbox", + onClick = { model.navigateTo(MessengerViewModel.Phase.Screen.NOTIFICATIONS) }, + ) + } SettingsSection { NotificationSection() } SectionBand() diff --git a/scripts/instrumented.sh b/scripts/instrumented.sh index d7e70d0..fd17389 100755 --- a/scripts/instrumented.sh +++ b/scripts/instrumented.sh @@ -34,6 +34,13 @@ MSG } shift || true +# A bare host, never a URL. The suites take the host and pick the scheme +# themselves (http for the emulator's loopback, https for anything else), +# so a scheme here becomes `https://https://...` inside the reachability +# probe and every test skips. Observed 2026-09-17: nineteen tests, nine +# skipped, verdict NOT a pass, for an argument that looked right. +RELAY="${RELAY#http://}"; RELAY="${RELAY#https://}"; RELAY="${RELAY%/}" + # Production is refused, not warned about. A run that creates accounts on # relay.flamenet.io cannot be undone by noticing afterwards. # @@ -99,6 +106,15 @@ else echo " (nothing installed, good)" fi +# To LOOK at the emulator while a suite runs, use the console path: +# +# adb emu screenrecord screenshot +# +# `adb exec-out screencap -p` returns a black frame on this emulator's GPU +# path while the display is fine. Observed 2026-09-17: the black capture read +# as a dead screen and cost an hour; the console screenshot showed the +# sign-up form, filled in, with the submit button below the fold. + echo "### relay $RELAY ยท classes $CLASSES" set +e ./gradlew --no-daemon :app:connectedDebugAndroidTest \