diff --git a/.specs/codebase/TESTING.md b/.specs/codebase/TESTING.md index c6ec0959..6b646d24 100644 --- a/.specs/codebase/TESTING.md +++ b/.specs/codebase/TESTING.md @@ -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. diff --git a/app/src/androidTest/java/com/keyflow/ui/OnboardingRoutingTest.kt b/app/src/androidTest/java/com/keyflow/ui/OnboardingRoutingTest.kt index 14c2094c..94b8614a 100644 --- a/app/src/androidTest/java/com/keyflow/ui/OnboardingRoutingTest.kt +++ b/app/src/androidTest/java/com/keyflow/ui/OnboardingRoutingTest.kt @@ -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 @@ -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() + } } diff --git a/app/src/debug/java/com/keyflow/ui/gallery/GalleryActivity.kt b/app/src/debug/java/com/keyflow/ui/gallery/GalleryActivity.kt index a8dca2b2..6b681e30 100644 --- a/app/src/debug/java/com/keyflow/ui/gallery/GalleryActivity.kt +++ b/app/src/debug/java/com/keyflow/ui/gallery/GalleryActivity.kt @@ -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 @@ -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") } @@ -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 @@ -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" } } diff --git a/scripts/verify-screens.sh b/scripts/verify-screens.sh index 04ca586b..fb26e1bc 100755 --- a/scripts/verify-screens.sh +++ b/scripts/verify-screens.sh @@ -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"