Interacciones básicas: tap, input, scroll y assertions en Appium
Login completo, scroll con UiScrollable, clear con reingreso y campos vacíos. Cuatro tests, cinco errores reales resueltos. Thread.sleep incluido.
De locators a código
En el Post 4 (Appium Inspector: encontrar elementos en la app con dispositivo físico) encontré los locators de cada elemento con Appium Inspector. Ahora toca usarlos.
El objetivo de este post: escribir tests reales que interactúen con la app. Tap en botones, ingresar texto, hacer scroll, verificar resultados. Todo lo que en Selenium hacés con click(), sendKeys() y assertions — pero en mobile, con sus diferencias.
Actualizando el setup: del emulador al Motorola
PrimerTest.java (del Post 3) todavía apuntaba al emulador. Lo primero que hice fue actualizar las capabilities para usar el Motorola G51 por USB.
Dos cambios:
// Antes (emulador)
options.setDeviceName("emulator-5554");
// Después (dispositivo físico)
options.setUdid("ZY32FJFXNF");
Y agregué appWaitActivity — el mismo fix que descubrí con Inspector en el Post 4:
options.setCapability("appium:appWaitActivity", "*");
Primer error: no incluí appWaitActivity en el código Java. Ya lo había resuelto en Inspector, pero no lo trasladé al código. El test falló con SplashActivity never started — el mismo error que ya conocía. Lo solucioné en 30 segundos, pero es un recordatorio: lo que configurás en Inspector no se aplica automáticamente a tus tests.
Corrí verificarAppAbre() contra el Motorola. Pasó en 26 segundos — mucho más rápido que el emulador.

