Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .specs/codebase/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ Estratégia escolhida: **cobertura completa** (unit + instrumented). Greenfield
`createComposeRule` (Compose 1.7) a composição coleta num dispatcher "unconfined": valor vindo de outra thread recompunha
ali — CalledFromWrongThreadException, "performMeasureAndLayout called during measure layout", SlotTable corrompida.
No app não muda nada. `CollectOnMainTest` confere.
- Teste que dispara uma janela do SISTEMA (seletor de teclado, diálogo de permissão) fecha-a antes de terminar: ela fica com
o foco depois de a Activity do teste fechar e derruba quem vem depois — testes do mesmo shard e a galeria inteira (as barras
do sistema só somem com foco; em 2026-10-11 o seletor que o "Pular" do onboarding abre ficou minutos aberto e 22 de 36
capturas saíram com a barra de status real). `OnboardingRoutingTest` espera o seletor até 2 s e fecha; o `verify-screens.sh`
aperta HOME antes de capturar; a galeria só marca pronto com as barras escondidas (ou avisa "barras do sistema ainda
visíveis" no logcat). Captura com barra de status no diff = foco roubado, não mudança de tela.
- Teste que precisa agir DENTRO de uma janela de tempo do produto (ex.: desfazer o cancelamento, 3 s) alonga a janela por um
gancho `…ForTest` em vez de correr contra o relógio: no emulador do CI um quadro do teclado chega a 1–2 s.

Expand Down
27 changes: 27 additions & 0 deletions app/src/androidTest/java/com/keyflow/ui/OnboardingRoutingTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.keyflow.data.SettingsRepository
import com.keyflow.ui.onboarding.ONBOARDING_SKIP_TAG
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
Expand Down Expand Up @@ -63,4 +64,30 @@ class OnboardingRoutingTest {
}
assertTrue("onboardingDone deveria ter sido persistido", persisted)
}

/**
* O "Pular" liga o que já está permitido: com o teclado do KeyFlow habilitado mas não ativo, abre o seletor de teclado
* do Android (`showInputMethodPicker`, assíncrono — sobe ~1 s depois). Se ele subir depois de a Activity fechar, fica
* aberto com o foco: no gate da main de 2026-10-11 ficou minutos, e a galeria inteira saiu com a barra de status (o
* modo imersivo só vale com foco); no meio da suíte, rouba o foco dos testes seguintes. Espera até 2 s e fecha.
*/
@After
fun closeKeyboardPickerLeftBySkip() {
val deadline = SystemClock.elapsedRealtime() + 2_000
while (SystemClock.elapsedRealtime() < deadline) {
if (focusedWindow().contains("Select input method")) {
shell("input keyevent KEYCODE_BACK")
return
}
SystemClock.sleep(100)
}
}

private fun focusedWindow(): String =
shell("dumpsys window").lineSequence().firstOrNull { "mCurrentFocus" in it }.orEmpty()

private fun shell(command: String): String =
InstrumentationRegistry.getInstrumentation().uiAutomation.executeShellCommand(command).use { fd ->
java.io.FileInputStream(fd.fileDescriptor).bufferedReader().readText()
}
}
21 changes: 21 additions & 0 deletions app/src/debug/java/com/keyflow/ui/gallery/GalleryActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.keyflow.ui.gallery

import android.os.Build
import android.os.Bundle
import android.os.SystemClock
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
Expand Down Expand Up @@ -103,6 +105,7 @@ class GalleryActivity : ComponentActivity() {
// Quadro da thread principal não é quadro na tela: com a rota composta depois do foco, a
// entrega à tela vinha atrasada, e a captura pegava a janela ainda vazia (o 1º quadro, que o
// Android sincroniza com a janela aparecer, já não é o da rota).
awaitSystemBarsHidden()
repeat(2) { awaitFrameCommit() }
writeReadyMarker(if (route != null) routeKey else "unknown:$routeKey")
}
Expand All @@ -127,6 +130,22 @@ class GalleryActivity : ComponentActivity() {
decor.invalidate()
}

/**
* Espera as barras do sistema sumirem de fato. O pedido do [enterImmersiveMode] só vale quando a janela ganha o foco; se o
* foco passa dos 3 s de [FOCUS_WAIT_MS], a rota compõe e ficava pronta com a barra de status ainda na tela — a captura
* pegava o relógio real e os ícones (gate da main de 2026-10-11: 22 das 36 telas de um recorte, ~0,24% dos pixels cada).
* Não sumiram em [BARS_WAIT_MS]: segue e avisa no log, para a captura errada se explicar.
*/
private suspend fun awaitSystemBarsHidden() {
val deadline = SystemClock.uptimeMillis() + BARS_WAIT_MS
while (SystemClock.uptimeMillis() < deadline) {
val insets = window.decorView.rootWindowInsets?.let { WindowInsetsCompat.toWindowInsetsCompat(it, window.decorView) }
if (insets != null && !insets.isVisible(WindowInsetsCompat.Type.systemBars())) return
withFrameNanos { }
}
Log.w(TAG, "barras do sistema ainda visíveis $BARS_WAIT_MS ms depois de pedir para esconder (foco=${windowFocused.value})")
}

override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) windowFocused.value = true
Expand Down Expand Up @@ -158,5 +177,7 @@ class GalleryActivity : ComponentActivity() {
const val LIST_ROUTES = "__list__"
const val ROUTES_FILE = "gallery_routes"
private const val FOCUS_WAIT_MS = 3_000L
private const val BARS_WAIT_MS = 10_000L
private const val TAG = "KeyFlow.Gallery"
}
}
6 changes: 6 additions & 0 deletions scripts/verify-screens.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ adbx shell settings put secure immersive_mode_confirmations confirmed >/dev/null
adbx shell settings put global window_animation_scale 0 >/dev/null || true
adbx shell settings put global transition_animation_scale 0 >/dev/null || true
adbx shell settings put global animator_duration_scale 0 >/dev/null || true
# Diálogo de sistema deixado aberto (ex.: o seletor de teclado que o "Pular" do onboarding abre quando o KeyFlow está
# habilitado mas não ativo) fica com o foco: a galeria nunca o recebe, as barras do sistema não somem e toda captura sai com
# a barra de status real (gate da main de 2026-10-11, 22 de 36 telas). O HOME fecha os diálogos de sistema; a aba de
# notificações, o collapse.
adbx shell input keyevent KEYCODE_HOME >/dev/null || true
adbx shell cmd statusbar collapse >/dev/null 2>&1 || true

# Lista de rotas a partir da fonte única (RouteCatalog), via modo lista da galeria.
adbx shell am force-stop "$PKG"
Expand Down