Modulo 24: Playwright
Objetivos del modulo
- Entender que aporta Playwright frente a Selenium.
- Crear una solucion E2E con
Microsoft.Playwright.NUnit. - Instalar navegadores de Playwright y ejecutar pruebas desde Visual Studio Community.
- Usar locators por rol, label, texto, placeholder y
data-testid. - Probar formularios, validaciones, APIs, screenshots, traces, estado de login, popups, dialogos y modo movil.
- Usar una web local dentro del proyecto para practicar sin depender de servicios externos.
- Practicar tambien contra la web publica
https://practice.expandtesting.com/, con ejemplos reales de login, inputs, tablas, dialogos, iframes, descargas, subidas y controles dinamicos. - Entender como organizar pruebas locales, pruebas externas, categorias y artefactos para que el proyecto sea util en clase y en Visual Studio Community.
1. Iniciacion
Concepto: Playwright es una herramienta moderna de testing end-to-end. Automatiza Chromium, Firefox y WebKit, incluye auto-waiting, locators robustos, trazas y herramientas de depuracion.
Comparacion rapida:
| Selenium | Playwright |
|---|---|
| Estandar WebDriver muy extendido | API moderna orientada a testing |
| Usa navegador real instalado | Gestiona sus propios navegadores |
| Requiere esperas explicitas con frecuencia | Incluye auto-waiting |
| Gran ecosistema historico | Trazas, video y screenshots integrados |
En este modulo construiremos Tienda.Playwright, con una web local y pruebas sobre UI y API.
Playwright se entiende mejor si separas estas piezas:
| Pieza | Que representa | Ejemplo |
|---|---|---|
Browser | Navegador arrancado por Playwright | Chromium, Firefox o WebKit |
BrowserContext | Sesion aislada, con cookies, permisos, viewport y locale propios | Un usuario independiente |
Page | Pestana concreta dentro del contexto | La pagina que el test manipula |
Locator | Forma viva de encontrar elementos | Page.GetByRole(...) |
Expect | Aserciones web-first con espera automatica | await Expect(locator).ToBeVisibleAsync() |
IAPIRequestContext | Cliente HTTP para probar o preparar APIs | request.GetAsync("/api/productos") |
| Trace | Registro visual y tecnico de una ejecucion | trace.zip |
La diferencia principal para el alumno principiante es esta:
Selenium suele exigir: buscar elemento -> esperar manualmente -> actuar -> volver a esperar.
Playwright favorece: crear locator -> actuar -> usar expect web-first.
Un test tipico de Playwright tiene este ritmo:
- Navegar a una pagina.
- Localizar elementos como los ve el usuario.
- Ejecutar acciones: click, fill, check, select, upload.
- Afirmar el resultado visible o el estado final.
- Guardar artefactos si falla: screenshot, trace o video.
Durante el tema usaremos dos escenarios:
| Escenario | Ventaja | Riesgo |
|---|---|---|
Web local Tienda.Playwright | Controlamos HTML, datos, API y servidor | Menos parecido a una web publica real |
| Web online ExpandTesting | Muchos ejercicios reales ya preparados | Depende de Internet y de una web externa |
2. Proyecto que vamos a construir
Tienda.Playwright/
Tienda.Playwright.sln
src/
Tienda.Core/
Producto.cs
Tienda.Api/
Program.cs
tests/
Tienda.Playwright.Tests/
Pages/
ExpandTestingLoginPage.cs
ProductosPage.cs
Support/
ExpandTestingTestBase.cs
LocalApiServer.cs
PlaywrightTestBase.cs
TestSettings.cs
Tests/
PrimerTest.cs
ProductosTests.cs
ApiProductosTests.cs
ControlesTests.cs
VentanasYDialogosTests.cs
DebugTests.cs
MobileTests.cs
ExpandTesting/
ExpandTestingLoginTests.cs
ExpandTestingInputsTests.cs
ExpandTestingDynamicElementsTests.cs
ExpandTestingControlsTests.cs
ExpandTestingDynamicTableTests.cs
ExpandTestingDynamicControlsTests.cs
ExpandTestingDialogsTests.cs
ExpandTestingUploadTests.cs
ExpandTestingDownloadTests.cs
ExpandTestingWindowsTests.cs
ExpandTestingHoversTests.cs
ExpandTestingIframeTests.cs
ExpandTestingKeyPressesTests.cs
ExpandTestingShadowDomTests.cs
La API local contiene estas paginas:
| Ruta | Para practicar |
|---|---|
/ | Titulo, navegacion y menu movil |
/login | Login y storage state |
/dashboard | Redireccion tras login |
/productos | Listado y locators accesibles |
/productos/nuevo | Formularios y validaciones |
/controles | Select, checkbox y radio |
/ventanas | Popup o nueva pestana |
/ayuda | Pagina abierta desde popup |
/alertas | Dialogo confirm |
/api/productos | Pruebas de API |
La solucion queda dividida asi:
| Carpeta | Responsabilidad |
|---|---|
src/Tienda.Core | Clases de dominio compartidas por la API y las pruebas |
src/Tienda.Api | Web local que sirve paginas HTML y endpoints HTTP |
tests/Tienda.Playwright.Tests/Support | Infraestructura comun: servidor local, configuracion y clases base |
tests/Tienda.Playwright.Tests/Pages | Page Objects que encapsulan acciones de pantallas concretas |
tests/Tienda.Playwright.Tests/Tests | Pruebas NUnit que explican escenarios de usuario |
Regla practica para el curso:
- Si quieres explicar un concepto de Playwright, empieza con la web local.
- Si quieres practicar casos variados de una web publica, usa ExpandTesting.
- Si una prueba depende de Internet, marcala con una categoria separada para poder excluirla cuando haga falta.
3. Crear la solucion en Visual Studio Community
- Crea una solucion vacia llamada
Tienda.Playwright. - Agrega un proyecto Class Library llamado
Tienda.Core. - Agrega un proyecto ASP.NET Core Empty llamado
Tienda.Api. - Agrega un proyecto NUnit Test Project llamado
Tienda.Playwright.Tests. - En
Tienda.Api, agrega referencia aTienda.Core. - En
Tienda.Playwright.Tests, agrega referencias aTienda.ApiyTienda.Core. - En
Tienda.Playwright.Tests, instalaMicrosoft.Playwright.NUnit. - Crea las carpetas
Pages,SupportyTests. - Compila el proyecto de pruebas.
- Ejecuta el script
playwright.ps1 installpara instalar navegadores.
Por terminal:
dotnet new sln -n Tienda.Playwright -f sln
dotnet new classlib -n Tienda.Core -o src/Tienda.Core --framework net10.0
dotnet new web -n Tienda.Api -o src/Tienda.Api --framework net10.0
dotnet new nunit -n Tienda.Playwright.Tests -o tests/Tienda.Playwright.Tests --framework net10.0
dotnet sln Tienda.Playwright.sln add src/Tienda.Core/Tienda.Core.csproj src/Tienda.Api/Tienda.Api.csproj tests/Tienda.Playwright.Tests/Tienda.Playwright.Tests.csproj
dotnet add src/Tienda.Api/Tienda.Api.csproj reference src/Tienda.Core/Tienda.Core.csproj
dotnet add tests/Tienda.Playwright.Tests/Tienda.Playwright.Tests.csproj reference src/Tienda.Api/Tienda.Api.csproj src/Tienda.Core/Tienda.Core.csproj
dotnet add tests/Tienda.Playwright.Tests/Tienda.Playwright.Tests.csproj package Microsoft.Playwright.NUnit
dotnet build
powershell -ExecutionPolicy Bypass -File tests/Tienda.Playwright.Tests/bin/Debug/net10.0/playwright.ps1 install
Si usas PowerShell 7, tambien puedes ejecutar el script con pwsh.
Que hace cada paquete o comando:
| Elemento | Para que sirve |
|---|---|
Microsoft.Playwright.NUnit | Integra Playwright con NUnit y proporciona PageTest |
dotnet build | Genera el script playwright.ps1 dentro de bin |
playwright.ps1 install | Descarga los navegadores que usara Playwright |
dotnet test | Ejecuta las pruebas desde terminal o Visual Studio |
En Visual Studio Community, despues de crear el proyecto:
- Abre Manage NuGet Packages en el proyecto de pruebas.
- Instala
Microsoft.Playwright.NUnit. - Compila la solucion.
- Abre una terminal en la carpeta de la solucion.
- Ejecuta el comando de instalacion de navegadores.
- Abre Test Explorer y comprueba que aparecen las pruebas NUnit.
Si el test falla con Executable doesn't exist, casi siempre significa que el paquete esta instalado pero los navegadores no. La solucion es volver a ejecutar playwright.ps1 install.
4. Crear la web local
Crea Producto.cs:
namespace Tienda.Core;
public record Producto
{
public int Id { get; init; }
public string Nombre { get; init; } = string.Empty;
public string Categoria { get; init; } = string.Empty;
public decimal Precio { get; init; }
public int Stock { get; init; }
}
En Tienda.Api/Program.cs, crea la aplicacion con ApiHost.Create(args):
WebApplication app = ApiHost.Create(args);
await app.RunAsync();
public static class ApiHost
{
public static WebApplication Create(string[] args)
{
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
app.MapGet("/", () => Results.Content(Page("Tienda", "<h1>Dashboard</h1>"), "text/html"));
app.MapGet("/login", () => Results.Content(LoginPage(), "text/html"));
app.MapGet("/productos", () => Results.Content(ProductosPage(), "text/html"));
app.MapGet("/productos/nuevo", () => Results.Content(ProductoForm(), "text/html"));
app.MapGet("/controles", () => Results.Content(ControlesPage(), "text/html"));
app.MapGet("/ventanas", () => Results.Content(VentanasPage(), "text/html"));
app.MapGet("/alertas", () => Results.Content(AlertasPage(), "text/html"));
app.MapGet("/api/productos", () => Results.Ok(ProductoStore.All()));
return app;
}
}
El objetivo es que cada tipo de prueba tenga una pagina real donde practicar.
5. Configuracion y servidor local
Crea Support/TestSettings.cs:
namespace Tienda.Playwright.Tests.Support;
public static class TestSettings
{
public static string BaseUrl { get; set; } =
Environment.GetEnvironmentVariable("E2E_BASE_URL")
?? "http://127.0.0.1:5124";
}
Crea Support/LocalApiServer.cs para arrancar la web en un puerto libre:
using System.Net;
using System.Net.Sockets;
using Microsoft.AspNetCore.Builder;
public sealed class LocalApiServer : IAsyncDisposable
{
private readonly WebApplication _app;
private LocalApiServer(WebApplication app, string url)
{
_app = app;
Url = url;
}
public string Url { get; }
public static async Task<LocalApiServer> StartAsync()
{
int port = GetFreePort();
string url = $"http://127.0.0.1:{port}";
WebApplication app = ApiHost.Create(["--urls", url]);
await app.StartAsync();
return new LocalApiServer(app, url);
}
public async ValueTask DisposeAsync()
{
await _app.StopAsync();
await _app.DisposeAsync();
}
private static int GetFreePort()
{
TcpListener listener = new(IPAddress.Loopback, 0);
listener.Start();
int port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
}
6. Clase base Playwright
Crea Support/PlaywrightTestBase.cs:
using Microsoft.Playwright;
using Microsoft.Playwright.NUnit;
public abstract class PlaywrightTestBase : PageTest
{
private LocalApiServer? _server;
[OneTimeSetUp]
public async Task OneTimeSetUp()
{
if (Environment.GetEnvironmentVariable("E2E_BASE_URL") is null)
{
_server = await LocalApiServer.StartAsync();
TestSettings.BaseUrl = _server.Url;
}
}
[OneTimeTearDown]
public async Task OneTimeTearDown()
{
if (_server is not null)
await _server.DisposeAsync();
}
public override BrowserNewContextOptions ContextOptions()
{
return new BrowserNewContextOptions
{
ViewportSize = new ViewportSize { Width = 1440, Height = 900 },
Locale = "es-ES",
TimezoneId = "Europe/Madrid",
IgnoreHTTPSErrors = true
};
}
}
PageTest crea una pagina nueva para cada test y expone Page, Context, Browser, Playwright y Expect.
Esto evita que tengas que escribir a mano toda la infraestructura de navegador. En una clase base propia nos centramos solo en lo especifico del curso:
| Parte | Por que la ponemos aqui |
|---|---|
OneTimeSetUp | Arranca la API local una vez por clase de pruebas |
OneTimeTearDown | Cierra la API local al terminar |
ContextOptions | Define viewport, idioma, zona horaria y opciones del navegador |
TestSettings.BaseUrl | Permite cambiar entre servidor local automatico o servidor arrancado manualmente |
El contexto de navegador es importante: cada test debe empezar limpio. Si una prueba depende de cookies, local storage o datos dejados por otra prueba, se vuelve fragil. Por eso Playwright crea contextos aislados y por eso guardaremos storage state solo cuando queramos reutilizar un login de forma explicita.
7. Primer test
Crea Tests/PrimerTest.cs:
using System.Text.RegularExpressions;
public class PrimerTest : PlaywrightTestBase
{
[Test]
public async Task PaginaPrincipal_MuestraTitulo()
{
await Page.GotoAsync(TestSettings.BaseUrl);
await Expect(Page).ToHaveTitleAsync(new Regex("Tienda"));
}
}
Ejecuta este test antes de continuar.
Lectura del test:
| Linea | Explicacion |
|---|---|
await Page.GotoAsync(...) | Abre una URL en la pestana del test |
Expect(Page) | Crea una asercion sobre la pagina completa |
ToHaveTitleAsync | Espera hasta que el titulo cumpla la condicion |
new Regex("Tienda") | No exige el titulo exacto; basta con que contenga Tienda |
Primer ejercicio guiado: cambia temporalmente "Tienda" por "No existe" y ejecuta el test. Luego abre el error y observa como Playwright explica que esperaba y que encontro.
8. Locators y formularios
Playwright recomienda localizar elementos como lo haria el usuario:
await Page.GetByRole(AriaRole.Button, new() { Name = "Guardar" }).ClickAsync();
await Page.GetByLabel("Nombre").FillAsync("Teclado mecanico");
await Page.GetByPlaceholder("Buscar productos").FillAsync("teclado");
await Page.GetByText("El nombre es obligatorio").ClickAsync();
await Page.GetByTestId("guardar-producto").ClickAsync();
Orden recomendado para elegir locators:
| Prioridad | Locator | Cuando usarlo |
|---|---|---|
| 1 | GetByRole | Botones, enlaces, headings, tablas, checkboxes y radios |
| 2 | GetByLabel | Inputs asociados a una etiqueta visible |
| 3 | GetByPlaceholder | Inputs sin label pero con placeholder |
| 4 | GetByText | Textos visibles, mensajes y resultados |
| 5 | GetByTestId | Contratos de testing definidos por el equipo |
| 6 | Locator("css") | Cuando no existe una opcion mas expresiva |
La idea no es memorizar todos los localizadores, sino leer la pagina como la lee una persona. Si el usuario pulsa un boton que pone Guardar, el test deberia parecerse a eso:
await Page.GetByRole(AriaRole.Button, new() { Name = "Guardar" }).ClickAsync();
Ventajas de este estilo:
- El test se entiende aunque no conozcas el HTML.
- Mejora la accesibilidad porque obliga a pensar en roles, labels y nombres.
- Los errores de Playwright suelen ser mas claros.
- El locator se vuelve a resolver cada vez que se usa, asi que tolera mejor cambios del DOM.
Prueba de producto:
[Test]
public async Task CrearProducto_DatosValidos_ApareceEnListado()
{
await Page.GotoAsync($"{TestSettings.BaseUrl}/productos/nuevo");
await Page.GetByLabel("Nombre").FillAsync("Teclado mecanico");
await Page.GetByLabel("Categoria").FillAsync("Perifericos");
await Page.GetByLabel("Precio").FillAsync("89.99");
await Page.GetByLabel("Stock").FillAsync("12");
await Page.GetByRole(AriaRole.Button, new() { Name = "Guardar" }).ClickAsync();
await Expect(Page).ToHaveURLAsync(new Regex(".*/productos"));
await Expect(Page.GetByText("Teclado mecanico")).ToBeVisibleAsync();
}
Validacion:
[Test]
public async Task CrearProducto_SinNombre_MuestraError()
{
await Page.GotoAsync($"{TestSettings.BaseUrl}/productos/nuevo");
await Page.GetByRole(AriaRole.Button, new() { Name = "Guardar" }).ClickAsync();
await Expect(Page.GetByText("El nombre es obligatorio")).ToBeVisibleAsync();
}
9. Page Object
Crea Pages/ProductosPage.cs:
public class ProductosPage
{
private readonly IPage _page;
public ProductosPage(IPage page)
{
_page = page;
}
public async Task AbrirNuevoAsync()
{
await _page.GotoAsync($"{TestSettings.BaseUrl}/productos/nuevo");
}
public async Task CrearAsync(string nombre, string categoria, decimal precio, int stock)
{
await _page.GetByLabel("Nombre").FillAsync(nombre);
await _page.GetByLabel("Categoria").FillAsync(categoria);
await _page.GetByLabel("Precio").FillAsync(precio.ToString(CultureInfo.InvariantCulture));
await _page.GetByLabel("Stock").FillAsync(stock.ToString(CultureInfo.InvariantCulture));
await _page.GetByRole(AriaRole.Button, new() { Name = "Guardar" }).ClickAsync();
}
public ILocator Producto(string nombre)
{
return _page.GetByText(nombre);
}
}
Uso:
[Test]
public async Task CrearProducto_ConPageObject()
{
ProductosPage productos = new(Page);
await productos.AbrirNuevoAsync();
await productos.CrearAsync("Raton", "Perifericos", 29.99m, 30);
await Expect(productos.Producto("Raton")).ToBeVisibleAsync();
}
Un Page Object no debe convertirse en una segunda aplicacion dentro de las pruebas. Su objetivo es esconder detalles repetitivos, no esconder el comportamiento que se esta verificando.
Buen reparto:
| En el Page Object | En el test |
|---|---|
| Navegar a la pagina | Decidir que escenario se prueba |
| Rellenar campos | Elegir los datos importantes |
| Pulsar botones | Hacer aserciones |
| Exponer locators relevantes | Explicar el resultado esperado |
Ejemplo de mala senal: si el Page Object tiene un metodo CrearProductoYComprobarQueSeHaCreadoAsync, probablemente esta mezclando accion y asercion. Para clase es mejor mantener:
await productos.CrearAsync("Raton", "Perifericos", 29.99m, 30);
await Expect(productos.Producto("Raton")).ToBeVisibleAsync();
10. Selects, checkboxes y radio buttons
La pagina /controles permite practicar controles HTML:
[Test]
public async Task SelectCheckboxYRadio_GuardaControles()
{
await Page.GotoAsync($"{TestSettings.BaseUrl}/controles");
await Page.Locator("#categoria").SelectOptionAsync(new[] { "perifericos" });
await Page.GetByLabel("Activo").CheckAsync();
await Page.Locator("input[name='envio'][value='urgente']").CheckAsync();
await Page.GetByTestId("guardar-controles").ClickAsync();
await Expect(Page.GetByTestId("resultado-controles")).ToHaveTextAsync("Controles guardados");
}
Que aprende el alumno aqui:
| Control | Metodo habitual | Nota |
|---|---|---|
| Select HTML | SelectOptionAsync | Puede seleccionar por valor, texto o indice |
| Checkbox | CheckAsync / UncheckAsync | No hace clic si ya esta en el estado deseado |
| Radio button | CheckAsync | Solo una opcion del grupo queda activa |
| Resultado visible | ToHaveTextAsync | Espera hasta que el texto sea el esperado |
CheckAsync es mas expresivo que ClickAsync para checkboxes y radios. Si el checkbox ya esta marcado, CheckAsync no lo desmarca por accidente.
11. Popups y dialogos
Popup:
[Test]
public async Task AbrirAyuda_EnPopup_MuestraTitulo()
{
await Page.GotoAsync($"{TestSettings.BaseUrl}/ventanas");
IPage popup = await Page.RunAndWaitForPopupAsync(async () =>
{
await Page.GetByRole(AriaRole.Button, new() { Name = "Abrir ayuda" }).ClickAsync();
});
await popup.WaitForLoadStateAsync();
await Expect(popup).ToHaveTitleAsync("Ayuda");
}
Dialogo:
[Test]
public async Task Eliminar_AceptaDialogo_MuestraResultado()
{
await Page.GotoAsync($"{TestSettings.BaseUrl}/alertas");
TaskCompletionSource<string> dialogMessage = new();
Page.Dialog += async (_, dialog) =>
{
dialogMessage.SetResult(dialog.Message);
await dialog.AcceptAsync();
};
await Page.GetByRole(AriaRole.Button, new() { Name = "Eliminar" }).ClickAsync();
Assert.That(await dialogMessage.Task, Does.Contain("Seguro?"));
await Expect(Page.GetByTestId("resultado-alerta")).ToHaveTextAsync("Eliminado");
}
Los popups y dialogos tienen una regla importante: hay que preparar la espera antes de la accion que los abre.
Correcto: esperar popup -> hacer click -> recibir popup
Fragil: hacer click -> intentar buscar popup despues
En los dialogos JavaScript, el navegador bloquea la interaccion normal con la pagina hasta que el dialogo se acepta o cancela. Por eso se registra el evento Page.Dialog antes de pulsar el boton.
12. Probar APIs con Playwright
Playwright tambien puede hacer peticiones HTTP:
[Test]
public async Task ApiProductos_DevuelveOk()
{
await using IAPIRequestContext request = await Playwright.APIRequest.NewContextAsync(new()
{
BaseURL = TestSettings.BaseUrl,
IgnoreHTTPSErrors = true
});
IAPIResponse response = await request.GetAsync("/api/productos");
Assert.That(response.Ok, Is.True);
}
Crear producto:
IAPIResponse response = await request.PostAsync("/api/productos", new()
{
DataObject = new
{
nombre = "Webcam",
categoria = "Perifericos",
precio = 49.99,
stock = 20
}
});
Assert.That(response.Status, Is.EqualTo(201));
Esto permite un patron muy usado en E2E:
- Preparar datos por API.
- Abrir la UI.
- Comprobar que la UI muestra o modifica esos datos.
La API sirve para ir rapido. La UI sirve para comprobar el flujo que ve el usuario. Si creamos todos los datos desde la UI, las pruebas E2E se vuelven lentas y repetitivas.
Ejemplo de criterio:
| Necesidad | Mejor via |
|---|---|
| Crear 20 productos de prueba | API |
| Comprobar que el formulario valida campos | UI |
| Consultar si el servidor responde | API |
| Verificar que un usuario puede guardar desde navegador | UI |
13. Screenshots, traces y estado de login
Screenshot:
await Page.ScreenshotAsync(new()
{
Path = "artifacts/productos.png",
FullPage = true
});
Trace:
await Context.Tracing.StartAsync(new()
{
Screenshots = true,
Snapshots = true,
Sources = true
});
await Page.GotoAsync($"{TestSettings.BaseUrl}/productos");
await Context.Tracing.StopAsync(new()
{
Path = "artifacts/trace.zip"
});
Ver trace:
powershell -ExecutionPolicy Bypass -File tests/Tienda.Playwright.Tests/bin/Debug/net10.0/playwright.ps1 show-trace tests/Tienda.Playwright.Tests/artifacts/trace.zip
Guardar estado de login:
await Context.StorageStateAsync(new()
{
Path = "auth/admin.json"
});
No subas estados reales al repositorio si contienen cookies o tokens sensibles.
Para explicar fallos en clase, el trace es especialmente valioso. Permite ver:
- Capturas paso a paso.
- DOM en cada momento.
- Acciones ejecutadas.
- Red, consola y errores.
- Tiempo consumido por cada accion.
Uso docente recomendado:
- Ejecuta una prueba que pasa.
- Cambia un locator para que falle.
- Guarda el trace.
- Abre
show-trace. - Pide al alumno que localice el punto exacto del fallo.
storage state es otra herramienta distinta: no sirve para depurar, sino para reutilizar cookies y local storage. Es util para no hacer login en todas las pruebas, pero hay que tratarlo como material sensible.
14. Pruebas moviles
Sobrescribe ContextOptions:
public override BrowserNewContextOptions ContextOptions()
{
return new BrowserNewContextOptions
{
ViewportSize = new ViewportSize
{
Width = 390,
Height = 844
},
IsMobile = true,
HasTouch = true,
DeviceScaleFactor = 3,
Locale = "es-ES"
};
}
Prueba:
[Test]
public async Task MenuMovil_SeAbre()
{
await Page.GotoAsync(TestSettings.BaseUrl);
await Page.GetByRole(AriaRole.Button, new() { Name = "Menu" }).ClickAsync();
await Expect(Page.GetByRole(AriaRole.Navigation)).ToBeVisibleAsync();
}
En pruebas moviles no estamos usando un telefono real. Estamos creando un contexto de navegador con viewport pequeno, tactil y escala de pantalla. Esto es suficiente para probar menus responsive, formularios y estilos adaptativos.
Conviene comprobar siempre:
- Que los botones importantes caben en pantalla.
- Que el menu movil se puede abrir y cerrar.
- Que no hay elementos ocultos detras de cabeceras fijas.
- Que los formularios siguen siendo usables con ancho reducido.
15. Ejecucion
Instalar navegadores:
dotnet build
powershell -ExecutionPolicy Bypass -File tests/Tienda.Playwright.Tests/bin/Debug/net10.0/playwright.ps1 install
Ejecutar:
dotnet test
dotnet test -- Playwright.LaunchOptions.Headless=false
dotnet test -- Playwright.BrowserName=chromium
dotnet test -- Playwright.BrowserName=firefox
dotnet test -- Playwright.BrowserName=webkit
Contra una web ya arrancada:
$env:E2E_BASE_URL="http://localhost:7001"
dotnet test
Filtros utiles:
dotnet test --filter "FullyQualifiedName~ProductosTests"
dotnet test --filter "TestCategory=ExpandTesting"
dotnet test --filter "TestCategory!=ExpandTesting"
Para una clase, una secuencia comoda es:
- Ejecutar una prueba en modo normal.
- Repetirla con
Headless=false. - Forzar un fallo.
- Abrir el trace o revisar screenshot.
- Ejecutarla en Firefox o WebKit.
16. CI con GitHub Actions
name: e2e
on:
push:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Install Playwright browsers
run: pwsh tests/Tienda.Playwright.Tests/bin/Debug/net10.0/playwright.ps1 install --with-deps
- name: Test
run: dotnet test --no-build
17. Plan docente recomendado
Playwright se presta muy bien a una clase demostrativa porque sus locators son legibles y sus trazas ayudan a explicar fallos. Una distribucion para 4 o 4,5 horas:
| Tiempo | Contenido | Resultado esperado |
|---|---|---|
| 20 min | Comparativa Selenium vs Playwright | El alumno entiende auto-waiting y locators |
| 30 min | Crear solucion e instalar navegadores | Proyecto ejecutable en Visual Studio |
| 25 min | Primer PageTest | PrimerTest en verde |
| 40 min | Formulario y validaciones | ProductosTests |
| 30 min | Page Object | ProductosPage |
| 30 min | Select, checkbox, radio | /controles probado |
| 30 min | API testing | ApiProductosTests |
| 30 min | Screenshots, traces y storage state | Artefactos generados |
| 25 min | Popups, dialogos y mobile | Tests de navegador avanzado |
| 25 min | Multi-browser y CI | Comandos y YAML explicados |
| 90 min | Practica con ExpandTesting | Ejemplos online por categoria |
Si este tema forma parte de un bloque mas largo, se puede ampliar hasta 6 u 8 horas anadiendo:
- Refactor de los ejemplos de ExpandTesting a Page Objects.
- Comparacion directa con los ejemplos Selenium del modulo anterior.
- Ejecucion multi-browser solo de pruebas locales.
- Ejecucion separada de pruebas online con categoria
ExpandTesting. - Analisis de traces reales generados por fallos provocados.
18. Ruta de construccion guiada
Etapa 1: validar instalacion
Despues de instalar navegadores, ejecuta:
dotnet test --filter "FullyQualifiedName~PrimerTest"
Si falla con “Executable doesn’t exist”, ejecuta:
dotnet build
powershell -ExecutionPolicy Bypass -File tests/Tienda.Playwright.Tests/bin/Debug/net10.0/playwright.ps1 install
Etapa 2: test minimo
[Test]
public async Task Home_CargaConTituloTienda()
{
await Page.GotoAsync(TestSettings.BaseUrl);
await Expect(Page).ToHaveTitleAsync(new Regex("Tienda"));
}
Etapa 3: locators accesibles
Prueba que la pagina de productos tiene un heading y un buscador:
[Test]
public async Task Productos_MuestraTituloYBuscador()
{
await Page.GotoAsync($"{TestSettings.BaseUrl}/productos");
await Expect(Page.GetByRole(AriaRole.Heading, new() { Name = "Productos" })).ToBeVisibleAsync();
await Expect(Page.GetByPlaceholder("Buscar productos")).ToBeVisibleAsync();
}
Etapa 4: formulario
[Test]
public async Task CrearProducto_DatosValidos_ApareceEnListado()
{
await Page.GotoAsync($"{TestSettings.BaseUrl}/productos/nuevo");
await Page.GetByLabel("Nombre").FillAsync("Teclado mecanico");
await Page.GetByLabel("Categoria").FillAsync("Perifericos");
await Page.GetByLabel("Precio").FillAsync("89.99");
await Page.GetByLabel("Stock").FillAsync("12");
await Page.GetByRole(AriaRole.Button, new() { Name = "Guardar" }).ClickAsync();
await Expect(Page).ToHaveURLAsync(new Regex(".*/productos"));
await Expect(Page.GetByText("Teclado mecanico")).ToBeVisibleAsync();
}
Etapa 5: debug con headed mode
dotnet test -- Playwright.LaunchOptions.Headless=false
Este comando abre el navegador y ayuda a explicar que ve Playwright.
Etapa 6: convertir a Page Object
Extrae acciones repetidas a ProductosPage, pero conserva las aserciones en el test:
ProductosPage productos = new(Page);
await productos.AbrirNuevoAsync();
await productos.CrearAsync("Raton", "Perifericos", 29.99m, 30);
await Expect(productos.Producto("Raton")).ToBeVisibleAsync();
19. Banco de ejemplos Playwright
19.1 Locators por rol
[Test]
public async Task Home_TieneNavegacionPrincipal()
{
await Page.GotoAsync(TestSettings.BaseUrl);
await Expect(Page.GetByRole(AriaRole.Navigation)).ToBeVisibleAsync();
await Expect(Page.GetByRole(AriaRole.Link, new() { Name = "Productos" })).ToBeVisibleAsync();
}
19.2 Locators por label
[Test]
public async Task Login_Email_ConservaValor()
{
await Page.GotoAsync($"{TestSettings.BaseUrl}/login");
await Page.GetByLabel("Email").FillAsync("ana@ejemplo.com");
await Expect(Page.GetByLabel("Email")).ToHaveValueAsync("ana@ejemplo.com");
}
19.3 Locators por placeholder
[Test]
public async Task BuscarProductos_UsaPlaceholder()
{
await Page.GotoAsync($"{TestSettings.BaseUrl}/productos");
await Page.GetByPlaceholder("Buscar productos").FillAsync("monitor");
await Expect(Page.GetByPlaceholder("Buscar productos")).ToHaveValueAsync("monitor");
}
19.4 Locators por data-testid
[Test]
public async Task Total_MuestraTextoEsperado()
{
await Page.GotoAsync($"{TestSettings.BaseUrl}/productos");
await Expect(Page.GetByTestId("total")).ToContainTextAsync("89,99");
}
19.5 CSS cuando hace falta
[Test]
public async Task PrimerProducto_EsVisible()
{
await Page.GotoAsync($"{TestSettings.BaseUrl}/productos");
ILocator primerProducto = Page.Locator("[data-testid='producto']").First;
await Expect(primerProducto).ToBeVisibleAsync();
}
19.6 Esperas web-first
En Playwright normalmente no escribes esperas manuales:
await Page.GetByRole(AriaRole.Button, new() { Name = "Guardar" }).ClickAsync();
await Expect(Page.GetByText("El nombre es obligatorio")).ToBeVisibleAsync();
La asercion espera hasta que se cumple o hasta agotar timeout.
Puedes cambiar timeout:
SetDefaultExpectTimeout(10_000);
19.7 Validar lista de productos
[Test]
public async Task Productos_ListadoTieneAlMenosUnProducto()
{
await Page.GotoAsync($"{TestSettings.BaseUrl}/productos");
ILocator productos = Page.GetByTestId("producto");
await Expect(productos.First).ToBeVisibleAsync();
Assert.That(await productos.CountAsync(), Is.GreaterThanOrEqualTo(1));
}
19.8 Select, checkbox y radio
[Test]
public async Task Controles_PermitenSeleccionarOpciones()
{
await Page.GotoAsync($"{TestSettings.BaseUrl}/controles");
await Page.Locator("#categoria").SelectOptionAsync(new[] { "pantallas" });
await Page.GetByLabel("Activo").CheckAsync();
await Page.Locator("input[name='envio'][value='normal']").CheckAsync();
await Expect(Page.GetByLabel("Activo")).ToBeCheckedAsync();
}
19.9 Dialogos
[Test]
public async Task Confirmacion_PuedeCancelarse()
{
await Page.GotoAsync($"{TestSettings.BaseUrl}/alertas");
TaskCompletionSource<string> message = new();
Page.Dialog += async (_, dialog) =>
{
message.SetResult(dialog.Message);
await dialog.DismissAsync();
};
await Page.GetByRole(AriaRole.Button, new() { Name = "Eliminar" }).ClickAsync();
Assert.That(await message.Task, Does.Contain("Seguro?"));
await Expect(Page.GetByTestId("resultado-alerta")).ToBeEmptyAsync();
}
19.10 Popups
[Test]
public async Task Popup_Ayuda_TieneHeading()
{
await Page.GotoAsync($"{TestSettings.BaseUrl}/ventanas");
IPage popup = await Page.RunAndWaitForPopupAsync(async () =>
{
await Page.GetByRole(AriaRole.Button, new() { Name = "Abrir ayuda" }).ClickAsync();
});
await Expect(popup.GetByRole(AriaRole.Heading, new() { Name = "Ayuda" })).ToBeVisibleAsync();
}
19.11 API: leer JSON
[Test]
public async Task ApiProductos_DevuelveJson()
{
await using IAPIRequestContext request = await Playwright.APIRequest.NewContextAsync(new()
{
BaseURL = TestSettings.BaseUrl
});
IAPIResponse response = await request.GetAsync("/api/productos");
Assert.That(response.Ok, Is.True);
string body = await response.TextAsync();
Assert.That(body, Does.Contain("Monitor"));
}
19.12 API: preparar datos antes de UI
[Test]
public async Task ProductoCreadoPorApi_ApareceEnListado()
{
await using IAPIRequestContext request = await Playwright.APIRequest.NewContextAsync(new()
{
BaseURL = TestSettings.BaseUrl
});
await request.PostAsync("/api/productos", new()
{
DataObject = new
{
nombre = "Microfono USB",
categoria = "Perifericos",
precio = 59.99,
stock = 15
}
});
await Page.GotoAsync($"{TestSettings.BaseUrl}/productos");
await Expect(Page.GetByText("Microfono USB")).ToBeVisibleAsync();
}
19.13 Screenshots en fallo
Puedes capturar manualmente:
await Page.ScreenshotAsync(new()
{
Path = $"artifacts/{TestContext.CurrentContext.Test.Name}.png",
FullPage = true
});
O hacerlo en [TearDown]:
[TearDown]
public async Task ScreenshotOnFailure()
{
bool failed = TestContext.CurrentContext.Result.Outcome.Status
== NUnit.Framework.Interfaces.TestStatus.Failed;
if (failed)
{
Directory.CreateDirectory("artifacts");
await Page.ScreenshotAsync(new()
{
Path = $"artifacts/{TestContext.CurrentContext.Test.Name}.png",
FullPage = true
});
}
}
19.14 Trazas solo en fallo
[SetUp]
public async Task StartTrace()
{
await Context.Tracing.StartAsync(new()
{
Screenshots = true,
Snapshots = true,
Sources = true
});
}
[TearDown]
public async Task StopTrace()
{
bool failed = TestContext.CurrentContext.Result.Outcome.Status
== NUnit.Framework.Interfaces.TestStatus.Failed;
Directory.CreateDirectory("artifacts");
if (failed)
{
await Context.Tracing.StopAsync(new()
{
Path = $"artifacts/{TestContext.CurrentContext.Test.Name}.zip"
});
}
else
{
await Context.Tracing.StopAsync();
}
}
19.15 Reutilizar storage state
Primero generas estado:
await Context.StorageStateAsync(new()
{
Path = "auth/admin.json"
});
Luego lo usas en otra clase:
public override BrowserNewContextOptions ContextOptions()
{
return new BrowserNewContextOptions
{
StorageStatePath = "auth/admin.json",
IgnoreHTTPSErrors = true
};
}
19.16 Multi-browser
dotnet test -- Playwright.BrowserName=chromium
dotnet test -- Playwright.BrowserName=firefox
dotnet test -- Playwright.BrowserName=webkit
Consejo para clase:
- Primero ejecuta en Chromium.
- Cuando todo este estable, prueba Firefox y WebKit.
- Si falla solo en un navegador, revisa compatibilidad CSS/HTML o comportamiento de dialogos.
19.17 Errores habituales
| Error | Causa habitual | Solucion |
|---|---|---|
Executable doesn't exist | Navegadores no instalados | Ejecutar playwright.ps1 install |
| Locator estricto falla | El locator encuentra varios elementos | Hacer el locator mas especifico |
Timeout en Expect | La UI no llego al estado esperado | Revisar accion previa o datos |
| Test falla solo en WebKit | Diferencia real de navegador | Revisar HTML, CSS o dialogos |
| Trace no se abre | Ruta incorrecta | Usar show-trace con path real |
20. Practica adicional con ExpandTesting
https://practice.expandtesting.com/ es una web publica pensada para practicar automatizacion con Playwright, Selenium, Cypress, WebdriverIO y herramientas de API. Incluye muchas paginas pequenas, cada una centrada en un problema concreto.
La usaremos como complemento, no como sustituto de la web local. La web local es estable y controlada por nosotros; ExpandTesting aporta variedad real: anuncios, scripts externos, tablas con contenido dinamico, iframes, descargas, subidas, dialogos y componentes que cambian con el tiempo.
Paginas exploradas para esta seccion:
| Pagina | Que permite practicar |
|---|---|
https://practice.expandtesting.com/login | Login correcto, usuario incorrecto y password incorrecto |
https://practice.expandtesting.com/inputs | Inputs number, text, password, date y salida generada |
https://practice.expandtesting.com/add-remove-elements | Elementos creados y eliminados dinamicamente |
https://practice.expandtesting.com/checkboxes | Checkboxes marcados y desmarcados |
https://practice.expandtesting.com/radio-buttons | Radios por grupos de opcion |
https://practice.expandtesting.com/dropdown | Selects HTML simples y selects largos |
https://practice.expandtesting.com/dynamic-table | Tabla con columnas y filas que cambian de posicion |
https://practice.expandtesting.com/dynamic-controls | Elementos que aparecen, desaparecen, se habilitan o deshabilitan |
https://practice.expandtesting.com/js-dialogs | Alert, confirm y prompt |
https://practice.expandtesting.com/upload | Subida de archivos |
https://practice.expandtesting.com/download | Descarga de archivos |
https://practice.expandtesting.com/windows | Nueva ventana o popup |
https://practice.expandtesting.com/hovers | Hover sobre elementos |
https://practice.expandtesting.com/iframe | Frame externo e iframe interno |
https://practice.expandtesting.com/key-presses | Teclado y eventos de tecla |
https://practice.expandtesting.com/shadowdom | Shadow DOM abierto |
Referencias oficiales de Playwright usadas como apoyo:
| Documentacion | Uso |
|---|---|
https://playwright.dev/dotnet/docs/intro | Instalacion, PageTest y ejecucion |
https://playwright.dev/dotnet/docs/locators | Locators por rol, label, texto y test id |
https://playwright.dev/dotnet/docs/actionability | Auto-waiting y comprobaciones antes de actuar |
https://playwright.dev/dotnet/docs/input | Click, fill, hover, check, teclado y upload |
https://playwright.dev/dotnet/docs/dialogs | Dialogos JavaScript |
https://playwright.dev/dotnet/docs/downloads | Descargas |
Nota docente: al depender de Internet, estas pruebas deben estar en una categoria propia. Asi puedes ejecutar el proyecto local aunque la web externa este lenta, cambie o no responda.
20.1 Clase base para pruebas online
Crea Support/ExpandTestingTestBase.cs:
using Microsoft.Playwright;
using Microsoft.Playwright.NUnit;
namespace Tienda.Playwright.Tests.Support;
[Category("ExpandTesting")]
public abstract class ExpandTestingTestBase : PageTest
{
protected const string ExpandTestingBaseUrl = "https://practice.expandtesting.com";
private bool _routesConfigured;
public override BrowserNewContextOptions ContextOptions()
{
return new BrowserNewContextOptions
{
ViewportSize = new ViewportSize
{
Width = 1440,
Height = 900
},
Locale = "en-US",
TimezoneId = "Europe/Madrid",
IgnoreHTTPSErrors = true
};
}
protected async Task OpenPracticeAsync(string path)
{
await ConfigureExternalRoutesAsync();
await Page.GotoAsync($"{ExpandTestingBaseUrl}{path}", new()
{
WaitUntil = WaitUntilState.DOMContentLoaded
});
await RemoveVisualNoiseAsync();
}
protected async Task RemoveVisualNoiseAsync()
{
await Page.EvaluateAsync("""
document
.querySelectorAll('ins.adsbygoogle, .ad-bar, #support-coffee, #menuleft, #menuright')
.forEach(element => element.remove());
""");
}
private async Task ConfigureExternalRoutesAsync()
{
if (_routesConfigured)
return;
await Page.RouteAsync("**/*", async route =>
{
string url = route.Request.Url;
bool isNoise =
url.Contains("googlesyndication") ||
url.Contains("doubleclick") ||
url.Contains("google-analytics") ||
url.Contains("googletagmanager") ||
url.Contains("socket.io");
if (isNoise)
{
await route.AbortAsync();
return;
}
await route.ContinueAsync();
});
_routesConfigured = true;
}
}
Que hace esta clase:
| Parte | Explicacion |
|---|---|
[Category("ExpandTesting")] | Permite ejecutar o excluir estas pruebas |
ContextOptions | Define un navegador previsible para la clase |
OpenPracticeAsync | Abre rutas de ExpandTesting sin repetir la URL base |
RemoveVisualNoiseAsync | Quita bloques visuales externos que pueden molestar en clase |
RouteAsync | Bloquea recursos externos de anuncios o tracking para reducir ruido |
No es obligatorio bloquear anuncios para usar Playwright. Aqui se hace porque estamos trabajando sobre una web publica de practica y queremos que el foco de la clase sea el concepto que se esta explicando.
20.2 Login: caso correcto y errores
Crea Tests/ExpandTesting/ExpandTestingLoginTests.cs:
using System.Text.RegularExpressions;
using Microsoft.Playwright;
using Tienda.Playwright.Tests.Support;
namespace Tienda.Playwright.Tests.Tests.ExpandTesting;
[TestFixture]
public class ExpandTestingLoginTests : ExpandTestingTestBase
{
[Test]
public async Task Login_CredencialesValidas_EntraEnZonaSegura()
{
await OpenPracticeAsync("/login");
await Page.GetByLabel("Username").FillAsync("practice");
await Page.GetByLabel("Password").FillAsync("SuperSecretPassword!");
await Page.GetByRole(AriaRole.Button, new() { Name = "Login" }).ClickAsync();
await Expect(Page).ToHaveURLAsync(new Regex(".*/secure"));
await Expect(Page.GetByText("You logged into a secure area!")).ToBeVisibleAsync();
await Expect(Page.GetByRole(AriaRole.Link, new() { Name = "Logout" })).ToBeVisibleAsync();
}
[Test]
public async Task Login_UsuarioIncorrecto_MuestraError()
{
await OpenPracticeAsync("/login");
await Page.GetByLabel("Username").FillAsync("wrongUser");
await Page.GetByLabel("Password").FillAsync("SuperSecretPassword!");
await Page.GetByRole(AriaRole.Button, new() { Name = "Login" }).ClickAsync();
await Expect(Page).ToHaveURLAsync(new Regex(".*/login"));
await Expect(Page.Locator("#flash")).ToContainTextAsync("Invalid username");
}
[Test]
public async Task Login_PasswordIncorrecta_MuestraError()
{
await OpenPracticeAsync("/login");
await Page.GetByLabel("Username").FillAsync("practice");
await Page.GetByLabel("Password").FillAsync("WrongPassword");
await Page.GetByRole(AriaRole.Button, new() { Name = "Login" }).ClickAsync();
await Expect(Page).ToHaveURLAsync(new Regex(".*/login"));
await Expect(Page.Locator("#flash")).ToContainTextAsync("Invalid password");
}
}
Puntos para explicar:
- La propia pagina publica las credenciales de practica: usuario
practicey passwordSuperSecretPassword!. GetByLabellocaliza los inputs por su etiqueta visible.GetByRolelocaliza el boton como lo percibe el usuario.- La URL final y el mensaje visible son el resultado de negocio.
- Las pruebas negativas deben comprobar que el usuario no entra en la zona segura.
20.3 Inputs: escribir, mostrar y limpiar datos
Crea Tests/ExpandTesting/ExpandTestingInputsTests.cs:
using System.Text.RegularExpressions;
using Microsoft.Playwright;
using Tienda.Playwright.Tests.Support;
namespace Tienda.Playwright.Tests.Tests.ExpandTesting;
[TestFixture]
public class ExpandTestingInputsTests : ExpandTestingTestBase
{
[Test]
public async Task Inputs_DisplayInputs_MuestraLosValoresIntroducidos()
{
await OpenPracticeAsync("/inputs");
await Page.Locator("#input-number").FillAsync("42");
await Page.Locator("#input-text").FillAsync("Curso Playwright");
await Page.Locator("#input-password").FillAsync("SuperSecretPassword!");
await Page.Locator("#input-date").FillAsync("2026-06-01");
await Page.GetByRole(AriaRole.Button, new() { Name = "Display Inputs" }).ClickAsync();
await Expect(Page.Locator("#output-number")).ToHaveTextAsync("42");
await Expect(Page.Locator("#output-text")).ToHaveTextAsync("Curso Playwright");
await Expect(Page.Locator("#output-password")).ToHaveTextAsync("SuperSecretPassword!");
await Expect(Page.Locator("#output-date")).ToHaveTextAsync("2026-06-01");
}
[Test]
public async Task Inputs_ClearInputs_LimpiaCamposYResultados()
{
await OpenPracticeAsync("/inputs");
await Page.Locator("#input-number").FillAsync("42");
await Page.Locator("#input-text").FillAsync("Curso Playwright");
await Page.GetByRole(AriaRole.Button, new() { Name = "Display Inputs" }).ClickAsync();
await Page.GetByRole(AriaRole.Button, new() { Name = "Clear Inputs" }).ClickAsync();
await Expect(Page.Locator("#input-number")).ToHaveValueAsync("");
await Expect(Page.Locator("#input-text")).ToHaveValueAsync("");
await Expect(Page.Locator("#result")).ToBeEmptyAsync();
}
}
Puntos para explicar:
FillAsyncreemplaza el contenido del input.- Los inputs de fecha esperan formato
yyyy-MM-dd. ToHaveTextAsyncespera hasta que la salida tenga exactamente ese texto.ToBeEmptyAsyncsirve para comprobar contenedores sin contenido.
20.4 Add/Remove Elements: elementos dinamicos
Crea Tests/ExpandTesting/ExpandTestingDynamicElementsTests.cs:
using Microsoft.Playwright;
using Tienda.Playwright.Tests.Support;
namespace Tienda.Playwright.Tests.Tests.ExpandTesting;
[TestFixture]
public class ExpandTestingDynamicElementsTests : ExpandTestingTestBase
{
[Test]
public async Task AddRemoveElements_AgregaYEliminaBotones()
{
await OpenPracticeAsync("/add-remove-elements");
await Page.GetByRole(AriaRole.Button, new() { Name = "Add Element" }).ClickAsync();
await Page.GetByRole(AriaRole.Button, new() { Name = "Add Element" }).ClickAsync();
await Page.GetByRole(AriaRole.Button, new() { Name = "Add Element" }).ClickAsync();
ILocator deleteButtons = Page.Locator("#elements .added-manually");
await Expect(deleteButtons).ToHaveCountAsync(3);
await deleteButtons.First.ClickAsync();
await Expect(deleteButtons).ToHaveCountAsync(2);
}
}
Puntos para explicar:
- Un
Locatorno es una foto fija del DOM; se vuelve a evaluar. - Por eso
deleteButtonssirve antes y despues de borrar. ToHaveCountAsynces una asercion web-first: espera a que el numero sea correcto.
20.5 Checkboxes, radios y dropdowns
Crea Tests/ExpandTesting/ExpandTestingControlsTests.cs:
using Microsoft.Playwright;
using Tienda.Playwright.Tests.Support;
namespace Tienda.Playwright.Tests.Tests.ExpandTesting;
[TestFixture]
public class ExpandTestingControlsTests : ExpandTestingTestBase
{
[Test]
public async Task Checkboxes_PermitenMarcarYDesmarcar()
{
await OpenPracticeAsync("/checkboxes");
ILocator checkbox1 = Page.Locator("#checkbox1");
ILocator checkbox2 = Page.Locator("#checkbox2");
await checkbox1.CheckAsync();
await checkbox2.UncheckAsync();
await Expect(checkbox1).ToBeCheckedAsync();
await Expect(checkbox2).Not.ToBeCheckedAsync();
}
[Test]
public async Task RadioButtons_PermitenSeleccionarColorYDeporte()
{
await OpenPracticeAsync("/radio-buttons");
await Page.Locator("#red").CheckAsync();
await Page.Locator("#football").CheckAsync();
await Expect(Page.Locator("#red")).ToBeCheckedAsync();
await Expect(Page.Locator("#football")).ToBeCheckedAsync();
await Expect(Page.Locator("#blue")).Not.ToBeCheckedAsync();
}
[Test]
public async Task Dropdowns_PermitenSeleccionarOpcionesPorValor()
{
await OpenPracticeAsync("/dropdown");
await Page.Locator("#dropdown").SelectOptionAsync(new[] { "1" });
await Page.Locator("#elementsPerPageSelect").SelectOptionAsync(new[] { "50" });
await Page.Locator("#country").SelectOptionAsync(new[] { "ES" });
await Expect(Page.Locator("#dropdown")).ToHaveValueAsync("1");
await Expect(Page.Locator("#elementsPerPageSelect")).ToHaveValueAsync("50");
await Expect(Page.Locator("#country")).ToHaveValueAsync("ES");
}
}
Puntos para explicar:
CheckAsyncyUncheckAsyncexpresan intencion mejor queClickAsync.- Los radios se agrupan por el atributo
name; solo puede quedar uno marcado por grupo. SelectOptionAsynctrabaja con el valor del<option>, no con el texto visible, salvo que uses opciones especificas.
20.6 Dynamic Table: tabla con columnas que cambian
En esta pagina las columnas pueden cambiar de posicion y los valores son aleatorios. El reto consiste en localizar la fila Chrome, encontrar la columna CPU y compararla con la etiqueta amarilla Chrome CPU.
Crea Tests/ExpandTesting/ExpandTestingDynamicTableTests.cs:
using Microsoft.Playwright;
using Tienda.Playwright.Tests.Support;
namespace Tienda.Playwright.Tests.Tests.ExpandTesting;
[TestFixture]
public class ExpandTestingDynamicTableTests : ExpandTestingTestBase
{
[Test]
public async Task DynamicTable_ChromeCpu_CoincideConLaEtiqueta()
{
await OpenPracticeAsync("/dynamic-table");
IReadOnlyList<string> headers = await Page.Locator("table thead th").AllInnerTextsAsync();
int cpuIndex = headers.ToList().IndexOf("CPU");
Assert.That(cpuIndex, Is.GreaterThanOrEqualTo(0), "La tabla debe contener la columna CPU.");
ILocator chromeRow = Page.Locator("table tbody tr").Filter(new()
{
HasTextString = "Chrome"
});
string cpuValue = await chromeRow.Locator("td").Nth(cpuIndex).InnerTextAsync();
await Expect(Page.Locator("#chrome-cpu")).ToHaveTextAsync($"Chrome CPU: {cpuValue}");
}
}
Puntos para explicar:
- No debemos asumir que CPU esta siempre en la misma columna.
- Primero leemos los encabezados.
- Calculamos el indice de la columna.
- Usamos ese indice para leer la celda correcta en la fila
Chrome. - Este ejemplo muestra por que las tablas son mas dificiles que los formularios.
20.7 Dynamic Controls: esperar cambios asincronos
Crea Tests/ExpandTesting/ExpandTestingDynamicControlsTests.cs:
using Microsoft.Playwright;
using Tienda.Playwright.Tests.Support;
namespace Tienda.Playwright.Tests.Tests.ExpandTesting;
[TestFixture]
public class ExpandTestingDynamicControlsTests : ExpandTestingTestBase
{
[Test]
public async Task DynamicControls_RemoveYAdd_CambiaVisibilidadDelCheckbox()
{
await OpenPracticeAsync("/dynamic-controls");
await Page.GetByRole(AriaRole.Button, new() { Name = "Remove" }).ClickAsync();
await Expect(Page.Locator("#checkbox")).ToBeHiddenAsync();
await Page.GetByRole(AriaRole.Button, new() { Name = "Add" }).ClickAsync();
await Expect(Page.Locator("#checkbox")).ToBeVisibleAsync();
}
[Test]
public async Task DynamicControls_Enable_HabilitaElInput()
{
await OpenPracticeAsync("/dynamic-controls");
ILocator input = Page.Locator("#input-example input");
await Expect(input).ToBeDisabledAsync();
await Page.GetByRole(AriaRole.Button, new() { Name = "Enable" }).ClickAsync();
await Expect(input).ToBeEnabledAsync();
await input.FillAsync("Playwright espera por mi");
await Expect(input).ToHaveValueAsync("Playwright espera por mi");
}
}
Puntos para explicar:
- No usamos
Thread.Sleep. - Playwright espera dentro de
Expect. - La prueba describe estados: visible, oculto, deshabilitado, habilitado.
- Esto es mas estable que esperar un numero fijo de segundos.
20.8 JavaScript Dialogs: alert, confirm y prompt
Crea Tests/ExpandTesting/ExpandTestingDialogsTests.cs:
using Microsoft.Playwright;
using Tienda.Playwright.Tests.Support;
namespace Tienda.Playwright.Tests.Tests.ExpandTesting;
[TestFixture]
public class ExpandTestingDialogsTests : ExpandTestingTestBase
{
[Test]
public async Task Dialogs_Alert_SeAcepta()
{
await OpenPracticeAsync("/js-dialogs");
string? message = null;
Page.Dialog += async (_, dialog) =>
{
message = dialog.Message;
await dialog.AcceptAsync();
};
await Page.Locator("#js-alert").ClickAsync();
Assert.That(message, Is.EqualTo("I am a Js Alert"));
await Expect(Page.Locator("#dialog-response")).ToHaveTextAsync("OK");
}
[Test]
public async Task Dialogs_Confirm_SeCancela()
{
await OpenPracticeAsync("/js-dialogs");
Page.Dialog += async (_, dialog) =>
{
await dialog.DismissAsync();
};
await Page.Locator("#js-confirm").ClickAsync();
await Expect(Page.Locator("#dialog-response")).ToHaveTextAsync("Cancel");
}
[Test]
public async Task Dialogs_Prompt_EnviaTexto()
{
await OpenPracticeAsync("/js-dialogs");
Page.Dialog += async (_, dialog) =>
{
await dialog.AcceptAsync("Fran");
};
await Page.Locator("#js-prompt").ClickAsync();
await Expect(Page.Locator("#dialog-response")).ToHaveTextAsync("Fran");
}
}
Puntos para explicar:
- Hay que registrar
Page.Dialogantes del clic. AcceptAsync()acepta alerts y confirms.DismissAsync()cancela confirms.AcceptAsync("texto")introduce texto en prompts.
20.9 File upload: subir un archivo pequeno
Crea Tests/ExpandTesting/ExpandTestingUploadTests.cs:
using System.Text.RegularExpressions;
using Microsoft.Playwright;
using Tienda.Playwright.Tests.Support;
namespace Tienda.Playwright.Tests.Tests.ExpandTesting;
[TestFixture]
public class ExpandTestingUploadTests : ExpandTestingTestBase
{
[Test]
public async Task Upload_ArchivoMenorDe500Kb_SeSubeCorrectamente()
{
await OpenPracticeAsync("/upload");
string filePath = Path.Combine(
TestContext.CurrentContext.WorkDirectory,
"expandtesting-upload.txt");
await File.WriteAllTextAsync(filePath, "Archivo pequeno creado por Playwright.");
await Page.Locator("#fileInput").SetInputFilesAsync(filePath);
await Expect(Page.Locator("#fileInput")).ToHaveValueAsync(new Regex("expandtesting-upload.txt"));
await Page.Locator("#fileSubmit").ClickAsync();
await Expect(Page.GetByRole(AriaRole.Heading, new() { Name = "File Uploaded!" })).ToBeVisibleAsync();
await Expect(Page.Locator("#uploaded-files")).ToContainTextAsync("expandtesting-upload.txt");
}
}
Puntos para explicar:
- Playwright no abre el explorador de Windows.
SetInputFilesAsyncescribe directamente en el inputtype=file.- El archivo se crea en la carpeta de ejecucion del test.
- La pagina solo acepta archivos menores de 500 KB, por eso generamos un texto pequeno.
20.10 File download: esperar una descarga
Crea Tests/ExpandTesting/ExpandTestingDownloadTests.cs:
using Microsoft.Playwright;
using Tienda.Playwright.Tests.Support;
namespace Tienda.Playwright.Tests.Tests.ExpandTesting;
[TestFixture]
public class ExpandTestingDownloadTests : ExpandTestingTestBase
{
[Test]
public async Task Download_SomeFileTxt_SeGuardaEnArtifacts()
{
await OpenPracticeAsync("/download");
IDownload download = await Page.RunAndWaitForDownloadAsync(async () =>
{
await Page.GetByRole(AriaRole.Link, new() { Name = "some-file.txt" }).ClickAsync();
});
Directory.CreateDirectory("artifacts");
string path = Path.Combine("artifacts", download.SuggestedFilename);
await download.SaveAsAsync(path);
Assert.Multiple(() =>
{
Assert.That(download.SuggestedFilename, Is.EqualTo("some-file.txt"));
Assert.That(File.Exists(path), Is.True);
});
}
}
Puntos para explicar:
- La espera de descarga se prepara antes del clic.
SuggestedFilenamees el nombre que propone el servidor.SaveAsAsyncmueve la descarga a una ruta controlada por el test.- En CI conviene guardar estos archivos como artefactos solo si son necesarios.
20.11 Windows: nueva pestana o popup
Crea Tests/ExpandTesting/ExpandTestingWindowsTests.cs:
using System.Text.RegularExpressions;
using Microsoft.Playwright;
using Tienda.Playwright.Tests.Support;
namespace Tienda.Playwright.Tests.Tests.ExpandTesting;
[TestFixture]
public class ExpandTestingWindowsTests : ExpandTestingTestBase
{
[Test]
public async Task Windows_ClickHere_AbreNuevaPagina()
{
await OpenPracticeAsync("/windows");
IPage popup = await Page.RunAndWaitForPopupAsync(async () =>
{
await Page.GetByRole(AriaRole.Link, new() { Name = "Click Here" }).ClickAsync();
});
await popup.WaitForLoadStateAsync();
await Expect(popup).ToHaveURLAsync(new Regex(".*/windows/new"));
await Expect(popup.GetByRole(AriaRole.Heading, new()
{
NameRegex = new Regex("Example of a new window")
})).ToBeVisibleAsync();
}
}
Puntos para explicar:
- En Playwright una nueva pestana se maneja como otra
IPage. - La pagina original sigue estando en
Page. - La pagina nueva queda en la variable
popup.
20.12 Hovers: mostrar contenido al pasar el raton
Crea Tests/ExpandTesting/ExpandTestingHoversTests.cs:
using Microsoft.Playwright;
using Tienda.Playwright.Tests.Support;
namespace Tienda.Playwright.Tests.Tests.ExpandTesting;
[TestFixture]
public class ExpandTestingHoversTests : ExpandTestingTestBase
{
[Test]
public async Task Hovers_User1_MuestraNombreYPerfil()
{
await OpenPracticeAsync("/hovers");
ILocator user1 = Page.GetByTestId("user-1");
await user1.HoverAsync();
await Expect(user1.GetByText("name: user1")).ToBeVisibleAsync();
await Expect(user1.GetByRole(AriaRole.Link, new() { Name = "View profile" })).ToBeVisibleAsync();
}
}
Puntos para explicar:
HoverAsyncsimula mover el raton sobre el elemento.- El contenido puede existir en el DOM pero estar oculto por CSS.
- La asercion debe comprobar que el contenido es visible, no solo que existe.
20.13 Iframes: trabajar dentro de otro documento
Crea Tests/ExpandTesting/ExpandTestingIframeTests.cs:
using Microsoft.Playwright;
using Tienda.Playwright.Tests.Support;
namespace Tienda.Playwright.Tests.Tests.ExpandTesting;
[TestFixture]
public class ExpandTestingIframeTests : ExpandTestingTestBase
{
[Test]
public async Task Iframe_EmailSubscribe_PermiteSuscribirse()
{
await OpenPracticeAsync("/iframe");
IFrameLocator frame = Page.FrameLocator("#email-subscribe");
await frame.Locator("#email").FillAsync("alumno@example.com");
await frame.Locator("#btn-subscribe").ClickAsync();
await Expect(frame.Locator("#success-message")).ToContainTextAsync("subscribed");
}
}
Puntos para explicar:
- Un iframe es una pagina dentro de otra pagina.
- Los locators normales no entran automaticamente en el iframe.
FrameLocatorcrea una ruta de localizacion dentro del iframe.- El test sigue siendo legible porque no cambiamos manualmente de contexto.
20.14 Key presses: probar teclado
Crea Tests/ExpandTesting/ExpandTestingKeyPressesTests.cs:
using Microsoft.Playwright;
using Tienda.Playwright.Tests.Support;
namespace Tienda.Playwright.Tests.Tests.ExpandTesting;
[TestFixture]
public class ExpandTestingKeyPressesTests : ExpandTestingTestBase
{
[Test]
public async Task KeyPresses_Enter_MuestraLaTeclaPulsada()
{
await OpenPracticeAsync("/key-presses");
await Page.Locator("#target").PressAsync("Enter");
await Expect(Page.Locator("#result")).ToContainTextAsync("ENTER");
}
[Test]
public async Task KeyPresses_Texto_MuestraUltimaLetra()
{
await OpenPracticeAsync("/key-presses");
await Page.Locator("#target").ClickAsync();
await Page.Keyboard.TypeAsync("abc");
await Expect(Page.Locator("#result")).ToContainTextAsync("C");
}
}
Puntos para explicar:
PressAsync("Enter")simula una tecla especial.FillAsync("abc")dispara eventos de entrada y deja el valor escrito.- La pagina muestra la ultima tecla detectada.
20.15 Shadow DOM: localizar dentro de un componente
Crea Tests/ExpandTesting/ExpandTestingShadowDomTests.cs:
using Microsoft.Playwright;
using Tienda.Playwright.Tests.Support;
namespace Tienda.Playwright.Tests.Tests.ExpandTesting;
[TestFixture]
public class ExpandTestingShadowDomTests : ExpandTestingTestBase
{
[Test]
public async Task ShadowDom_BotonInterno_EsVisible()
{
await OpenPracticeAsync("/shadowdom");
ILocator shadowButton = Page
.Locator("#shadow-host")
.GetByRole(AriaRole.Button, new()
{
Name = "This button is inside a Shadow DOM."
});
await Expect(shadowButton).ToBeVisibleAsync();
await shadowButton.ClickAsync();
}
}
Puntos para explicar:
- Shadow DOM encapsula HTML dentro de un componente.
- Playwright puede atravesar shadow roots abiertos con locators.
- XPath no atraviesa Shadow DOM de la misma forma; es mejor usar locators de Playwright.
20.16 Page Object para ExpandTesting Login
Cuando una pantalla se repite, podemos extraer un Page Object.
Crea Pages/ExpandTestingLoginPage.cs:
using Microsoft.Playwright;
namespace Tienda.Playwright.Tests.Pages;
public class ExpandTestingLoginPage
{
private readonly IPage _page;
public ExpandTestingLoginPage(IPage page)
{
_page = page;
}
public async Task LoginAsync(string username, string password)
{
await _page.GetByLabel("Username").FillAsync(username);
await _page.GetByLabel("Password").FillAsync(password);
await _page.GetByRole(AriaRole.Button, new() { Name = "Login" }).ClickAsync();
}
public ILocator Flash()
{
return _page.Locator("#flash");
}
public ILocator Logout()
{
return _page.GetByRole(AriaRole.Link, new() { Name = "Logout" });
}
}
Uso:
[Test]
public async Task Login_ConPageObject()
{
await OpenPracticeAsync("/login");
ExpandTestingLoginPage login = new(Page);
await login.LoginAsync("practice", "SuperSecretPassword!");
await Expect(login.Logout()).ToBeVisibleAsync();
}
Puntos para explicar:
- El Page Object contiene acciones y locators de la pantalla.
- El test conserva la asercion.
- No hace falta crear Page Object para cada ejercicio pequeno; se crea cuando reduce repeticion real.
20.17 Ejecucion de pruebas ExpandTesting
Ejecutar solo las pruebas online:
dotnet test --filter "TestCategory=ExpandTesting"
Ejecutar todo excepto las pruebas online:
dotnet test --filter "TestCategory!=ExpandTesting"
Ejecutar una clase concreta:
dotnet test --filter "FullyQualifiedName~ExpandTestingLoginTests"
Ejecutar en navegador visible:
dotnet test --filter "TestCategory=ExpandTesting" -- Playwright.LaunchOptions.Headless=false
Ejecutar contra Firefox:
dotnet test --filter "TestCategory=ExpandTesting" -- Playwright.BrowserName=firefox
20.18 Problemas habituales con webs externas
| Problema | Causa probable | Solucion para clase |
|---|---|---|
| Timeout al abrir pagina | Red lenta o sitio externo saturado | Repetir, subir timeout o usar la web local |
| Falla solo una prueba online | Cambio en HTML externo | Revisar locator con inspector |
| Falla por anuncios o tracking | Elementos externos interceptan la pagina | Bloquear rutas o limpiar elementos visuales |
| Falla solo en WebKit | Diferencia real entre navegadores | Revisar comportamiento y simplificar locator |
| Descarga no aparece | Link cambiado o nombre distinto | Inspeccionar pagina y usar SuggestedFilename |
| Upload falla por tamano | Archivo supera 500 KB | Generar un archivo pequeno en el test |
20.19 Mini plan docente con ExpandTesting
Ruta recomendada de 2 a 3 horas:
- Abrir manualmente
https://practice.expandtesting.com/. - Explicar que es una web externa de practica.
- Crear
ExpandTestingTestBase. - Ejecutar
Login_CredencialesValidas_EntraEnZonaSegura. - Ejecutar los dos casos negativos de login.
- Practicar inputs y aserciones de salida.
- Practicar add/remove y
ToHaveCountAsync. - Practicar checkboxes, radios y dropdowns.
- Resolver la tabla dinamica leyendo cabeceras.
- Practicar dynamic controls sin
Thread.Sleep. - Practicar dialogos.
- Practicar upload y download.
- Practicar popup, iframe y hover.
- Refactorizar login a Page Object.
- Ejecutar solo la categoria
ExpandTesting. - Comparar estabilidad con las pruebas locales.
21. Guion de explicacion en Visual Studio
- Ensena la solucion
Tienda.Playwright. - Explica que los navegadores se instalan aparte.
- Ejecuta
PrimerTest. - Abre el navegador visible con
Headless=false. - Crea un test de formulario.
- Cambia un label para provocar fallo y mostrar el mensaje.
- Muestra un trace.
- Extrae
ProductosPage. - Prueba API sin navegador.
- Usa API para preparar datos y UI para comprobarlos.
- Prueba select, checkbox y radio.
- Prueba popup y dialogo.
- Crea screenshot.
- Cambia a mobile.
- Ejecuta multi-browser.
- Crea
ExpandTestingTestBase. - Ejecuta una prueba online de login.
- Muestra como filtrar
TestCategory=ExpandTesting. - Practica inputs y tabla dinamica.
- Practica upload, download, iframe y Shadow DOM.
- Compara cuando conviene usar web local y cuando conviene usar web publica.
22. Ejercicios
Ejercicio 1: Login completo
Prueba login valido, password incorrecto y logout.
Ejercicio 2: CRUD de productos
Crea producto desde API, editalo desde UI y borralo desde UI.
Ejercicio 3: Trazas
Activa tracing y guarda trace.zip solo cuando falle una prueba.
Ejercicio 4: Multi-browser
Ejecuta las mismas pruebas en Chromium, Firefox y WebKit.
Ejercicio 5: Movil
Prueba menu responsive y un formulario en viewport movil.
Ejercicio 6: ExpandTesting Login
Crea tres pruebas contra /login: login correcto, usuario incorrecto y password incorrecta. Todas deben estar en la categoria ExpandTesting.
Ejercicio 7: ExpandTesting Inputs
Rellena number, text, password y date. Pulsa Display Inputs y comprueba todos los outputs. Despues pulsa Clear Inputs y comprueba que los campos quedan vacios.
Ejercicio 8: ExpandTesting Dynamic Table
Lee los encabezados de la tabla, localiza la columna CPU, busca la fila Chrome y compara su valor con la etiqueta amarilla.
Ejercicio 9: ExpandTesting Dynamic Controls
Automatiza remove/add del checkbox y enable del input sin usar Thread.Sleep.
Ejercicio 10: ExpandTesting Upload y Download
Genera un archivo temporal pequeno, subelo y comprueba que aparece en la pagina de resultado. Despues descarga some-file.txt y guardalo en artifacts.
Ejercicio 11: ExpandTesting Dialogs
Crea tres pruebas: alert aceptado, confirm cancelado y prompt con texto.
Ejercicio 12: ExpandTesting Iframe y Shadow DOM
Automatiza la suscripcion dentro del iframe interno y localiza el boton que esta dentro del Shadow DOM.
Ejercicio 13: Page Object externo
Refactoriza las pruebas de login de ExpandTesting creando ExpandTestingLoginPage.
Resumen
| Concepto | Sintaxis |
|---|---|
| Test base | PageTest |
| Navegar | await Page.GotoAsync(url) |
| Click | await locator.ClickAsync() |
| Escribir | await locator.FillAsync(texto) |
| Rol accesible | Page.GetByRole(...) |
| Label | Page.GetByLabel(...) |
| Select | locator.SelectOptionAsync(...) |
| Dialogo | Page.Dialog += ... |
| Popup | RunAndWaitForPopupAsync(...) |
| API | Playwright.APIRequest.NewContextAsync(...) |
| Screenshot | Page.ScreenshotAsync(...) |
| Trace | Context.Tracing.StartAsync(...) |
| Instalar navegadores | playwright.ps1 install |
| Categoria online | [Category("ExpandTesting")] |
| Upload | locator.SetInputFilesAsync(path) |
| Download | RunAndWaitForDownloadAsync(...) |
| Iframe | Page.FrameLocator(...) |
| Hover | locator.HoverAsync() |
| Teclado | locator.PressAsync("Enter") |
| Shadow DOM | Locators de Playwright sobre shadow roots abiertos |