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

browserstack-automate

Author and run E2E tests on BrowserStack Automate - cloud grid covering 3000+ real device + browser combinations. Covers BROWSERSTACK_USERNAME + ACCESS_KEY auth, hub URL https://hub-cloud.browserstack.com/wd/hub, W3C capabilities + bstack:options (projectName, buildName, sessionName), BrowserStackLocal for testing against localhost / internal environments, parallel session limits, and CI integration. Use for cross-browser regression on real devices + browsers - distinct from running a single test framework locally, and from a matrix runner limited to the browser engines bundled on the local machine.

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.

lambdatest-automate

Author and run E2E tests on LambdaTest - cloud grid for cross-browser + real-device testing with W3C WebDriver, Cypress, Playwright, and Appium support. Covers LT_USERNAME + LT_ACCESS_KEY auth, hub URL hub.lambdatest.com/wd/hub, W3C capabilities + LT:Options dict (build, name, project, smartUI, network, console, video, tunnel), LambdaTest Tunnel for internal apps. Use for cross-browser regression with LambdaTest as the cloud grid; complements BrowserStack + Sauce Labs.

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, and GitHub Actions CI integration. Use for new test authoring, flakiness remediation, and CI setup; for reviewing codegen output specifically, use a dedicated codegen-review pass.

puppeteer-testing

Authors browser automation scripts using Puppeteer - Chrome / Chromium-only headless / headed automation, Page object via `page.*` API, network interception, PDF generation, screenshot capture, scraping. Distinct from Playwright (Puppeteer's older sibling, Chrome-only) - use Puppeteer for Chrome-only browser automation tasks (scraping, generating PDFs from HTML, screenshot pipelines) where Playwright's multi-browser support is unneeded overhead. Use when a project already depends on `puppeteer` / `puppeteer-core`, or when a Chrome-only script must emit PDFs, screenshots, or scraped data rather than assert on a page.

saucelabs-automate

Author and run E2E tests on Sauce Labs - cloud grid for cross-browser + real-device testing with W3C WebDriver, Cypress, Playwright, and Appium support. Covers SAUCE_USERNAME + SAUCE_ACCESS_KEY auth, regional hub URLs (us-west-1 / us-east-4 / eu-central-1), W3C capabilities, sauce:options dict (build, name, screenResolution, tunnelName), Sauce Connect Proxy for internal-environment testing. Use for cross-browser regression with Sauce Labs as the cloud grid; complements BrowserStack + LambdaTest as alternative providers.

testcafe-testing

Authors TestCafe E2E tests - `npm install testcafe`, fixture/test syntax, `Selector` API for queries, automatic-waits, no WebDriver required (TestCafe injects scripts via a proxy), supports any browser including remote / cloud farms. Use when the team prefers a no-WebDriver architecture and one of TestCafe's specific features (e.g., role-based auth) matters.

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.