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