lecture 18 of 200 completed

Starting and stopping the browser properly

Selenium makes browser lifecycle your job. Getting it wrong leaks processes until the machine stops responding.

A build server at a company I worked with slowed to a crawl every few days until someone restarted it. The cause was a test suite that left Chrome processes running. Each run leaked a handful. After a week there were hundreds.

Cypress and Playwright manage the browser for you. Selenium does not, and this is where that costs you.

The rule: every browser you start must be stopped, including when the test fails.

The naive version has a bug:

it('shows the contact list', async () => {
  const driver = await new Builder().forBrowser('chrome').build();
  await driver.get('http://localhost:3000/contacts');
  expect(await driver.getTitle()).to.equal('Contacts');
  await driver.quit();               // never runs if the assertion fails
});

When the assertion fails, the function throws and quit() is skipped. The browser stays open forever.

Use hooks instead, so cleanup runs whatever happens:

describe('contacts', () => {
  let driver;

  beforeEach(async () => {
    driver = await new Builder().forBrowser('chrome').build();
    await driver.get('http://localhost:3000/contacts');
  });

  afterEach(async () => {
    await driver.quit();
  });

  it('shows the contact list', async () => {
    const rows = await driver.findElements(By.css('[data-testid="contact-row"]'));
    expect(rows.length).to.be.greaterThan(0);
  });
});

afterEach runs even when the test fails. That is the whole point.

Per test or per file? This is the real decision, and it is the same independence question from every other track, with an extra cost attached.

A fresh browser per test (beforeEach and afterEach) gives complete isolation. No cookies, no localStorage, nothing left behind. It also costs one to three seconds per test, which is the price of starting a browser.

One browser for the file (before and after) is much faster and leaks state between tests. A test that logs in leaves the next one logged in, which is convenient right up until the day it is the reason a test fails mysteriously.

The usual compromise: one browser per file, with explicit cleanup between tests.

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

beforeEach(async () => {
  await driver.manage().deleteAllCookies();
  await driver.get('http://localhost:3000/');
});

after(async () => {
  await driver.quit();
});

You pay the startup cost once and still start each test from a known state. Note that clearing cookies is deliberate: it is what makes each test start logged out.

close() is not quit(). close() closes one window. quit() ends the session and releases the driver process. Only quit() prevents the leak, and using close() when you meant quit() is exactly how the story at the top of this lecture happens.

Remember this: start in a hook, quit in a hook, and reset state between tests rather than trusting what came before.

Reference: Driver sessions.