Modulo 23: Selenium WebDriver

Objetivos del modulo

  • Entender que es Selenium y cuando usarlo.
  • Crear una solucion E2E con NUnit, Selenium WebDriver y una web local.
  • Automatizar navegacion, formularios, validaciones, selects, checkboxes, radios, pestanas y alertas.
  • Usar esperas explicitas para evitar pruebas fragiles.
  • Aplicar Page Object Model.
  • Preparar datos por API antes de una prueba de navegador.
  • Practicar tambien contra una web publica de entrenamiento, https://demoqa.com/, diferenciando los riesgos de probar una web externa.
  • Ejecutar pruebas desde Visual Studio Community, terminal y modo headless.

1. Iniciacion

Concepto: Selenium WebDriver permite controlar un navegador real desde codigo. Se usa para probar una aplicacion web como la usaria una persona: abrir paginas, hacer clic, escribir datos y comprobar resultados.

Selenium no sustituye a NUnit o xUnit. Normalmente se usa junto a un framework de testing:

Tipo de prueba Que prueba Herramienta tipica
Unitaria Una clase aislada NUnit, xUnit
Integracion Varias piezas juntas NUnit, xUnit
End-to-end Flujo real de usuario Selenium, Playwright

En este modulo construiremos Tienda.Selenium, una solucion con una web local para poder probar todos los ejemplos sin depender de paginas externas.


2. Proyecto que vamos a construir

Tienda.Selenium/
  Tienda.Selenium.sln
  src/
    Tienda.Core/
      Producto.cs
    Tienda.Api/
      Program.cs
  tests/
    Tienda.E2E.Tests/
      Pages/
        LoginPage.cs
      Support/
        LocalApiServer.cs
        SeleniumTestBase.cs
        TestDataApi.cs
        TestSettings.cs
      Tests/
        PrimerTest.cs
        LoginTests.cs
        ProductosTests.cs
        VentanasYAlertasTests.cs

La API local contiene estas paginas:

Ruta Para practicar
/ Navegacion y titulo
/login Formularios y Page Object
/dashboard Esperar cambio de URL
/productos Listados y busqueda
/productos/nuevo Formularios y validacion
/controles Select, checkbox y radio
/ventanas Nueva pestana
/ayuda Pagina abierta en pestana nueva
/alertas Confirmaciones JavaScript
/api/test/productos Preparar datos para pruebas E2E

3. Crear la solucion en Visual Studio Community

  1. Crea una solucion vacia llamada Tienda.Selenium.
  2. Agrega un proyecto Class Library llamado Tienda.Core.
  3. Agrega un proyecto ASP.NET Core Empty llamado Tienda.Api.
  4. Agrega un proyecto NUnit Test Project llamado Tienda.E2E.Tests.
  5. En Tienda.Api, agrega referencia a Tienda.Core.
  6. En Tienda.E2E.Tests, agrega referencias a Tienda.Api y Tienda.Core.
  7. En Tienda.E2E.Tests, instala:
    • Selenium.WebDriver
    • Selenium.Support
  8. Crea las carpetas Pages, Support y Tests.

Por terminal:

dotnet new sln -n Tienda.Selenium -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.E2E.Tests -o tests/Tienda.E2E.Tests --framework net10.0

dotnet sln Tienda.Selenium.sln add src/Tienda.Core/Tienda.Core.csproj src/Tienda.Api/Tienda.Api.csproj tests/Tienda.E2E.Tests/Tienda.E2E.Tests.csproj

dotnet add src/Tienda.Api/Tienda.Api.csproj reference src/Tienda.Core/Tienda.Core.csproj
dotnet add tests/Tienda.E2E.Tests/Tienda.E2E.Tests.csproj reference src/Tienda.Api/Tienda.Api.csproj src/Tienda.Core/Tienda.Core.csproj

dotnet add tests/Tienda.E2E.Tests/Tienda.E2E.Tests.csproj package Selenium.WebDriver
dotnet add tests/Tienda.E2E.Tests/Tienda.E2E.Tests.csproj package Selenium.Support

Selenium 4 incluye Selenium Manager, que localiza o descarga el driver compatible con Chrome en la mayoria de escenarios.


4. Crear la web local de pruebas

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, extrae la creacion de la aplicacion a 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("Inicio", "<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"));

        return app;
    }
}

La idea importante no es memorizar el HTML, sino tener paginas controladas para practicar cada tipo de interaccion.


5. Configuracion de entorno y servidor local

Crea Support/TestSettings.cs:

namespace Tienda.E2E.Tests.Support;

public static class TestSettings
{
    public static string BaseUrl { get; set; } =
        Environment.GetEnvironmentVariable("E2E_BASE_URL")
        ?? "http://127.0.0.1:5123";
}

Crea Support/LocalApiServer.cs para arrancar la web durante las pruebas:

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;
    }
}

Con este enfoque, el alumno no necesita arrancar la API manualmente antes de ejecutar tests.


6. Clase base para Selenium

Crea Support/SeleniumTestBase.cs:

using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Support.UI;

public abstract class SeleniumTestBase
{
    private LocalApiServer? _server;

    protected IWebDriver Driver { get; private set; } = null!;
    protected WebDriverWait Wait { get; private set; } = null!;

    [OneTimeSetUp]
    public async Task OneTimeSetUp()
    {
        if (Environment.GetEnvironmentVariable("E2E_BASE_URL") is null)
        {
            _server = await LocalApiServer.StartAsync();
            TestSettings.BaseUrl = _server.Url;
        }
    }

    [SetUp]
    public void SetUp()
    {
        ChromeOptions options = new();

        if (Environment.GetEnvironmentVariable("CI") == "true")
            options.AddArgument("--headless=new");

        options.AddArgument("--window-size=1920,1080");

        Driver = new ChromeDriver(options);
        Wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10));
    }

    [TearDown]
    public void TearDown()
    {
        Driver.Quit();
        Driver.Dispose();
    }
}

7. Primer test con navegador

Crea Tests/PrimerTest.cs:

[TestFixture]
public class PrimerTest : SeleniumTestBase
{
    [Test]
    public void AbrirPaginaPrincipal_MuestraTitulo()
    {
        Driver.Navigate().GoToUrl(TestSettings.BaseUrl);

        Assert.That(Driver.Title, Does.Contain("Inicio"));
    }
}

Ejecuta solo este test primero. Si falla, revisa Chrome, Selenium Manager y que la API local arranque correctamente.


8. Localizadores

Los localizadores indican como encontrar elementos en la pagina:

IWebElement porId = Driver.FindElement(By.Id("email"));
IWebElement porCss = Driver.FindElement(By.CssSelector("button[type='submit']"));
IWebElement porXPath = Driver.FindElement(By.XPath("//*[contains(text(), 'Teclado')]"));
IWebElement porTag = Driver.FindElement(By.TagName("h1"));
Localizador Uso
By.Id Muy estable si el id es unico
By.CssSelector Flexible y util para data-testid
By.TagName Elementos simples como h1
By.LinkText Enlaces con texto claro
By.XPath Potente, pero puede ser fragil

