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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
4f0f90b4cd
commit
ee8465fa34
@@ -7,6 +7,7 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
|||||||
import androidx.compose.ui.test.onNodeWithTag
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
import androidx.compose.ui.test.onRoot
|
import androidx.compose.ui.test.onRoot
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.compose.ui.test.performScrollTo
|
||||||
import androidx.compose.ui.test.performTextClearance
|
import androidx.compose.ui.test.performTextClearance
|
||||||
import androidx.compose.ui.test.performTextInput
|
import androidx.compose.ui.test.performTextInput
|
||||||
import androidx.compose.ui.test.printToString
|
import androidx.compose.ui.test.printToString
|
||||||
@@ -150,8 +151,16 @@ class ChatFlowTest {
|
|||||||
compose.onNodeWithTag("passphrase").performTextInput("a-long-enough-passphrase")
|
compose.onNodeWithTag("passphrase").performTextInput("a-long-enough-passphrase")
|
||||||
// The age gate, which nothing here filled.
|
// The age gate, which nothing here filled.
|
||||||
compose.onNodeWithTag("birth-year").performTextInput("1990")
|
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()
|
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
|
if (nodesWithTag("nav-account").isEmpty()) return
|
||||||
compose.onNodeWithTag("nav-account").performClick()
|
compose.onNodeWithTag("nav-account").performClick()
|
||||||
awaitTag("sign-out", 20_000)
|
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()
|
compose.onNodeWithTag("sign-out").performClick()
|
||||||
awaitTag("relay", 30_000)
|
awaitTag("relay", 30_000)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
|||||||
import androidx.compose.ui.test.onNodeWithTag
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
import androidx.compose.ui.test.onRoot
|
import androidx.compose.ui.test.onRoot
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.compose.ui.test.performScrollTo
|
||||||
import androidx.compose.ui.test.performTextClearance
|
import androidx.compose.ui.test.performTextClearance
|
||||||
import androidx.compose.ui.test.performTextInput
|
import androidx.compose.ui.test.performTextInput
|
||||||
import androidx.compose.ui.test.printToString
|
import androidx.compose.ui.test.printToString
|
||||||
@@ -51,6 +52,12 @@ class CrossPlatformTest {
|
|||||||
compose.onNodeWithTag("passphrase").performTextInput("a-long-enough-passphrase")
|
compose.onNodeWithTag("passphrase").performTextInput("a-long-enough-passphrase")
|
||||||
// The age gate, which nothing here filled.
|
// The age gate, which nothing here filled.
|
||||||
compose.onNodeWithTag("birth-year").performTextInput("1990")
|
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()
|
compose.onNodeWithTag("submit").performClick()
|
||||||
awaitTag("nav-contacts", 150_000)
|
awaitTag("nav-contacts", 150_000)
|
||||||
android.util.Log.i("fnxplat", "ANDROID_HANDLE=$handle")
|
android.util.Log.i("fnxplat", "ANDROID_HANDLE=$handle")
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
|||||||
import androidx.compose.ui.test.onNodeWithTag
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
import androidx.compose.ui.test.onRoot
|
import androidx.compose.ui.test.onRoot
|
||||||
import androidx.compose.ui.test.performClick
|
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.performTextClearance
|
||||||
import androidx.compose.ui.test.performTextInput
|
import androidx.compose.ui.test.performTextInput
|
||||||
import androidx.compose.ui.test.printToString
|
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())
|
assumeTrue("the relay is unreachable - skipped, and a skip is not a pass", relayReachable())
|
||||||
signUp()
|
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 {
|
listOf("online", "away", "busy", "offline").forEach {
|
||||||
|
compose.onNodeWithTag("presence-$it").performScrollTo()
|
||||||
compose.onNodeWithTag("presence-$it").assertExists()
|
compose.onNodeWithTag("presence-$it").assertExists()
|
||||||
}
|
}
|
||||||
|
compose.onNodeWithTag("nav-contacts").performClick()
|
||||||
|
awaitTag("add-contact", 20_000)
|
||||||
|
|
||||||
compose.onNodeWithTag("add-contact").performClick()
|
compose.onNodeWithTag("add-contact").performClick()
|
||||||
compose.onNodeWithTag("new-contact").performTextInput(peerHandle)
|
compose.onNodeWithTag("new-contact").performTextInput(peerHandle)
|
||||||
@@ -106,8 +115,10 @@ class CrudSurfaceTest {
|
|||||||
awaitNode(hasText(peerHandle, substring = true), 60_000)
|
awaitNode(hasText(peerHandle, substring = true), 60_000)
|
||||||
|
|
||||||
// Read the id the row was given, then open the contact screen.
|
// 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()
|
val peerId = peerIdFromTags()
|
||||||
compose.onNodeWithTag("contact-menu-$peerId").performClick()
|
compose.onNodeWithTag("buddy-circle-$peerId").performTouchInput { longClick() }
|
||||||
compose.onNodeWithTag("contact-message").assertExists()
|
compose.onNodeWithTag("contact-message").assertExists()
|
||||||
compose.onNodeWithTag("contact-call").assertExists()
|
compose.onNodeWithTag("contact-call").assertExists()
|
||||||
compose.onNodeWithTag("contact-block").assertExists()
|
compose.onNodeWithTag("contact-block").assertExists()
|
||||||
@@ -125,11 +136,17 @@ class CrudSurfaceTest {
|
|||||||
// Stories are reachable from the contact list, and say what they are
|
// 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
|
// when there are none, an empty strip with no explanation is a feature
|
||||||
// nobody discovers.
|
// 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()
|
// Alerts left the bar on 2026-08-28; the door is a row on Account.
|
||||||
awaitTag("mark-all-read", 20_000)
|
compose.onNodeWithTag("nav-account").performClick()
|
||||||
compose.onNodeWithTag("mark-all-read").assertExists()
|
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()
|
compose.onNodeWithTag("nav-account").performClick()
|
||||||
awaitTag("display-name", 20_000)
|
awaitTag("display-name", 20_000)
|
||||||
@@ -165,7 +182,15 @@ class CrudSurfaceTest {
|
|||||||
|
|
||||||
awaitTag("draft", 20_000)
|
awaitTag("draft", 20_000)
|
||||||
compose.onNodeWithTag("draft").assertExists()
|
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()
|
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("attach").assertExists()
|
||||||
compose.onNodeWithTag("back").assertExists()
|
compose.onNodeWithTag("back").assertExists()
|
||||||
|
|
||||||
@@ -187,7 +212,7 @@ class CrudSurfaceTest {
|
|||||||
private fun peerIdFromTags(): Long {
|
private fun peerIdFromTags(): Long {
|
||||||
val dump = visibleText()
|
val dump = visibleText()
|
||||||
val match =
|
val match =
|
||||||
Regex("contact-menu-(\\d+)").find(dump)
|
Regex("buddy-circle-(\\d+)").find(dump)
|
||||||
?: throw AssertionError("no contact row found. On screen: $dump")
|
?: throw AssertionError("no contact row found. On screen: $dump")
|
||||||
return match.groupValues[1].toLong()
|
return match.groupValues[1].toLong()
|
||||||
}
|
}
|
||||||
@@ -201,6 +226,12 @@ class CrudSurfaceTest {
|
|||||||
compose.onNodeWithTag("passphrase").performTextInput("a-long-enough-passphrase")
|
compose.onNodeWithTag("passphrase").performTextInput("a-long-enough-passphrase")
|
||||||
// The age gate, which nothing here filled.
|
// The age gate, which nothing here filled.
|
||||||
compose.onNodeWithTag("birth-year").performTextInput("1990")
|
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()
|
compose.onNodeWithTag("submit").performClick()
|
||||||
awaitTag("nav-contacts", 120_000)
|
awaitTag("nav-contacts", 120_000)
|
||||||
}
|
}
|
||||||
@@ -220,6 +251,9 @@ class CrudSurfaceTest {
|
|||||||
if (nodesWithTag("nav-account").isEmpty()) return
|
if (nodesWithTag("nav-account").isEmpty()) return
|
||||||
compose.onNodeWithTag("nav-account").performClick()
|
compose.onNodeWithTag("nav-account").performClick()
|
||||||
awaitTag("sign-out", 20_000)
|
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()
|
compose.onNodeWithTag("sign-out").performClick()
|
||||||
runCatching { awaitTag("relay", 30_000) }.onFailure {
|
runCatching { awaitTag("relay", 30_000) }.onFailure {
|
||||||
throw AssertionError("could not get back to signed out. On screen: " + visibleText())
|
throw AssertionError("could not get back to signed out. On screen: " + visibleText())
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
|||||||
import androidx.compose.ui.test.onNodeWithTag
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
import androidx.compose.ui.test.onRoot
|
import androidx.compose.ui.test.onRoot
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.compose.ui.test.performScrollTo
|
||||||
import androidx.compose.ui.test.performTextClearance
|
import androidx.compose.ui.test.performTextClearance
|
||||||
import androidx.compose.ui.test.performTextInput
|
import androidx.compose.ui.test.performTextInput
|
||||||
import androidx.compose.ui.test.printToString
|
import androidx.compose.ui.test.printToString
|
||||||
@@ -96,11 +97,21 @@ class LiveSignUpFleetTest {
|
|||||||
compose.onNodeWithTag("passphrase").performTextInput("a-long-enough-passphrase")
|
compose.onNodeWithTag("passphrase").performTextInput("a-long-enough-passphrase")
|
||||||
// See SignUpFlowTest: a disabled submit swallows the tap silently.
|
// See SignUpFlowTest: a disabled submit swallows the tap silently.
|
||||||
compose.onNodeWithTag("birth-year").performTextInput("1990")
|
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()
|
compose.onNodeWithTag("submit").performClick()
|
||||||
|
|
||||||
// Generous: a network round trip to two services, then a device
|
// Generous: a network round trip to two services, then a device
|
||||||
// registration publishing twenty prekeys, on an emulator.
|
// 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()
|
compose.onNodeWithTag("device-id").assertIsDisplayed()
|
||||||
android.util.Log.i("fnlive", "ANDROID_SIGNED_UP=$name relay=$relay")
|
android.util.Log.i("fnlive", "ANDROID_SIGNED_UP=$name relay=$relay")
|
||||||
}
|
}
|
||||||
@@ -119,6 +130,9 @@ class LiveSignUpFleetTest {
|
|||||||
if (nodesWithTag("nav-account").isEmpty()) return
|
if (nodesWithTag("nav-account").isEmpty()) return
|
||||||
compose.onNodeWithTag("nav-account").performClick()
|
compose.onNodeWithTag("nav-account").performClick()
|
||||||
awaitTag("sign-out", 20_000)
|
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()
|
compose.onNodeWithTag("sign-out").performClick()
|
||||||
awaitTag("relay", 30_000)
|
awaitTag("relay", 30_000)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -588,7 +588,11 @@ fun ColumnScope.NotificationsScreen(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
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.
|
// Only when there is something to mark.
|
||||||
if (unread > 0) {
|
if (unread > 0) {
|
||||||
KitChip(
|
KitChip(
|
||||||
@@ -728,6 +732,22 @@ fun ColumnScope.AccountScreen(
|
|||||||
SettingsSection { DisplaySection() }
|
SettingsSection { DisplaySection() }
|
||||||
|
|
||||||
SectionBand()
|
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() }
|
SettingsSection { NotificationSection() }
|
||||||
|
|
||||||
SectionBand()
|
SectionBand()
|
||||||
|
|||||||
@@ -34,6 +34,13 @@ MSG
|
|||||||
}
|
}
|
||||||
shift || true
|
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
|
# Production is refused, not warned about. A run that creates accounts on
|
||||||
# relay.flamenet.io cannot be undone by noticing afterwards.
|
# relay.flamenet.io cannot be undone by noticing afterwards.
|
||||||
#
|
#
|
||||||
@@ -99,6 +106,15 @@ else
|
|||||||
echo " (nothing installed, good)"
|
echo " (nothing installed, good)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# To LOOK at the emulator while a suite runs, use the console path:
|
||||||
|
#
|
||||||
|
# adb emu screenrecord screenshot <dir>
|
||||||
|
#
|
||||||
|
# `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"
|
echo "### relay $RELAY · classes $CLASSES"
|
||||||
set +e
|
set +e
|
||||||
./gradlew --no-daemon :app:connectedDebugAndroidTest \
|
./gradlew --no-daemon :app:connectedDebugAndroidTest \
|
||||||
|
|||||||
Reference in New Issue
Block a user