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?
2. A test fails with `NoSuchElementException` for an element you can plainly see in the browser, inside an iframe. What is the correct fix?
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?
4. A custom React dropdown (not a <select>) needs an option chosen. What is the correct approach?
Flashcards
Question
Who resolves the browser driver in Selenium 4, and what replaced manual chromedriver downloads?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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)?
Click to reveal answer
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.