Selenium WebDriver is the oldest and most widely used browser automation tool. It has been the industry standard for more than 15 years, and it is still asked for in many QA job interviews. If you understand Selenium, you understand the foundation that newer tools like Playwright and Cypress built on.
This tutorial uses the JavaScript bindings. You can run every example in the TestAcademy editor.
Your first Selenium test
const { Builder, By, until } = require('selenium-webdriver');
const assert = require('assert');
describe('Login', function () {
it('signs in with valid credentials', async function () {
const driver = await new Builder().forBrowser('chrome').build();
await driver.get('/');
await driver.findElement(By.css('[data-testid="email-input"]'))
.sendKeys('demo@nimbus.app');
await driver.findElement(By.css('[data-testid="password-input"]'))
.sendKeys('secure123');
await driver.findElement(By.css('[data-testid="login-button"]')).click();
const welcome = await driver.wait(
until.elementLocated(By.css('[data-testid="welcome-message"]')),
5000
);
assert.strictEqual(await welcome.getText(), 'Welcome back, Alex');
await driver.quit();
});
});
The flow is always the same:
- Build a driver. This starts a browser session.
- Navigate with
driver.get(url). - Find elements and interact with them.
- Assert with a library like node's
assert. - Quit the driver to close the session.
Locating elements with By
Selenium finds elements with locator strategies:
By.id('email') // element with id="email"
By.css('[data-testid="save"]') // any CSS selector
By.name('password') // element with name attribute
By.className('btn-primary') // by CSS class
By.tagName('h1') // by tag
By.linkText('Forgot password?') // a link with exact text
By.partialLinkText('Forgot') // a link that contains text
By.xpath('//button[text()="Save"]') // XPath expression
Advice: use By.css with stable attributes like data-testid most of the time. XPath is powerful but harder to read and easier to break. Avoid locators that depend on layout, like div > div:nth-child(3).
Working with elements
const input = await driver.findElement(By.css('#search'));
await input.sendKeys('tent'); // type text
await input.clear(); // clear an input
await input.sendKeys('tent', Key.ENTER); // type and press Enter
const button = await driver.findElement(By.css('#submit'));
await button.click();
const title = await driver.findElement(By.css('h1'));
console.log(await title.getText()); // read text
console.log(await title.getAttribute('class')); // read attribute
console.log(await button.isDisplayed()); // visible?
console.log(await button.isEnabled()); // clickable?
findElements (plural) returns an array. It is useful for counting:
const rows = await driver.findElements(By.css('[data-testid="contact-row"]'));
assert.strictEqual(rows.length, 8);
Waits: the most important Selenium topic
This is where most beginners struggle. Unlike Playwright and Cypress, classic Selenium does not wait for elements automatically by default. If the app is still loading, findElement can fail.
The correct solution is an explicit wait with driver.wait and until:
// Wait until an element appears (up to 5 seconds)
const el = await driver.wait(
until.elementLocated(By.css('[data-testid="toast"]')),
5000
);
// Wait until it is visible
await driver.wait(until.elementIsVisible(el), 5000);
// Wait for a URL change
await driver.wait(until.urlContains('/dashboard'), 5000);
// Custom condition
await driver.wait(async () => {
const rows = await driver.findElements(By.css('tr'));
return rows.length === 1 ? rows : null;
}, 5000);
Never use fixed sleeps like driver.sleep(3000) to "fix" timing problems. They make tests slow and still fail on a slow day. Interviewers often ask about the difference between implicit waits, explicit waits and sleeps, explicit waits are the professional answer.
Common mistakes
- No waiting strategy. Calling
findElementright after a click, before the page updates. Usedriver.waitwithuntil. - Using
driver.sleepeverywhere. Slow and unreliable. Wait for a real condition instead. - Fragile XPath. Long absolute XPaths break with any HTML change. Prefer short CSS selectors with stable attributes.
- Forgetting
await. Every Selenium call returns a promise in JavaScript. Missingawaitcauses actions to overlap. - Not closing the driver. Always call
driver.quit(), or browser sessions stay open.
Best practices
- Keep locators in one place (a page object) so a UI change means one fix, not fifty.
- Wait for conditions, not time.
- Make every test independent: it should create or reset its own data.
- Assert user-visible results (text, URL), not internal details.
Selenium or a newer tool?
Selenium is still everywhere: large companies, long-lived projects, and any team that needs Java, Python or C# bindings. Newer tools add comfort features like auto-waiting, but the core skills, locators, waits, assertions, are the same. Learn the concepts once and you can work with any of them. See our Playwright vs Cypress comparison for how the newer tools differ.
Try it now: open the the Selenium editor and complete the challenges against a real CRM app.