Módulo 11: Automatización UI con Playwright

Objetivos del módulo

  • Comprender las ventajas de Playwright sobre Selenium
  • Configurar Playwright con .NET y NUnit
  • Dominar selectores, auto-waiting y acciones
  • Usar tracing y screenshots para depuración
  • Escribir tests robustos y rápidos

1. ¿Qué es Playwright?

Playwright (de Microsoft) es un framework moderno de automatización de navegadores. Fue diseñado para resolver los problemas de Selenium.

Playwright vs Selenium

Aspecto Selenium Playwright
Auto-waiting Manual (WebDriverWait) Automático
Velocidad Más lento Más rápido
Estabilidad Tests flaky frecuentes Muy estable
Navegadores Chrome, Firefox, Edge, Safari Chromium, Firefox, WebKit
Paralelo Requiere Grid Integrado
Network mocking Requiere proxy Integrado
Tracing No nativo Integrado (videos, screenshots)
Codegen No Genera código grabando acciones
Instalación drivers Manual o NuGet playwright install

📘 Concepto: Playwright tiene auto-waiting integrado: espera automáticamente a que los elementos sean visibles, clicables y estables antes de actuar.


2. Configurar el proyecto

# Crear proyecto
dotnet new nunit -n MiApp.PlaywrightTests
cd MiApp.PlaywrightTests

# Instalar Playwright
dotnet add package Microsoft.Playwright.NUnit

# Compilar (necesario antes de instalar navegadores)
dotnet build

# Instalar navegadores
pwsh bin/Debug/net9.0/playwright.ps1 install

3. Primer test

using Microsoft.Playwright;
using Microsoft.Playwright.NUnit;

namespace MiApp.PlaywrightTests;

[Parallelizable(ParallelScope.Self)]
[TestFixture]
public class ExampleTests : PageTest
{
    [Test]
    public async Task PaginaPrincipal_Carga_TituloCorrecto()
    {
        await Page.GotoAsync("https://playwright.dev");

        await Expect(Page).ToHaveTitleAsync(
            new Regex("Playwright"));
    }

    [Test]
    public async Task BotonGetStarted_Click_NavegarDocumentacion()
    {
        await Page.GotoAsync("https://playwright.dev");

        await Page.GetByRole(AriaRole.Link,
            new() { Name = "Get started" }).ClickAsync();

        await Expect(Page.GetByRole(AriaRole.Heading,
            new() { Name = "Installation" })).ToBeVisibleAsync();
    }
}

Ejecutar

dotnet test

4. Selectores en Playwright

Selectores recomendados (por prioridad)

Selector Ejemplo Cuándo usar
GetByRole GetByRole(AriaRole.Button, new() { Name = "Enviar" }) Siempre que sea posible
GetByText GetByText("Bienvenido") Texto visible
GetByLabel GetByLabel("Email") Campos de formulario
GetByPlaceholder GetByPlaceholder("Buscar...") Inputs con placeholder
GetByTestId GetByTestId("submit-btn") Atributo data-testid
Locator CSS Locator("css=#email") Cuando no hay alternativa
Locator XPath Locator("xpath=//div") Último recurso

Ejemplos prácticos

// Por rol (accesibilidad) — PREFERIDO
await Page.GetByRole(AriaRole.Button, new() { Name = "Iniciar sesión" }).ClickAsync();
await Page.GetByRole(AriaRole.Link, new() { Name = "Registro" }).ClickAsync();
await Page.GetByRole(AriaRole.Heading, new() { Name = "Dashboard" }).IsVisibleAsync();

// Por texto
await Page.GetByText("Bienvenido, Ana").IsVisibleAsync();
await Page.GetByText("Aceptar", new() { Exact = true }).ClickAsync();

// Por label (formularios)
await Page.GetByLabel("Email").FillAsync("ana@test.com");
await Page.GetByLabel("Contraseña").FillAsync("Pass123!");

// Por placeholder
await Page.GetByPlaceholder("Buscar productos...").FillAsync("camiseta");

