Skip to content
SDETRole
SDETRole

Lesson 4 of 12 · Automation Frameworks

Selenium WebDriver Basics

How Selenium 4 drives browsers, Selenium Manager, locators, interactions, explicit waits with WebDriverWait, and the exceptions you will meet, in Java with JUnit 5.

beginner 6 min read Updated

You will learn to

  • Explain the WebDriver architecture and what Selenium Manager does
  • Write a JUnit 5 test that opens a browser, finds elements and interacts with them
  • Synchronise reliably with explicit waits instead of sleeps
  • Diagnose the common Selenium exceptions and their usual causes

Prerequisites

  • Java for Testers
  • Choosing an Automation Tool

Selenium is the longest-established browser automation project and still one of the most used, especially in Java shops. Its WebDriver API became a W3C standard, every major browser vendor ships a driver for it, and a huge amount of existing test code is written with it. Even if your next project uses Playwright, you will meet Selenium in interviews and in legacy suites.

This lesson covers Selenium 4 with Java 21 and JUnit 5. The concepts carry over directly to the Python, C# and JavaScript bindings.

How WebDriver works

Selenium is not a browser and does not run inside one. It is a client library that sends commands over HTTP to a driver process, which controls the real browser:

Your test (Java) --HTTP + JSON (W3C WebDriver)--> chromedriver --> Chrome
                                                  geckodriver  --> Firefox
                                                  msedgedriver --> Edge
                                                  safaridriver --> Safari

Every call such as findElement or click is a separate HTTP round trip. That explains two of Selenium’s characteristics: it can drive any browser that has a driver, and it does not automatically wait for the page to be ready. Synchronisation is your job.

Selenium Grid extends the same protocol over the network: tests send commands to a Grid, which routes them to browsers on other machines. Your test code changes only in how the driver is created (RemoteWebDriver instead of ChromeDriver).

Selenium Manager

Older tutorials tell you to download chromedriver and set webdriver.chrome.driver. Since Selenium 4.6, Selenium Manager does this for you: when you create a driver, it detects the installed browser version, downloads a matching driver into a cache and uses it. It can also download a browser if none is installed. You no longer need WebDriverManager or manual driver management for local runs.

Project setup

<dependencies>
  <dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>selenium-java</artifactId>
    <version>4.27.0</version>
  </dependency>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.11.4</version>
    <scope>test</scope>
  </dependency>
</dependencies>

Use the newest 4.x release from Maven Central; the API shown here is stable across Selenium 4.

A first test

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.time.Duration;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

class LoginTest {
    private WebDriver driver;
    private WebDriverWait wait;

    @BeforeEach
    void setUp() {
        ChromeOptions options = new ChromeOptions();
        if (System.getenv("CI") != null) {
            options.addArguments("--headless=new", "--window-size=1366,768");
        }
        driver = new ChromeDriver(options); // Selenium Manager resolves chromedriver
        wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    }

    @AfterEach
    void tearDown() {
        if (driver != null) driver.quit();
    }

    @Test
    void validUserSeesDashboard() {
        driver.get("https://staging.example.com/login");

        driver.findElement(By.id("email")).sendKeys("ana@example.com");
        driver.findElement(By.id("password")).sendKeys(System.getenv("TEST_PASSWORD"));
        driver.findElement(By.cssSelector("button[type='submit']")).click();

        var heading = wait.until(ExpectedConditions.visibilityOfElementLocated(By.tagName("h1")));
        assertEquals("Dashboard", heading.getText());
        assertEquals("https://staging.example.com/dashboard", driver.getCurrentUrl());
    }
}

Key points:

  • driver.quit() ends the session and closes every window. driver.close() closes only the current window, and leaves the driver process running if it was the last one. Always quit() in teardown.
  • findElement returns the first match or throws NoSuchElementException. findElements returns a possibly empty list and never throws, which is the right tool for asserting that something is absent.

Locators

Selenium’s By class offers eight strategies. In practice you will use a few:

Locator Example When to use
By.id By.id("email") Stable, unique ids: the first choice when available
By.cssSelector By.cssSelector("[data-testid='cart-total']") Most other cases; fast and readable
By.xpath By.xpath("//tr[td[text()='Blue Mug']]//button") Text-based or structural queries CSS cannot express
By.name By.name("q") Form fields with stable name attributes
By.linkText By.linkText("Forgot password?") Links with fixed visible text

Prefer attributes that exist for testing or accessibility (id, name, data-testid, aria-label) over layout-dependent paths like /html/body/div[3]/div[2]/span, which break on any markup change. Test selectors in the browser console with document.querySelector(...) or $x(...), or with the XPath and CSS Selector Tester.

Selenium 4 also added relative locators, which find elements by their visual position relative to others:

import static org.openqa.selenium.support.locators.RelativeLocator.with;

WebElement passwordField = driver.findElement(
    with(By.tagName("input")).below(By.id("email")));

They are handy for poorly marked-up forms but depend on layout, so use them sparingly.

Interactions

// Text and clicks
WebElement search = driver.findElement(By.name("q"));
search.clear();
search.sendKeys("mechanical keyboard", Keys.ENTER);

