Skip to content
intermediate Phase 6 · Automation Foundations

Selenium WebDriver Basics

Drive browsers with WebDriver sessions, element interactions, and clean teardown.

1h 15m
0 problems
Topic Progress 0%

Opening a Session: Driver Startup, Navigation, and the First Locator

Selenium's mental model is smaller than Playwright's: a session is a conversation with the browser over the WebDriver protocol, and every line you write is a message in that conversation. Understanding the session lifecycle is what separates a suite whose browsers close cleanly from a CI machine with seventeen orphaned Chrome processes.

Starting a session. Modern Selenium (4.x) ships Selenium Manager, a built-in driver manager that resolves and downloads the matching ChromeDriver/GeckoDriver automatically — the era of manually downloading chromedriver.exe and matching versions is over. Java and Python look like this:

// Java
WebDriver driver = new ChromeDriver();          // Selenium Manager resolves the driver
driver.get("https://staging.example.com/login"); // navigate (blocks until page load)
# Python
from selenium import webdriver
from selenium.webdriver.common.by import By

driver = webdriver.Chrome()          # Selenium Manager resolves chromedriver
driver.get("https://staging.example.com/login")
driver.set_page_load_timeout(30)     # set timeouts early — defaults can hang forever

Each new ChromeDriver() (or webdriver.Chrome()) opens a TCP connection to a driver process that launches the browser and negotiates a session id. Options configure the browser before the session: headless mode (--headless=new), window size, download directories, and capability settings for grids and cloud providers.

ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new", "--window-size=1920,1080");
WebDriver driver = new ChromeDriver(options);

Navigation is four commands, not one. get(url) waits for the page to load; navigate().back(), navigate().forward(), and navigate().refresh() cover history; driver.getCurrentUrl() and driver.getTitle() are your first assertions' raw material. On a single-page app, get() returns after the initial document loads — React may not have rendered yet, which is exactly the synchronization gap you will handle with waits in the next topic.

Locating elements. The findElement(By...) family returns the first match; findElements(...) returns a list (empty, never null, when nothing matches). The core locators:

driver.findElement(By.id("username"));
driver.findElement(By.name("password"));
driver.findElement(By.cssSelector("form.login input[type=email]"));
driver.findElement(By.xpath("//form[@class='login']//input"));
driver.findElement(By.linkText("Forgot password?"));
driver.findElement(By.partialLinkText("Forgot"));

findElement throws NoSuchElementException when nothing matches — an exception you will catch when locating intentionally (waiting for an element to appear) and prevent with stable locators everywhere else. Element references are live: a WebElement located before a re-render may throw StaleElementReferenceException afterwards, meaning the DOM node it pointed at was replaced. The standard response is re-locating, which is precisely why locating should live in one layer of your code rather than being sprinkled through assertions — the seed of page objects in Page Object Basics.

Before going further, install the ecosystem companion every Selenium suite eventually needs:

pip install selenium webdriver-manager   # Python; Java: add selenium + testng/junit deps

On Java, webdriver-manager is optional now because Selenium Manager is built in, but the pattern of declaring dependencies in Maven/Gradle with pinned versions is not optional — a suite that works because "someone's IDE has an old jar" is a suite that fails on every other machine. Pinning also protects you from surprise: a major Selenium version bump can change locator helpers or deprecate APIs, and discovering that during a release-week run is exactly how trust in the suite erodes. Finally, note what the session gives you for free: the driver prints the browser's console logs only when you ask for them (LoggingPreferences in Java, goog:loggingPrefs capability), and capturing the browser log category in every test costs nothing and turns console errors during a failure from hearsay into evidence — a habit worth forming before you have a hundred tests that cannot explain themselves.

Clicks, Inputs, Dropdowns, and Alerts: The Core Interaction Commands

Once a session and a located element exist, interaction is a small, closed vocabulary. Learning it precisely — including the two cases that trip everyone up (dropdowns and alerts) — covers most of what day-to-day Selenium scripting asks for.

