Mantiene la copertina YouTube se il telefono cade e avvia ffmpeg sul nodo, non via SSH.
Il relay HLS→RTMPS resta sul worker/agent del nodo assegnato, così tre dirette contemporanee restano in onda sul sito e sul canale della società senza schermo nero. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+202
-34
@@ -37,10 +37,26 @@ class E2EWizardFlowTest {
|
||||
|
||||
@Test
|
||||
fun login_newMatch_wizard_reachesBroadcastScreen() {
|
||||
waitForAnyText(s(R.string.login_email), su(R.string.login_submit), timeoutMs = 45_000)
|
||||
fillLogin()
|
||||
waitForText(su(R.string.matches_new), timeoutMs = 45_000)
|
||||
tapClickableText(su(R.string.matches_new))
|
||||
waitForAnyText(
|
||||
s(R.string.login_email),
|
||||
su(R.string.login_submit),
|
||||
"Email",
|
||||
"ACCEDI",
|
||||
"LOG IN",
|
||||
su(R.string.matches_new),
|
||||
"NUOVA PARTITA",
|
||||
"NEW MATCH",
|
||||
timeoutMs = 45_000,
|
||||
)
|
||||
if (!hasMatchList()) {
|
||||
fillLogin()
|
||||
}
|
||||
if (!hasMatchList()) {
|
||||
tapLoginSubmit()
|
||||
SystemClock.sleep(4000)
|
||||
}
|
||||
waitForAnyText(su(R.string.matches_new), "NUOVA PARTITA", "NEW MATCH", timeoutMs = 60_000)
|
||||
tapFirstAvailable(su(R.string.matches_new), "NUOVA PARTITA", "NEW MATCH")
|
||||
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)
|
||||
@@ -48,20 +64,46 @@ class E2EWizardFlowTest {
|
||||
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(s(R.string.wizard_action_next))
|
||||
waitForText(s(R.string.wizard_step_title_network), timeoutMs = 45_000)
|
||||
selectYoutubeIfRequested()
|
||||
val onNetworkStep = {
|
||||
device.hasObject(By.text(s(R.string.wizard_network_test_start_label))) ||
|
||||
device.hasObject(By.text("AVVIA TEST RETE")) ||
|
||||
device.hasObject(By.text("START NETWORK TEST"))
|
||||
}
|
||||
repeat(4) {
|
||||
if (onNetworkStep()) return@repeat
|
||||
runCatching { tapClickableText(s(R.string.wizard_action_next)) }
|
||||
if (onNetworkStep()) return@repeat
|
||||
// AVANTI è in fondo alla colonna: uno swipe verso l'alto lo riporta in vista.
|
||||
scrollUp()
|
||||
SystemClock.sleep(1_200)
|
||||
}
|
||||
waitForAnyText(
|
||||
s(R.string.wizard_network_test_start_label),
|
||||
"AVVIA TEST RETE",
|
||||
"START NETWORK TEST",
|
||||
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(s(R.string.wizard_action_start))
|
||||
if (wantsYoutube()) {
|
||||
waitForTextContaining("youtube.com", "youtu.be", timeoutMs = 60_000)
|
||||
}
|
||||
tapStartLive()
|
||||
waitForAnyText(
|
||||
s(R.string.broadcast_status_live),
|
||||
s(R.string.broadcast_status_connecting),
|
||||
s(R.string.broadcast_status_reconnecting),
|
||||
timeoutMs = 60_000,
|
||||
s(R.string.broadcast_close_set_button),
|
||||
s(R.string.score_action_close_set),
|
||||
s(R.string.broadcast_terminate_cd),
|
||||
"IN DIRETTA",
|
||||
"LIVE",
|
||||
"CONNESSIONE…",
|
||||
"CONNECTING…",
|
||||
timeoutMs = 90_000,
|
||||
)
|
||||
// CLOSE SET è un SideIconButton: in hierarchy compare come content-desc, non sempre come text.
|
||||
assertTrue(
|
||||
@@ -73,6 +115,9 @@ class E2EWizardFlowTest {
|
||||
s(R.string.broadcast_terminate_cd),
|
||||
),
|
||||
)
|
||||
// Tiene la diretta accesa per visione browser / secondo emulatore.
|
||||
val holdMs = InstrumentationRegistry.getArguments().getString("holdLiveMs")?.toLongOrNull() ?: 0L
|
||||
if (holdMs > 0) SystemClock.sleep(holdMs)
|
||||
}
|
||||
|
||||
private fun grantRuntimePermissions() {
|
||||
@@ -94,14 +139,108 @@ class E2EWizardFlowTest {
|
||||
}
|
||||
|
||||
private fun fillLogin() {
|
||||
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")
|
||||
device.pressKeyCode(KeyEvent.KEYCODE_ENTER)
|
||||
// Preferisci il bottone (ultimo match uppercase), non il titolo.
|
||||
tapLastMatchingText(su(R.string.login_submit))
|
||||
repeat(3) {
|
||||
val email = waitForRes("login_email")
|
||||
val password = waitForRes("login_password")
|
||||
pasteIntoField(email, "coach@matchlivetv.test")
|
||||
pasteIntoField(password, "Password123")
|
||||
device.pressKeyCode(KeyEvent.KEYCODE_ENTER)
|
||||
device.waitForIdle()
|
||||
SystemClock.sleep(1500)
|
||||
tapLoginSubmit()
|
||||
device.waitForIdle()
|
||||
SystemClock.sleep(4000)
|
||||
if (hasMatchList()) return
|
||||
}
|
||||
}
|
||||
|
||||
private fun wantsYoutube(): Boolean =
|
||||
InstrumentationRegistry.getArguments().getString("platform").equals("youtube", ignoreCase = true)
|
||||
|
||||
private fun tapStartLive() {
|
||||
repeat(6) {
|
||||
val tagged = device.findObject(By.res("wizard_start_live"))
|
||||
?: device.findObject(By.res("$pkg:id/wizard_start_live"))
|
||||
if (tagged != null && tagged.isEnabled) {
|
||||
val b = tagged.visibleBounds
|
||||
if (b.height() >= 20 && b.bottom <= device.displayHeight && b.top >= 0) {
|
||||
tagged.click()
|
||||
device.waitForIdle()
|
||||
return
|
||||
}
|
||||
}
|
||||
val label = s(R.string.wizard_action_start)
|
||||
val nodes = device.findObjects(By.text(label))
|
||||
val target = nodes.lastOrNull { it.isClickable && it.isEnabled }
|
||||
if (target != null && target.visibleBounds.height() >= 20) {
|
||||
target.click()
|
||||
device.waitForIdle()
|
||||
return
|
||||
}
|
||||
val x = device.displayWidth / 2
|
||||
device.swipe(x, (device.displayHeight * 0.88).toInt(), x, (device.displayHeight * 0.62).toInt(), 24)
|
||||
device.waitForIdle()
|
||||
SystemClock.sleep(300)
|
||||
}
|
||||
tapClickableText(s(R.string.wizard_action_start))
|
||||
}
|
||||
|
||||
private fun selectYoutubeIfRequested() {
|
||||
if (!wantsYoutube()) return
|
||||
waitForAnyText("YouTube Live", timeoutMs = 20_000)
|
||||
tapClickableText("YouTube Live")
|
||||
waitForAnyText(
|
||||
s(R.string.wizard_transmission_unlisted_label),
|
||||
"NON IN ELENCO",
|
||||
"UNLISTED",
|
||||
timeoutMs = 10_000,
|
||||
)
|
||||
tapFirstAvailable(s(R.string.wizard_transmission_unlisted_label), "NON IN ELENCO", "UNLISTED")
|
||||
device.waitForIdle()
|
||||
SystemClock.sleep(400)
|
||||
}
|
||||
|
||||
private fun hasMatchList(): Boolean =
|
||||
device.hasObject(By.text(su(R.string.matches_new))) ||
|
||||
device.hasObject(By.text("NUOVA PARTITA")) ||
|
||||
device.hasObject(By.text("NEW MATCH"))
|
||||
|
||||
private fun waitForRes(tag: String): UiObject2 {
|
||||
val obj = device.wait(Until.findObject(By.res(tag)), 10_000)
|
||||
?: device.wait(Until.findObject(By.res("$pkg:id/$tag")), 2_000)
|
||||
?: error("Nodo non trovato: $tag")
|
||||
return obj
|
||||
}
|
||||
|
||||
private fun tapLoginSubmit() {
|
||||
val btn = device.wait(Until.findObject(By.res("login_submit")), 3_000)
|
||||
?: device.wait(Until.findObject(By.res("$pkg:id/login_submit")), 1_000)
|
||||
if (btn != null) {
|
||||
val bounds = btn.visibleBounds
|
||||
device.click(bounds.centerX(), bounds.centerY())
|
||||
device.waitForIdle()
|
||||
return
|
||||
}
|
||||
if (hasMatchList()) return
|
||||
val nodes = device.findObjects(By.text(su(R.string.login_submit))) +
|
||||
device.findObjects(By.text("ACCEDI")) +
|
||||
device.findObjects(By.text("LOG IN"))
|
||||
val target = nodes.lastOrNull { it.isClickable } ?: nodes.lastOrNull()
|
||||
if (target != null) {
|
||||
target.click()
|
||||
device.waitForIdle()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private fun tapFirstAvailable(vararg texts: String) {
|
||||
for (text in texts) {
|
||||
if (device.hasObject(By.text(text))) {
|
||||
tapClickableText(text)
|
||||
return
|
||||
}
|
||||
}
|
||||
error("Nessun testo cliccabile: ${texts.joinToString()}")
|
||||
}
|
||||
|
||||
private fun pasteIntoField(field: UiObject2, value: String) {
|
||||
@@ -118,6 +257,18 @@ class E2EWizardFlowTest {
|
||||
return obj!!
|
||||
}
|
||||
|
||||
private fun waitForTextContaining(vararg needles: String, timeoutMs: Long) {
|
||||
val deadline = SystemClock.elapsedRealtime() + timeoutMs
|
||||
while (SystemClock.elapsedRealtime() < deadline) {
|
||||
for (needle in needles) {
|
||||
if (device.hasObject(By.textContains(needle))) return
|
||||
}
|
||||
scrollDown(1)
|
||||
SystemClock.sleep(250)
|
||||
}
|
||||
error("Nessun testo contenente: ${needles.joinToString()}")
|
||||
}
|
||||
|
||||
private fun waitForAnyText(vararg texts: String, timeoutMs: Long) {
|
||||
val deadline = SystemClock.elapsedRealtime() + timeoutMs
|
||||
while (SystemClock.elapsedRealtime() < deadline) {
|
||||
@@ -158,25 +309,31 @@ class E2EWizardFlowTest {
|
||||
}
|
||||
|
||||
private fun tapClickableText(text: String) {
|
||||
device.findObject(By.text(text).clickable(true))?.let { node ->
|
||||
node.click()
|
||||
device.waitForIdle()
|
||||
return
|
||||
}
|
||||
val label = device.wait(Until.findObject(By.text(text)), 15_000)
|
||||
?: error("Testo non trovato: $text")
|
||||
var node: UiObject2? = label
|
||||
for (i in 0 until 6) {
|
||||
val current = node ?: break
|
||||
if (current.isClickable) {
|
||||
current.click()
|
||||
device.waitForIdle()
|
||||
return
|
||||
repeat(6) {
|
||||
val clickable = device.findObject(By.text(text).clickable(true))
|
||||
val label = clickable ?: device.findObject(By.text(text))
|
||||
if (label != null) {
|
||||
val b = label.visibleBounds
|
||||
if (b.height() >= 16 && b.bottom <= device.displayHeight && b.top >= 0) {
|
||||
var node: UiObject2? = label
|
||||
for (i in 0 until 6) {
|
||||
val current = node ?: break
|
||||
if (current.isClickable) {
|
||||
current.click()
|
||||
device.waitForIdle()
|
||||
return
|
||||
}
|
||||
node = current.parent
|
||||
}
|
||||
label.click()
|
||||
device.waitForIdle()
|
||||
return
|
||||
}
|
||||
}
|
||||
node = current.parent
|
||||
scrollDown(1)
|
||||
device.waitForIdle()
|
||||
}
|
||||
label.click()
|
||||
device.waitForIdle()
|
||||
error("Testo non cliccabile visibile: $text")
|
||||
}
|
||||
|
||||
private fun waitUntilEnabled(text: String, timeoutMs: Long) {
|
||||
@@ -206,4 +363,15 @@ class E2EWizardFlowTest {
|
||||
SystemClock.sleep(300)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scrollUp(steps: Int = 1) {
|
||||
val centerX = device.displayWidth / 2
|
||||
val startY = (device.displayHeight * 0.35).toInt()
|
||||
val endY = (device.displayHeight * 0.75).toInt()
|
||||
repeat(steps) {
|
||||
device.swipe(centerX, startY, centerX, endY, 24)
|
||||
device.waitForIdle()
|
||||
SystemClock.sleep(300)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -26,6 +26,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -263,7 +264,9 @@ fun StepNetworkTestScreen(
|
||||
starting = false
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(2f),
|
||||
modifier = Modifier
|
||||
.weight(2f)
|
||||
.testTag("wizard_start_live"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -22,6 +22,15 @@ class RtmpIngestUrlTest {
|
||||
assertEquals("rtmp://10.0.2.2:1935/live/match_abc", resolved)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun preservesLabIngestPort() {
|
||||
val resolved = RtmpIngestUrl.resolve(
|
||||
"rtmp://127.0.0.1:11935/live/match_abc",
|
||||
apiBaseUrl = "http://10.0.2.2:3000",
|
||||
)
|
||||
assertEquals("rtmp://10.0.2.2:11935/live/match_abc", resolved)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun keepsProductionHost() {
|
||||
val input = "rtmp://stream.matchlivetv.it:1935/live/match_abc"
|
||||
|
||||
Reference in New Issue
Block a user