// Dropdowns built with <select>
new Select(driver.findElement(By.id("country"))).selectByVisibleText("India");

// Hover, drag and drop, keyboard chords
new Actions(driver).moveToElement(driver.findElement(By.id("account-menu"))).perform();

// Alerts, frames and windows
driver.switchTo().alert().accept();
driver.switchTo().frame("payment-frame");
driver.switchTo().defaultContent();
driver.switchTo().newWindow(WindowType.TAB);

// Reading state
String total = driver.findElement(By.cssSelector("[data-testid='cart-total']")).getText();
boolean enabled = driver.findElement(By.id("checkout")).isEnabled();
String typed = driver.findElement(By.id("email")).getDomProperty("value"); // current value

Screenshots for failure evidence:

File png = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
Files.copy(png.toPath(), Path.of("target/screenshots/failure.png"), StandardCopyOption.REPLACE_EXISTING);

Waiting correctly

Because Selenium does not auto-wait, most flaky Selenium tests come from acting before the page is ready. There are three tools, and one to avoid:

  • Thread.sleep: avoid. It is either too short (flaky) or too long (slow), usually both on different machines.
  • Implicit wait (driver.manage().timeouts().implicitlyWait(...)): makes every findElement poll for up to N seconds until the element exists. Simple, but it only waits for presence, not visibility or clickability, and it slows down every check for absence.
  • Explicit wait (WebDriverWait): waits for a specific condition on a specific element. This is the recommended default.
  • Fluent wait: an explicit wait with custom polling interval and ignored exceptions.
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

WebElement button = wait.until(ExpectedConditions.elementToBeClickable(By.id("checkout")));
button.click();

wait.until(ExpectedConditions.textToBe(By.cssSelector("[data-testid='cart-total']"), "$12.00"));
wait.until(ExpectedConditions.invisibilityOfElementLocated(By.className("spinner")));

// Any condition can be a lambda that returns a truthy value
wait.until(d -> d.findElements(By.cssSelector("tbody tr")).size() == 5);

Exceptions you will meet

Exception Usual cause Fix
NoSuchElementException Locator wrong, or element not rendered yet Verify the locator; wait for the element
TimeoutException An explicit wait’s condition never became true Read the condition and screenshot; it is often a real bug
StaleElementReferenceException The DOM re-rendered after you found the element Re-find the element; store By locators, not WebElement fields
ElementClickInterceptedException Another element (overlay, cookie banner, spinner) covers the target Wait for the overlay to disappear; close banners in setup
ElementNotInteractableException Element present but hidden or disabled Wait for visibility or clickability

When a test fails, these names are your first clue. A TimeoutException waiting for a success message is frequently the application failing, not the test.

Practice exercise

Write a JUnit 5 test for this scenario on a shop’s staging site: search for “mouse”, wait for the results list, check that every result title contains “Mouse” (case-insensitive), add the first in-stock result to the cart, and assert the cart badge shows 1. Assume these elements: a search box name="q", results as [data-testid='product-card'] each with an h3 title and an “Add to cart” button (disabled when out of stock), and a cart badge [data-testid='cart-count']. Use only explicit waits. You can practise the locator and wait patterns in the Selenium Lab.

Show a model answer
@Test
void addFirstInStockMouseToCart() {
    driver.get("https://staging.example.com");

    WebElement search = wait.until(ExpectedConditions.elementToBeClickable(By.name("q")));
    search.sendKeys("mouse", Keys.ENTER);

    By cards = By.cssSelector("[data-testid='product-card']");
    List<WebElement> results = wait.until(ExpectedConditions.numberOfElementsToBeMoreThan(cards, 0));

    for (WebElement card : results) {
        String title = card.findElement(By.tagName("h3")).getText();
        assertTrue(title.toLowerCase().contains("mouse"), "Unexpected result: " + title);
    }

    WebElement firstInStock = results.stream()
        .map(card -> card.findElement(By.xpath(".//button[normalize-space()='Add to cart']")))
        .filter(WebElement::isEnabled)
        .findFirst()
        .orElseThrow(() -> new AssertionError("No in-stock mouse found"));
    firstInStock.click();

    wait.until(ExpectedConditions.textToBe(By.cssSelector("[data-testid='cart-count']"), "1"));
}

Points that matter: the wait for results uses a condition rather than a sleep; the XPath starts with .// so it searches inside each card rather than the whole page; the final assertion is itself a wait, because the badge updates asynchronously after the click.

Key takeaways

  • Selenium sends W3C WebDriver commands over HTTP to a browser-specific driver; Selenium Manager now finds or downloads that driver for you.
  • Prefer By.id and CSS selectors on stable, test-friendly attributes; use XPath for text and structure.
  • Selenium does not auto-wait. Use explicit waits (WebDriverWait with Duration), never Thread.sleep, and do not mix implicit and explicit waits.
  • Always quit() the driver in teardown; use findElements to assert absence.
  • Exception names (StaleElementReference, ElementClickIntercepted, Timeout) point straight to the usual causes.

Key terms

Finished this lesson?

Progress is saved in this browser. No account needed.

Type to search across SDETRole.

Full-text search by Pagefind · runs in your browser Open search page →