Text input and clicks. element.clear() then element.sendKeys("text") is the safe pair for inputs: sendKeys appends to existing value, so a field pre-filled with a placeholder value silently corrupts your test data unless you clear first. Keys are typed, not set — a focused sendKeys on a contenteditable div behaves differently from an <input>. Clicks are element.click(), which is not guaranteed to succeed: if the element is covered by a sticky header or cookie banner, Selenium throws ElementClickInterceptedException, and the fix is scrolling or dismissing the overlay — not retrying blindly.

WebElement email = driver.findElement(By.id("email"));
email.clear();
email.sendKeys("qa+run42@example.com");
driver.findElement(By.id("submit")).click();

Modifier keys live in Actions: new Actions(driver).keyDown(CONTROL).click(a).click(b).keyUp(CONTROL).perform() gives multi-select; dragAndDrop(source, target), contextClick (right-click), and doubleClick come from the same builder. Actions also provides hover, which matters because Selenium has no auto-hover — moving to an element to reveal a dropdown menu is actions.moveToElement(menu).perform().

Dropdowns: the Select special case. Native <select> elements get first-class treatment from org.openqa.selenium.support.ui.Select:

Select role = new Select(driver.findElement(By.id("role")));
role.selectByVisibleText("Moderator");   // most readable — matches what the user sees
role.selectByValue("mod");               // matches the <option value>
role.selectByIndex(1);                    // position — brittle, last resort
role.getOptions().size();                 // count of options
role.getFirstSelectedOption().getText();  // what is actually selected

The trap: custom dropdowns (React Select, chosen.js, most modern UI kits) are not <select> elements, and Select throws UnexpectedTagNameException on them. You must click the control to open the list, then locate the option — usually an <li> or <div role='option'> — and click it. Recognizing which kind of dropdown you are facing before writing the line is a five-second check that saves a confused half hour.

Alerts, confirms, prompts. JavaScript dialogs have no DOM — they belong to the browser window, so they cannot be found with findElement. Switch explicitly:

Alert alert = driver.switchTo().alert();
alert.accept();                        // OK
alert.dismiss();                       // Cancel
alert.sendKeys("reason");              // for prompt() only
String text = alert.getText();         // the message text

Windows and frames — the other switch. Any findElement runs against the current context. If an element lives in an iframe, you must driver.switchTo().frame(idOrElement) before locating, and switchTo().defaultContent() to come back — forgetting this is the classic NoSuchElementException on an element you can see in the browser. New tabs open as new window handles: driver.getWindowHandles() returns all of them and switchTo().window(handle) picks one; Selenium has no auto-switching to new tabs, unlike Playwright. Closing is driver.close() (current window) versus driver.quit() (whole session) — and that distinction is the subject of the next chapter's CI hygiene rules.

Verifying results at each step. Interaction without verification is typing into the void: every act should be followed by an assertion — a redirect confirmed with driver.getCurrentUrl(), a created record confirmed by locating it in the list, a validation message confirmed with getText() on the error element. Raw assertions come from your test framework (JUnit assertEquals, pytest assert), while Selenium supplies the values; you will use explicit waits in front of each of these reads, because the UI updates after the async request completes, and that timing gap is where beginner suites get their reputation for flakiness.

Reliable Teardown and the Honest Playwright-vs-Selenium Trade-Off

A CI runner that executes your suite overnight will happily leave forty Chrome processes alive if teardown is an afterthought. Orphaned drivers consume memory, hold ports, and cause the next run to fail with SessionNotCreatedException or a machine that grinds to 100% CPU. Teardown is not cleanup politeness — it is suite correctness, and the two mechanisms that guarantee it are quit() in a finally block (Java) or a with context (Python).

