Checkout completo en Appium: del carrito a la confirmación de compra
4 pages nuevas para el circuito de checkout: formularios, scroll, waits post-scroll, espacios invisibles en assertions. 16 tests, 9 pages, 0 fallos.
4 pages nuevas, 4 tests de checkout, y los errores reales que aparecen cuando Appium interactúa con formularios y scroll en una app móvil.
CONTEXTO
En el post anterior Product Detail y Cart en Appium cubrí las primeras 2 pantallas del flujo de compra: detalle del producto y carrito. 12 tests, 5 pages.
Ahora toca el checkout completo: formulario de shipping, formulario de pago, review del pedido y confirmación. Son 4 pages nuevas (CheckoutShippingPage, CheckoutPaymentPage, ReviewOrderPage, CheckoutCompletePage), un método nuevo en CartPage, y 4 tests que recorren el circuito de punta a punta.
El flujo completo
Products → Product Detail → Add to Cart → Cart → Proceed To Checkout
→ Login (si no logueado) → Shipping → Payment → Review Order
→ Place Order → Checkout Complete → Continue Shopping → Products
La app requiere login antes del checkout. Si tocás "Proceed To Checkout" sin estar logueado, redirige a Login. Después del login, vuelve al flujo de checkout.
Las 4 pages nuevas
CheckoutShippingPage

El formulario de shipping tiene 7 campos: Full Name, Address Line 1, Address Line 2, City, State/Region, Zip Code, Country.
public class CheckoutShippingPage extends BasePage {
private static final String FULL_NAME = "com.saucelabs.mydemoapp.android:id/fullNameET";
private static final String ADDRESS_1 = "com.saucelabs.mydemoapp.android:id/address1ET";
private static final String CITY = "com.saucelabs.mydemoapp.android:id/cityET";
private static final String ZIP_CODE = "com.saucelabs.mydemoapp.android:id/zipET";
private static final String COUNTRY = "com.saucelabs.mydemoapp.android:id/countryET";
private static final String TO_PAYMENT_ACC = "Saves user info for checkout";
// ...
}
Para no repetir 5 llamadas en cada test, armé completarFormulario():
public void completarFormulario(String fullName, String address1, String city,
String zipCode, String country) {
ingresarFullName(fullName);
ingresarAddress1(address1);
ingresarCity(city);
ingresarZipCode(zipCode);
ingresarCountry(country);
}
La navegación a Payment espera un elemento exclusivo de la siguiente pantalla:
public CheckoutPaymentPage irAPayment() {
driver.findElement(AppiumBy.accessibilityId(TO_PAYMENT_ACC)).click();
wait.until(ExpectedConditions.visibilityOfElementLocated(
AppiumBy.id("com.saucelabs.mydemoapp.android:id/nameET")
));
return new CheckoutPaymentPage(driver, wait);
}
CheckoutPaymentPage

Misma estructura que Shipping pero con campos de tarjeta:
public class CheckoutPaymentPage extends BasePage {
private static final String FULL_NAME = "com.saucelabs.mydemoapp.android:id/nameET";
private static final String CARD_NUMBER = "com.saucelabs.mydemoapp.android:id/cardNumberET";
private static final String EXPIRATION_DATE = "com.saucelabs.mydemoapp.android:id/expirationDateET";
private static final String SECURITY_CODE = "com.saucelabs.mydemoapp.android:id/securityCodeET";
private static final String REVIEW_ORDER_ACC = "Saves payment info and launches screen to review checkout data";
// ...
}
Mismo patrón: completarFormulario() para llenar todo de una, irAReviewOrder() para navegar.
ReviewOrderPage — la pantalla más densa

