ScreenshotNeo

BlogHow-to

How to Automate Browser Tests With Selenium and JavaScript

Build reliable browser tests with Selenium and JavaScript: install WebDriver, interact with a page, assert results, and run tests with a JavaScript test runner.

By the ScreenshotNeo team4 October 202610 min read

Selenium browser tests use the selenium-webdriver package in Node.js to open a real browser, perform user-like actions, and check what the page displays. A useful test has four parts: create a WebDriver session, navigate and interact, wait for the result, then assert it and close the browser in a finally block.

This guide starts with a runnable local test, then shows how to organize it with Mocha, handle asynchronous pages, choose selectors, run remotely, and diagnose common failures. The Selenium JavaScript API currently specifies Node.js 22 or newer. Check the current JavaScript API reference for changes to runtime and browser support.

1. Install Selenium and prepare a browser

Create a project and install Selenium’s JavaScript binding:

mkdir selenium-browser-test
cd selenium-browser-test
npm init -y
npm install selenium-webdriver

Use Node.js 22 or newer, as required by the current Selenium JavaScript API reference. Choose a browser available in your environment. Selenium Manager can manage drivers when one is not provided; it has been included in Selenium releases since Selenium 4.11.0. Its ability to fetch or use a browser and driver can still depend on network access, permissions, cache state, and CI restrictions. See the Selenium Manager documentation.

The binding communicates with a browser-specific driver, which controls the browser. For many local setups, the first run can use Selenium Manager without manually downloading a driver. If your environment supplies a driver or restricts downloads, configure that environment according to the official browser and driver documentation.

2. Write and run a complete browser test

The following standalone script opens a page, searches for a phrase, waits for the result link, asserts the page title and visible result, and closes the browser even if a step fails. It uses a public demonstration page; replace it with an application page and stable selectors for your own test.

// save as test.js
const assert = require('node:assert/strict');
const { Builder, By, until } = require('selenium-webdriver');