9. Page Object para login

Crea Pages/LoginPage.cs:

public class LoginPage
{
    private readonly IWebDriver _driver;
    private readonly WebDriverWait _wait;

    public LoginPage(IWebDriver driver)
    {
        _driver = driver;
        _wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
    }

    public void Abrir()
    {
        _driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/login");
    }

    public void IniciarSesion(string email, string password)
    {
        _driver.FindElement(By.Id("email")).SendKeys(email);
        _driver.FindElement(By.Id("password")).SendKeys(password);
        _driver.FindElement(By.CssSelector("button[type='submit']")).Click();
    }

    public bool EstaEnDashboard()
    {
        return _wait.Until(driver => driver.Url.Contains("/dashboard"));
    }
}

Prueba:

[Test]
public void Login_ConCredencialesValidas_EntraEnDashboard()
{
    LoginPage login = new(Driver);

    login.Abrir();
    login.IniciarSesion("ana@ejemplo.com", "Password123!");

    Assert.That(login.EstaEnDashboard(), Is.True);
}

10. Formularios y validaciones

Prueba crear producto:

[Test]
public void CrearProducto_DatosValidos_MuestraProductoEnListado()
{
    Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/productos/nuevo");

    Driver.FindElement(By.Id("nombre")).SendKeys("Teclado mecanico");
    Driver.FindElement(By.Id("categoria")).SendKeys("Perifericos");
    Driver.FindElement(By.Id("precio")).Clear();
    Driver.FindElement(By.Id("precio")).SendKeys("89.99");
    Driver.FindElement(By.Id("stock")).Clear();
    Driver.FindElement(By.Id("stock")).SendKeys("12");
    Driver.FindElement(By.CssSelector("[data-testid='guardar-producto']")).Click();

    Wait.Until(d => d.Url.EndsWith("/productos", StringComparison.OrdinalIgnoreCase));

    string listado = Driver.FindElement(By.CssSelector("main")).Text;

    Assert.That(listado, Does.Contain("Teclado mecanico"));
}

Prueba validacion:

[Test]
public void CrearProducto_SinNombre_MuestraError()
{
    Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/productos/nuevo");

    Driver.FindElement(By.CssSelector("[data-testid='guardar-producto']")).Click();

    IWebElement error = Wait.Until(d => d.FindElement(By.CssSelector(".validation-error")));

    Assert.That(error.Text, Does.Contain("El nombre es obligatorio"));
}

11. Selects, checkboxes y radio buttons

La pagina /controles existe para practicar controles HTML:

[Test]
public void SelectCheckboxYRadio_EjemploDeLocalizadores()
{
    Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/controles");

    SelectElement categoria = new(Driver.FindElement(By.Id("categoria")));
    categoria.SelectByText("Perifericos");

    IWebElement activo = Driver.FindElement(By.Id("activo"));
    if (!activo.Selected)
        activo.Click();

    Driver.FindElement(By.CssSelector("input[name='envio'][value='urgente']")).Click();
    Driver.FindElement(By.CssSelector("[data-testid='guardar-controles']")).Click();

    IWebElement resultado = Wait.Until(d =>
        d.FindElement(By.CssSelector("[data-testid='resultado-controles']")));

    Assert.That(resultado.Text, Is.EqualTo("Controles guardados"));
}

12. Ventanas, pestanas y alertas

Nueva pestana:

[Test]
public void AbrirAyuda_EnNuevaPestana_MuestraTitulo()
{
    Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/ventanas");
    string original = Driver.CurrentWindowHandle;

    Driver.FindElement(By.Id("abrir-ayuda")).Click();

    Wait.Until(d => d.WindowHandles.Count == 2);
    string nueva = Driver.WindowHandles.Single(handle => handle != original);
    Driver.SwitchTo().Window(nueva);

    Assert.That(Driver.Title, Does.Contain("Ayuda"));

    Driver.Close();
    Driver.SwitchTo().Window(original);
}

Alerta:

[Test]
public void Eliminar_ConfirmaAlerta_MuestraResultado()
{
    Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/alertas");

    Driver.FindElement(By.Id("eliminar")).Click();
    IAlert alert = Driver.SwitchTo().Alert();

    Assert.That(alert.Text, Does.Contain("Seguro?"));

    alert.Accept();
}

13. Preparar datos por API

Crea Support/TestDataApi.cs:

public static class TestDataApi
{
    public static async Task CrearProductoAsync(string nombre)
    {
        using HttpClient client = new()
        {
            BaseAddress = new Uri(TestSettings.BaseUrl)
        };

        HttpResponseMessage response = await client.PostAsJsonAsync("/api/test/productos", new
        {
            nombre,
            categoria = "Perifericos",
            precio = 89.99m,
            stock = 10
        });

        response.EnsureSuccessStatusCode();
    }
}

Uso:

[Test]
public async Task BuscarProducto_ProductoExistente_ApareceEnResultados()
{
    await TestDataApi.CrearProductoAsync("Teclado E2E");

    Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/productos");
    Driver.FindElement(By.Id("buscar")).SendKeys("Teclado E2E");

    IWebElement resultado = Wait.Until(d =>
        d.FindElement(By.XPath("//*[contains(text(), 'Teclado E2E')]")));

    Assert.That(resultado.Displayed, Is.True);
}

14. Ejecucion

Desde Visual Studio:

  1. Compila la solucion.
  2. Abre Test > Test Explorer.
  3. Ejecuta primero PrimerTest.
  4. Ejecuta despues todo el proyecto.

Desde terminal:

dotnet test
dotnet test --filter "FullyQualifiedName~Productos"
$env:CI="true"; dotnet test

Contra una web ya arrancada:

$env:E2E_BASE_URL="http://localhost:7001"
dotnet test

15. Plan docente recomendado

Selenium suele necesitar mas tiempo practico que los frameworks unitarios, porque intervienen navegador, HTML, tiempos de carga y datos. Una distribucion para 4 horas:

Tiempo Contenido Resultado esperado
20 min Que es E2E y cuando usarlo El alumno entiende coste y valor
30 min Crear solucion y web local API local arrancando desde tests
30 min Primer ChromeDriver y primer test PrimerTest en verde
35 min Localizadores y formularios Crear producto y validar errores
30 min Esperas explicitas Evitar Thread.Sleep
35 min Page Object de login LoginPage reutilizable
30 min Select, checkbox, radio /controles probado
25 min Pestanas y alertas /ventanas y /alertas probadas
25 min Datos por API y headless Tests repetibles y preparados para CI

16. Ruta de construccion guiada

Etapa 1: test minimo contra la home

Objetivo: comprobar que el navegador abre la web local.

[Test]
public void Home_CargaCorrectamente()
{
    Driver.Navigate().GoToUrl(TestSettings.BaseUrl);

    Assert.That(Driver.Title, Does.Contain("Inicio"));
}