// Por data-testid
await Page.GetByTestId("cart-count").TextContentAsync();

// CSS Selector (cuando no hay alternativa)
await Page.Locator(".alert-danger").TextContentAsync();
await Page.Locator("#total-price").TextContentAsync();

Filtrar y encadenar

// Filtrar dentro de un contenedor
var card = Page.Locator(".product-card").Filter(
    new() { HasText = "Camiseta" });
await card.GetByRole(AriaRole.Button, new() { Name = "Comprar" }).ClickAsync();

// Nth element
await Page.Locator(".item").Nth(2).ClickAsync();  // Tercer elemento

// First/Last
await Page.Locator(".item").First.ClickAsync();
await Page.Locator(".item").Last.ClickAsync();

5. Acciones

// Navegación
await Page.GotoAsync("https://miapp.com/login");
await Page.GoBackAsync();
await Page.GoForwardAsync();
await Page.ReloadAsync();

// Formularios
await Page.GetByLabel("Email").FillAsync("ana@test.com");  // Limpia y escribe
await Page.GetByLabel("Email").ClearAsync();
await Page.GetByLabel("Email").PressSequentiallyAsync("ana@test.com");  // Tecla a tecla

// Click
await Page.GetByRole(AriaRole.Button, new() { Name = "Enviar" }).ClickAsync();
await Page.GetByText("Opción").DblClickAsync();          // Doble clic
await Page.GetByText("Elemento").ClickAsync(
    new() { Button = MouseButton.Right });                // Clic derecho

// Checkbox y radio
await Page.GetByLabel("Acepto los términos").CheckAsync();
await Page.GetByLabel("Acepto los términos").UncheckAsync();
await Page.GetByLabel("Premium").CheckAsync();

// Select/Dropdown
await Page.GetByLabel("País").SelectOptionAsync("ES");
await Page.GetByLabel("País").SelectOptionAsync(
    new SelectOptionValue { Label = "España" });

// Teclado
await Page.GetByLabel("Buscar").PressAsync("Enter");
await Page.Keyboard.PressAsync("Escape");

// Subir archivos
await Page.GetByLabel("Foto de perfil").SetInputFilesAsync("foto.jpg");

6. Assertions (Expect)

Playwright tiene assertions que auto-esperan hasta que la condición se cumpla.

// Página
await Expect(Page).ToHaveTitleAsync("Dashboard");
await Expect(Page).ToHaveURLAsync(new Regex("/dashboard"));

// Visibilidad
await Expect(Page.GetByText("Bienvenido")).ToBeVisibleAsync();
await Expect(Page.GetByText("Cargando")).ToBeHiddenAsync();

// Texto
await Expect(Page.GetByTestId("total")).ToHaveTextAsync("99.99€");
await Expect(Page.GetByTestId("total")).ToContainTextAsync("99");

// Atributos
await Expect(Page.GetByLabel("Email")).ToHaveValueAsync("ana@test.com");
await Expect(Page.GetByRole(AriaRole.Button)).ToBeEnabledAsync();
await Expect(Page.GetByRole(AriaRole.Button)).ToBeDisabledAsync();

// Checkbox
await Expect(Page.GetByLabel("Términos")).ToBeCheckedAsync();

// Conteo
await Expect(Page.Locator(".product-card")).ToHaveCountAsync(10);

// CSS
await Expect(Page.GetByTestId("error")).ToHaveCSSAsync("color", "rgb(255, 0, 0)");

7. Page Object Model con Playwright

// Pages/LoginPage.cs
public class LoginPage
{
    private readonly IPage _page;

    public LoginPage(IPage page) => _page = page;

    // Localizadores
    private ILocator EmailInput => _page.GetByLabel("Email");
    private ILocator PasswordInput => _page.GetByLabel("Contraseña");
    private ILocator LoginButton => _page.GetByRole(AriaRole.Button,
        new() { Name = "Iniciar sesión" });
    private ILocator ErrorAlert => _page.Locator(".alert-danger");

