Most Selenium tutorials you will find were written years ago, and they start by telling you to download ChromeDriver and keep its version in sync with your browser. That advice is out of date. Selenium Manager now does it for you.
Selenium is the oldest of the major browser automation tools, and it is still the most widely used in job postings. It drives a real browser through the WebDriver protocol, a W3C standard, from a normal program in almost any language.
This tutorial uses JavaScript, because it runs in your browser here with no setup. The ideas transfer directly to Java, Python and C#. Every example runs in the TestAcademy editor.
What makes Selenium different
Two things separate Selenium from Cypress and Playwright, and both cut in each direction.
It is a standard, not a product. WebDriver is a W3C specification. Browser vendors implement it themselves. That is why Selenium drives real Safari through Apple's own driver, and why it supports more languages than anything else.
It does not wait for you. Cypress and Playwright wait automatically before acting. Selenium does not. You decide what to wait for and for how long. That is more code and more ways to get it wrong. It is also more control, because nothing is happening behind your back.
If you have used Playwright and Selenium feels verbose, that is why.
Install Selenium
npm install selenium-webdriver
That is the whole setup. You do not need to download a driver. Selenium Manager ships inside Selenium, works out which browser you have, and fetches the matching driver automatically.
If you do hit a version error, it is nearly always the classic one, and this guide explains it.
Your first script, line by line
Here is a complete Selenium script, in the shape the official documentation uses.
const { By, Builder, Browser } = require('selenium-webdriver');
const assert = require('assert');
(async function firstTest() {
let driver;
try {
driver = await new Builder().forBrowser(Browser.CHROME).build();
await driver.get('https://www.selenium.dev/selenium/web/web-form.html');
let title = await driver.getTitle();
assert.equal('Web form', title);
await driver.manage().setTimeouts({ implicit: 500 });
let textBox = await driver.findElement(By.name('my-text'));
let submitButton = await driver.findElement(By.css('button'));
await textBox.sendKeys('Selenium');
await submitButton.click();
let message = await driver.findElement(By.id('message'));
let value = await message.getText();
assert.equal('Received!', value);
} catch (e) {
console.log(e);
} finally {
await driver.quit();
}
}());
The official docs describe every Selenium script as eight steps, and you can see all eight above:
- Start the session.
new Builder().forBrowser(...).build()launches the browser. - Act on the browser.
driver.get(url)navigates. - Ask the browser for information.
driver.getTitle(). - Set a waiting strategy.
setTimeouts({ implicit: 500 }). - Find an element.
driver.findElement(By.name('my-text')). - Act on the element.
sendKeys,click. - Ask the element for information.
getText(). - End the session.
driver.quit().
Two details matter more than they look.
driver.quit() belongs in a finally block. If an assertion throws and you skip it, the browser process stays alive. Do that a few hundred times in CI and you will run the machine out of memory. I have seen a build agent fall over for exactly this reason, and it took a while to connect the two.
Everything is awaited. In the JavaScript bindings every command returns a promise.
Finding elements: the By strategies
driver.findElement(By.id('email'));
driver.findElement(By.name('my-text'));
driver.findElement(By.className('product-card'));
driver.findElement(By.css('[data-testid="submit"]'));
driver.findElement(By.linkText('Sign out'));
driver.findElement(By.partialLinkText('Sign'));
driver.findElement(By.xpath('//button[text()="Add to cart"]'));
findElement returns the first match and throws NoSuchElementException if there is none. findElements returns an array, and an empty array when nothing matches, which is the one to use when you want to count things or check that something is absent.
const rows = await driver.findElements(By.css('[data-testid="contact-row"]'));
assert.equal(rows.length, 8);
Which strategy should you use? Prefer By.css with a dedicated test attribute. It is fast, readable and stable.
Use XPath only when CSS genuinely cannot express what you need, usually selecting by text content or walking up to a parent. XPath is powerful and slow to read six months later.
Avoid selectors built on layout position:
// Fragile. One new wrapper div and this is wrong.
driver.findElement(By.xpath('/html/body/div[2]/div/div[3]/button'));
NoSuchElementException can mean the test needs a wait or uses an incorrect selector.
Waiting properly
This is the part that decides whether your Selenium suite is stable or miserable.
Implicit wait sets one global timeout. Every findElement will poll for up to that long before giving up.
await driver.manage().setTimeouts({ implicit: 5000 });
It is one line and it helps, but it only waits for an element to exist. It cannot wait for an element to become clickable, or for text to change, or for a spinner to disappear.
Explicit wait waits for a specific condition, and it is what you should reach for:
const { until } = require('selenium-webdriver');
// Wait until the element is on the page
const toast = await driver.wait(
until.elementLocated(By.css('[data-testid="toast"]')),
5000,
);
// Wait until it is actually visible
await driver.wait(until.elementIsVisible(toast), 5000);
// Wait until its text is what you expect
await driver.wait(until.elementTextIs(toast, 'Saved'), 5000);
Useful conditions include elementLocated, elementIsVisible, elementIsEnabled, elementTextIs, elementTextContains, titleIs and urlContains.
Do not mix implicit and explicit waits. The combination can produce unpredictable timeouts, because both are polling at once. Pick explicit waits and set the implicit wait to zero.
Never use a plain sleep:
await driver.sleep(3000); // do not do this
A fixed sleep is a bet on a number that changes. It is the single biggest cause of slow, flaky Selenium suites, and it is why so many people believe Selenium is unreliable. The tool is not unreliable. Sleeps are. Fixing flaky tests goes through the causes.
Acting on elements
await element.click();
await element.sendKeys('some text');
await element.clear();
await element.sendKeys(Key.ENTER); // requires Key from selenium-webdriver
await element.submit();
await element.getText();
await element.getAttribute('href');
await element.isDisplayed();
await element.isEnabled();
await element.isSelected();
If a click fails with an error about the element not being interactable, or another element receiving the click, the element is usually hidden, disabled, or behind an overlay. That is common enough to have its own guide.
Dropdowns
Native <select> elements have a helper:
const { Select } = require('selenium-webdriver/lib/select');
const select = new Select(await driver.findElement(By.id('country')));
await select.selectByVisibleText('Germany');
await select.selectByValue('DE');
await select.selectByIndex(2);
Custom dropdowns built from div elements are not <select> elements, so Select will not work. Click them like any other element.
Windows, tabs and frames
Because Selenium drives from outside the browser, these are ordinary operations.
// Switch to a new tab
const handles = await driver.getAllWindowHandles();
await driver.switchTo().window(handles[1]);
// Work inside an iframe, then come back out
await driver.switchTo().frame(await driver.findElement(By.css('iframe#payment')));
await driver.findElement(By.id('card-number')).sendKeys('4242424242424242');
await driver.switchTo().defaultContent();
// Handle a native alert
const alert = await driver.switchTo().alert();
await alert.accept();
The rule with frames is simple and easy to forget: while you are switched into a frame, Selenium cannot see anything outside it. Switch back with defaultContent(). We cover this in depth in handling iframes.
Running headless
Headless means no visible window. It is what CI uses.
const chrome = require('selenium-webdriver/chrome');
const options = new chrome.Options();
options.addArguments('--headless=new');
options.addArguments('--window-size=1280,720');
const driver = await new Builder()
.forBrowser(Browser.CHROME)
.setChromeOptions(options)
.build();
Set the window size explicitly. A headless browser defaults to a small window, elements end up behind responsive menus, and you get tests that pass locally and fail in CI for no visible reason.
What is new in Selenium
Two changes are worth knowing, because they undo the two oldest complaints about the tool.
Selenium Manager is built in and handles drivers for you. No downloads, no PATH configuration, no version pinning.
WebDriver BiDi is a W3C bidirectional protocol that lets the browser push events to your test. It brings network interception, request mocking, console log capture and JavaScript error listening to Selenium as a cross-browser standard, rather than the Chrome-only workarounds people used before. Recent Selenium releases have been rolling it out across the language bindings.
If your mental picture of Selenium is from 2019, it is worth updating. Playwright vs Cypress vs Selenium compares the three as they are now.
Selenium Grid, briefly
Grid runs your tests on other machines, in parallel, across browsers and operating systems. You start a hub, connect nodes, and point the driver at the hub instead of a local browser.
It is genuinely useful at scale and it is real infrastructure to maintain. Most teams start locally, move to CI, and only reach for Grid when they need broad browser coverage or serious parallelism.
Common errors and what they mean
| Error | What it means | Fix |
|---|---|---|
NoSuchElementException |
The element was not on the page when you looked | Check the selector, then add an explicit wait. Guide |
StaleElementReferenceException |
The page re-rendered after you found the element | Find it again. Guide |
ElementNotInteractableException |
It exists but cannot be clicked or typed into | Wait for it to be visible and enabled. Guide |
ElementClickInterceptedException |
Something is on top of it | Wait for the overlay to close |
SessionNotCreatedException |
Browser and driver versions disagree | Usually solved by Selenium Manager. Guide |
TimeoutException |
Your wait condition never became true | Check that the condition is the right one |
Five mistakes to avoid in your first Selenium tests
- Using
driver.sleep(). Use explicit waits for the condition you actually need. - Mixing implicit and explicit waits. Pick explicit, and set implicit to zero.
- Forgetting
driver.quit(). Put it infinallyor you leak browser processes. - Reusing an element after the page changed. That is what
StaleElementReferenceExceptionmeans. Find it again. - Long absolute XPath. One layout change and every test breaks.
Frequently asked questions
Do I still need to download ChromeDriver? No. Selenium Manager is built in and fetches the right driver for your browser automatically.
Which language should I learn Selenium in? Java has the deepest ecosystem and appears in the most job postings. Python is the fastest to read and write. The API is nearly identical across both.
Why are my Selenium tests so slow? Usually sleeps, or logging in through the UI in every test. Replace sleeps with explicit waits and set up state directly where you can.
Is Selenium still worth learning in 2026? Yes, if you are targeting enterprise or Java roles, or you need real Safari or older browsers. If you are choosing freely for a new JavaScript project, Playwright is usually the easier start.
Can Selenium test mobile apps? Not directly. Appium is built on the same WebDriver protocol and handles native iOS and Android.
What is the difference between findElement and findElements?
findElement returns the first match and throws if there is none. findElements returns an array, empty if nothing matches.
Remember this
Selenium is a W3C standard, so it works in nearly any language and drives real browsers, including Safari. It does not wait for you, so explicit waits are the skill that decides whether your suite is stable. Always quit the driver in a finally block. And Selenium Manager means the old driver-juggling advice no longer applies.
The official documentation is at selenium.dev.
Practice now
Reading about a test is not the same as writing one. Open the Selenium editor and try it.
Then take the graded problem below. You write the test, and we run it twice: once against the working app, and once against a copy where we broke the exact behaviour on purpose. If your test still passes on the broken copy, it was not really checking anything, and we will tell you which behaviour it missed.