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.

Pantalla Checkout Complete de My Demo App mostrando mensaje Thank you for your order y botón Continue Shopping
Checkout Complete: la última pantalla del circuito de compra. Si llegás acá, las 8 pantallas anteriores funcionaron.

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

Pantalla Checkout con formulario de shipping mostrando campos Full Name Address City State Zip Code Country y boton To Payment
Checkout Shipping: 7 campos, botón "To Payment". Los datos "Rebecca Winter" y "Mandorley 112" son placeholders, no datos reales.

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

Pantalla de pago mostrando campos Full Name Card Number Expiration Date Security Code checkbox billing address y boton Review Order
Payment: 4 campos obligatorios, checkbox de billing address, botón "Review Order".

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

Pantalla Review Order en Appium Inspector mostrando producto Sauce Labs Backpack dirección de envío datos de pago DHL Standard Delivery y total 35.98
Review Order: producto, dirección, pago, envío, totales. Todo verificable.

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

Pantalla Checkout Complete en Appium Inspector con elemento completeTV seleccionado mostrando texto Checkout Complete y boton Continue Shopping
Checkout Complete: la última pantalla del circuito. 4 textos y un botón.

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

Consola de IntelliJ mostrando 16 tests pasados en 10 minutos 33 segundos con CartTest CheckoutTest LoginTest y ProductDetailTest
16 de 16. El framework completo hasta acá.
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)

Repo: github.com/cesarbeassuarez/appium-java-framework