Si falla:

  • Revisa que Chrome este instalado.
  • Revisa que LocalApiServer actualice TestSettings.BaseUrl.
  • Revisa que Driver.Quit() se ejecute siempre en TearDown.

Etapa 2: localizar elementos visibles

[Test]
public void Productos_MuestraBuscador()
{
    Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/productos");

    IWebElement buscador = Driver.FindElement(By.Id("buscar"));

    Assert.Multiple(() =>
    {
        Assert.That(buscador.Displayed, Is.True);
        Assert.That(buscador.GetAttribute("placeholder"), Is.EqualTo("Buscar productos"));
    });
}

Explicacion:

  • Displayed verifica que el elemento se muestra.
  • GetAttribute sirve para leer valores de HTML.
  • No todo se comprueba con .Text.

Etapa 3: formulario con datos validos

Primero se prueba el camino feliz: rellenar y guardar.

Driver.FindElement(By.Id("nombre")).SendKeys("Teclado mecanico");
Driver.FindElement(By.Id("categoria")).SendKeys("Perifericos");
Driver.FindElement(By.Id("precio")).Clear();
Driver.FindElement(By.Id("precio")).SendKeys("89.99");
Driver.FindElement(By.Id("stock")).Clear();
Driver.FindElement(By.Id("stock")).SendKeys("12");
Driver.FindElement(By.CssSelector("[data-testid='guardar-producto']")).Click();

Despues se espera a que la navegacion termine:

Wait.Until(d => d.Url.EndsWith("/productos", StringComparison.OrdinalIgnoreCase));

Etapa 4: validacion

El caso de error es igual de importante:

[Test]
public void CrearProducto_SinNombre_MuestraError()
{
    Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/productos/nuevo");

    Driver.FindElement(By.CssSelector("[data-testid='guardar-producto']")).Click();

    IWebElement error = Wait.Until(d => d.FindElement(By.CssSelector(".validation-error")));

    Assert.That(error.Text, Does.Contain("El nombre es obligatorio"));
}

Etapa 5: extraer Page Object

Cuando un flujo crece, crea una clase de pagina:

public class ProductoFormPage
{
    private readonly IWebDriver _driver;
    private readonly WebDriverWait _wait;

    public ProductoFormPage(IWebDriver driver)
    {
        _driver = driver;
        _wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
    }

    public void Abrir()
    {
        _driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/productos/nuevo");
    }

    public void Rellenar(string nombre, string categoria, string precio, string stock)
    {
        _driver.FindElement(By.Id("nombre")).SendKeys(nombre);
        _driver.FindElement(By.Id("categoria")).SendKeys(categoria);
        _driver.FindElement(By.Id("precio")).Clear();
        _driver.FindElement(By.Id("precio")).SendKeys(precio);
        _driver.FindElement(By.Id("stock")).Clear();
        _driver.FindElement(By.Id("stock")).SendKeys(stock);
    }

    public void Guardar()
    {
        _driver.FindElement(By.CssSelector("[data-testid='guardar-producto']")).Click();
    }

    public void EsperarListado()
    {
        _wait.Until(d => d.Url.EndsWith("/productos", StringComparison.OrdinalIgnoreCase));
    }
}

Uso:

[Test]
public void CrearProducto_ConPageObject_ApareceEnListado()
{
    ProductoFormPage form = new(Driver);

    form.Abrir();
    form.Rellenar("Webcam", "Perifericos", "49.99", "20");
    form.Guardar();
    form.EsperarListado();

    Assert.That(Driver.FindElement(By.CssSelector("main")).Text, Does.Contain("Webcam"));
}

17. Banco de ejemplos Selenium

17.1 Espera explicita reutilizable

Puedes crear helpers para condiciones frecuentes:

protected IWebElement EsperarVisible(By by)
{
    return Wait.Until(driver =>
    {
        IWebElement element = driver.FindElement(by);
        return element.Displayed ? element : null;
    });
}

Uso:

IWebElement titulo = EsperarVisible(By.TagName("h1"));

Assert.That(titulo.Text, Is.EqualTo("Productos"));

17.2 Esperar texto

[Test]
public void GuardarControles_MuestraMensaje()
{
    Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/controles");

    Driver.FindElement(By.CssSelector("[data-testid='guardar-controles']")).Click();

    Wait.Until(d =>
        d.FindElement(By.CssSelector("[data-testid='resultado-controles']")).Text == "Controles guardados");

    Assert.Pass();
}

17.3 Evitar Thread.Sleep

Evita:

Thread.Sleep(3000);

Mejor:

Wait.Until(d => d.FindElement(By.CssSelector(".validation-error")).Displayed);

Explicacion:

  • Thread.Sleep siempre espera el mismo tiempo aunque la pagina este lista.
  • Si la pagina tarda mas, la prueba falla.
  • Si tarda menos, pierdes tiempo.

17.4 Localizadores robustos

Preferible:

Driver.FindElement(By.CssSelector("[data-testid='guardar-producto']")).Click();

Mas fragil:

Driver.FindElement(By.CssSelector("form > button")).Click();

Razon: el primer selector expresa intencion de prueba; el segundo depende de la estructura visual del HTML.

17.5 Comprobar URL

[Test]
public void Login_DespuesDeEntrar_RedireccionaADashboard()
{
    LoginPage login = new(Driver);

    login.Abrir();
    login.IniciarSesion("ana@ejemplo.com", "Password123!");

    Wait.Until(d => d.Url.Contains("/dashboard"));

    Assert.That(Driver.Url, Does.Contain("/dashboard"));
}

17.6 Comprobar atributos

[Test]
public void Login_Email_ConservaValorEscrito()
{
    Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/login");

    IWebElement email = Driver.FindElement(By.Id("email"));
    email.SendKeys("ana@ejemplo.com");

    Assert.That(email.GetAttribute("value"), Is.EqualTo("ana@ejemplo.com"));
}

17.7 Comprobar tabla o listado

Aunque el ejemplo usa article, la idea es la misma para tablas:

[Test]
public async Task Productos_ListadoContieneProductoCreadoPorApi()
{
    await TestDataApi.CrearProductoAsync("Producto listado");

    Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/productos");

    IReadOnlyCollection<IWebElement> productos = Driver.FindElements(By.CssSelector("[data-testid='producto']"));

    Assert.That(productos.Any(p => p.Text.Contains("Producto listado")), Is.True);
}

17.8 Select completo

[Test]
public void Categoria_Select_PermiteElegirPantallas()
{
    Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/controles");

    SelectElement categoria = new(Driver.FindElement(By.Id("categoria")));
    categoria.SelectByValue("pantallas");

    Assert.That(categoria.SelectedOption.Text, Is.EqualTo("Pantallas"));
}

17.9 Checkbox

