lecture 6 of 160 completed

Building the wait library your suite depends on

Ninety engineers writing waits individually produces ninety subtly different bugs. Encode correctness once.

In a suite with no wait library, every engineer writes waits from memory. Some wait for presence when they meant visibility. Some read the element before waiting. Some hard-code timeouts. Every one of those is a flaky test waiting for a slow day.

The fix is not training. It is making the correct thing the easiest thing to write.

Start from what people actually get wrong.

Presence instead of visibility. elementLocated returns as soon as the element is in the DOM, which can be before it is visible or interactive. Then the click fails.

No wait before reading. Covered in the previous lecture, and the most common of all.

Timeouts scattered as literals. 5000 typed in ninety places cannot be tuned for a slower environment.

No message. Every timeout reads identically, so diagnosing means opening the test.

The library removes all four by construction.

// support/waits.js
const { until, By } = require('selenium-webdriver');
const config = require('../config');

async function forVisible(driver, locator, message, timeout = config.timeout) {
  const el = await driver.wait(
    until.elementLocated(locator),
    timeout,
    message ?? `Element not found: ${locator}`,
  );
  return driver.wait(until.elementIsVisible(el), timeout, message ?? `Element not visible: ${locator}`);
}

async function forClickable(driver, locator, message, timeout = config.timeout) {
  const el = await forVisible(driver, locator, message, timeout);
  return driver.wait(until.elementIsEnabled(el), timeout, message ?? `Element not enabled: ${locator}`);
}

async function forGone(driver, locator, message, timeout = config.timeout) {
  return driver.wait(async () => (await driver.findElements(locator)).length === 0, timeout,
    message ?? `Element still present: ${locator}`);
}

async function forText(driver, locator, expected, timeout = config.timeout) {
  const el = await forVisible(driver, locator, undefined, timeout);
  return driver.wait(until.elementTextIs(el, expected), timeout,
    `Expected text "${expected}" at ${locator}`);
}

Note what forVisible does: located then visible. The correct two-step wait, written once. Now nobody has to remember it.

Provide a safe read, because the read-before-wait bug needs removing structurally:

async function readText(driver, locator, timeout = config.timeout) {
  const el = await forVisible(driver, locator, undefined, timeout);
  return el.getText();
}

An engineer reaching for the text now gets the wait automatically. That is the whole design principle: make the correct path the shortest path.

Timeouts as named tiers, not literals:

// config/timeouts.js
module.exports = {
  instant: 2000,     // already-rendered UI
  standard: 10000,   // normal interactions
  slow: 30000,       // report generation, large uploads
};

Named tiers document intent and are tunable per environment in one place. forVisible(driver, locator, msg, timeouts.slow) says something a literal cannot.

Retrying stale elements. Selenium's own answer, and worth wrapping:

async function clickStably(driver, locator, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    try {
      const el = await forClickable(driver, locator);
      await el.click();
      return;
    } catch (err) {
      if (err.name !== 'StaleElementReferenceError' || i === attempts - 1) throw err;
    }
  }
}

Note it only swallows staleness and only re-throws everything else. A blanket retry that catches all errors would hide real bugs, which is the trap this pattern usually falls into.

Enforcement. A library nobody uses changes nothing. Two things make it stick: a lint rule failing the build on driver.sleep or raw driver.wait in test files, and a review habit of asking "why not the helper?" when someone hand-rolls a wait.

The measurement that justifies the work. Track flaky failures before and after. When a team sees timing failures drop by most of their volume after a library lands, the argument for the next investment is already made.

Remember this: encode the correct wait once, make the safe read the easiest read, name your timeout tiers, and enforce it with a lint rule.

Reference: Waits.

now prove it

Reading is the setup. Solve this problem in the editor. We break the app on purpose to check your test would catch it.