Testland
Browse all skills & agents

selenium-testing

Authors Selenium WebDriver tests in any of its 6+ supported languages (Java, Python, JavaScript, C#, Ruby, Kotlin, PHP) - picks the appropriate language binding, configures WebDriver per browser, uses `By.*` locators with the team's accessibility-first preference where supported, runs locally + via Selenium Grid for distributed execution, parses results to JUnit XML. Use for legacy Selenium-locked stacks; new projects pick Playwright or Cypress.

Install with skills.sh (any agent)

npx skills add testland/qa --skill selenium-testing
View source

selenium-testing

Overview

Selenium WebDriver is a W3C-standard browser automation protocol with the broadest language support: Java, Python, JavaScript, C#, Ruby, Kotlin, PHP - all official.

When to use

  • The team has substantial existing Selenium investment; migration cost is prohibitive.
  • The codebase is in a language with limited modern E2E support (e.g., older C# / Ruby / PHP projects).
  • W3C standard adherence is contractually required.
  • Cross-browser including IE11 is required (rare; deprecated).

For new projects in 2026+: pick Playwright or Cypress unless constraints dictate Selenium.

How to use

  1. Pick the language binding that matches the stack (Java, Python, C#, Ruby, Kotlin, PHP) and add the Selenium dependency plus WebDriverManager.
  2. Instantiate a driver in setup and quit() it in teardown so no browser process leaks.
  3. Prefer stable By.cssSelector / data-testid locators; keep XPath for relationship queries only.
  4. Replace every Thread.sleep() with WebDriverWait + ExpectedConditions so the test waits on a condition, not a clock.
  5. Author one test per user flow with fresh per-test setup rather than one giant flow.
  6. For breadth, point a RemoteWebDriver at Selenium Grid (or a managed grid) to fan out across browsers.
  7. Emit JUnit XML in CI (target/surefire-reports/) and feed it to junit-xml-analysis.

Step 1 - Install (Java + JUnit example)

<!-- pom.xml -->
<dependency>
  <groupId>org.seleniumhq.selenium</groupId>
  <artifactId>selenium-java</artifactId>
  <version>4.27.0</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>io.github.bonigarcia</groupId>
  <artifactId>webdrivermanager</artifactId>
  <version>5.9.2</version>
  <scope>test</scope>
</dependency>

WebDriverManager handles browser-driver download (saves the "download chromedriver.exe" step).

Step 2 - Author a test (Java)

import org.junit.jupiter.api.*;
import org.openqa.selenium.*;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.*;
import io.github.bonigarcia.wdm.WebDriverManager;

import java.time.Duration;

class CheckoutTest {

    private WebDriver driver;
    private WebDriverWait wait;

    @BeforeAll
    static void setupClass() {
        WebDriverManager.chromedriver().setup();
    }

    @BeforeEach
    void setup() {
        driver = new ChromeDriver();
        wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    }

    @AfterEach
    void teardown() {
        driver.quit();
    }

    @Test
    void completeCheckout() {
        driver.get("http://localhost:3000/login");

        driver.findElement(By.cssSelector("[data-testid=email]"))
              .sendKeys("user@example.com");
        driver.findElement(By.cssSelector("[data-testid=password]"))
              .sendKeys("test-password");
        driver.findElement(By.cssSelector("button[type=submit]")).click();

        wait.until(ExpectedConditions.visibilityOfElementLocated(
            By.xpath("//h1[contains(text(), 'Welcome')]")));

        driver.get("http://localhost:3000/products/BOOK-001");
        driver.findElement(By.cssSelector("[data-testid=add-to-cart]")).click();

        WebElement cartCount = driver.findElement(By.cssSelector("[data-testid=cart-count]"));
        Assertions.assertEquals("1", cartCount.getText());
    }
}

Step 3 - Locator strategies

StrategyWhen
By.idElement has stable id
By.nameForm field with name attribute
By.cssSelectorMost cases (preferred over XPath)
By.xpathComplex relationship queries (avoid otherwise)
By.linkTextAnchor by text
By.partialLinkTextAnchor by partial text
By.tagNameGeneric (e.g., all input elements)
By.classNameSingle CSS class (brittle)

Prefer data-testid selectors; avoid XPath / classes.

For accessibility-first equivalents (Selenium doesn't ship getByRole natively), evaluate selenium-axe-core for a11y testing and consider Selenium 4's relative locators:

// Selenium 4 relative locators
import static org.openqa.selenium.support.locators.RelativeLocator.*;

driver.findElement(with(By.tagName("button")).near(By.id("password-field")));

Step 4 - Explicit waits

// WebDriverWait with ExpectedConditions
WebElement element = wait.until(
    ExpectedConditions.elementToBeClickable(By.cssSelector("button.submit"))
);
element.click();

Never use Thread.sleep() - WebDriverWait polls until the condition is met. Sleep is the most common Selenium flake source.

Step 5 - Selenium Grid for distributed execution

# docker-compose.grid.yml
services:
  selenium-hub:
    image: selenium/hub:4.27.0
    ports: ["4444:4444"]

  chrome-node:
    image: selenium/node-chrome:4.27.0
    depends_on: [selenium-hub]
    environment:
      SE_EVENT_BUS_HOST: selenium-hub
      SE_EVENT_BUS_PUBLISH_PORT: 4442
      SE_EVENT_BUS_SUBSCRIBE_PORT: 4443

  firefox-node:
    image: selenium/node-firefox:4.27.0
    # ... same env

Connect from tests:

WebDriver driver = new RemoteWebDriver(
    new URL("http://localhost:4444"),
    new ChromeOptions()
);

Grid distributes tests across nodes - handles parallelism. For managed grids, see commercial: BrowserStack, Sauce Labs, LambdaTest.

Verify: curl http://localhost:4444/status reports "ready": true before pointing RemoteWebDriver at the grid; if not, the hub or nodes haven't registered - check docker compose -f docker-compose.grid.yml ps and the node container logs.

Step 6 - Other language bindings

The Java spine above ports 1:1 to Selenium's other official bindings. Python (pytest) and C# (xUnit) worked examples: references/language-bindings.md.

Step 7 - CI integration

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: '21' }
      - run: mvn test
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: surefire-reports
          path: target/surefire-reports/

JUnit XML lands at target/surefire-reports/; feeds junit-xml-analysis (in the qa-test-reporting plugin).

Verify: confirm target/surefire-reports/*.xml exists after mvn test before handing off to junit-xml-analysis; if the directory is empty the run emitted no reports - check that tests compiled and actually ran rather than being skipped.

Worked example

A legacy Java suite has one CheckoutTest method that drives login, add-to-cart, and checkout in a single flow, synchronizing with Thread.sleep(2000) between steps and never calling driver.quit(). Browser processes leak on CI and the test flakes when the runner is slow.

  1. The single method is split into per-behavior @Test methods, each with @BeforeEach creating a fresh ChromeDriver and @AfterEach calling driver.quit().
  2. Every Thread.sleep(2000) is replaced with wait.until(ExpectedConditions.elementToBeClickable(...)) so the step blocks only until the element is ready.
  3. Class-name selectors move to By.cssSelector("[data-testid=...]"), and the hardcoded chromedriver path is dropped for WebDriverManager.chromedriver().setup().
  4. A Dockerized Selenium Grid is added and tests connect via RemoteWebDriver, so Chrome and Firefox run on parallel nodes.
  5. mvn test writes JUnit XML to target/surefire-reports/, which junit-xml-analysis ingests for the flake trend.

Result: browsers no longer leak (every test quits its driver), the fixed sleeps are gone, and the flow runs across two browsers on the grid.

Anti-patterns

Anti-patternWhy it failsFix
Single test class with one giant flowFailure mid-test obscures cause.Per-flow tests with @BeforeEach setup.
Skipping driver.quit()Browser instance leaks; CI runner OOM.Always quit() in @AfterEach (Step 2).
Hardcoded ChromeDriver pathDrift; brittle to Chrome updates.WebDriverManager (Step 1).

Limitations

  • Slower than Playwright / Cypress. WebDriver protocol overhead; per-action HTTP round-trips.
  • Async / Promise handling weaker. Per-language; some frameworks better than others.
  • No native mobile. Mobile via Appium (uses Selenium WebDriver protocol underneath) per appium-testing (in the qa-mobile plugin).
  • Per-language idioms vary. A Python pytest test looks different from a Java JUnit test.

References

  • Selenium project at selenium.dev.
  • W3C WebDriver spec.
  • playwright-testing, cypress-testing - modern alternatives.
  • appium-testing - mobile via WebDriver protocol.

Selenium language bindings (Python, C#)

View source (opens in new window)

Selenium language bindings (Python, C#)

The Java + JUnit binding is the worked spine in selenium-testing. Selenium ships official bindings for Python, JavaScript, C#, Ruby, Kotlin, and PHP; the WebDriver calls map 1:1 across them. Two of the most common non-Java bindings are shown below.

Python (pytest)

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

def test_checkout():
    driver = webdriver.Chrome()
    driver.get('http://localhost:3000/login')

    driver.find_element(By.CSS_SELECTOR, '[data-testid=email]').send_keys('user@example.com')
    driver.find_element(By.CSS_SELECTOR, '[data-testid=password]').send_keys('pwd')
    driver.find_element(By.CSS_SELECTOR, 'button[type=submit]').click()

    WebDriverWait(driver, 10).until(
        EC.visibility_of_element_located((By.XPATH, "//h1[contains(., 'Welcome')]"))
    )

    driver.quit()

C# (xUnit)

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

public class CheckoutTest : IDisposable
{
    private readonly IWebDriver driver = new ChromeDriver();
    private readonly WebDriverWait wait;

    public CheckoutTest()
    {
        wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
    }

    [Fact]
    public void CompleteCheckout()
    {
        driver.Navigate().GoToUrl("http://localhost:3000/login");
        driver.FindElement(By.CssSelector("[data-testid=email]")).SendKeys("user@example.com");
        // ...
    }

    public void Dispose() => driver.Quit();
}

Related skills

browser-matrix-strategy-reference

Pure-reference for designing and reviewing a browser / OS / device test matrix from traffic data - the T1/T2/T3 tier-membership heuristics (T1 >=5% traffic, T2 1-5% or statutory, T3 <1% with customer demand), the traffic-share sources (own analytics, StatCounter, MDN browser-compat-data), a worked matrix template with tier-change log, the matrix review checklist (staleness, T1 oversize, below-threshold T1 entries, missing real-device coverage), how to justify dropping a legacy browser (IE11, old iOS Safari), and the compatibility budget (tier caps, CI cost formula, published support statement) in references/compatibility-budget.md. Use when designing an initial matrix, capping or publishing a support policy, running a quarterly re-tier review, or making the case to drop a browser. This is the WHAT-to-test strategy reference - to execute the matrix use playwright-testing browser projects (bundled engines), selenium-grid-4-runner (self-hosted), or cloud-grid-e2e (managed grids).

cloud-grid-e2e

Author and run E2E tests on a cloud browser grid - BrowserStack Automate, Sauce Labs, or LambdaTest. All three follow one pattern: username + access-key env vars, a W3C WebDriver hub URL, a vendor options dict inside the capabilities (bstack:options / sauce:options / LT:Options), a local tunnel binary for internal apps, session pass/fail reporting, and a CI matrix throttled to the plan's parallel-session limit. Worked example uses BrowserStack; per-vendor deltas live in references/. Use for cross-browser regression on real devices + browsers beyond the engines bundled on the local machine - distinct from a local matrix runner and from self-hosted Selenium Grid.

cypress-testing

Authors and improves Cypress E2E tests - installs Cypress, configures `cypress.config.ts`, authors `cy.*` command chains, refactors existing specs (`cy.wait(ms)` sleeps into assertions, repeated flows into `cy.session` custom commands), and debugs with the time-travel GUI; Cypress Cloud for parallel runs and recording. Use for both greenfield test authoring and improving hand-written specs already in the codebase. For automated refactor of raw Cypress Studio recordings specifically, use a dedicated codegen-review pass.

playwright-testing

Authors and remediates Playwright E2E tests across Chromium, Firefox, WebKit - `npm init playwright@latest` scaffolding, `playwright.config.ts` browser projects, accessibility-first locators (`getByRole`/`getByLabelText`) to replace brittle CSS selectors, web-first assertions to eliminate `waitForTimeout` flakiness, Page Object pattern, trace viewer debugging, sharded parallel execution with merged HTML reporting, mobile-web emulation via the `devices` catalog (viewport / DPR / touch per-device projects), the cross-browser matrix with branded channels (chrome / msedge) in references/browser-matrix.md, and GitHub Actions CI integration. Use for new test authoring, flakiness remediation, mobile-breakpoint regression, cross-browser matrix setup, and CI setup; for reviewing codegen output specifically, use a dedicated codegen-review pass.

selenium-grid-4-runner

Author and operate Selenium Grid 4 - self-hosted distributed WebDriver. Covers the six-component architecture (Router / Distributor / Session Map / Event Bus / New Session Queue / Node), standalone vs hub-and-node modes, the Docker-image stack (selenium/standalone-chrome, selenium/hub, selenium/node-chrome), node registration, session-queue tuning, and observability. Use for self-hosted cross-browser testing when data residency or cost-control require an on-prem grid. This is the self-hosted execution RUNNER - for the zero-infra alternative use playwright-testing browser projects (bundled engines); for managed cloud grids use cloud-grid-e2e (BrowserStack / Sauce Labs / LambdaTest); to decide WHICH browsers and tiers to run use browser-matrix-strategy-reference.

web-e2e-overview

Teaches web end-to-end testing from first principles: what browser-driven E2E covers and how it differs from unit and integration tests, a decision table for choosing between Playwright, Cypress, Selenium WebDriver, WebdriverIO, Puppeteer, TestCafe and the BrowserStack / Sauce Labs / LambdaTest cloud grids based on files already present in the repo, install and first-run commands for each, and the flakiness traps (fixed sleeps, CSS and XPath selectors, state shared between tests) that sink new suites. Use when a web application has no E2E coverage yet, when picking or replacing an E2E framework, or when a first browser test needs to go green end to end.

webdriverio-testing

Authors WebdriverIO E2E tests - `npm init wdio@latest` scaffolding, services architecture (sauce, browserstack, appium, devtools), reporters (spec, allure, junit), built-in Mocha/Jasmine/Cucumber framework integrations. WebdriverIO sits between Selenium (W3C protocol) and Playwright (modern API) - Selenium-protocol-compatible with rich plugin ecosystem. Use when the team needs WebDriver protocol + service-based device-farm integration.