Review Order muestra un resumen de todo: producto, dirección, método de pago, envío y total. Es la pantalla con más locators del framework.
public class ReviewOrderPage extends BasePage {
// Producto
private static final String PRODUCT_NAME = "com.saucelabs.mydemoapp.android:id/titleTV";
private static final String PRODUCT_PRICE = "com.saucelabs.mydemoapp.android:id/priceTV";
// Dirección
private static final String DELIVERY_NAME = "com.saucelabs.mydemoapp.android:id/fullNameTV";
private static final String DELIVERY_CITY = "com.saucelabs.mydemoapp.android:id/cityTV";
private static final String DELIVERY_COUNTRY = "com.saucelabs.mydemoapp.android:id/countryTV";
// Envío
private static final String SHIPPING_COST = "com.saucelabs.mydemoapp.android:id/amountTV";
// Totales
private static final String TOTAL_ITEMS = "com.saucelabs.mydemoapp.android:id/itemNumberTV";
private static final String TOTAL_AMOUNT = "com.saucelabs.mydemoapp.android:id/totalAmountTV";
private static final String PLACE_ORDER_ACC = "Completes the process of checkout";
// ...
}
CheckoutCompletePage

Los locators de esta pantalla no los había capturado en la sesión del Post 7 (límite de imágenes en el chat). Los capturé ahora con Inspector:
public class CheckoutCompletePage extends BasePage {
private static final String TITLE = "com.saucelabs.mydemoapp.android:id/completeTV";
private static final String THANK_YOU = "com.saucelabs.mydemoapp.android:id/thankYouTV";
private static final String SWAG_MESSAGE = "com.saucelabs.mydemoapp.android:id/swagTV";
private static final String ORDER_MESSAGE = "com.saucelabs.mydemoapp.android:id/orderTV";
private static final String CONTINUE_SHOPPING_ACC = "Tap to open catalog";
// ...
}
continuarComprando() devuelve ProductsPage — cierra el circuito completo.
proceedToCheckout() en CartPage
CartPage tenía el locator declarado pero no el método. Lo agregué:
public CheckoutShippingPage proceedToCheckout() {
driver.findElement(AppiumBy.accessibilityId(PROCEED_CHECKOUT_ACC)).click();
wait.until(ExpectedConditions.visibilityOfElementLocated(
AppiumBy.id("com.saucelabs.mydemoapp.android:id/fullNameET")
));
return new CheckoutShippingPage(driver, wait);
}
Los helpers del test
Cada test de checkout necesita: login → seleccionar producto → agregar al carrito → ir al carrito → proceed to checkout. Son 6 pasos antes de llegar a la primera pantalla de checkout.
En vez de repetir eso en cada test, armé dos helpers:
private CheckoutShippingPage navegarHastaShipping() {
LoginPage loginPage = productsPage.irAlLogin();
loginPage.ingresarCredenciales("[email protected]", "10203040");
productsPage = loginPage.tapLogin();
ProductDetailPage detailPage = productsPage.seleccionarProducto("Sauce Labs Backpack");
detailPage.agregarAlCarrito();
CartPage cartPage = detailPage.irAlCarrito();
return cartPage.proceedToCheckout();
}
private ReviewOrderPage navegarHastaReview() {
CheckoutShippingPage shippingPage = navegarHastaShipping();
shippingPage.completarFormulario(
"Cesar Beas", "Av Colon 1234", "Cordoba", "5000", "Argentina"
);
CheckoutPaymentPage paymentPage = shippingPage.irAPayment();
paymentPage.completarFormulario(
"Cesar Beas", "3258126984521346", "03/29", "123"
);
return paymentPage.irAReviewOrder();
}
navegarHastaShipping() llega al formulario de dirección. navegarHastaReview() llena shipping y payment y llega al resumen. Los tests llaman al helper que necesitan y continúan desde ahí.
Error 1: el wait equivocado en realizarPedido()
realizarPedido() hace click en "Place Order" y espera la pantalla de confirmación. Usé checkoutTitleTV como elemento de espera, que es el título "Checkout" que aparece en Shipping, Payment y Review.
Pero Checkout Complete no tiene checkoutTitleTV. Tiene completeTV.
// MAL — checkoutTitleTV no existe en Checkout Complete
wait.until(ExpectedConditions.visibilityOfElementLocated(
AppiumBy.id("com.saucelabs.mydemoapp.android:id/checkoutTitleTV")
));
// BIEN
wait.until(ExpectedConditions.visibilityOfElementLocated(
AppiumBy.id("com.saucelabs.mydemoapp.android:id/completeTV")
));
Error de asumir que el mismo locator sirve en todas las pantallas. Cada pantalla de la app reutiliza algunos resource-ids pero no todos. checkoutTitleTV se comparte entre Shipping, Payment y Review, pero Complete tiene su propio completeTV.
Error 2: la ciudad con espacio invisible
La assertion de ciudad en Review Order:
Assert.assertEquals(reviewPage.obtenerCiudad(), "Cordoba,");
Fallaba. TestNG mostraba:
Expected :Cordoba,
Actual :Cordoba,
Parecen iguales. No lo son. El actual tiene un espacio trailing: "Córdoba, ". Review Order combina ciudad + state en cityTV, y como no llené State/Region, quedó "Cordoba, " — coma, espacio, y nada.
Assert.assertEquals(reviewPage.obtenerCiudad().trim(), "Cordoba,");
Otro dato: countryTV combina país + zip como "Argentina, 5000". No es lo que ingresé por separado (country = "Argentina", zip = "5000"). La app concatena campos en Review de forma distinta a como los muestra en Shipping.
Error 3: el scroll que pasó de largo
Review Order tiene contenido que no entra en la pantalla. El costo de envío ($5.99), total items y total precio están debajo del fold. Necesitan scroll.
Primer intento: scrollTextIntoView("Place Order"). Funcionó para llegar al botón, pero scrolleó demasiado. "Place Order" está al final de la pantalla, y amountTV ($5.99) quedó arriba del viewport, invisible.
// MAL — scrollea demasiado, amountTV queda arriba
".scrollTextIntoView(\"Place Order\")"
// BIEN — scrollea justo hasta el costo de envío
".scrollTextIntoView(\"$5.99\")"
Además, después del scroll, los elementos pueden tardar un instante en ser accesibles. Cambié los métodos de totales de findElement directo a wait.until:
// ANTES
public String obtenerCostoEnvio() {
return driver.findElement(AppiumBy.id(SHIPPING_COST)).getText();
}
// DESPUÉS
public String obtenerCostoEnvio() {
return wait.until(ExpectedConditions.visibilityOfElementLocated(
AppiumBy.id(SHIPPING_COST)
)).getText();
}
Lo mismo con obtenerTotalItems() y obtenerTotalAmount().
Los 4 tests
checkoutCompleto
El test E2E completo: login, producto, carrito, shipping, payment, review, place order, verificar confirmación.
@Test
public void checkoutCompleto() {
ReviewOrderPage reviewPage = navegarHastaReview();
CheckoutCompletePage completePage = reviewPage.realizarPedido();
Assert.assertEquals(completePage.obtenerTitulo(), "Checkout Complete");
}
Una sola assertion. Si "Checkout Complete" aparece, las 8 pantallas anteriores funcionaron.
verificarDatosEnReview
El test con más assertions del framework. Verifica que los datos ingresados en Shipping y Payment aparecen correctos en Review Order.
@Test
public void verificarDatosEnReview() {
ReviewOrderPage reviewPage = navegarHastaReview();
// Producto
Assert.assertEquals(reviewPage.obtenerNombreProducto(), "Sauce Labs Backpack");
Assert.assertEquals(reviewPage.obtenerPrecioProducto(), "$ 29.99");
// Dirección
Assert.assertEquals(reviewPage.obtenerNombreDireccion(), "Cesar Beas");
Assert.assertEquals(reviewPage.obtenerCiudad().trim(), "Cordoba,");
Assert.assertEquals(reviewPage.obtenerPais(), "Argentina, 5000");
// Pago
Assert.assertEquals(reviewPage.obtenerTitularTarjeta(), "Cesar Beas");
reviewPage.scrollHastaTotal();
// Totales
Assert.assertEquals(reviewPage.obtenerTotalItems(), "1 Items");
Assert.assertEquals(reviewPage.obtenerCostoEnvio(), "$5.99");
Assert.assertEquals(reviewPage.obtenerTotalAmount(), "$ 35.98");
}
$29.99 producto + $5.99 envío = $35.98. 9 assertions verificando el resumen completo.
verificarCheckoutComplete
Verifica los 4 textos de la pantalla de confirmación:
@Test
public void verificarCheckoutComplete() {
ReviewOrderPage reviewPage = navegarHastaReview();
CheckoutCompletePage completePage = reviewPage.realizarPedido();
Assert.assertEquals(completePage.obtenerTitulo(), "Checkout Complete");
Assert.assertEquals(completePage.obtenerThankYou(), "Thank you for your order");
Assert.assertEquals(completePage.obtenerSwagMessage(), "Your new swag is on its way");
Assert.assertEquals(completePage.obtenerOrderMessage(),
"Your order has been dispatched and will arrive as fast as the pony gallops!");
}
continuarComprandoVuelveAProducts
Verifica que "Continue Shopping" cierra el circuito y devuelve a Products:
@Test
public void continuarComprandoVuelveAProducts() {
ReviewOrderPage reviewPage = navegarHastaReview();
CheckoutCompletePage completePage = reviewPage.realizarPedido();
ProductsPage products = completePage.continuarComprando();
Assert.assertEquals(products.obtenerTitulo(), "Products");
}
16 tests verdes

