Corregge cloud-init montato, PublisherOnline multi-nodo e E2E locale-aware.

Senza volume stream-node i nodi Hetzner nascevano senza MediaMTX; sync live usa ora Client.for_session e rtmpconns (MediaMTX 1.20).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-11 08:48:28 +02:00
co-authored by Cursor
parent 714602f3da
commit 5a0ca8ef1c
9 changed files with 137 additions and 39 deletions
@@ -17,11 +17,16 @@ import org.junit.runner.RunWith
/**
* E2E sul simulatore: login → nuova partita → wizard 3 step → schermata diretta.
* Usa le stringhe dell'app (locale del device), non testo hardcodato IT/EN.
*/
@RunWith(AndroidJUnit4::class)
class E2EWizardFlowTest {
private lateinit var device: UiDevice
private val pkg = "com.matchlivetv.match_live_tv"
private val ctx by lazy { InstrumentationRegistry.getInstrumentation().targetContext }
private fun s(id: Int): String = ctx.getString(id)
private fun su(id: Int): String = s(id).uppercase()
@Before
fun setUp() {
@@ -32,30 +37,42 @@ class E2EWizardFlowTest {
@Test
fun login_newMatch_wizard_reachesBroadcastScreen() {
waitForAnyText("Email", "ACCEDI", timeoutMs = 45_000)
waitForAnyText(s(R.string.login_email), su(R.string.login_submit), timeoutMs = 45_000)
fillLogin()
waitForText("NUOVA PARTITA", timeoutMs = 45_000)
tapClickableText("NUOVA PARTITA")
waitForText("Avvia subito", timeoutMs = 15_000)
tapClickableText("Avvia subito")
waitForText("01 · Partita", timeoutMs = 45_000)
waitForText(su(R.string.matches_new), timeoutMs = 45_000)
tapClickableText(su(R.string.matches_new))
waitForText(s(R.string.sheet_quick_option), timeoutMs = 15_000)
tapClickableText(s(R.string.sheet_quick_option))
waitForText(s(R.string.wizard_step_title_match), timeoutMs = 45_000)
scrollDown()
tapClickableText("AVANTI >")
waitForText("02 · Trasmissione", timeoutMs = 45_000)
waitForText("Piattaforma", timeoutMs = 30_000)
tapClickableText(s(R.string.wizard_action_next))
waitForText(s(R.string.wizard_step_title_transmission), timeoutMs = 45_000)
waitForText(s(R.string.wizard_transmission_platform_title), timeoutMs = 30_000)
scrollDown()
tapClickableText("AVANTI >")
waitForText("03 · Test rete", timeoutMs = 45_000)
waitForText("AVVIA TEST RETE", timeoutMs = 30_000)
tapClickableText("AVVIA TEST RETE")
waitForText("INIZIA >", timeoutMs = 30_000)
waitUntilEnabled("INIZIA >", timeoutMs = 25_000)
tapClickableText(s(R.string.wizard_action_next))
waitForText(s(R.string.wizard_step_title_network), timeoutMs = 45_000)
waitForText(s(R.string.wizard_network_test_start_label), timeoutMs = 30_000)
tapClickableText(s(R.string.wizard_network_test_start_label))
waitForText(s(R.string.wizard_action_start), timeoutMs = 30_000)
waitUntilEnabled(s(R.string.wizard_action_start), timeoutMs = 25_000)
scrollDown()
tapClickableText("INIZIA >")
val diretta = waitForText("Diretta", timeoutMs = 60_000)
assertNotNull(diretta)
assertNotNull(waitForText("TERMINA DIRETTA", timeoutMs = 30_000))
assertNotNull(waitForText("CHIUDI SET", timeoutMs = 20_000))
tapClickableText(s(R.string.wizard_action_start))
waitForAnyText(
s(R.string.broadcast_status_live),
s(R.string.broadcast_status_connecting),
s(R.string.broadcast_status_reconnecting),
timeoutMs = 60_000,
)
// CLOSE SET è un SideIconButton: in hierarchy compare come content-desc, non sempre come text.
assertTrue(
"Schermata diretta non pronta (né CLOSE SET né End live)",
waitForTextOrDesc(
timeoutMs = 30_000,
s(R.string.broadcast_close_set_button),
s(R.string.score_action_close_set),
s(R.string.broadcast_terminate_cd),
),
)
}
private fun grantRuntimePermissions() {
@@ -69,11 +86,10 @@ class E2EWizardFlowTest {
}
private fun launchApp() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val intent = context.packageManager.getLaunchIntentForPackage(pkg)?.apply {
val intent = ctx.packageManager.getLaunchIntentForPackage(pkg)?.apply {
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
} ?: error("Launch intent mancante per $pkg")
context.startActivity(intent)
ctx.startActivity(intent)
device.wait(Until.hasObject(By.pkg(pkg).depth(0)), 15_000)
}
@@ -81,8 +97,10 @@ class E2EWizardFlowTest {
val fields = device.wait(Until.findObjects(By.clazz("android.widget.EditText")), 15_000)
if (fields.size < 2) error("Campi login non trovati (${fields.size})")
pasteIntoField(fields[0], "coach@matchlivetv.test")
pasteIntoField(fields[1], "password123")
pasteIntoField(fields[1], "Password123")
device.pressKeyCode(KeyEvent.KEYCODE_ENTER)
// Preferisci il bottone (ultimo match uppercase), non il titolo.
tapLastMatchingText(su(R.string.login_submit))
device.waitForIdle()
}
@@ -111,6 +129,34 @@ class E2EWizardFlowTest {
error("Nessuno dei testi trovato: ${texts.joinToString()}")
}
private fun waitForTextOrDesc(timeoutMs: Long, vararg labels: String): Boolean {
val deadline = SystemClock.elapsedRealtime() + timeoutMs
while (SystemClock.elapsedRealtime() < deadline) {
for (label in labels) {
if (device.hasObject(By.text(label)) || device.hasObject(By.desc(label))) return true
}
SystemClock.sleep(250)
}
return false
}
private fun tapLastMatchingText(text: String) {
val nodes = device.findObjects(By.text(text))
val target = nodes.lastOrNull { it.isClickable }
?: nodes.lastOrNull()?.let { label ->
var node: UiObject2? = label
repeat(6) {
val current = node ?: return@repeat
if (current.isClickable) return@let current
node = current.parent
}
label
}
?: error("Testo non trovato: $text")
target.click()
device.waitForIdle()
}
private fun tapClickableText(text: String) {
device.findObject(By.text(text).clickable(true))?.let { node ->
node.click()