Selenium WebDriver Tutorial: Learn the Basics with JavaScript

9 min readupdated July 11, 2026

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:

  1. Build a driver. This starts a browser session.
  2. Navigate with driver.get(url).
  3. Find elements and interact with them.
  4. Assert with a library like node's assert.
  5. 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

  1. No waiting strategy. Calling findElement right after a click, before the page updates. Use driver.wait with until.
  2. Using driver.sleep everywhere. Slow and unreliable. Wait for a real condition instead.
  3. Fragile XPath. Long absolute XPaths break with any HTML change. Prefer short CSS selectors with stable attributes.
  4. Forgetting await. Every Selenium call returns a promise in JavaScript. Missing await causes actions to overlap.
  5. 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.

now prove it

Prove the search actually filters

Reading is the easy part. Write the test yourself, then we break the app on purpose to check it would really catch the bug.

Solve this problem →

Keep reading