Tests passed: 16 of 16 tests – 10 min 33 sec
CartTest 2 min 15 sec
agregarDosProductosDistintos
eliminarProductoDelCarrito
verificarCantidadMultipleEnCarrito
verificarProductoEnCarrito
LoginTest 2 min 19 sec
loginConCamposVacios
loginConClearYReingreso
loginExitoso
scrollHastaProducto
CheckoutTest 4 min 2 sec
checkoutCompleto 30 sec
continuarComprandoVuelveAProducts 33 sec
verificarCheckoutComplete 30 sec
verificarDatosEnReview 32 sec
ProductDetailTest 1 min 57 sec
agregarAlCarrito
modificarCantidad
seleccionarColor
verificarDetalleProducto
Cada test de checkout tarda ~30 segundos — el flujo completo pasa por 8 pantallas. Es más lento que los tests de login (~5 sec) o product detail (~3 sec), pero es el circuito E2E real.
Estructura actual
appium-java-framework/
├── src/
│ ├── main/java/
│ │ └── pages/
│ │ ├── BasePage.java
│ │ ├── CartPage.java (+ proceedToCheckout)
│ │ ├── CheckoutCompletePage.java ← nuevo
│ │ ├── CheckoutPaymentPage.java ← nuevo
│ │ ├── CheckoutShippingPage.java ← nuevo
│ │ ├── LoginPage.java
│ │ ├── ProductDetailPage.java
│ │ ├── ProductsPage.java
│ │ └── ReviewOrderPage.java ← nuevo
│ └── test/java/
│ └── tests/
│ ├── CartTest.java
│ ├── CheckoutTest.java ← nuevo
│ ├── LoginTest.java
│ ├── PrimerTest.java
│ └── ProductDetailTest.java
├── apk/
│ └── mda-2.2.0-25.apk
└── pom.xml
9 pages, 5 test classes, 16 tests. El circuito de compra completo: catálogo → detalle → carrito → login → shipping → payment → review → confirmación → vuelta al catálogo.
Errores del Post 9
| Error | Causa | Solución |
|---|---|---|
| TimeoutException esperando checkoutTitleTV después de Place Order | Checkout Complete usa completeTV, no checkoutTitleTV |
Cambiar el wait a completeTV |
| Expected "Cordoba," actual "Cordoba," (assertion falla pero parecen iguales) | Espacio trailing invisible: "Córdoba, " |
Usar .trim() en la assertion |
| countryTV muestra "Argentina, 5000" en vez de solo "Argentina" | Review Order concatena country + zip en countryTV |
Ajustar el expected a "Argentina, 5000" |
| NoSuchElementException en amountTV después del scroll | scrollTextIntoView("Place Order") scrolleó demasiado, amountTV quedó arriba |
Scrollear hasta "$5.99" en vez de "Place Order" |
| NoSuchElementException intermitente en totales post-scroll | Después del scroll, los elementos tardan un instante en ser accesibles | Usar wait.until en vez de findElement directo |
Estado actual
- 9 pages: BasePage, ProductsPage, LoginPage, ProductDetailPage, CartPage, CheckoutShippingPage, CheckoutPaymentPage, ReviewOrderPage, CheckoutCompletePage
- 16 tests: 4 login, 4 product detail, 4 cart, 4 checkout
- Flujo cubierto: circuito de compra completo, ida y vuelta
- Próximo post: gestos avanzados (swipe, long press, drag & drop)