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.
75
94%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
High
Do not use without reviewing
Selenium WebDriver is a W3C-standard browser automation protocol with the broadest language support: Java, Python, JavaScript, C#, Ruby, Kotlin, PHP - all official.
For new projects in 2026+: pick Playwright or Cypress unless constraints dictate Selenium.
quit() it in teardown so no browser process leaks.By.cssSelector / data-testid locators; keep XPath for relationship queries only.Thread.sleep() with WebDriverWait + ExpectedConditions so the test waits on a condition, not a clock.RemoteWebDriver at Selenium Grid (or a managed grid) to fan out across browsers.target/surefire-reports/) and feed it to junit-xml-analysis.<!-- 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).
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());
}
}| Strategy | When |
|---|---|
By.id | Element has stable id |
By.name | Form field with name attribute |
By.cssSelector | Most cases (preferred over XPath) |
By.xpath | Complex relationship queries (avoid otherwise) |
By.linkText | Anchor by text |
By.partialLinkText | Anchor by partial text |
By.tagName | Generic (e.g., all input elements) |
By.className | Single 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")));// 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.
# 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 envConnect 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.
The Java spine above ports 1:1 to Selenium's other official bindings. Python (pytest) and C# (xUnit) worked examples: references/language-bindings.md.
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.
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.
@Test methods, each
with @BeforeEach creating a fresh ChromeDriver and @AfterEach
calling driver.quit().Thread.sleep(2000) is replaced with
wait.until(ExpectedConditions.elementToBeClickable(...)) so the
step blocks only until the element is ready.By.cssSelector("[data-testid=...]"),
and the hardcoded chromedriver path is dropped for
WebDriverManager.chromedriver().setup().RemoteWebDriver, so Chrome and Firefox run on parallel nodes.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-pattern | Why it fails | Fix |
|---|---|---|
| Single test class with one giant flow | Failure 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 path | Drift; brittle to Chrome updates. | WebDriverManager (Step 1). |
appium-testing (in the qa-mobile plugin).selenium.dev.playwright-testing,
cypress-testing - modern
alternatives.appium-testing - mobile via WebDriver protocol.