Tip: desbloqueo del celular
El celular tiene que estar desbloqueado antes de correr los tests. Appium puede desbloquear automáticamente con capabilities (unlockType, unlockKey), pero para desarrollo local lo más práctico es desactivar el tiempo de bloqueo automático: Ajustes → Pantalla → Tiempo de espera → máximo posible.
LoginTest: el primer test de interacciones
Creé LoginTest.java. El flujo: abrir app → menú hamburguesa → login → ingresar credenciales → verificar que vuelve a Products.
Error 1: "Log In" no existe
El primer intento usó accessibilityId("Log In") para tocar el item de login en el menú lateral. Falló con NoSuchElementException.
El problema: asumí el texto del botón sin verificarlo. Abrí Inspector, navegué al menú hamburguesa y encontré que el accessibility id real era "Login Menu Item", no "Log In".
// Falló
driver.findElement(AppiumBy.accessibilityId("Log In")).click();
// Funciona
driver.findElement(AppiumBy.accessibilityId("Login Menu Item")).click();
Este es el tipo de error que en Selenium también pasa: asumís un selector sin inspeccionarlo. En mobile es peor porque no podés hacer click derecho → Inspect. Necesitás Inspector abierto con una sesión activa.
Un detalle operativo: Inspector y el test no pueden correr al mismo tiempo. UiAutomator2 solo permite una sesión por dispositivo. Hay que cerrar la sesión de Inspector (End Session) antes de ejecutar tests.
Error 2: timing en la verificación
El test hacía login, la app redirigía a Products, pero la assertion fallaba con NoSuchElementException buscando el título "Products".
El problema: el código buscaba el elemento inmediatamente después del tap en Login, pero la pantalla de Products todavía no había cargado.
// 5. Tap en botón Login
driver.findElement(AppiumBy.accessibilityId("Tap to login with given credentials")).click();
// 6. Esperar que cargue Products
Thread.sleep(3000);
// 7. Verificar que volvió a Products
String titulo = driver.findElement(AppiumBy.accessibilityId("title")).getText();
Assert.assertEquals(titulo, "Products", "No volvió a la pantalla de Products después del login");
Sí, Thread.sleep es una mala práctica. Es un wait fijo que desperdicia tiempo si la app carga rápido y falla si carga lento. El siguiente Post los reemplaza por WebDriverWait con condiciones explícitas.
Acá lo uso porque necesito confirmar que el flujo funciona antes de elegir la estrategia de espera.
Error 3: InterruptedException
Thread.sleep en Java obliga a declarar throws InterruptedException en la firma del método. Sin eso, el código no compila.
// No compila
public void loginExitoso() {
// Compila
public void loginExitoso() throws InterruptedException {
Un error de compilación, no de lógica. Pero si nunca usaste Thread.sleep en Java, te frena.
El test verde
Después de los tres fixes, el test pasó:

Tests passed: 1 of 1 test – 34 sec 885 ms
El test completo:
@Test
public void loginExitoso() throws InterruptedException {
// 1. Tap en menú hamburguesa
driver.findElement(AppiumBy.accessibilityId("View menu")).click();
Thread.sleep(1000);
// 2. Tap en "Log In" dentro del menú
driver.findElement(AppiumBy.accessibilityId("Login Menu Item")).click();
Thread.sleep(1000);
// 3. Ingresar username
driver.findElement(AppiumBy.id("com.saucelabs.mydemoapp.android:id/nameET")).sendKeys("[email protected]");
// 4. Ingresar password
driver.findElement(AppiumBy.id("com.saucelabs.mydemoapp.android:id/passwordET")).sendKeys("10203040");
// 5. Tap en botón Login
driver.findElement(AppiumBy.accessibilityId("Tap to login with given credentials")).click();
// 6. Esperar que cargue Products
Thread.sleep(3000);
// 7. Verificar que volvió a Products
String titulo = driver.findElement(AppiumBy.accessibilityId("title")).getText();
Assert.assertEquals(titulo, "Products", "No volvió a la pantalla de Products después del login");
}
Lo que verifica: que el flujo de navegación funciona y que después del login la app redirige a Products. No verifica que el usuario quedó autenticado (podría validar que el menú ahora muestre "Log Out" en vez de "Log In"). Para este post, alcanza. Validación más exhaustiva viene con POM.
Tres locators, tres formas de encontrar elementos
En loginExitoso usé tres tipos de locator distintos:
AppiumBy.accessibilityId() — para el menú hamburguesa y el botón de login. Usa el content-desc del elemento. Es el locator más estable en mobile.
driver.findElement(AppiumBy.accessibilityId("View menu")).click();
AppiumBy.id() — para los campos de username y password. Usa el resource-id. Funciona cuando el id es único en la pantalla.
driver.findElement(AppiumBy.id("com.saucelabs.mydemoapp.android:id/nameET")).sendKeys("[email protected]");
AppiumBy.androidUIAutomator() — para scroll y búsqueda por texto. Usa selectores de UiAutomator2, exclusivos de Android. Lo veremos en el test de scroll.
En Selenium tenés By.id(), By.cssSelector(), By.xpath(). En Appium tenés AppiumBy.accessibilityId(), AppiumBy.id(), AppiumBy.androidUIAutomator(). El concepto es el mismo, los selectores son diferentes.
Scroll: UiScrollable
En web, el scroll es casi invisible — si un elemento está más abajo en la página, Selenium lo encuentra igual. En mobile no. Si un producto no está visible en la pantalla, Appium no lo encuentra.
UiScrollable resuelve esto. Es un selector de Android que hace scroll automáticamente hasta encontrar el texto que le pedís.
@Test
public void scrollHastaProducto() throws InterruptedException {
// Scroll hasta un producto que no está visible
driver.findElement(AppiumBy.androidUIAutomator(
"new UiScrollable(new UiSelector().scrollable(true))" +
".scrollTextIntoView(\"Sauce Labs Onesie\")"
));
// Verificar que el producto es visible
String producto = driver.findElement(AppiumBy.androidUIAutomator(
"new UiSelector().text(\"Sauce Labs Onesie\")"
)).getText();
Assert.assertEquals(producto, "Sauce Labs Onesie", "No encontró el producto después del scroll");
}
UiScrollable no existe en Selenium. Es una API exclusiva de Android que Appium expone a través de androidUIAutomator. Hace scroll dentro de un contenedor scrollable hasta que el texto sea visible.
UiSelector es el equivalente a un locator, pero con la sintaxis de Android: new UiSelector().text("Sauce Labs Onesie") busca por texto exacto. También podés usar .className(), .resourceId(), .description().
En la pantalla del celular se veía cómo Appium scrolleaba automáticamente hasta llegar al producto.

Clear: limpiar y reingresar texto
clear() funciona igual que en Selenium. Borra el contenido del campo y podés volver a escribir con sendKeys().
@Test
public void loginConClearYReingreso() throws InterruptedException {
// 1. Ir al login
driver.findElement(AppiumBy.accessibilityId("View menu")).click();
Thread.sleep(1000);
driver.findElement(AppiumBy.accessibilityId("Login Menu Item")).click();
Thread.sleep(1000);
// 2. Ingresar username incorrecto
driver.findElement(AppiumBy.id("com.saucelabs.mydemoapp.android:id/nameET")).sendKeys("usuario_equivocado");
// 3. Limpiar y poner el correcto
driver.findElement(AppiumBy.id("com.saucelabs.mydemoapp.android:id/nameET")).clear();
driver.findElement(AppiumBy.id("com.saucelabs.mydemoapp.android:id/nameET")).sendKeys("[email protected]");
// 4. Password y login
driver.findElement(AppiumBy.id("com.saucelabs.mydemoapp.android:id/passwordET")).sendKeys("10203040");
driver.findElement(AppiumBy.accessibilityId("Tap to login with given credentials")).click();
// 5. Verificar
Thread.sleep(3000);
String titulo = driver.findElement(AppiumBy.accessibilityId("title")).getText();
Assert.assertEquals(titulo, "Products");
}
En el celular se veía cómo primero aparecía "usuario_equivocado", después se borraba y se escribía "[email protected]". Funciona como esperarías.
Validación de campos vacíos
Quise agregar un test de login fallido. Probé manualmente y la app acepta cualquier combinación de username y password. No hay validación de credenciales.
Lo que sí valida es que los campos no estén vacíos. Si tocás Login sin escribir nada, aparece "Username is required" debajo del campo.

@Test
public void loginConCamposVacios() throws InterruptedException {
// 1. Ir al login
driver.findElement(AppiumBy.accessibilityId("View menu")).click();
Thread.sleep(1000);
driver.findElement(AppiumBy.accessibilityId("Login Menu Item")).click();
Thread.sleep(1000);
// 2. Tap en Login sin ingresar nada
driver.findElement(AppiumBy.accessibilityId("Tap to login with given credentials")).click();
// 3. Verificar mensaje de error en username
String errorUsername = driver.findElement(
AppiumBy.id("com.saucelabs.mydemoapp.android:id/nameErrorTV")
).getText();
Assert.assertEquals(errorUsername, "Username is required", "No mostró error de username vacío");
}
Este test verifica que la app muestra feedback visual cuando faltan datos. Es un caso de uso real: en apps de producción, validar mensajes de error es tan importante como validar el flujo exitoso.
Error 4: timing al correr los 4 tests juntos
Con 3 tests todo pasaba verde. Al agregar el cuarto, loginExitoso y loginConCamposVacios empezaron a fallar con NoSuchElementException.
El problema: TestNG ejecuta los tests en orden alfabético. Con 4 tests, el timing cambia. Los clicks en el menú hamburguesa y en "Login Menu Item" no esperaban a que la pantalla cargara.
La solución: agregar Thread.sleep(1000) después de cada tap de navegación.
driver.findElement(AppiumBy.accessibilityId("View menu")).click();
Thread.sleep(1000); // Esperar a que el menú se abra
driver.findElement(AppiumBy.accessibilityId("Login Menu Item")).click();
Thread.sleep(1000); // Esperar a que la pantalla de login cargue
Esto es material perfecto para el siguiente post. Thread.sleep es un parche: funciona pero es frágil. Si la app tarda más de 1 segundo en abrir el menú, falla. Si tarda 100ms, desperdicia 900ms. La solución real es WebDriverWait con condiciones explícitas — esperar hasta que el elemento sea visible, no esperar un tiempo fijo.
4 de 4 verdes. 2 minutos 24 segundos los 4 tests.

Paralelo con Selenium
| Acción | Selenium (web) | Appium (mobile) |
|---|---|---|
| Click / Tap | element.click() |
element.click() |
| Ingresar texto | element.sendKeys() |
element.sendKeys() |
| Limpiar campo | element.clear() |
element.clear() |
| Scroll | Casi nunca necesario | UiScrollable (obligatorio) |
| Hover | Actions.moveToElement() |
No existe |
| Right click | Actions.contextClick() |
No existe |
| Buscar por id | By.id("miId") |
AppiumBy.id("package:id/miId") |
| Buscar por accesibilidad | By.cssSelector("[data-testid]") |
AppiumBy.accessibilityId("texto") |
| Buscar por texto | XPath o CSS | UiSelector().text("texto") |
| Long press | No estándar | Actions o gestos (Post 9) |
Lo que más cambia: scroll es obligatorio en mobile, hover no existe, y los locators usan AppiumBy en vez de By.
Las interacciones básicas (click, sendKeys, clear, assertions) son iguales. Si venís de Selenium, esa parte ya la sabés.
Estado actual
4 tests en LoginTest.java:
| Test | Qué hace | Interacciones |
|---|---|---|
loginExitoso |
Login completo con credenciales | tap, sendKeys, assertEquals |
scrollHastaProducto |
Scroll a producto no visible | UiScrollable, UiSelector, assertEquals |
loginConClearYReingreso |
Ingresa texto equivocado, limpia, reingresa | clear, sendKeys, assertEquals |
loginConCamposVacios |
Login sin datos, verifica error | tap, getText, assertEquals |
Errores resueltos:
| Error | Causa | Solución |
|---|---|---|
| SplashActivity never started | Faltaba appWaitActivity en código Java |
options.setCapability("appium:appWaitActivity", "*") |
| NoSuchElement "Log In" | Accessibility id incorrecto | Inspector → "Login Menu Item" |
| NoSuchElement título Products | Pantalla no cargó antes de la assertion | Thread.sleep(3000) (temporal) |
| InterruptedException | Java obliga a declarar checked exceptions | throws InterruptedException |
| Fallas intermitentes con 4 tests | Timing entre navegaciones | Thread.sleep(1000) después de cada tap de navegación |
Thread.sleep aparece 5 veces en el código. Cada uno es deuda técnica. El futuro Post 6 los reemplaza.
Código completo



Repo: github.com/cesarbeassuarez/appium-java-framework
Siguiente en la serie
Waits y sincronización. Los Thread.sleep de este post se reemplazan por WebDriverWait con condiciones explícitas. Por qué mobile es más inestable que web, qué esperar y cómo esperarlo.
Tabla de contenido de la serie
| Post | Tema | Estado |
|---|---|---|
| 1 | Por qué Appium: decisiones técnicas | Publicado |
| 2 | Setup completo: Node.js, Android Studio, Appium Server y emulador | Publicado |
| 3 | Primer test: abrir la app en el emulador | Publicado |
| 4 | Appium Inspector: encontrar elementos con dispositivo físico | Publicado |
| 5 | Interacciones básicas: tap, input, scroll, assertions | ← Este post |
| 6 | Waits y sincronización en mobile | Siguiente |