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>
194 lines
6.3 KiB
Kotlin
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)
|
|
}
|