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 |