Files
flamenet-messenger-android/app/src/androidTest/kotlin/io/flamenet/messenger/app/LiveSignUpFleetTest.kt
T
Paul HittandClaude Fable 5.1 2fa430f21c API-style comments and ktlint formatting
Every comment is reduced to its first sentence plus @param, @return
and @throws tags. Comment lines: 20040 to 8526. ktlint 1.8 applied
with its default (official Kotlin) style across all modules; 262 files
reformatted. The 264 remaining ktlint findings are naming and filename
rules, which are code changes, and are left alone.

Comments and formatting only. 1208 tests pass, counted from the XML.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 20:02:07 -04:00

194 lines
6.3 KiB
Kotlin

package io.flamenet.messenger.app
import androidx.compose.ui.test.assertIsDisplayed
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.performClick
import androidx.compose.ui.test.performTextClearance
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.printToString
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/**
* Sign-up on the two shapes that hold their accounts in the shared service.
*/
@RunWith(AndroidJUnit4::class)
class LiveSignUpFleetTest {
/**
* Granted BEFORE the activity launches, and that ordering is the point.
*/
@get:Rule(order = 0)
val permissions: GrantPermissionRule =
GrantPermissionRule.grant(
android.Manifest.permission.POST_NOTIFICATIONS,
)
@get:Rule(order = 1)
val compose = createAndroidComposeRule<MainActivity>()
private val args get() = InstrumentationRegistry.getArguments()
/** Ours, and pinned in the client rather than taken from the relay. */
private val managed get() = args.getString("managed") ?: "relay.flamenet.io"
/**
* Somebody else's relay, keeping its accounts in the shared service.
*/
private val sharedSelfHosted get() = args.getString("shared") ?: "relay2.flamenet.io"
private lateinit var handle: String
@Before
fun startSignedOut() {
handle = "andl" + (System.currentTimeMillis() % 100_000_000)
signOutIfSignedIn()
}
/**
* `assumeTrue`, never a bare `return`.
*/
private fun requireLiveOptIn() =
assumeTrue(
"pass -e live true to run these, they create real accounts on accounts.flamenet.io",
args.getString("live") == "true",
)
@Test
fun signUpOnTheManagedRelay() {
requireLiveOptIn()
signUpLive(managed, "m")
}
@Test
fun signUpOnASharedSelfHostedRelay() {
requireLiveOptIn()
signUpLive(sharedSelfHosted, "s")
}
/**
* One whole sign-up, driven the way a person drives it.
*/
private fun signUpLive(
relay: String,
tag: String,
) {
if (!reachable(relay)) return
val name = "$handle$tag"
compose.onNodeWithTag("relay").performTextClearance()
compose.onNodeWithTag("relay").performTextInput(relay)
compose.onNodeWithTag("toggle-register").performClick()
// Wait for discovery BEFORE deciding anything about the form.
awaitTag("identity-mode", 30_000)
awaitNoTag("email", 30_000)
compose.onNodeWithTag("handle").performTextInput(name)
compose.onNodeWithTag("passphrase").performTextInput("a-long-enough-passphrase")
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)
compose.onNodeWithTag("device-id").assertIsDisplayed()
android.util.Log.i("fnlive", "ANDROID_SIGNED_UP=$name relay=$relay")
}
// MARK: - the same waits the rest of the suite uses
/**
* Get to a signed-out app.
*/
private fun signOutIfSignedIn() {
awaitAny(listOf("relay", "nav-contacts", "nav-account", "back"), 30_000)
if (nodesWithTag("back").isNotEmpty()) {
compose.onNodeWithTag("back").performClick()
awaitTag("nav-contacts", 20_000)
}
if (nodesWithTag("nav-account").isEmpty()) return
compose.onNodeWithTag("nav-account").performClick()
awaitTag("sign-out", 20_000)
compose.onNodeWithTag("sign-out").performClick()
awaitTag("relay", 30_000)
}
private fun nodesWithTag(tag: String): List<Any> =
runCatching {
compose.onAllNodes(hasTestTag(tag)).fetchSemanticsNodes()
}.getOrDefault(emptyList())
private fun awaitAny(
tags: List<String>,
timeoutMillis: Long,
) {
pumping(timeoutMillis) { tags.any { nodesWithTag(it).isNotEmpty() } }
}
private fun awaitTag(
tag: String,
timeoutMillis: Long,
) {
if (!pumping(timeoutMillis) { nodesWithTag(tag).isNotEmpty() }) {
throw AssertionError("'$tag' never appeared within ${timeoutMillis}ms. On screen: ${visibleText()}")
}
}
/** The inverse, and the one this test actually turns on. */
private fun awaitNoTag(
tag: String,
timeoutMillis: Long,
) {
if (!pumping(timeoutMillis) { nodesWithTag(tag).isEmpty() }) {
throw AssertionError(
"'$tag' was still on screen after ${timeoutMillis}ms, discovery did not land, " +
"so this relay is being treated as if it kept its own accounts. " +
"On screen: ${visibleText()}",
)
}
}
/**
* Poll a condition, advancing the clock by hand.
*/
private fun pumping(
timeoutMillis: Long,
condition: () -> Boolean,
): Boolean {
compose.mainClock.autoAdvance = false
try {
val deadline = System.currentTimeMillis() + timeoutMillis
while (System.currentTimeMillis() < deadline) {
compose.mainClock.advanceTimeBy(100)
if (runCatching(condition).getOrDefault(false)) return true
Thread.sleep(100)
}
return false
} finally {
compose.mainClock.autoAdvance = true
}
}
private fun visibleText(): String =
runCatching {
compose.onRoot().printToString(maxDepth = 30)
}.getOrElse { "(could not read the screen: $it)" }
private fun reachable(relay: String): Boolean =
runCatching {
(java.net.URL("https://$relay/e2e/log/head").openConnection() as java.net.HttpURLConnection)
.apply {
connectTimeout = 8000
readTimeout = 8000
}.responseCode in 200..299
}.getOrDefault(false)
}