All posts
seleniumscrapingautomation

Selenium: A Practical Guide for Full-Stack Developers

A practical guide to Selenium — setup, core concepts, common mistakes, and production tips for full-stack developers.

SR

Suhail Roushan

April 7, 2026

·
6 min read

Selenium is an open-source framework that automates web browsers, letting you test and interact with web applications programmatically.

As a full-stack developer, I've integrated Selenium into projects for end-to-end testing, web scraping, and automating repetitive browser tasks. While modern frameworks like Cypress and Playwright have gained popularity, Selenium remains a foundational tool with unparalleled browser support. Understanding its core concepts is valuable, even if you eventually choose a different tool. This guide will walk through practical setup, key patterns, and common pitfalls.

Why Selenium Matters (and When to Skip It)

Selenium matters because it's the universal adapter for browser automation. Its WebDriver API is a W3C standard, which means support is baked into browsers themselves. This gives you stability and longevity that newer, proprietary tools can't guarantee. For testing a complex application across Chrome, Firefox, and Safari on different operating systems, Selenium is still a robust choice.

However, you should skip Selenium for new projects where developer experience is the top priority. Tools like Playwright offer better out-of-the-box handling for iframes, network interception, and auto-waiting. If your team is building a modern React or Vue app and only needs to support Chromium browsers, starting with Playwright will likely make you more productive.

Getting Started with Selenium

The fastest way to start is with Node.js and the selenium-webdriver package. You'll also need the browser-specific driver (like chromedriver). Here's a minimal, runnable setup to open a page and get its title.

First, install the packages:

npm install selenium-webdriver chromedriver

Now, write a basic script:

import { Builder, Browser } from 'selenium-webdriver';
import * as chrome from 'selenium-webdriver/chrome';

async function quickStart() {
  // Set up Chrome options (headless is useful for CI)
  let options = new chrome.Options();
  // options.headless(); // Uncomment to run without a GUI

  // Build the WebDriver instance
  const driver = await new Builder()
    .forBrowser(Browser.CHROME)
    .setChromeOptions(options)
    .build();

  try {
    await driver.get('https://www.suhailroushan.com');
    const title = await driver.getTitle();
    console.log(`Page title is: ${title}`);
  } finally {
    // Always quit the driver to clean up processes
    await driver.quit();
  }
}

quickStart();

Core Selenium Concepts Every Developer Should Know

1. Locators: Finding Elements on the Page You interact with pages by finding elements. Selenium provides multiple locator strategies. Prefer id, css, or xpath in that order, as they are the most reliable.

import { By, until } from 'selenium-webdriver';

// Assume 'driver' is a WebDriver instance
await driver.get('https://example.com/login');

// Find by ID (fastest and most stable)
const emailField = await driver.findElement(By.id('email'));
await emailField.sendKeys('user@example.com');

// Find by CSS selector (flexible for classes, attributes)
const submitButton = await driver.findElement(By.css('button[type="submit"]'));

// Find by XPath (powerful but brittle) - use sparingly
const footerLink = await driver.findElement(By.xpath('//footer//a'));

2. Explicit Waits: Synchronizing with Dynamic Content Never use static sleep calls. Use explicit waits to pause execution until a condition is met, such as an element being visible or clickable.

// Wait for a maximum of 10 seconds for the element to be visible
const dynamicElement = await driver.wait(
  until.elementLocated(By.id('dynamic-content')),
  10000
);

// Further wait for it to be clickable
await driver.wait(
  until.elementIsVisible(dynamicElement),
  5000
);
await dynamicElement.click();

3. The Page Object Model: Organizing Your Tests This design pattern encapsulates a page's elements and actions in a class, making your scripts maintainable and reusable.

class LoginPage {
  constructor(private driver) {}

  // Locators
  elements = {
    emailInput: By.id('email'),
    passwordInput: By.id('password'),
    submitButton: By.css('button.primary'),
    errorMessage: By.className('alert-error')
  };

  // Actions
  async login(email: string, password: string) {
    await this.driver.findElement(this.elements.emailInput).sendKeys(email);
    await this.driver.findElement(this.elements.passwordInput).sendKeys(password);
    await this.driver.findElement(this.elements.submitButton).click();
  }

  async getErrorMessage() {
    const element = await this.driver.findElement(this.elements.errorMessage);
    return await element.getText();
  }
}

// Usage in a test
const loginPage = new LoginPage(driver);
await loginPage.login('test@example.com', 'wrongpass');
const error = await loginPage.getErrorMessage();
console.assert(error.includes('Invalid credentials'));

Common Selenium Mistakes and How to Fix Them

1. Using Implicit Waits or Thread.sleep() Implicit waits set a global timeout for all findElement calls, which can lead to unpredictable, slow tests. Thread.sleep() is even worse—it blocks unconditionally.

Fix: Use explicit waits (driver.wait with until conditions) for specific elements. This is faster and more reliable.

2. Relying on Brittle XPath or CSS Selectors Selectors that depend on complex DOM hierarchies (e.g., div > div:nth-child(3) > span) break with the slightest UI change.

Fix: Work with your front-end team to add stable data-testid attributes to key elements. Locate them using By.css('[data-testid="login-button"]'). This creates a contract between development and testing.

3. Not Quitting the WebDriver Session Forgetting to call driver.quit() leaves browser processes and driver executables running in the background, consuming memory.

Fix: Always use a try/finally block to ensure driver.quit() is called, even if your script fails. See the setup example above.

When Should You Use Selenium?

Use Selenium when you need cross-browser testing across multiple vendors (Chrome, Firefox, Safari, Edge) and their legacy versions. It's also the right choice for automating tasks in a browser environment where you don't control the website's code, such as certain web scraping scenarios. For teams with existing Selenium expertise and a large suite of stable tests, migrating may not be worth the cost.

Avoid Selenium for projects where you only target Chromium-based browsers or need advanced features like mobile device emulation, network request mocking, or video recording out-of-the-box. In those cases, a modern alternative will provide a better developer experience.

Selenium in Production

In a production CI/CD pipeline, always run Selenium in headless mode. Configure your driver setup to include common arguments for stability and performance.

let options = new chrome.Options();
options.addArguments('--headless=new');
options.addArguments('--no-sandbox'); // Often needed in Linux CI environments
options.addArguments('--disable-dev-shm-usage'); // Overcomes limited resource problems

Secondly, implement robust logging and screenshot capture on failure. When a test fails, a screenshot is more valuable than a stack trace for diagnosing UI issues.

try {
  await someTestAction();
} catch (error) {
  const screenshot = await driver.takeScreenshot();
  require('fs').writeFileSync('failure.png', screenshot, 'base64');
  console.error('Test failed. Screenshot saved.');
  throw error;
}

Integrate your Selenium tests with a reporting dashboard or your existing monitoring tools to track test health over time.

Add a data-testid attribute to your next critical UI element and write a Selenium locator for it—this one practice will make your tests infinitely more resilient.

Related posts

Written by Suhail Roushan — Full-stack developer. More posts on AI, Next.js, and building products at suhailroushan.com/blog.

Get in touch