Accessibility Scanner — detección manual

# Accessibility Scanner es una app de Google que analiza la UI y reporta problemas
# Descargar: Play Store → "Accessibility Scanner"
# Al activarlo aparece un botón flotante que captura y analiza la pantalla actual

# Lo que detecta automáticamente:
# → Elementos sin contentDescription
# → Touch targets menores a 48dp
# → Contraste de texto insuficiente
# → Texto en dp en lugar de sp

# Lo que NO detecta (requiere prueba manual):
# → Orden de foco incorrecto
# → Foco atrapado
# → Anuncios incorrectos de TalkBack
# → Acciones que solo funcionan con gestos específicos

# Alternativa integrada: Android Studio Layout Inspector
# Con la app corriendo: View → Tool Windows → Layout Inspector
# Pestaña "Accessibility" → muestra problemas directamente en el preview

AccessibilityChecks en Espresso — automatización

// build.gradle
androidTestImplementation("androidx.test.espresso:espresso-accessibility:3.5.1")

// En el test — habilitar checks de accesibilidad automáticos
@RunWith(AndroidJUnit4::class)
class PantallaLoginAccessibilityTest {

    @get:Rule
    val activityRule = ActivityScenarioRule(LoginActivity::class.java)

    @Before
    fun setUp() {
        // Activar checks de accesibilidad en TODOS los tests de esta clase
        // Cada vez que Espresso interactúa con la UI, verifica accesibilidad
        AccessibilityChecks.enable()
            .setRunChecksFromRootView(true)  // verificar toda la pantalla, no solo el elemento
    }

    @Test
    fun formulario_login_es_accesible() {
        // Este test simplemente navega la pantalla
        // AccessibilityChecks.enable() hace que falle si hay problemas
        onView(withId(R.id.et_email)).perform(click())
        onView(withId(R.id.et_password)).perform(click())
        onView(withId(R.id.btn_login)).perform(click())
        // Si algún elemento tiene contraste insuficiente, touch target pequeño,
        // o falta de contentDescription → el test falla automáticamente
    }

    @Test
    fun pantalla_principal_no_tiene_problemas_de_accesibilidad() {
        // Verificar la pantalla sin interactuar
        onView(isRoot()).check(matches(isDisplayed()))
        // AccessibilityChecks verifica todo lo visible
    }
}

// Filtrar checks que no aplican al proyecto:
AccessibilityChecks.enable()
    .setRunChecksFromRootView(true)
    .setSuppressingResultMatcher(
        // Ignorar warning de touch target en íconos informativos (no interactivos)
        matchesCheckNames(is("TouchTargetSizeCheck"))
            .and(matchesViews(withId(R.id.iv_decorativo)))
    )

Tests de semántica en Compose

// Los tests de Compose pueden verificar la semántica directamente
@RunWith(AndroidJUnit4::class)
class ProductoCardSemanticaTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun `boton favorito tiene descripcion accesible`() {
        var esFavorito by mutableStateOf(false)

        composeTestRule.setContent {
            FavoritoBoton(esFavorito = esFavorito, onToggle = { esFavorito = !esFavorito })
        }

        // Verificar que el nodo tiene contentDescription
        composeTestRule
            .onNodeWithContentDescription("Favorito")
            .assertIsDisplayed()

        // Verificar el stateDescription del estado inicial
        composeTestRule
            .onNodeWithContentDescription("Favorito")
            .assert(hasStateDescription("No es favorito"))

        // Toggle y verificar nuevo estado
        composeTestRule.onNodeWithContentDescription("Favorito").performClick()
        composeTestRule
            .onNodeWithContentDescription("Favorito")
            .assert(hasStateDescription("Marcado como favorito"))
    }

    @Test
    fun `imagen decorativa no esta en el arbol de accesibilidad`() {
        composeTestRule.setContent {
            Image(
                painter = painterResource(R.drawable.fondo),
                contentDescription = null
            )
        }
        // null contentDescription → el nodo no debe ser visible para accesibilidad
        composeTestRule
            .onAllNodes(hasContentDescription(""))
            .assertCountEquals(0)
    }

    @Test
    fun `card agrupa elementos correctamente`() {
        composeTestRule.setContent {
            ProductoCard(
                producto = Producto(1, "Mouse Logitech", 5999.0, null, "")
            )
        }

        // La card debería ser navegable como un único nodo con descripción completa
        composeTestRule
            .onNodeWithContentDescription("Mouse Logitech, $5999.0", substring = true)
            .assertIsDisplayed()
    }
}

Integración en CI

# En el pipeline de CI — correr los tests de accesibilidad como parte del build:

# .github/workflows/ci.yml
# - name: Tests instrumentados con accesibilidad
#   run: ./gradlew connectedAndroidTest
# Los tests con AccessibilityChecks.enable() corren en el emulador de CI
# y fallan si hay nuevos problemas de accesibilidad

# Tipos de problemas detectables en CI automáticamente:
# ✓ Elementos sin contentDescription (detectable)
# ✓ Touch targets pequeños (detectable)
# ✓ Contraste insuficiente (detectable)
# ✗ Orden de foco incorrecto (requiere prueba manual)
# ✗ Comportamiento de TalkBack (requiere prueba manual)

# Regla práctica para el equipo:
# → AccessibilityChecks en todos los tests instrumentados existentes (no agrega trabajo)
# → Una sesión de TalkBack manual por feature nueva antes del merge
# → Accessibility Scanner corrido por QA en cada release

Checklist manual — lo que el scanner no detecta

# Probar con TalkBack activo — una vez por feature nueva:

# NAVEGACIÓN:
# ✓ Todos los elementos interactivos son alcanzables con gestos de swipe
# ✓ El orden de foco es lógico (de arriba a abajo, izquierda a derecha)
# ✓ Los elementos decorativos son ignorados
# ✓ Al abrir un diálogo, el foco va adentro del diálogo
# ✓ Al cerrar un diálogo, el foco vuelve al elemento que lo abrió

# DESCRIPCIONES:
# ✓ Cada elemento interactivo tiene una descripción útil
# ✓ Las descripciones son concisas (menos de 150 caracteres idealmente)
# ✓ Los estados se anuncian (seleccionado/no, habilitado/deshabilitado)
# ✓ Los errores de formulario se leen cuando aparecen

# FORMULARIOS:
# ✓ Cada campo tiene una etiqueta clara
# ✓ Los errores de validación se anuncian
# ✓ El orden de foco sigue el flujo lógico del formulario
# ✓ El botón de envío es alcanzable y describe su acción

# CONTENIDO DINÁMICO:
# ✓ Los cambios importantes se anuncian (nuevos mensajes, actualización de estado)
# ✓ Los estados de carga son anunciados
# ✓ Los toasts y snackbars son accesibles