[Test]
public void Activo_Checkbox_PuedeMarcarseYDesmarcarse()
{
    Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/controles");

    IWebElement activo = Driver.FindElement(By.Id("activo"));

    if (!activo.Selected)
        activo.Click();

    Assert.That(activo.Selected, Is.True);

    activo.Click();

    Assert.That(activo.Selected, Is.False);
}

17.10 Radio button

[Test]
public void Envio_Urgente_PuedeSeleccionarse()
{
    Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/controles");

    IWebElement urgente = Driver.FindElement(By.CssSelector("input[name='envio'][value='urgente']"));
    urgente.Click();

    Assert.That(urgente.Selected, Is.True);
}

17.11 Captura manual

[Test]
public void Productos_GuardaCapturaDelListado()
{
    Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/productos");

    GuardarCaptura("listado-productos");

    Assert.Pass();
}

Helper:

protected void GuardarCaptura(string nombre)
{
    Screenshot screenshot = ((ITakesScreenshot)Driver).GetScreenshot();

    Directory.CreateDirectory("screenshots");
    string path = Path.Combine("screenshots", $"{nombre}-{DateTime.UtcNow:yyyyMMddHHmmss}.png");

    screenshot.SaveAsFile(path);
}

17.12 Ejecutar contra Chrome visible o headless

Visible para clase:

ChromeOptions options = new();
options.AddArgument("--window-size=1920,1080");

Headless para CI:

if (Environment.GetEnvironmentVariable("CI") == "true")
    options.AddArgument("--headless=new");

Comando:

$env:CI="true"
dotnet test

17.13 Errores habituales

Error Causa habitual Solucion
NoSuchElementException El elemento aun no existe Usa WebDriverWait
ElementClickInterceptedException Otro elemento tapa el boton Espera visibilidad o revisa UI
StaleElementReferenceException El DOM se actualizo Busca de nuevo el elemento
Chrome no arranca Driver/navegador incompatible Actualiza Chrome o Selenium
Test pasa solo a veces Esperas incorrectas o datos compartidos Aisla datos y espera condiciones

18. Practica adicional con DemoQA

Ademas de la web local de Tienda.Selenium, puedes usar https://demoqa.com/ como pagina publica de entrenamiento. DemoQA contiene pantallas preparadas para practicar formularios, botones, tablas, alertas, ventanas, frames, selects, carga de archivos e interacciones.

La web local sigue siendo la base principal del modulo porque controlamos el codigo HTML, los datos y el servidor. DemoQA es util para una segunda fase: el alumno ya conoce Selenium y se enfrenta a una pagina externa, con HTML real, componentes React, anuncios, scroll, esperas y pequenos problemas que aparecen en proyectos reales.

Paginas consultadas para esta seccion:

Pagina Uso en el curso
https://demoqa.com/text-box Inputs, textareas, submit y comprobacion de salida
https://demoqa.com/automation-practice-form Formulario largo, radios, checkboxes, autocompletado, carga de archivo y modal
https://demoqa.com/webtables Alta, busqueda, edicion y borrado en tabla
https://demoqa.com/buttons Doble clic, clic derecho y clic normal
https://demoqa.com/alerts Alert, confirm, prompt y alerta con espera
https://demoqa.com/select-menu Select HTML normal y select multiple
https://demoqa.com/frames Cambio a iframe y vuelta al documento principal
https://demoqa.com/dynamic-properties Esperas sobre elementos que cambian con el tiempo

Referencias oficiales de Selenium usadas como apoyo:

Documentacion Uso en los ejemplos
https://www.selenium.dev/documentation/webdriver/waits/ Esperas explicitas con WebDriverWait
https://www.selenium.dev/documentation/webdriver/actions_api/ Interacciones complejas con Actions
https://www.selenium.dev/documentation/webdriver/interactions/alerts/ Manejo de alertas, confirms y prompts
https://www.selenium.dev/documentation/webdriver/support_features/select_lists/ Uso de SelectElement

Importante: las pruebas contra DemoQA dependen de Internet y de una web que no controlamos. Sirven para practicar, pero no deben mezclarse con el pipeline obligatorio de una aplicacion real. En el curso las marcaremos con [Category("DemoQA")].

18.1 Crear una base independiente para DemoQA

No vamos a reutilizar SeleniumTestBase directamente porque esa clase arranca la API local de la tienda. Para DemoQA nos interesa abrir solo Chrome y navegar a una web externa.

Crea Support/DemoQaTestBase.cs:

using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Support.UI;

namespace Tienda.E2E.Tests.Support;

public abstract class DemoQaTestBase
{
    protected const string DemoQaBaseUrl = "https://demoqa.com";

    protected IWebDriver Driver { get; private set; } = null!;
    protected WebDriverWait Wait { get; private set; } = null!;

    [SetUp]
    public void SetUp()
    {
        ChromeOptions options = new();

        if (Environment.GetEnvironmentVariable("CI") == "true")
            options.AddArgument("--headless=new");

        options.AddArgument("--window-size=1920,1080");
        options.AddArgument("--disable-notifications");

        Driver = new ChromeDriver(options);
        Wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(12));
    }

    [TearDown]
    public void TearDown()
    {
        if (TestContext.CurrentContext.Result.Outcome.Status == NUnit.Framework.Interfaces.TestStatus.Failed)
            GuardarCaptura(TestContext.CurrentContext.Test.Name);

        Driver.Quit();
        Driver.Dispose();
    }

    protected void IrADemoQa(string ruta)
    {
        Driver.Navigate().GoToUrl($"{DemoQaBaseUrl}{ruta}");
        CerrarElementosQueTapanLaPagina();
    }

    protected IWebElement BuscarVisible(By locator)
    {
        return Wait.Until(driver =>
        {
            try
            {
                IWebElement element = driver.FindElement(locator);
                return element.Displayed ? element : null;
            }
            catch (NoSuchElementException)
            {
                return null;
            }
            catch (StaleElementReferenceException)
            {
                return null;
            }
        })!;
    }

    protected IWebElement BuscarDisponible(By locator)
    {
        return Wait.Until(driver =>
        {
            try
            {
                IWebElement element = driver.FindElement(locator);
                return element.Displayed && element.Enabled ? element : null;
            }
            catch (NoSuchElementException)
            {
                return null;
            }
            catch (StaleElementReferenceException)
            {
                return null;
            }
        })!;
    }

    protected void Escribir(By locator, string texto)
    {
        IWebElement element = BuscarDisponible(locator);
        ScrollHasta(element);
        element.Clear();
        element.SendKeys(texto);
    }

    protected void ClickSeguro(By locator)
    {
        CerrarElementosQueTapanLaPagina();

        IWebElement element = BuscarDisponible(locator);
        ClickSeguro(element);
    }

    protected void ClickSeguro(IWebElement element)
    {
        CerrarElementosQueTapanLaPagina();

        ScrollHasta(element);
        element.Click();
    }

    protected void ScrollHasta(IWebElement element)
    {
        ((IJavaScriptExecutor)Driver).ExecuteScript(
            "arguments[0].scrollIntoView({ block: 'center', inline: 'nearest' });",
            element);
    }

    protected IAlert EsperarAlerta()
    {
        return Wait.Until(driver =>
        {
            try
            {
                return driver.SwitchTo().Alert();
            }
            catch (NoAlertPresentException)
            {
                return null;
            }
        })!;
    }

    protected void GuardarCaptura(string nombre)
    {
        Screenshot screenshot = ((ITakesScreenshot)Driver).GetScreenshot();

        Directory.CreateDirectory("screenshots");
        string path = Path.Combine("screenshots", $"{nombre}-{DateTime.UtcNow:yyyyMMddHHmmss}.png");

        screenshot.SaveAsFile(path);
    }

    private void CerrarElementosQueTapanLaPagina()
    {
        ((IJavaScriptExecutor)Driver).ExecuteScript(
            "document.querySelectorAll('iframe[id^=\"google_ads\"], iframe[name^=\"google_ads\"], #fixedban, footer').forEach(e => e.remove());");
    }
}