// Java — quit runs whether the test passed, failed, or threw
WebDriver driver = null;
try {
    driver = new ChromeDriver();
    driver.get(url);
    // ... test steps ...
} finally {
    if (driver != null) driver.quit();   // closes session AND the browser process
}
# Python — context-manager form is the idiomatic guarantee
from selenium import webdriver

with webdriver.Chrome() as driver:
    driver.get(url)
    # ... test steps ...
# driver.quit() runs automatically on the way out, even on exception

close() versus quit() is the distinction to memorize: close() closes the current window; if others remain open, the session — and the driver process — stays alive. quit() ends the session, stops chromedriver, and lets the browser exit. In TestNG/JUnit or pytest fixtures, teardown belongs in @AfterEach/teardown fixtures so it runs per test regardless of outcome; a driver created in @BeforeAll and shared across tests trades this guarantee for speed and breaks the second a test fails mid-way and leaves the context dirty. Modern convention favors one session per test (fast enough with today's machines) over one session per class, precisely because isolation beats a few saved seconds.

When the browser will not die anyway. The residual failure mode is a hung process after a crashed runner. Defensive fixes: set explicit page load timeout and script timeout, have CI kill the process tree at job end (pkill -f chromedriver on Linux runners, or the runner's own workspace cleanup), and keep one worker per browser instance — never share a driver across threads. Selenium's WebDriver interface is explicitly not thread-safe: two threads calling findElement on one driver interleave protocol messages and corrupt each other's state. Parallelism means one driver per thread, full stop.

Choosing between the tools, honestly. The comparison worth being able to defend in a design review:

Criterion Selenium Playwright
Waiting Manual explicit waits (WebDriverWait) Auto-waiting + retrying assertions built in
Languages Java, Python, C#, Ruby, JS JS/TS first; Java, Python, C# ports
Multi-tab / iframes Manual switchTo() Native, trivial
Browser breadth Anything WebDriver-capable Chromium, Firefox, WebKit
Ecosystem Vast: grids, device farms, decades of examples Younger, but fast-growing; first-class traces
Network control DevTools Protocol add-ons (Selenium 4) Routing, mocking, HAR built in

Choose Selenium when the team's language, an existing suite, or exotic browser/grid requirements dominate; choose Playwright when you are starting fresh in a web project and want synchronization handled for you. Both are defensible; the interview answer that fails is "Playwright is newer so it is better" without criteria. Whatever you pick, the structure around it — suites, config, helpers — is what makes hundreds of tests maintainable, which is Structuring an Automation Suite next.

Quiz

1. A Python Selenium test finds an element and calls `sendKeys("qa@example.com")` on an email field that already contains "previous@run.com". What happens and what is the correct line?

Question 1 options

2. A test fails with `NoSuchElementException` for an element you can plainly see in the browser, inside an iframe. What is the correct fix?

Question 2 options

3. Your CI job finishes but the next job fails: 12 Chrome processes from the previous run are still alive. What prevents this class of failure?

Question 3 options

4. A custom React dropdown (not a <select>) needs an option chosen. What is the correct approach?

Question 4 options

Flashcards

Question

Who resolves the browser driver in Selenium 4, and what replaced manual chromedriver downloads?

Answer

Selenium Manager, built into Selenium 4.6+, resolves and downloads the matching ChromeDriver/GeckoDriver/EdgeDriver automatically at session start (with caching). Manual version-matching survives only in offline or air-gapped environments, where drivers must be pre-provisioned and pinned.

Question

close() vs quit() in Selenium — what does each do and which belongs in teardown?

Answer

close() closes the current window; if others are open the session and driver process remain. quit() ends the session, stops the driver, and closes all its browsers. Teardown must call quit() (in finally/@AfterEach/with-block) so processes cannot outlive the test, even on exceptions.

Question

Why is WebDriver not thread-safe, and how does a suite get parallelism?

Answer

A WebDriver instance maps to one session over one protocol connection; concurrent calls interleave messages and corrupt state. Parallelism = one driver instance per thread/test, managed by the runner (TestNG/JUnit/pytest-xdist), with a grid or container per worker — never shared drivers.

Question

How do you handle a native <select> versus a custom React dropdown in Selenium?

Answer

Native select: org.openqa.selenium.support.ui.Select with selectByVisibleText (preferred), selectByValue, selectByIndex (brittle). Custom dropdown: Select throws UnexpectedTagNameException — click the control to open, locate the option element ([role='option']/li) and click it, then verify selection.

Question

Three concrete criteria for choosing Selenium over Playwright (and vice versa)?

Answer

Selenium when: the team's language is Java/Python/C# in a large existing suite, exotic browsers/grids/device farms are required, or WebDriver/Appium skills must transfer. Playwright when starting fresh in web: built-in auto-wait and retrying assertions, native multi-tab/iframes, routing/HAR/traces built in, and first-class cross-browser projects. The failing interview answer is picking by popularity without criteria.

Revision Notes

Key Takeaways

  • 1. A Selenium session is a WebDriver-protocol conversation; Selenium Manager now auto-resolves drivers, removing version-matching as a setup failure mode
  • 2. Interaction is a small vocabulary — clear+sendKeys, click, Actions, Select, alert/frame/window switches — and each has one classic trap (append semantics, interception, custom dropdowns, context switching)
  • 3. quit() in a finally/with-block or teardown fixture is non-negotiable: close() leaves sessions alive and orphaned browsers break the next CI run
  • 4. WebDriver is single-threaded per instance; parallel suites mean one driver per thread, never shared state
  • 5. Playwright-vs-Selenium is a criteria-driven decision (language, waiting model, tab handling, ecosystem) — popularity arguments fail design reviews

Interview Tips

  • • "Explain the Selenium architecture." — client library → driver process → browser via WebDriver protocol; mention Selenium Manager auto-resolving drivers in 4.6+
  • • "What is the difference between close and quit?" — window vs session; then volunteer the CI consequence: orphaned processes and the finally-block guarantee
  • • "How do you handle flaky waits in Selenium?" — WebDriverWait with expected conditions (visibilityOfElementLocated, elementToBeClickable) plus fluent timeouts; contrast with Playwright's built-in auto-wait
  • • "Why is WebDriver not thread-safe and how do you parallelize?" — one session per instance, protocol interleaving; answer with one-driver-per-thread plus a grid, and name TestNG/pytest-xdist as the orchestrator

Cheat Sheet

Selenium WebDriver Basics

Session lifecycle:

ChromeOptions o = new ChromeOptions();
o.addArguments("--headless=new", "--window-size=1920,1080");
WebDriver driver = new ChromeDriver(o);   // Selenium Manager resolves driver
try { driver.get(url); /* steps */ }
finally { driver.quit(); }                // ALWAYS quit(), not close()
with webdriver.Chrome() as driver:         # auto-quit on exit
    driver.get(url)

Interaction vocabulary:

Task API
Type el.clear(); el.sendKeys(text) — clear first, sendKeys appends
Click el.click() — may throw ElementClickInterceptedException (overlays)
Hover/multi-select new Actions(driver).moveToElement(el).perform() / keyDown-click-keyUp
Native dropdown new Select(el).selectByVisibleText(...) — ByValue, ByIndex(avoid)
Custom dropdown click to open → locate [role=option] → click → verify
JS dialog driver.switchTo().alert() → accept/dismiss/sendKeys/getText
iframe switchTo().frame(...) before locating; defaultContent() after
new tab getWindowHandles() → switchTo().window(handle) — no auto-switch

Hard rules: WebDriver is not thread-safe (one driver per thread); close() ≠ quit(); locate stale elements again instead of caching them.

Tool choice: Selenium = language freedom + grids + exotic browsers, manual waits/switching · Playwright = auto-wait + native multi-tab + traces, JS-first ecosystem.

Continue Learning