Merge branch 'fix/instrumented-suite-catches-up' into 'main'
The on-device suite catches up with the app See merge request paulhitt/flamenet-messenger-android!76
This commit was merged in pull request #103.
This commit is contained in:
@@ -4,7 +4,10 @@ import androidx.compose.ui.test.hasTestTag
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onRoot
|
||||
import androidx.compose.ui.test.assertIsEnabled
|
||||
import androidx.compose.ui.test.onAllNodesWithTag
|
||||
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
|
||||
@@ -55,6 +58,12 @@ class FeatureUseTest {
|
||||
@get:Rule(order = 0)
|
||||
val permissions: GrantPermissionRule = GrantPermissionRule.grant(
|
||||
android.Manifest.permission.POST_NOTIFICATIONS,
|
||||
// 🔴 **RECORD_AUDIO, or the recorder test cannot see the app.** Tapping
|
||||
// record raises the system microphone dialog, which is not part of the
|
||||
// app's Compose tree — so the failure is "No compose hierarchies
|
||||
// found", which reads as the app having died rather than as a dialog
|
||||
// sitting on top of it.
|
||||
android.Manifest.permission.RECORD_AUDIO,
|
||||
)
|
||||
|
||||
@get:Rule(order = 1)
|
||||
@@ -129,6 +138,12 @@ class FeatureUseTest {
|
||||
assumeTrue("the relay is unreachable - skipped, and a skip is not a pass", relayReachable())
|
||||
signUpAndAddPeer()
|
||||
|
||||
// ⚠️ Groups live on the MESSAGES tab, not Home. Home is the roster and
|
||||
// the stories strip; the conversation list and the groups moved when
|
||||
// the tabs were split.
|
||||
compose.onNodeWithTag("nav-messages").performClick()
|
||||
awaitTag("new-group", 20_000)
|
||||
compose.onNodeWithTag("new-group").performScrollTo()
|
||||
compose.onNodeWithTag("new-group").performClick()
|
||||
awaitTag("group-name", 20_000)
|
||||
compose.onNodeWithTag("group-name").performTextInput("Test Crew")
|
||||
@@ -142,7 +157,7 @@ class FeatureUseTest {
|
||||
compose.onNodeWithTag("create-group").performClick()
|
||||
|
||||
// The group list carries a `group-<id>` row once the relay answers.
|
||||
awaitNodeMatching(Regex("group-\\d+"), 60_000,
|
||||
awaitNodeMatching(Regex("group-\\d+"), 150_000,
|
||||
"the group never appeared in the list after Create")
|
||||
}
|
||||
|
||||
@@ -161,11 +176,23 @@ class FeatureUseTest {
|
||||
assumeTrue("the relay is unreachable - skipped, and a skip is not a pass", relayReachable())
|
||||
signUp()
|
||||
|
||||
compose.onNodeWithTag("stories").assertExists()
|
||||
// ⚠️ The stories surface is its own tab. Asserting from Home found
|
||||
// nothing and reported it as the strip being absent.
|
||||
compose.onNodeWithTag("nav-stories").performClick()
|
||||
// 🔴 **`new-story`, not `stories`.** The `stories` tag belongs to the
|
||||
// STRIP on Home; the Stories TAB is its own screen and carries the
|
||||
// compose button instead. Asserting the strip's tag from the tab found
|
||||
// nothing and reported it as the surface being absent — while the
|
||||
// screen was right there, headed "Stories", saying "Nothing here yet".
|
||||
awaitTag("new-story", 20_000)
|
||||
compose.onNodeWithTag("new-story").assertExists()
|
||||
// ⚠️ The point of the test is the EXPLANATION, not the strip: an empty
|
||||
// surface with nothing telling you what it is for is how a feature goes
|
||||
// undiscovered.
|
||||
assertTrue(
|
||||
"the stories strip is empty with no explanation, which is how a " +
|
||||
"feature goes undiscovered",
|
||||
nodeExists("stories-empty") || nodeExists("post-story"),
|
||||
"the stories surface is empty with no explanation of what it is for",
|
||||
visibleText().contains("Nothing here yet") &&
|
||||
visibleText().contains("disappears"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -207,17 +234,37 @@ class FeatureUseTest {
|
||||
|
||||
private fun signUpAndAddPeer() {
|
||||
signUp()
|
||||
compose.onNodeWithTag("new-contact").performClick()
|
||||
awaitTag("lookup-contact", 20_000)
|
||||
compose.onNodeWithTag("lookup-contact").performTextInput(PEER)
|
||||
// 🔴 **The add-contact flow, as it exists.** `new-contact` is the text
|
||||
// FIELD, not a button that opens one, and `lookup-contact` is not in
|
||||
// the app at all — the form is opened by `add-contact` and submitted
|
||||
// with `confirm-contact`.
|
||||
compose.onNodeWithTag("add-contact").performScrollTo()
|
||||
compose.onNodeWithTag("add-contact").performClick()
|
||||
awaitTag("new-contact", 20_000)
|
||||
compose.onNodeWithTag("new-contact").performTextInput(PEER)
|
||||
// ⚠️ **Look up, THEN add.** `confirm-contact` only exists once the
|
||||
// lookup has found somebody — the app refuses to turn a typed string
|
||||
// straight into a contact, because a typo would silently become a
|
||||
// stranger and the first thing sent to them is a message meant for
|
||||
// someone else.
|
||||
compose.onNodeWithTag("lookup-contact").performScrollTo()
|
||||
compose.onNodeWithTag("lookup-contact").performClick()
|
||||
awaitTag("confirm-contact", 30_000)
|
||||
compose.onNodeWithTag("confirm-contact").performScrollTo()
|
||||
compose.onNodeWithTag("confirm-contact").performClick()
|
||||
awaitNodeMatching(Regex("buddy-\\d+"), 60_000,
|
||||
// ⚠️ `buddy-circle-N` as well as `buddy-N`. Home shows the Recent
|
||||
// circles row, whose tags are `buddy-circle-<id>`; the plain
|
||||
// `buddy-<id>` belongs to the list further in. Matching only the
|
||||
// latter reported "never became a contact" about a contact that was
|
||||
// right there on screen — the roster had the person, under the other
|
||||
// name for the same thing.
|
||||
awaitNodeMatching(Regex("buddy-(circle-)?\\d+"), 150_000,
|
||||
"'$PEER' never became a contact — does that account exist on $relay?")
|
||||
}
|
||||
|
||||
private fun signUpAndOpenAThread() {
|
||||
signUpAndAddPeer()
|
||||
val buddyTag = Regex("buddy-(\\d+)").find(visibleText())?.value
|
||||
val buddyTag = Regex("buddy-(circle-)?\\d+").find(visibleText())?.value
|
||||
?: throw AssertionError("no contact row to open")
|
||||
compose.onNodeWithTag(buddyTag).performClick()
|
||||
awaitTag("draft", 20_000)
|
||||
@@ -238,19 +285,60 @@ class FeatureUseTest {
|
||||
// surfaced two minutes later as "nav-contacts did not appear" with no
|
||||
// error anywhere on screen.
|
||||
compose.onNodeWithTag("birth-year").performTextInput("1990")
|
||||
// 🔴 **Scroll to it first.** The sign-up form grew a "Year of birth"
|
||||
// field and its explanation, and `submit` went below the fold on a
|
||||
// 1080x2400 screen. An off-screen node still reports ENABLED — that is
|
||||
// a state check, not a visibility one — and the gesture reached
|
||||
// nothing. The app's own logging proved it: `signIn` was never called,
|
||||
// through 450 seconds of waiting for a screen nobody had asked for.
|
||||
compose.onNodeWithTag("submit").performScrollTo()
|
||||
compose.onNodeWithTag("submit").performClick()
|
||||
awaitTag("nav-contacts", 120_000)
|
||||
val began = System.currentTimeMillis()
|
||||
// ⏱ How long sign-up actually takes here, logged so the answer is a
|
||||
// number rather than a guess.
|
||||
awaitTag("nav-contacts", 420_000)
|
||||
android.util.Log.e("FNTEST", "sign-up completed in " +
|
||||
((System.currentTimeMillis() - began) / 1000) + "s")
|
||||
}
|
||||
|
||||
/**
|
||||
* Get back to the signed-out screen, or establish that we are already there.
|
||||
*
|
||||
* 🔴 **Two things made this a no-op.** `sign-out` sits at the bottom of the
|
||||
* Account screen and needs scrolling to — an off-screen node accepts a
|
||||
* click and does nothing. And the destructive rows **confirm in place**:
|
||||
* the first tap changes the row to say what it is about to do, and a second
|
||||
* tap performs it. One tap armed it and left the session intact, so every
|
||||
* test after the first opened signed in and could not find the sign-up
|
||||
* form, failing on `TestTag = 'relay'` as though the screen were missing.
|
||||
*
|
||||
* ⚠️ Not wrapped in `runCatching` any more. Swallowing a failure here made
|
||||
* the next test's failure the visible one, three screens away from the
|
||||
* cause.
|
||||
*/
|
||||
private fun signOutIfSignedIn() {
|
||||
runCatching {
|
||||
if (!awaitTagOrNull("nav-account", 8_000)) return
|
||||
compose.onNodeWithTag("nav-account").performClick()
|
||||
if (awaitTagOrNull("sign-out", 10_000)) {
|
||||
compose.onNodeWithTag("sign-out").performClick()
|
||||
}
|
||||
awaitTagOrNull("submit", 20_000)
|
||||
if (!awaitTagOrNull("nav-account", 8_000)) return
|
||||
compose.onNodeWithTag("nav-account").performClick()
|
||||
// 🔴 **`sign-out-erase`, not `sign-out`.** The two doors do different
|
||||
// things on purpose: a plain sign-out KEEPS the device and the stored
|
||||
// credential, so signing back in picks up where you left off — correct
|
||||
// for a person, wrong for a suite whose next test wants a DIFFERENT
|
||||
// account. The old session was still in the vault, the app kept trying
|
||||
// to use it, and the next sign-up sat behind "Your session expired.
|
||||
// Sign out and back in to keep going."
|
||||
if (!awaitTagOrNull("sign-out-erase", 10_000)) return
|
||||
compose.onNodeWithTag("sign-out-erase").performScrollTo()
|
||||
compose.onNodeWithTag("sign-out-erase").performClick()
|
||||
// ⚠️ Confirm in place: the first tap changes the row to "Really erase —
|
||||
// this cannot be undone", and the second performs it. One tap only
|
||||
// armed it.
|
||||
if (awaitTagOrNull("sign-out-erase", 3_000)) {
|
||||
compose.onNodeWithTag("sign-out-erase").performClick()
|
||||
}
|
||||
assertTrue(
|
||||
"signed out, but the sign-up form never came back",
|
||||
awaitTagOrNull("relay", 30_000),
|
||||
)
|
||||
}
|
||||
|
||||
private fun nodeExists(tag: String) = runCatching {
|
||||
|
||||
Reference in New Issue
Block a user