Que aporta esta clase:

Metodo Para que sirve
IrADemoQa Navega a una ruta de DemoQA y limpia elementos fijos que pueden tapar botones
BuscarVisible Espera hasta que un elemento exista y sea visible
BuscarDisponible Espera hasta que exista, sea visible y este habilitado
Escribir Limpia un input y escribe texto
ClickSeguro Hace scroll al elemento antes de hacer clic
EsperarAlerta Espera hasta que el navegador tenga una alerta activa
GuardarCaptura Genera una captura si la prueba falla

Nota docente: quitar anuncios con JavaScript no es una tecnica para probar una aplicacion propia. Aqui se usa porque DemoQA es una web publica de practica y algunos elementos externos pueden interceptar clics durante la clase.

18.2 Text Box: inputs, textareas y resultado

Crea Tests/DemoQa/DemoQaTextBoxTests.cs:

using OpenQA.Selenium;
using Tienda.E2E.Tests.Support;

namespace Tienda.E2E.Tests.Tests.DemoQa;

[TestFixture]
[Category("DemoQA")]
public class DemoQaTextBoxTests : DemoQaTestBase
{
    [Test]
    public void TextBox_EnviaFormulario_MuestraLosDatosIntroducidos()
    {
        IrADemoQa("/text-box");

        Escribir(By.Id("userName"), "Ana Garcia");
        Escribir(By.Id("userEmail"), "ana.garcia@example.com");
        Escribir(By.Id("currentAddress"), "Calle Mayor 1");
        Escribir(By.Id("permanentAddress"), "Calle Luna 2");

        ClickSeguro(By.Id("submit"));

        IWebElement output = BuscarVisible(By.Id("output"));

        Assert.Multiple(() =>
        {
            Assert.That(output.Text, Does.Contain("Ana Garcia"));
            Assert.That(output.Text, Does.Contain("ana.garcia@example.com"));
            Assert.That(output.Text, Does.Contain("Calle Mayor 1"));
            Assert.That(output.Text, Does.Contain("Calle Luna 2"));
        });
    }

    [Test]
    public void TextBox_EmailInvalido_MarcaElInputComoError()
    {
        IrADemoQa("/text-box");

        Escribir(By.Id("userName"), "Ana Garcia");
        Escribir(By.Id("userEmail"), "email-invalido");

        ClickSeguro(By.Id("submit"));

        IWebElement email = BuscarVisible(By.Id("userEmail"));

        Assert.That(email.GetAttribute("class"), Does.Contain("field-error"));
    }
}

Puntos a explicar:

  • By.Id("userName") es un localizador robusto porque el id identifica directamente el input.
  • output.Text permite comprobar el resultado visible que ve el usuario.
  • En una prueba E2E no comprobamos metodos internos, comprobamos comportamiento observable.
  • La prueba de email invalido no busca un mensaje de servidor, busca el cambio visual que aplica la pagina.

18.3 Practice Form: formulario largo, radio, checkbox, autocompletado y upload

Esta pantalla es muy util para practicar formularios reales. Tiene inputs normales, radios ocultos detras de etiquetas, checkboxes, campos con autocompletado, seleccion de fecha, subida de archivo y modal final.

Crea Tests/DemoQa/DemoQaPracticeFormTests.cs:

using OpenQA.Selenium;
using Tienda.E2E.Tests.Support;

namespace Tienda.E2E.Tests.Tests.DemoQa;

[TestFixture]
[Category("DemoQA")]
public class DemoQaPracticeFormTests : DemoQaTestBase
{
    [Test]
    public void PracticeForm_ConDatosValidos_MuestraModalDeConfirmacion()
    {
        IrADemoQa("/automation-practice-form");

        Escribir(By.Id("firstName"), "Laura");
        Escribir(By.Id("lastName"), "Perez");
        Escribir(By.Id("userEmail"), "laura.perez@example.com");

        ClickSeguro(By.CssSelector("label[for='gender-radio-2']"));

        Escribir(By.Id("userNumber"), "612345678");
        SeleccionarFecha("15 Jan 2000");
        SeleccionarSubject("Maths");

        ClickSeguro(By.CssSelector("label[for='hobbies-checkbox-2']"));
        SubirArchivoDePrueba();

        Escribir(By.Id("currentAddress"), "Avenida Central 10");
        SeleccionarEstadoYCiudad("NCR", "Delhi");

        ClickSeguro(By.Id("submit"));

        IWebElement tituloModal = BuscarVisible(By.Id("example-modal-sizes-title"));
        IWebElement tablaModal = BuscarVisible(By.CssSelector(".table-responsive"));

        Assert.Multiple(() =>
        {
            Assert.That(tituloModal.Text, Is.EqualTo("Thanks for submitting the form"));
            Assert.That(tablaModal.Text, Does.Contain("Laura Perez"));
            Assert.That(tablaModal.Text, Does.Contain("laura.perez@example.com"));
            Assert.That(tablaModal.Text, Does.Contain("Female"));
            Assert.That(tablaModal.Text, Does.Contain("612345678"));
            Assert.That(tablaModal.Text, Does.Contain("Maths"));
            Assert.That(tablaModal.Text, Does.Contain("Reading"));
            Assert.That(tablaModal.Text, Does.Contain("NCR Delhi"));
        });
    }

    private void SeleccionarFecha(string fecha)
    {
        IWebElement fechaNacimiento = BuscarDisponible(By.Id("dateOfBirthInput"));
        ScrollHasta(fechaNacimiento);
        fechaNacimiento.SendKeys(Keys.Control + "a");
        fechaNacimiento.SendKeys(fecha);
        fechaNacimiento.SendKeys(Keys.Enter);
    }

    private void SeleccionarSubject(string subject)
    {
        IWebElement subjects = BuscarDisponible(By.Id("subjectsInput"));
        ScrollHasta(subjects);
        subjects.SendKeys(subject);
        subjects.SendKeys(Keys.Enter);
    }