async function main() {
  const driver = await new Builder().forBrowser('chrome').build();
  try {
    await driver.get('https://www.selenium.dev/selenium/web/web-form.html');

    const title = await driver.getTitle();
    assert.equal(title, 'Web form');

    const textBox = await driver.findElement(By.name('my-text'));
    await textBox.sendKeys('Selenium WebDriver');
    await driver.findElement(By.css('button')).click();

    const message = await driver.wait(
      until.elementLocated(By.id('message')),
      5000,
      'confirmation message did not appear'
    );
    await driver.wait(until.elementIsVisible(message), 5000);
    assert.equal(await message.getText(), 'Received!');
  } finally {
    await driver.quit();
  }
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Run it with:

node test.js

The assertions use Node’s built-in assert module, so no test runner is needed for this first example. Builder creates the browser session, get() navigates, By describes a locator, and quit() ends the session. The finally block matters: a failed assertion should not leave a browser running.

3. Choose robust selectors and actions

Selenium locators identify elements in the rendered page. Prefer selectors that express the element’s purpose and remain stable as layout changes. If your application can provide dedicated test IDs, use them consistently.

Locator Example Good fit
ID By.id('submit') A unique, stable element ID.
Name By.name('email') Form fields with a meaningful name.
CSS selector By.css('[data-testid="save"]') Test IDs, attributes, and concise relationships.
Link text By.linkText('Documentation') A link whose visible text is stable and user-facing.
XPath By.xpath('//button[@type="submit"]') Relationships that CSS cannot express clearly; keep it readable.

Typical interactions are asynchronous and should be awaited:

await driver.findElement(By.name('email')).sendKeys('reader@example.com');
await driver.findElement(By.css('button[type="submit"]')).click();
await driver.findElement(By.id('terms')).click();

Use selectors based on the page’s intended behavior rather than incidental styling classes or element positions. A selector that identifies “the third button” can silently target the wrong control when the page changes. If a control lives inside an iframe, switch into that frame before locating it; switch back to the default content after the interaction. For a newly opened tab or window, wait for the new window handle and switch to it before querying its page.

4. Synchronize on page state

Modern pages often update after navigation through JavaScript. A navigation call completing does not prove that the specific result your test needs is ready. Wait for the condition the next step depends on, such as an element appearing or becoming visible. Selenium documents its wait strategies in the WebDriver waits guide.

const result = await driver.wait(
  until.elementLocated(By.css('[data-testid="search-result"]')),
  10000,
  'search result did not appear'
);
await driver.wait(until.elementIsVisible(result), 5000);
assert.match(await result.getText(), /expected result/i);

Set timeouts to allow for realistic variation in your application and environment. Keep them finite so failures report promptly. A fixed sleep can be useful for a deliberately time-based behavior, but it is a poor default readiness check: a short delay races the page, while a long one slows every successful run. Wait for observable state instead.

Avoid mixing implicit and explicit waits without a clear reason; combinations can make total wait times confusing. For the most predictable suite, use explicit waits around the actual condition being tested and consult Selenium’s current wait documentation for the exact API supported by your installed binding.

5. Organize tests with Mocha

Selenium controls the browser; a test runner organizes test cases, reports results, and provides lifecycle hooks. Mocha is a common JavaScript option in Selenium’s examples, and Jest is another option. The runner is a project choice, not a Selenium requirement.

Install Mocha:

npm install --save-dev mocha

Add a script to package.json:

{
  "scripts": {
    "test": "mocha"
  }
}

Save the following as test/browser.test.js:

const assert = require('node:assert/strict');
const { Builder, By, until } = require('selenium-webdriver');

describe('web form', function () {
  this.timeout(20000);
  let driver;

  before(async function () {
    driver = await new Builder().forBrowser('chrome').build();
  });

  after(async function () {
    if (driver) await driver.quit();
  });

  it('shows a confirmation after submission', async function () {
    await driver.get('https://www.selenium.dev/selenium/web/web-form.html');
    await driver.findElement(By.name('my-text')).sendKeys('Automated test');
    await driver.findElement(By.css('button')).click();

    const message = await driver.wait(
      until.elementLocated(By.id('message')),
      5000,
      'confirmation message did not appear'
    );
    await driver.wait(until.elementIsVisible(message), 5000);
    assert.equal(await message.getText(), 'Received!');
  });
});

Then run:

npm test

This example creates one session for the suite and closes it in an after hook. For independent tests, decide whether each test should receive a fresh browser session or whether sharing one is acceptable. A fresh session reduces state leaking between tests but adds session startup work; a shared session can be faster but requires deliberate cleanup of cookies, storage, windows, and page state.

6. Configure the browser and execution target

Builder selects a browser and can be configured for local or remote execution. The simplest local example is:

const driver = await new Builder().forBrowser('chrome').build();

Use the browser name supported by your installed Selenium binding and available environment. Headless operation and other browser capabilities depend on browser-specific options and current binding support; consult the relevant Selenium browser options documentation rather than assuming one option works identically across browsers.

To use a remote Selenium Server or Grid, point the builder at its remote URL:

const remoteUrl = process.env.SELENIUM_REMOTE_URL;
if (!remoteUrl) throw new Error('Set SELENIUM_REMOTE_URL');

const driver = await new Builder()
  .forBrowser('chrome')
  .usingServer(remoteUrl)
  .build();

Keep the remote endpoint in configuration or an environment variable rather than hard-coding it into test source. Local execution is a straightforward place to develop the test. Selenium Grid is intended for execution across machines and platform combinations when coverage or capacity calls for it. See the Selenium Grid documentation.

Remote runs add network and infrastructure dependencies. A browser may be unavailable, a session may fail to start, or a slow remote page may need a suitable explicit timeout. Include enough error context in runner output to identify the test, target URL, and failed condition.

7. When browser events matter: WebDriver BiDi

Classic WebDriver covers the command-and-response interactions in the examples above. Selenium also documents WebDriver BiDi, a bidirectional protocol using a WebSocket connection that can expose browser events such as network requests, console messages, and JavaScript errors. This is useful when a test must react to event-driven browser signals or collect diagnostics that are difficult to infer from the rendered page alone.

Feature availability varies by browser and binding. Verify support for the specific BiDi event or command in the current Selenium documentation and your chosen browser before designing a suite around it. See Selenium’s WebDriver BiDi guide.

8. Make the suite reliable and efficient

  • Keep tests focused. Assert a user-visible outcome for one behavior, and keep setup data controlled.
  • Use explicit waits. Wait for the state under test, with a finite timeout and a useful failure message.
  • Always end sessions. Use finally in standalone scripts and runner teardown hooks in suites.
  • Isolate state intentionally. Choose per-test or per-suite sessions based on the cost of startup and the risk of shared state.
  • Keep selectors stable. Prefer meaningful IDs, labels, or test attributes over brittle layout-dependent selectors.
  • Scale only when needed. Start locally, then consider remote WebDriver and Grid when machine, platform, or execution capacity needs grow.
  • Separate browser tests from simple checks. Selenium is useful for browser behavior and user flows; a screenshot alone cannot replace assertions about interaction and page state.

Browser startup and real page loading make browser tests more resource-intensive than tests that do not launch a browser. Reuse can reduce startup overhead, while isolated sessions improve independence; measure your own suite before choosing a balance. The research sources provide no general runtime benchmark or cost figure, so performance and infrastructure cost depend on the application, browser, machines, and execution setup.

9. Troubleshooting common failures

Symptom Likely cause What to do
Browser or driver cannot be found Browser absent, driver setup blocked, incompatible environment, or Selenium Manager cannot fetch what it needs. Confirm the browser is installed and executable, check network and filesystem permissions, inspect Selenium Manager setup, or provide the driver through your environment.
Unsupported Node.js version The runtime is below the current API’s stated minimum. Use Node.js 22 or newer and check the current Selenium JavaScript API reference for updated requirements.
NoSuchElementError The selector is wrong, the element has not appeared, or it is in a different frame or window. Inspect the rendered page and selector, wait for the element, and switch to the correct frame or window first.
Element is not interactable or click is intercepted The element is hidden, covered, disabled, or not yet ready. Wait until it is visible and enabled, resolve overlays, and interact with the user-facing control. Avoid using JavaScript to bypass behavior the user would encounter.
Test intermittently fails after navigation The test assumes the page is ready immediately. Wait for the specific result or state needed by the assertion instead of relying on a fixed delay.
Mocha reports a timeout The test-runner timeout is shorter than the operation, or the page condition never occurs. Check the failing wait and page behavior; set a reasonable runner timeout for browser work and retain finite condition-specific waits.
Browser remains open after failure Session cleanup was skipped on an exceptional path. Put driver.quit() in finally or an always-run teardown hook.
Remote session cannot be created Remote URL is wrong, server is unavailable, requested browser is unsupported, or infrastructure capacity is exhausted. Verify SELENIUM_REMOTE_URL, server health, browser availability, and remote logs; retry only transient infrastructure errors.

10. Or skip the browser setup

If the goal is a page image rather than an interactive browser assertion, ScreenshotNeo provides a screenshot API and MCP server for developers. One GET request returns a PNG, JPEG, WebP, or PDF. It is separate from Selenium: use Selenium for browser behavior and assertions; use a screenshot capture when the deliverable is an image or PDF.

Here is the one-call API example in each common environment. See the ScreenshotNeo API documentation for parameters and options.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo accepts cookie and consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each of those steps can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response includes X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Sign up free for 1,000 screenshots a month, with no card required.

11. Frequently asked questions

Does Selenium require Mocha?

No. Selenium provides browser control. You can run a standalone Node.js script or use a test runner such as Mocha or Jest.

Can Selenium run against a browser on another machine?

Yes. Configure remote WebDriver with a Selenium Server or Grid endpoint. The remote environment must provide the requested browser and accept sessions.

When should I use BiDi?

Consider it when your test needs browser events such as network activity, console messages, or JavaScript errors. Confirm the exact feature is supported by your browser and binding.

Can a screenshot prove that a browser flow passed?

A screenshot can show rendered appearance at a point in time, but it does not by itself establish that an interaction or application behavior succeeded. Use Selenium assertions for those outcomes.

Official references