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 \