    private void SeleccionarEstadoYCiudad(string estado, string ciudad)
    {
        IWebElement state = BuscarDisponible(By.Id("react-select-3-input"));
        state.SendKeys(estado);
        state.SendKeys(Keys.Enter);

        IWebElement city = BuscarDisponible(By.Id("react-select-4-input"));
        city.SendKeys(ciudad);
        city.SendKeys(Keys.Enter);
    }

    private void SubirArchivoDePrueba()
    {
        string path = Path.Combine(TestContext.CurrentContext.WorkDirectory, "demoqa-upload.txt");
        File.WriteAllText(path, "Archivo temporal creado por Selenium.");

        Driver.FindElement(By.Id("uploadPicture")).SendKeys(path);
    }
}

Puntos a explicar:

  • Los radios y checkboxes de DemoQA tienen inputs ocultos; por eso hacemos clic en el label.
  • SendKeys(Keys.Control + "a") selecciona el texto actual de la fecha para reemplazarlo.
  • El campo subjectsInput es un componente React: se escribe una opcion y se confirma con Enter.
  • Para subir archivos no se abre el explorador de Windows. Selenium envia la ruta del fichero al input type="file".
  • El modal final es el resultado de negocio de esta pantalla: si aparece y contiene los datos, el flujo funciona.

18.4 Web Tables: alta, busqueda, edicion y borrado

La pantalla webtables permite practicar acciones muy comunes en aplicaciones de gestion: crear un registro, buscarlo, editarlo y eliminarlo.

Crea Tests/DemoQa/DemoQaWebTablesTests.cs:

using OpenQA.Selenium;
using Tienda.E2E.Tests.Support;

namespace Tienda.E2E.Tests.Tests.DemoQa;

[TestFixture]
[Category("DemoQA")]
public class DemoQaWebTablesTests : DemoQaTestBase
{
    [Test]
    public void WebTables_CrearBuscarEditarYEliminarRegistro()
    {
        IrADemoQa("/webtables");

        string email = $"laura.{Guid.NewGuid():N}@example.com";

        CrearRegistro("Laura", "Perez", "34", email, "35000", "QA");
        Buscar(email);

        IWebElement fila = BuscarFilaPorEmail(email);

        Assert.That(fila.Text, Does.Contain("Laura"));
        Assert.That(fila.Text, Does.Contain("QA"));

        EditarEdadDeFila(fila, "35");
        Buscar(email);

        IWebElement filaEditada = BuscarFilaPorEmail(email);
        Assert.That(filaEditada.Text, Does.Contain("35"));

        EliminarFila(filaEditada);

        Wait.Until(driver => !driver.FindElement(By.CssSelector(".rt-table")).Text.Contains(email));

        Assert.That(Driver.FindElement(By.CssSelector(".rt-table")).Text, Does.Not.Contain(email));
    }

    private void CrearRegistro(
        string firstName,
        string lastName,
        string age,
        string email,
        string salary,
        string department)
    {
        ClickSeguro(By.Id("addNewRecordButton"));

        Escribir(By.Id("firstName"), firstName);
        Escribir(By.Id("lastName"), lastName);
        Escribir(By.Id("age"), age);
        Escribir(By.Id("userEmail"), email);
        Escribir(By.Id("salary"), salary);
        Escribir(By.Id("department"), department);

        ClickSeguro(By.Id("submit"));
    }

    private void Buscar(string texto)
    {
        Escribir(By.Id("searchBox"), texto);
    }

    private IWebElement BuscarFilaPorEmail(string email)
    {
        return BuscarVisible(By.XPath($"//div[contains(@class,'rt-tr-group')][.//div[text()='{email}']]"));
    }

    private void EditarEdadDeFila(IWebElement fila, string nuevaEdad)
    {
        ClickSeguro(fila.FindElement(By.CssSelector("[title='Edit']")));

        IWebElement age = BuscarDisponible(By.Id("age"));
        age.SendKeys(Keys.Control + "a");
        age.SendKeys(nuevaEdad);

        ClickSeguro(By.Id("submit"));
    }

    private void EliminarFila(IWebElement fila)
    {
        ClickSeguro(fila.FindElement(By.CssSelector("[title='Delete']")));
    }
}

Puntos a explicar:

  • El email usa Guid.NewGuid() para que cada ejecucion tenga un dato unico.
  • La busqueda reduce la tabla a la fila que nos interesa.
  • El XPath busca una fila que contenga una celda con el email exacto.
  • Primero hacemos una afirmacion tras crear, despues otra tras editar y finalmente comprobamos que el email ya no aparece.

18.5 Buttons: doble clic, clic derecho y clic normal

La pantalla buttons sirve para introducir Actions, la API de Selenium para interacciones de usuario mas complejas.

Crea Tests/DemoQa/DemoQaButtonsTests.cs:

using OpenQA.Selenium;
using OpenQA.Selenium.Interactions;
using Tienda.E2E.Tests.Support;

namespace Tienda.E2E.Tests.Tests.DemoQa;

[TestFixture]
[Category("DemoQA")]
public class DemoQaButtonsTests : DemoQaTestBase
{
    [Test]
    public void Buttons_PermiteDobleClickClickDerechoYClickNormal()
    {
        IrADemoQa("/buttons");

        Actions actions = new(Driver);

        IWebElement doubleClickButton = BuscarVisible(By.Id("doubleClickBtn"));
        ScrollHasta(doubleClickButton);
        actions.DoubleClick(doubleClickButton).Perform();

        IWebElement rightClickButton = BuscarVisible(By.Id("rightClickBtn"));
        ScrollHasta(rightClickButton);
        actions.ContextClick(rightClickButton).Perform();

        IWebElement clickMeButton = BuscarVisible(By.XPath("//button[normalize-space()='Click Me']"));
        ClickSeguro(clickMeButton);

        Assert.Multiple(() =>
        {
            Assert.That(BuscarVisible(By.Id("doubleClickMessage")).Text, Does.Contain("double click"));
            Assert.That(BuscarVisible(By.Id("rightClickMessage")).Text, Does.Contain("right click"));
            Assert.That(BuscarVisible(By.Id("dynamicClickMessage")).Text, Does.Contain("dynamic click"));
        });
    }
}

Puntos a explicar:

  • element.Click() cubre el clic normal.
  • Actions.DoubleClick(element).Perform() simula doble clic real.
  • Actions.ContextClick(element).Perform() simula clic derecho.
  • Perform() ejecuta la accion que se ha construido.

18.6 Alerts: alert, confirm, prompt y alerta diferida

Crea Tests/DemoQa/DemoQaAlertsTests.cs:

using OpenQA.Selenium;
using Tienda.E2E.Tests.Support;

namespace Tienda.E2E.Tests.Tests.DemoQa;

