From 2cce247e733111a32256ecbdb402309998ebff59 Mon Sep 17 00:00:00 2001 From: Yan Lucas Date: Sat, 10 Oct 2026 23:22:31 -0300 Subject: [PATCH 1/2] test(onboarding): o teste fecha o seletor de teclado que o "Pular" abre MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit O "Pular por agora" liga o que já está permitido: com o KeyFlow habilitado mas não ativo, abre o seletor de teclado do Android (assíncrono, ~1 s depois). No gate da main (f6cdb5e) a 1ª tentativa falhou com toque recusado e a repetição rodou sozinha no fim, com o Gboard ativo: o seletor subiu depois de a Activity fechar e ficou aberto com o foco, e a galeria do mesmo shard saiu com a barra de status em 22 de 36 telas. Reproduzido no emulador; agora o teste espera o seletor até 2 s e o fecha (3 de 3 voltas, o seletor abre e fecha antes de o teste terminar). --- .../com/keyflow/ui/OnboardingRoutingTest.kt | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) 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() + } } From 66bd83ec476bf22bf6d1d5616ba773ef64f13ae7 Mon Sep 17 00:00:00 2001 From: Yan Lucas Date: Sat, 10 Oct 2026 23:22:31 -0300 Subject: [PATCH 2/2] =?UTF-8?q?fix(galeria):=20a=20captura=20n=C3=A3o=20co?= =?UTF-8?q?me=C3=A7a=20com=20di=C3=A1logo=20de=20sistema=20aberto=20e=20s?= =?UTF-8?q?=C3=B3=20fica=20pronta=20com=20as=20barras=20escondidas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As barras do sistema só somem quando a janela da galeria tem o foco. Com um diálogo de sistema segurando o foco (o seletor de teclado deixado pela suíte, no gate da main de 2026-10-11) toda rota esperava os 3 s do foco, ficava pronta com a barra de status na tela e a captura pegava o relógio real e os ícones — 4,4–5,8 s até pronto, contra 1,3 s de mediana numa rodada sã. O verify-screens.sh aperta HOME (fecha diálogos de sistema) e recolhe a aba antes de capturar; a galeria só escreve o marcador de pronto com as barras escondidas de fato e, se passarem 10 s, segue e avisa "barras do sistema ainda visíveis" no logcat. Conferido no emulador com a CPU limitada como o CI: 36 de 36 capturas do recorte dentro do limiar, sem nenhum aviso. --- .specs/codebase/TESTING.md | 6 ++++++ .../com/keyflow/ui/gallery/GalleryActivity.kt | 21 +++++++++++++++++++ scripts/verify-screens.sh | 6 ++++++ 3 files changed, 33 insertions(+) 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/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"