Arquitectura Page Object Model para Automatización de Interfaces con Playwright

Fundamentos del Patrón Page Object Model

Incrustrar selectores y acciones de interfaz directamente dentro de las funciones de prueba es viable para prototipos rápidos, pero se convierte en una carga técnica insostenible cuando la suite crece. Cualquier modificación en el DOM, como un cambio de atributo class o un ajuste en la jerarquía de nodos, obliga a refactorizar decenas de archivos. El patrón Page Object Model (POM) resuelve este acoplamiento mediante la separación estricta de responsabilidades: la estructura visual y las interacciones se encapsulan en clases dedicadas, mientras que los tests se limitan a orquestar flujos de negocio y validar resultados.

Estructura de Directorios Recomendada


suite_automatizacion/
├── conftest.py              # Configuración global y fixtures
├── pytest.ini
├── paginas/                 # Capa de abstracción de UI
│   ├── __init__.py
│   ├── base.py              # Funcionalidades transversales
│   ├── acceso.py            # Pantalla de login
│   └── panel.py             # Vista post-autenticación
├── fragmentos/              # UI compartida entre múltiples vistas
│   └── barra_lateral.py
└── escenarios/              # Casos de prueba puros
    └── test_autenticacion.py

Implementación de la Clase Base

La clase fundacional centraliza operaciones repetitivas como navegación, captura de estado y utilidades de sincronización. Esto evita duplicación y estandariza el comportamiento de todas las páginas derivadas.

# paginas/base.py
from playwright.sync_api import Page

class VistaBase:
    RUTA_DESTINO = None

    def __init__(self, controlador: Page):
        self.navegador = controlador

    def ir_a(self, url_personalizada: str = None):
        destino = url_personalizada or self.RUTA_DESTINO
        if destino:
            self.navegador.goto(destino)
        return self

    def titulo_actual(self) -> str:
        return self.navegador.title()

    def guardar_evidencia(self, nombre_archivo: str):
        self.navegador.screenshot(path=f"evidencias/{nombre_archivo}.png")
        return self

Definición de Páginas Específicas

Cada vista se modela como una entidad independiente. Los localizadores se exponen como propiedades de solo lectura y las acciones devuelven referencias a objetos, habilitando una interfaz fluida.

# paginas/acceso.py
from playwright.sync_api import Page
from paginas.base import VistaBase
from paginas.panel import PanelControl

class PantallaAcceso(VistaBase):
    RUTA_DESTINO = "https://app.demo/login"

    @property
    def input_identificador(self):
        return self.navegador.get_by_placeholder("Usuario o email")

    @property
    def input_secreto(self):
        return self.navegador.get_by_placeholder("Clave de acceso")

    @property
    def btn_confirmar(self):
        return self.navegador.get_by_role("button", name="Iniciar sesión")

    @property
    def aviso_fallo(self):
        return self.navegador.locator("div.alerta-error")

    def ingresar_credenciales(self, usuario: str, clave: str):
        self.input_identificador.fill(usuario)
        self.input_secreto.fill(clave)
        return self

    def ejecutar_login(self) -> PanelControl:
        self.btn_confirmar.click()
        return PanelControl(self.navegador)

    def autenticar_completo(self, usr: str, pwd: str) -> PanelControl:
        self.ir_a()
        return self.ingresar_credenciales(usr, pwd).ejecutar_login()

# paginas/panel.py
from playwright.sync_api import Page
from paginas.base import VistaBase

class PanelControl(VistaBase):
    RUTA_DESTINO = "https://app.demo/dashboard"

    @property
    def etiqueta_bienvenida(self):
        return self.navegador.get_by_text("Hola,")

    @property
    def enlace_salir(self):
        return self.navegador.get_by_role("link", name="Cerrar sesión")

    def obtener_nombre_sesion(self) -> str:
        return self.etiqueta_bienvenida.inner_text()

Capa de Pruebas y Ejecución

Los archivos de test nunca deben contener selectores ni llamadas directas al DOM. Su único propósito es describir el comportamiento esperado del sistema y verificar los estados resultantes.

# escenarios/test_autenticacion.py
import pytest
from playwright.sync_api import Page, expect
from paginas.acceso import PantallaAcceso

@pytest.fixture
def vista_login(page: Page) -> PantallaAcceso:
    return PantallaAcceso(page)

def test_acceso_correcto_redirige_al_panel(vista_login):
    credenciales = {"u": "coord_admin", "p": "Str0ngP@ss!"}
    panel = vista_login.autenticar_completo(credenciales["u"], credenciales["p"])

    expect(panel.navegador).to_have_url("**/dashboard")
    assert "coord_admin" in panel.obtener_nombre_sesion()

def test_credenciales_invalidas_muestran_alerta(vista_login):
    vista_login.ir_a().ingresar_credenciales("coord_admin", "clave_erronea").ejecutar_login()
    
    expect(vista_login.aviso_fallo).to_be_visible()
    expect(vista_login.aviso_fallo).to_contain_text("Autenticación fallida")

Extracción de Componentes Reutilizables

Elementos que persisten a través de múltiples rutas (menús, modales globales, pies de página) deben desacoplarse en fragmentos independientes. Estas clases se componen dentro de las páginas que los requieren.

# fragmentos/barra_lateral.py
from playwright.sync_api import Page

class MenuLateral:
    def __init__(self, contexto: Page):
        self.ctx = contexto

    def seleccionar_modulo(self, nombre_modulo: str):
        self.ctx.get_by_role("link", name=nombre_modulo).click()
        return self

# Integración en paginas/panel.py
class PanelControl(VistaBase):
    def __init__(self, controlador: Page):
        super().__init__(controlador)
        self.menu = MenuLateral(controlador)

Uso en pruebas: panel.menu.seleccionar_modulo("Reportes"). La lógica de navegación se mantiene en un único punto de verdad.

Inyección de Dependencias con pytest

El archivo conftest.py actúa como proveedor centralizado. Al registrar las páginas como fixtures, se elimina la necesidad de instanciación manual dentro de cada test.

# conftest.py
import pytest
from paginas.acceso import PantallaAcceso
from paginas.panel import PanelControl

@pytest.fixture
def ui_acceso(page):
    return PantallaAcceso(page)

@pytest.fixture
def ui_panel(page):
    return PanelControl(page)

Directrices de Arquitectura y Errores Comunes

Práctica Recomendada Antipatrón a Evitar
Localizadores aislados en propiedades de la clase Selectores hardcodeados dentro de los asserts o pasos del test
Métodos que retornan self o la siguiente página Funciones void que rompen la cadena de llamadas
Nombres orientados al dominio (autenticar_completo) Operaciones atómicas expuestas (click_boton, llenar_campo)
Validaciones (expect/assert) exclusivas del test Lógica de verificación mezclada dentro de los Page Objects
Uso de selectores semánticos (get_by_role, get_by_text) XPath profundos o selectores CSS dependientes de la maquetación

Cuándo Aplicar Esta Arquitectura

Para colecciones menores a cinco casos, un script lineal es suficiente. Entre diez y varios cientos de escenarios, POM reduce drásticamente la deuda técnica al confinar los cambios de UI a una sola capa. En ecosistemas corporativos con interfaces moudlares complejas, la combinación de POM + Fragmentos compone la estrategia más equilibrada entre legibilidad y esfuerzo de mantenimiento. Patrones más abstractos como Screenplay solo se justifican cuando múltiples roles de usuario interactúan con flujos altamente dinámicos y la curva de aprendizaje está justificada por la escala del proyecto.

Etiquetas: Playwright Python pytest page-object-model ui-testing

Publicado el 9-24 23:53