[TestFixture]
[Category("DemoQA")]
public class DemoQaAlertsTests : DemoQaTestBase
{
    [Test]
    public void Alerts_AlertNormal_SeAcepta()
    {
        IrADemoQa("/alerts");

        ClickSeguro(By.Id("alertButton"));

        IAlert alert = EsperarAlerta();

        Assert.That(alert.Text, Does.Contain("You clicked a button"));

        alert.Accept();
    }

    [Test]
    public void Alerts_AlertaDiferida_EsperaHastaQueAparece()
    {
        IrADemoQa("/alerts");

        ClickSeguro(By.Id("timerAlertButton"));

        IAlert alert = EsperarAlerta();

        Assert.That(alert.Text, Does.Contain("This alert appeared after 5 seconds"));

        alert.Accept();
    }

    [Test]
    public void Alerts_Confirm_SePuedeCancelar()
    {
        IrADemoQa("/alerts");

        ClickSeguro(By.Id("confirmButton"));

        IAlert alert = EsperarAlerta();
        alert.Dismiss();

        Assert.That(BuscarVisible(By.Id("confirmResult")).Text, Does.Contain("Cancel"));
    }

    [Test]
    public void Alerts_Prompt_EnviaTexto()
    {
        IrADemoQa("/alerts");

        ClickSeguro(By.Id("promtButton"));

        IAlert alert = EsperarAlerta();
        alert.SendKeys("Fran");
        alert.Accept();

        Assert.That(BuscarVisible(By.Id("promptResult")).Text, Does.Contain("Fran"));
    }
}

Puntos a explicar:

  • Cuando hay una alerta abierta, Selenium no puede interactuar con la pagina normal hasta aceptarla o cancelarla.
  • Driver.SwitchTo().Alert() cambia el foco del driver a la alerta.
  • Accept() equivale a aceptar.
  • Dismiss() equivale a cancelar.
  • SendKeys() solo tiene sentido en prompts que aceptan texto.

18.7 Select Menu: selects HTML normales y multiples

DemoQA mezcla selects HTML reales con componentes React. SelectElement solo sirve para etiquetas HTML <select>, no para desplegables dibujados con div.

Crea Tests/DemoQa/DemoQaSelectMenuTests.cs:

using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using Tienda.E2E.Tests.Support;

namespace Tienda.E2E.Tests.Tests.DemoQa;

[TestFixture]
[Category("DemoQA")]
public class DemoQaSelectMenuTests : DemoQaTestBase
{
    [Test]
    public void SelectMenu_OldStyleSelect_PermiteSeleccionPorTexto()
    {
        IrADemoQa("/select-menu");

        SelectElement oldSelect = new(BuscarVisible(By.Id("oldSelectMenu")));
        oldSelect.SelectByText("Blue");

        Assert.That(oldSelect.SelectedOption.Text, Is.EqualTo("Blue"));
    }

    [Test]
    public void SelectMenu_MultipleSelect_PermiteSeleccionarVariasOpciones()
    {
        IrADemoQa("/select-menu");

        SelectElement cars = new(BuscarVisible(By.Id("cars")));

        cars.SelectByText("Volvo");
        cars.SelectByText("Audi");

        List<string> seleccionados = cars.AllSelectedOptions
            .Select(option => option.Text)
            .ToList();

        Assert.Multiple(() =>
        {
            Assert.That(seleccionados, Does.Contain("Volvo"));
            Assert.That(seleccionados, Does.Contain("Audi"));
        });
    }
}

Puntos a explicar:

  • SelectElement esta en Selenium.Support.
  • SelectByText selecciona por el texto visible.
  • SelectedOption devuelve la opcion activa de un select simple.
  • AllSelectedOptions devuelve todas las opciones activas de un select multiple.

18.8 Frames: cambiar al iframe y volver

Crea Tests/DemoQa/DemoQaFramesTests.cs:

using OpenQA.Selenium;
using Tienda.E2E.Tests.Support;

namespace Tienda.E2E.Tests.Tests.DemoQa;

[TestFixture]
[Category("DemoQA")]
public class DemoQaFramesTests : DemoQaTestBase
{
    [Test]
    public void Frames_CambiaAlFrameYLeeContenido()
    {
        IrADemoQa("/frames");

        Driver.SwitchTo().Frame("frame1");

        IWebElement heading = BuscarVisible(By.Id("sampleHeading"));

        Assert.That(heading.Text, Is.EqualTo("This is a sample page"));

        Driver.SwitchTo().DefaultContent();

        Assert.That(BuscarVisible(By.TagName("body")).Text, Does.Contain("Frames"));
    }
}

Puntos a explicar:

  • Un iframe es un documento HTML dentro de otro documento.
  • Selenium no puede encontrar elementos dentro del iframe hasta hacer SwitchTo().Frame(...).
  • Para volver a la pagina principal usamos SwitchTo().DefaultContent().

18.9 Dynamic Properties: esperar cambios en la pagina

Esta pantalla es perfecta para explicar por que no usamos Thread.Sleep. Algunos elementos se habilitan o aparecen tras varios segundos.

Crea Tests/DemoQa/DemoQaDynamicPropertiesTests.cs:

using OpenQA.Selenium;
using Tienda.E2E.Tests.Support;

namespace Tienda.E2E.Tests.Tests.DemoQa;

[TestFixture]
[Category("DemoQA")]
public class DemoQaDynamicPropertiesTests : DemoQaTestBase
{
    [Test]
    public void DynamicProperties_EsperaHastaQueBotonEsteHabilitado()
    {
        IrADemoQa("/dynamic-properties");

        IWebElement boton = Wait.Until(driver =>
        {
            IWebElement element = driver.FindElement(By.Id("enableAfter"));
            return element.Enabled ? element : null;
        })!;

        ClickSeguro(boton);

        Assert.That(boton.Enabled, Is.True);
    }

    [Test]
    public void DynamicProperties_EsperaHastaQueElementoSeaVisible()
    {
        IrADemoQa("/dynamic-properties");

        IWebElement visibleAfter = BuscarVisible(By.Id("visibleAfter"));

        Assert.That(visibleAfter.Displayed, Is.True);
    }
}

Puntos a explicar:

  • WebDriverWait ejecuta la condicion varias veces hasta que se cumple o se agota el tiempo.
  • La condicion debe devolver el elemento cuando esta listo.
  • Si devuelve null, Selenium sigue esperando.
  • Esta tecnica hace las pruebas mas estables que Thread.Sleep.

18.10 Page Object aplicado a DemoQA

Una vez que los alumnos han escrito los tests directos, se puede refactorizar una pantalla a Page Object.

Crea Pages/DemoQaTextBoxPage.cs:

using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;

namespace Tienda.E2E.Tests.Pages;

public class DemoQaTextBoxPage
{
    private readonly IWebDriver _driver;
    private readonly WebDriverWait _wait;

    public DemoQaTextBoxPage(IWebDriver driver)
    {
        _driver = driver;
        _wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
    }