    public async Task GotoAsync()
    {
        await _page.GotoAsync("http://localhost:3000/login");
    }

    public async Task LoginAsync(string email, string password)
    {
        await EmailInput.FillAsync(email);
        await PasswordInput.FillAsync(password);
        await LoginButton.ClickAsync();
    }

    public async Task<string> GetErrorAsync()
    {
        return await ErrorAlert.TextContentAsync() ?? "";
    }
}

// Tests/LoginTests.cs
[TestFixture]
public class LoginTests : PageTest
{
    private LoginPage _loginPage = null!;

    [SetUp]
    public async Task SetUp()
    {
        _loginPage = new LoginPage(Page);
        await _loginPage.GotoAsync();
    }

    [Test]
    public async Task Login_CredencialesValidas_Redirige()
    {
        await _loginPage.LoginAsync("admin@test.com", "Pass123!");
        await Expect(Page).ToHaveURLAsync(new Regex("/dashboard"));
    }

    [Test]
    public async Task Login_PasswordIncorrecta_MuestraError()
    {
        await _loginPage.LoginAsync("admin@test.com", "incorrecta");
        var error = await _loginPage.GetErrorAsync();
        Assert.That(error, Does.Contain("Credenciales incorrectas"));
    }
}

8. Tracing y depuración

Tracing (grabación de la sesión)

// Iniciar tracing
await Context.Tracing.StartAsync(new()
{
    Screenshots = true,
    Snapshots = true,
    Sources = true
});

// ... ejecutar tests ...

// Guardar tracing
await Context.Tracing.StopAsync(new()
{
    Path = "trace.zip"
});
# Ver el trace en el visor
pwsh bin/Debug/net9.0/playwright.ps1 show-trace trace.zip

Screenshots

// Screenshot de la página completa
await Page.ScreenshotAsync(new()
{
    Path = "captura.png",
    FullPage = true
});

// Screenshot de un elemento
await Page.GetByTestId("chart").ScreenshotAsync(new()
{
    Path = "grafico.png"
});

Video

// En el setup, configurar grabación de video
[SetUp]
public async Task Setup()
{
    var context = await Browser.NewContextAsync(new()
    {
        RecordVideoDir = "videos/",
        RecordVideoSize = new RecordVideoSize { Width = 1280, Height = 720 }
    });
    var page = await context.NewPageAsync();
}

Codegen (generador de código)

# Abre un navegador y graba tus acciones como código C#
pwsh bin/Debug/net9.0/playwright.ps1 codegen https://miapp.com --target csharp-nunit

9. Network Mocking

// Interceptar peticiones API
await Page.RouteAsync("**/api/usuarios", async route =>
{
    await route.FulfillAsync(new()
    {
        ContentType = "application/json",
        Body = "[{\"id\":1,\"nombre\":\"Mock User\"}]"
    });
});

// Interceptar y modificar respuesta real
await Page.RouteAsync("**/api/productos", async route =>
{
    var response = await route.FetchAsync();
    var json = await response.JsonAsync();
    // Modificar datos...
    await route.FulfillAsync(new() { Response = response });
});

10. Ejercicios

Ejercicio 1

Automatiza con Playwright: abrir https://www.saucedemo.com/, hacer login con standard_user / secret_sauce, y verificar que se listan productos.

Ejercicio 2

Crea Page Objects para la demo de SauceDemo: LoginPage, InventoryPage, CartPage. Escribe tests para el flujo de login → añadir producto → verificar carrito.

Ejercicio 3

Usa codegen para grabar un flujo y luego convierte el código generado a Page Objects.


Resumen

Concepto Descripción
Playwright Framework moderno de automatización (Microsoft)
Auto-waiting Espera automática en cada acción
GetByRole Selector preferido, basado en accesibilidad
Expect Assertions con auto-wait
Tracing Grabación de sesión para depuración
Codegen Genera código grabando acciones en el navegador
Network mocking Interceptar y simular respuestas API