    public void Completar(
        string nombre,
        string email,
        string direccionActual,
        string direccionPermanente)
    {
        Escribir(By.Id("userName"), nombre);
        Escribir(By.Id("userEmail"), email);
        Escribir(By.Id("currentAddress"), direccionActual);
        Escribir(By.Id("permanentAddress"), direccionPermanente);
    }

    public void Enviar()
    {
        IWebElement submit = BuscarVisible(By.Id("submit"));
        ((IJavaScriptExecutor)_driver).ExecuteScript(
            "arguments[0].scrollIntoView({ block: 'center' });",
            submit);
        submit.Click();
    }

    public string Resultado()
    {
        return BuscarVisible(By.Id("output")).Text;
    }

    private void Escribir(By locator, string texto)
    {
        IWebElement element = BuscarVisible(locator);
        element.Clear();
        element.SendKeys(texto);
    }

    private IWebElement BuscarVisible(By locator)
    {
        return _wait.Until(driver =>
        {
            IWebElement element = driver.FindElement(locator);
            return element.Displayed ? element : null;
        })!;
    }
}

Uso del Page Object en una prueba:

using Tienda.E2E.Tests.Pages;
using Tienda.E2E.Tests.Support;

namespace Tienda.E2E.Tests.Tests.DemoQa;

[TestFixture]
[Category("DemoQA")]
public class DemoQaTextBoxPageObjectTests : DemoQaTestBase
{
    [Test]
    public void TextBox_ConPageObject_MuestraDatos()
    {
        IrADemoQa("/text-box");

        DemoQaTextBoxPage page = new(Driver);

        page.Completar(
            "Carlos Ruiz",
            "carlos.ruiz@example.com",
            "Calle Norte 3",
            "Calle Sur 4");

        page.Enviar();

        Assert.That(page.Resultado(), Does.Contain("Carlos Ruiz"));
    }
}

Puntos a explicar:

  • El test queda mas corto y expresa la intencion de usuario.
  • Los selectores quedan dentro del Page Object.
  • Si cambia el HTML de la pagina, normalmente se modifica el Page Object y no todas las pruebas.
  • No hace falta crear Page Object desde el primer minuto; primero conviene entender el flujo directo.

18.11 Ejecucion de pruebas DemoQA

Para ejecutar solo las pruebas de DemoQA:

dotnet test --filter "TestCategory=DemoQA"

Para ejecutar todo excepto DemoQA:

dotnet test --filter "TestCategory!=DemoQA"

En Visual Studio Community:

  1. Abre Test Explorer.
  2. Compila la solucion.
  3. Filtra por categoria DemoQA.
  4. Ejecuta primero una prueba visible, sin modo headless.
  5. Si todo funciona, prueba con:
$env:CI="true"
dotnet test --filter "TestCategory=DemoQA"

18.12 Problemas habituales con DemoQA

Problema Causa probable Solucion didactica
ElementClickInterceptedException Un anuncio o footer tapa el elemento Hacer scroll, esperar visibilidad o usar el helper de limpieza
NoSuchElementException La pagina aun no ha pintado el componente React Usar WebDriverWait
El radio no se selecciona El input real esta oculto Hacer clic en el label[for='...']
El subject no se selecciona Es un autocomplete React Escribir el texto y confirmar con Keys.Enter
El test falla solo en CI Tamano de ventana o tiempos distintos Fijar --window-size y aumentar la espera
El sitio externo esta caido DemoQA no depende de nuestro proyecto Repetir mas tarde o continuar con la web local

18.13 Mini plan docente con DemoQA

Una ruta de 90 a 120 minutos:

  1. Abrir manualmente https://demoqa.com/text-box y localizar ids desde DevTools.
  2. Crear DemoQaTestBase.
  3. Automatizar Text Box sin Page Object.
  4. Provocar un fallo cambiando un id y revisar la captura.
  5. Automatizar Practice Form paso a paso.
  6. Explicar por que los radios se pulsan por label.
  7. Automatizar Buttons para introducir Actions.
  8. Automatizar Alerts para practicar SwitchTo().Alert().
  9. Automatizar Web Tables para practicar datos dinamicos.
  10. Refactorizar Text Box a Page Object.
  11. Ejecutar solo la categoria DemoQA.
  12. Comparar con la web local de la tienda: control, estabilidad y mantenimiento.

19. Guion de explicacion en Visual Studio

  1. Ensena la diferencia entre test unitario y E2E.
  2. Crea la solucion con API local.
  3. Abre la API en navegador para ver las paginas.
  4. Crea SeleniumTestBase.
  5. Ejecuta PrimerTest con Chrome visible.
  6. Rellena formulario de producto.
  7. Rompe un selector para provocar NoSuchElementException.
  8. Sustituye busqueda directa por WebDriverWait.
  9. Extrae LoginPage.
  10. Prueba /controles.
  11. Prueba pestanas y alertas.
  12. Prepara datos por API.
  13. Ejecuta en headless.
  14. Revisa capturas en fallo.
  15. Crea DemoQaTestBase.
  16. Ejecuta una prueba contra https://demoqa.com/text-box.
  17. Explica la diferencia entre probar una web propia y una web publica.
  18. Ejecuta la categoria DemoQA.

20. Ejercicios

Ejercicio 1: Login

Automatiza login correcto, password incorrecto y usuario vacio.

Ejercicio 2: CRUD de productos

Crea, busca, edita y elimina un producto desde el navegador.

Ejercicio 3: Page Objects

Refactoriza productos usando ProductosPage y ProductoFormPage.

Ejercicio 4: Controles avanzados

Agrega mas opciones a /controles y prueba select multiple, checkbox desmarcado y radio normal.

Ejercicio 5: Capturas

Guarda capturas automaticamente cuando falle una prueba.

Ejercicio 6: DemoQA Text Box

Crea una prueba contra https://demoqa.com/text-box que rellene todos los campos y compruebe que el bloque de salida contiene los datos.

Ejercicio 7: DemoQA Practice Form

Automatiza el formulario de estudiantes con datos validos. Debe seleccionar genero, hobby, subject, estado, ciudad y subir un archivo temporal.

Ejercicio 8: DemoQA Web Tables

Crea un registro, buscalo por email, editalo, valida el cambio y eliminalo.

Ejercicio 9: DemoQA Alerts

Crea cuatro pruebas: alert normal, alert diferida, confirm cancelado y prompt con texto.

Ejercicio 10: DemoQA Page Object

Refactoriza DemoQaPracticeFormTests creando un Page Object para la pantalla completa.


Resumen

Concepto Sintaxis
Driver Chrome new ChromeDriver(options)
Navegar Driver.Navigate().GoToUrl(url)
Buscar por id Driver.FindElement(By.Id("id"))
Buscar por CSS Driver.FindElement(By.CssSelector("..."))
Escribir element.SendKeys("texto")
Click element.Click()
Espera explicita new WebDriverWait(driver, timeout)
Select new SelectElement(element)
Alerta Driver.SwitchTo().Alert()
Pestana Driver.SwitchTo().Window(handle)
Headless options.AddArgument("--headless=new")