Selenium WebDriver
Selenium WebDriver is an open-source library that drives a real browser through the W3C WebDriver protocol. Your test calls a language binding, here the selenium-webdriver package for JavaScript; the binding sends each command as an HTTP request to a browser driver such as chromedriver or geckodriver; the driver makes the browser carry it out. await driver.findElement(By.css('[data-testid=login-submit]')).click() becomes two requests: one finds the button, the other clicks it the way a user would.
Selenium is a library, not a test framework. You pick the runner (Mocha in this lesson, JUnit or TestNG in Java, pytest in Python) and build the rest yourself: configuration, waits, screenshots and reports. This lesson sets up a small JavaScript project, runs it locally and in CI, handles signed-in sessions, dialogs, tabs and uploads, and shows where the protocol stops.
Why it matters
WebDriver is a W3C standard implemented by drivers that the browser vendors ship themselves, and Selenium has official bindings for Java, Python, C#, JavaScript and Ruby. Many established suites, especially in Java teams, are Selenium suites, and Appium for mobile apps uses the same protocol. A tester who joins such a team has to run, read and repair these tests from the first week.
Selenium also does less for you than newer tools: apart from page loads, nothing waits for the application unless the test asks, which is a common cause of flaky Selenium tests. Once you know that every call is one request with one answer, the error names start to mean something: no such element, stale element reference and element click intercepted each point to a different cause, and you can tell a product defect from a test defect.
How a command travels: protocol, drivers, locators, waits and Grid
A session starts when the binding sends POST /session with capabilities: the browser name and options such as headless mode. The driver launches the browser with a fresh, empty profile and returns a session id. Every later call is one request on that session: driver.get(url) returns once the page has loaded, findElement returns a reference to one DOM node, and click() acts on that reference. The answer is a value or an error code defined by the W3C specification. When the page re-renders, the old node is gone and the reference becomes stale, so page objects find elements again instead of keeping them.
Each browser vendor ships the driver for its browser: chromedriver for Chrome, geckodriver for Firefox, msedgedriver for Edge, and safaridriver, which comes with macOS. The driver must be compatible with the browser: chromedriver and msedgedriver need the same major version as Chrome or Edge. Since Selenium 4.6 every release includes Selenium Manager, which finds or downloads a matching driver when none is available, and later releases can also download Chrome, Firefox or Edge themselves.
- Test process
selenium-webdriver + Mocha - Browser driver
chromedriver · geckodriver · Grid - Browser Fresh, empty profile
- 1 From Test process To Browser driver: Start a session Capabilities: browser name and options such as headless
POST /session - 2 From Browser driver To Browser: Launch the browser
- 3 From Browser driver To Test process: Session id reply
- 4 From Test process To Browser driver: Open the login page Answers once the page load completes
POST /session/{id}/url - 5 From Test process To Browser driver: Find the sign-in button
css selector,[data-testid=login-submit]POST /session/{id}/element - 6 From Browser driver To Browser: Search the current DOM Once, because the implicit wait is 0
- 7 From Browser driver To Test process: Element reference, or
no such elementreply - 8 Test process internally: Explicit wait: repeat the find until it succeeds or the timeout ends
driver.wait(until.elementLocated(…), 10000)runs here, not in the browser - 9 From Test process To Browser driver: Click that element By the element reference
{eid}from the findPOST /session/{id}/element/{eid}/click - 10 From Browser driver To Browser: Scroll into view, check nothing covers it, click Otherwise
element click interceptedorelement not interactable - 11 From Test process To Browser driver: End the session In
afterEach, so it also runs after a failureDELETE /session/{id} - 12 From Browser driver To Browser: Close the browser
Outcome: Each call is one request and one answer; apart from page loads, nothing waits for the application unless the test asks.
- Locators: the protocol defines five strategies (CSS selector, link text, partial link text, tag name and XPath); the bindings translate
By.id,By.nameandBy.classNameinto CSS. Apply the order from the locator strategies guide (its section on Selenium and Cypress): a test id such asBy.css('[data-testid=login-email]'), then a stable id or name, and XPath only for text or structure that CSS cannot express. There is no built-in equivalent of Playwright’sgetByRole. - Explicit waits:
driver.wait(condition, ms)repeats the condition in your test process until it is true, or throws aTimeoutError. Use the ready-made conditions inuntil:elementLocated,elementIsVisible,elementTextIs,urlContains,alertIsPresent. Keep the implicit wait at 0; mixing it with explicit waits makes timeouts unpredictable. - Grid: Selenium Grid accepts the same
POST /sessionand forwards it to a node that has the requested browser, so the test changes only the address it talks to. The official Docker imageselenium/standalone-chrome, a ready-made package of Grid, Chrome and chromedriver that Docker runs as an isolated container, is a one-node Grid on port4444.
Install and lay out a JavaScript project
In an empty folder run npm init -y, then npm install --save-dev selenium-webdriver mocha mocha-junit-reporter. No driver download is needed. Mocha supplies describe, it, hooks, timeouts, retries and reporters; node:assert/strict supplies the assertions; mocha-junit-reporter writes the XML report that CI servers read.
Settings live in two places. The runner’s .mocharc.json holds timeouts, retries and the reporter. A small config module reads the base URL, browser, headless mode, Grid address and test account from environment variables, so the same code runs on a laptop, on a Grid and in CI. One function builds every browser session, so a change of browser or options happens in one file.
package.json:"test": "mocha"; by default Mocha runs every.jsfile intest/.- Modules: the examples use Node’s
require()andmodule.exports(CommonJS), the older equivalent ofimportandexport, because a project created withnpm init -yis CommonJS, and animportin it fails withCannot use import statement outside a module. To useimport, set"type": "module"inpackage.json; relative imports then need the file extension, as in'../pages/LoginPage.js'. .mocharc.json: a timeout of 60 seconds per test, because Mocha’s default of 2 seconds is too short for a browser; retries at 0, so flaky tests stay visible; thespecreporter locally.support/:config.js,driver.js(below) andsession.js.pages/: one page object per page.test/: the spec files.screenshots/: failure screenshots, ignored by Git.
// package.json "scripts": { "test": "mocha" }
// .mocharc.json { "timeout": 60000, "retries": 0, "reporter": "spec" }
// support/config.js: values that differ between machines come from the environment
module.exports = {
baseUrl: process.env.BASE_URL ?? 'http://localhost:3000',
browser: process.env.BROWSER ?? 'chrome', // or 'firefox'
headless: process.env.HEADLESS === '1',
gridUrl: process.env.GRID_URL, // e.g. http://localhost:4444
waitMs: 10_000,
user: {
email: process.env.TEST_USER_EMAIL,
password: process.env.TEST_USER_PASSWORD,
},
};
// support/driver.js: the only place that knows how a browser session is built
const { Builder } = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');
const firefox = require('selenium-webdriver/firefox');
const config = require('./config');
async function buildDriver() {
const chromeOptions = new chrome.Options();
const firefoxOptions = new firefox.Options();
if (config.headless) {
chromeOptions.addArguments('--headless=new');
firefoxOptions.addArguments('-headless');
}
const builder = new Builder()
.forBrowser(config.browser)
.setChromeOptions(chromeOptions)
.setFirefoxOptions(firefoxOptions);
if (config.gridUrl) builder.usingServer(config.gridUrl); // remote browser on a Grid
const driver = await builder.build();
await driver.manage().setTimeouts({ implicit: 0, pageLoad: 30_000, script: 10_000 });
return driver;
}
module.exports = { buildDriver };Run one test, watch it, debug it, then run it in CI
The runner, not Selenium, decides which tests run. Because the config module reads environment variables, the same command can run headed on your laptop, headless, in Firefox or against a Grid.
In CI, the GitHub Actions workflow below (a YAML file in .github/workflows/) runs one job on a fresh Linux machine. Its services: key starts the official selenium/standalone-chrome image as a container next to the job, so the CI machine needs no browser, and the health check with the image’s own check-grid.sh script makes the job wait until the Grid is ready. The steps install the project with npm ci and point the tests at the Grid on port 4444; secrets.* and vars.* read values stored in the repository settings, so no password sits in the file. Two details matter. The browser runs inside the container, where localhost is the container itself, so BASE_URL must be a deployed test environment or another service. And pin a full image tag, as the docker-selenium project recommends, so a browser release does not change your results overnight. Chrome needs more shared memory than Docker’s default, hence --shm-size=2g; the report and screenshots are uploaded even when tests fail.
- Everything:
npm test. One file:npx mocha test/account.spec.js. One test: add-g "wrong password", a regular expression matched against the full test title. - Headed is Selenium’s default: the browser opens on your screen. For a headless run set the variable:
HEADLESS=1 npx mochain bash,$env:HEADLESS='1'; npx mochain PowerShell. - Debug: put a
debugger;statement before the step you want to inspect and runnpx mocha --inspect-brk test/account.spec.js. Attach fromchrome://inspector a JavaScript Debug Terminal in VS Code and step through while the browser stays open;--inspect-brkalso turns off Mocha’s timeout. - Another browser or a Grid:
BROWSER=firefox npx mocha, or, with Docker installed, startdocker run -d -p 4444:4444 -p 7900:7900 --shm-size=2g selenium/standalone-chromeand run withGRID_URL=http://localhost:4444. Port7900shows the container’s browser in a web page (noVNC, default passwordsecret).
# .github/workflows/ui-tests.yml
name: ui-tests
on: [push, pull_request]
jobs:
selenium:
runs-on: ubuntu-latest
services:
selenium: # a one-node Grid with Chrome inside
image: selenium/standalone-chrome:latest # pin a full version tag in real use
ports: ['4444:4444']
options: >-
--shm-size=2g
--health-cmd /opt/bin/check-grid.sh
--health-interval 5s --health-retries 12
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
- run: npm ci
- run: npx mocha --reporter mocha-junit-reporter # writes test-results.xml
env:
GRID_URL: http://localhost:4444
BASE_URL: ${{ vars.STAGING_URL }} # a deployed site, not localhost
TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }}
TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}
- uses: actions/upload-artifact@v7
if: always()
with:
name: ui-test-results
path: |
test-results.xml
screenshots/Signed-in sessions, network stubs, dialogs, tabs and uploads
Every new session starts with an empty profile, so every test would sign in through the form. Sign in once instead, read the cookies with driver.manage().getCookies() and add them to each new browser with addCookie(). WebDriver accepts a cookie only for the site that is open, so first load a small page of that site (the Selenium documentation suggests a 404 page), add the cookies, then open the page under test. Keep at least one test that signs in through the form. If the application keeps its token in localStorage rather than a cookie, copying cookies does nothing: ask the developers where the session lives.
Classic WebDriver has no command to intercept or stub a network request. The common workarounds sit outside the browser: point the test environment at a stub server, or seed the back end so it returns the case you need. The standard route inside the browser is WebDriver BiDi, a W3C draft that adds a two-way WebSocket connection to the session, requested with the webSocketUrl: true capability (options.enableBidi() in selenium-webdriver). Its network module can pause matching requests (network.addIntercept), then answer them with network.provideResponse or fail them with network.failRequest. Selenium is moving its implementation to BiDi, and its high-level network API still differs between languages and releases, so check the current documentation before you build on it.
- Dialogs:
alert,confirmandpromptare not part of the page, so locators cannot see them. Wait withuntil.alertIsPresent(), readgetText(), thenaccept()ordismiss();sendKeys()types into a prompt. While a dialog is open, other commands fail withunexpected alert open. A modal built in HTML is ordinary page content: use locators. - New tabs: a link with
target="_blank"opens a new window handle, but the driver stays on the old one. Remember the current handle, wait for the second one, switch to it, check it, close it and switch back. - Uploads: WebDriver cannot operate the operating system’s file picker, so send the absolute path to the
input[type=file]withsendKeys(). When the browser runs on a Grid, set a file detector so the binding copies the file to the node first.
// support/session.js: sign in once, then give every new browser the session cookies
const { until } = require('selenium-webdriver');
const { buildDriver } = require('./driver');
const { LoginPage } = require('../pages/LoginPage');
const { baseUrl, waitMs, user } = require('./config');
async function captureSession() {
const driver = await buildDriver();
try {
const login = await new LoginPage(driver).open();
await login.signIn(user.email, user.password);
await driver.wait(until.urlContains('/account'), waitMs);
return await driver.manage().getCookies();
} finally {
await driver.quit();
}
}
async function restoreSession(driver, cookies) {
await driver.get(`${baseUrl}/404`); // cookies are accepted only for the open site
for (const cookie of cookies) await driver.manage().addCookie(cookie);
}
module.exports = { captureSession, restoreSession };
// Inside a test: a link that opens a new tab
const original = await driver.getWindowHandle();
await driver.findElement(By.css('[data-testid=returns-policy]')).click();
const twoTabs = async () => (await driver.getAllWindowHandles()).length === 2;
await driver.wait(twoTabs, waitMs);
const handles = await driver.getAllWindowHandles();
await driver.switchTo().window(handles.find((h) => h !== original));
assert.equal(await driver.getTitle(), 'Returns policy');
await driver.close(); // closes the tab, not the session
await driver.switchTo().window(original);
// Inside a test: upload by typing the path into the input, never via the file picker
// const remote = require('selenium-webdriver/remote'); const path = require('node:path');
driver.setFileDetector(new remote.FileDetector()); // only for a remote (Grid) browser
await driver.findElement(By.css('input[type=file]'))
.sendKeys(path.resolve('fixtures/receipt.pdf'));Worked example: four account tests with page objects
The account area of a shop has a login form with test ids, an account page that shows the user’s e-mail and saved addresses, and a remove button that asks for confirmation with the browser’s native confirm() dialog. The four tests cover a successful login, a wrong password, a restored session and a cancelled removal.
The page objects own locators and waits; the tests own the assertions, as in the page object lesson. before captures the session once per file, beforeEach gives every test a fresh browser, and afterEach saves a screenshot when a test fails and always quits the browser. The first test needs no assert: if /account never loads, the wait throws and the test fails. The Java and Python bindings follow the same protocol; only the spelling changes:
- Start a browser: JavaScript
await new Builder().forBrowser('chrome').build(); Javanew ChromeDriver(); Pythonwebdriver.Chrome(). - Find an element:
driver.findElement(By.css(s)); Javadriver.findElement(By.cssSelector(s)); Pythondriver.find_element(By.CSS_SELECTOR, s). - Wait:
driver.wait(until.elementLocated(loc), 10000); Javanew WebDriverWait(driver, Duration.ofSeconds(10)).until(ExpectedConditions.presenceOfElementLocated(loc)); PythonWebDriverWait(driver, 10).until(EC.presence_of_element_located(loc)). - Remote browser on a Grid:
.usingServer(url); Javanew RemoteWebDriver(new URL(url), new ChromeOptions()); Pythonwebdriver.Remote(command_executor=url, options=ChromeOptions()). - Cookies, dialogs and windows: Java matches JavaScript (
driver.manage().addCookie(c),driver.switchTo().alert(),driver.switchTo().window(h)); Python usesdriver.add_cookie(c),driver.switch_to.alertanddriver.switch_to.window(h). Runners: JUnit 5 or TestNG in Java, pytest in Python.
// pages/LoginPage.js: locators and actions of one page; assertions stay in the tests
const { By, until } = require('selenium-webdriver');
const { baseUrl, waitMs } = require('../support/config');
const EMAIL = By.css('[data-testid=login-email]');
const PASSWORD = By.css('[data-testid=login-password]');
const SUBMIT = By.css('[data-testid=login-submit]');
const ERROR = By.css('[data-testid=login-error]');
class LoginPage {
constructor(driver) { this.driver = driver; }
async open() {
await this.driver.get(`${baseUrl}/login`);
await this.driver.wait(until.elementLocated(EMAIL), waitMs);
return this;
}
async signIn(email, password) {
await this.driver.findElement(EMAIL).sendKeys(email);
await this.driver.findElement(PASSWORD).sendKeys(password);
await this.driver.findElement(SUBMIT).click();
}
async errorText() {
const el = await this.driver.wait(until.elementLocated(ERROR), waitMs);
await this.driver.wait(until.elementIsVisible(el), waitMs);
return el.getText();
}
}
module.exports = { LoginPage };
// pages/AccountPage.js: same pattern (imports as in LoginPage.js)
const ACCOUNT_EMAIL = By.css('[data-testid=account-email]');
const ADDRESS = By.css('[data-testid=address]');
const REMOVE_ADDRESS = By.css('[data-testid=address-remove]');
class AccountPage {
constructor(driver) { this.driver = driver; }
async open() {
await this.driver.get(`${baseUrl}/account`);
await this.driver.wait(until.elementLocated(ACCOUNT_EMAIL), waitMs);
return this;
}
async email() { return this.driver.findElement(ACCOUNT_EMAIL).getText(); }
async addressCount() { return (await this.driver.findElements(ADDRESS)).length; }
// only clicks: the confirm() dialog that opens belongs to the test
async removeFirstAddress() { await this.driver.findElement(REMOVE_ADDRESS).click(); }
}
module.exports = { AccountPage };
// test/account.spec.js
const assert = require('node:assert/strict');
const fs = require('node:fs');
const { until } = require('selenium-webdriver');
const { buildDriver } = require('../support/driver');
const { captureSession, restoreSession } = require('../support/session');
const { LoginPage } = require('../pages/LoginPage');
const { AccountPage } = require('../pages/AccountPage');
const { waitMs, user } = require('../support/config');
describe('Account', function () {
let driver;
let cookies;
before(async function () { cookies = await captureSession(); });
beforeEach(async function () { driver = await buildDriver(); });
afterEach(async function () {
if (this.currentTest.state === 'failed') {
fs.mkdirSync('screenshots', { recursive: true });
const png = await driver.takeScreenshot();
fs.writeFileSync(`screenshots/${this.currentTest.title}.png`, png, 'base64');
}
await driver.quit();
});
it('signs in with valid credentials', async function () {
const login = await new LoginPage(driver).open();
await login.signIn(user.email, user.password);
await driver.wait(until.urlContains('/account'), waitMs); // the wait is the check
});
it('rejects a wrong password and stays on the login page', async function () {
const login = await new LoginPage(driver).open();
await login.signIn(user.email, 'not-the-password');
assert.equal(await login.errorText(), 'Wrong e-mail or password');
assert.ok((await driver.getCurrentUrl()).includes('/login'));
});
it('opens the account of a restored session', async function () {
await restoreSession(driver, cookies);
const account = await new AccountPage(driver).open();
assert.equal(await account.email(), user.email);
});
it('keeps the address when the removal is cancelled', async function () {
await restoreSession(driver, cookies);
const account = await new AccountPage(driver).open();
const count = await account.addressCount();
await account.removeFirstAddress();
const dialog = await driver.wait(until.alertIsPresent(), waitMs);
assert.equal(await dialog.getText(), 'Remove this address?');
await dialog.dismiss();
assert.equal(await account.addressCount(), count);
});
});Common mistakes
- Using
driver.sleep(3000)to wait for the page, or setting an implicit wait and adding explicit waits on top. Wait for a condition withdriver.wait()and keep the implicit wait at 0. - Calling
driver.quit()only at the end of a passing test. A failed test then leaves a browser and a driver process running, and on a Grid it keeps a slot busy; quit inafterEachorfinally. - Keeping an element found before a navigation or re-render and using it afterwards, which fails with
stale element reference. Find elements inside the page object method that uses them. - Keeping an old chromedriver on the
PATHor in the repository. Selenium Manager steps in only when no driver is found, so the outdated one is used and fails after the next browser update. - Pointing a browser that runs in a container or on a Grid node at
localhost, which there means the container, not your machine or the CI runner. - Turning on retries so the build goes green without recording which tests needed a retry. A retry contains a flaky test; it does not fix it.
The QA perspective
When you join a team with a Selenium suite, first run one spec file against the test environment from the command line, then read the driver factory and the config: where the base URL, browser, credentials and Grid address come from. When a test fails, read the error before the stack trace. no such element means the locator is wrong or the page is not in the expected state; element click intercepted usually means a banner, overlay or spinner covers the target; a TimeoutError from a wait means the condition never became true, which may be a defect or a wrong expectation. Record the screenshot, the exact command and the browser version, which (await driver.getCapabilities()).get('browserVersion') returns, in the bug report or the flaky-test ticket.
Agree with developers on data-testid attributes and on a supported way to create sessions and data in test environments, such as an API login or a seed endpoint. Agree with whoever owns CI on the pinned Grid image and the browsers you run. For flows through third-party pages such as a payment provider or a single sign-on service, use the provider’s sandbox or stop at your own boundary rather than automating someone else’s production site.
Practice exercise
Make a laptop-only test work in CI
The test below passed on its author’s laptop, where GRID_URL is not set and Chrome runs locally. Since then two things have changed. The suite now runs in the CI job from this lesson, where the browser runs in the selenium/standalone-chrome container. And the application changed: the Help link now opens in a new tab, and the avatar form re-renders after a file is chosen, replacing its buttons. The test now fails.
Find every problem, say which failure each one can cause, and describe the corrected tests.
// test/profile.spec.js: passed on the author's laptop, now fails in CI
const assert = require('node:assert/strict');
const { By } = require('selenium-webdriver');
const { buildDriver } = require('../support/driver');
describe('Profile', function () {
it('uploads an avatar and opens help', async function () {
const driver = await buildDriver();
await driver.get('http://localhost:3000/login');
await driver.findElement(By.id('email')).sendKeys('sara@example.com');
await driver.findElement(By.id('password')).sendKeys('Sara-Pass-2026');
await driver.findElement(By.id('submit')).click();
await driver.sleep(3000); // login redirects to /profile
const save = await driver.findElement(
By.xpath('/html/body/div[2]/main/div[3]/form/button[2]'));
await driver.findElement(By.css('input[type=file]')).sendKeys('avatar.png');
await save.click();
await driver.findElement(By.linkText('Help')).click(); // opens in a new tab
assert.equal(await driver.getTitle(), 'Help centre');
await driver.quit();
});
});- Mark every hard-coded value and decide where it should come from.
- Find each place where the test acts before the page is ready or keeps an element too long.
- Check how the upload and the Help link behave when the browser is remote and opens a new tab.
- Split the test, move the locators into a page object and write the corrected version.
Show the model answer
Configuration: http://localhost:3000 is the container itself when the browser runs on the Grid, so the page never loads in CI. Use baseUrl from the config module. The e-mail and password belong in environment variables or CI secrets, not in the code.
Timing and locators: driver.sleep(3000) is a guess that is too short on a slow runner and wasted time on a fast one; wait for until.urlContains('/profile') or let the page object’s open() wait. The absolute XPath breaks with any layout change; ask for a test id such as [data-testid=avatar-save]. save is found before the upload and the re-render replaces the button, so save.click() fails with stale element reference, or passes only when the click happens to win the race; find the button right before clicking it, inside the page object.
Upload: sendKeys('avatar.png') needs an absolute path such as path.resolve('fixtures/avatar.png'), and a remote browser also needs driver.setFileDetector(new remote.FileDetector()); otherwise the node looks for a file that exists only on the runner. Then assert the result, for example a success message, instead of assuming that no error means success.
New tab: the driver stays on the profile tab, so getTitle() reads the profile page and the assertion fails. Remember the original handle, wait for two handles, switch to the new one, assert the title "Help centre", close it and switch back.
Structure: one test checks two unrelated behaviours and signs in through the form, which the login tests already cover. Split it into "uploads an avatar" and "Help opens in a new tab", restore the session from cookies in both, and move driver.quit() into afterEach, which also runs after a failure, with a screenshot on failure.
Frequently asked questions
Do I still need to download chromedriver myself?
Usually not. Since Selenium 4.6, Selenium Manager finds a matching driver, or downloads one, when no driver is on the PATH. Install drivers yourself only where the machine cannot download them, and then pin the browser and driver versions together, for example through a pinned Docker image.
Should a new project choose Selenium or Playwright?
Decide by your team’s language, the browsers and devices you must cover, and the infrastructure you already run. Selenium follows a W3C standard implemented by the browser vendors and has bindings in more languages; Playwright ships its own runner with auto-waiting, network interception and a trace viewer. Both can carry a maintainable suite; suite design and test data discipline matter more than the tool.
Is Selenium IDE the same as Selenium WebDriver?
No. Selenium IDE is a tool that records and replays clicks. WebDriver is the library and protocol you use to write tests as code. A recording is useful for exploring a page, but it needs page objects, explicit waits and better locators before it belongs in a suite.
References
- WebDriver (W3C)The W3C protocol browsers implement for remote control by automation tools such as Selenium (the Level 2 edition is a Working Draft).
- Selenium DocumentationOfficial documentation for browser automation with WebDriver, including waits, locators and Grid.
- Waiting Strategies — SeleniumWhy browser tests race the page, and how implicit and explicit waits synchronise a WebDriver script with the application.
- GitHub Actions documentationHow workflows, jobs, runners and artifacts are defined in GitHub Actions — where automated tests run on each push or pull request.
Practise with Mutqan tools
Beautify HTML with proper indentation of nested elements, preserving pre, script and style blocks.
Parse Set-Cookie headers and check Secure, HttpOnly, SameSite, Domain, Path, expiry and size best practices.
Format and validate YAML with consistent indentation and quoting.
Beautify JavaScript and TypeScript code with consistent indentation and spacing.
Go deeper
Your progress on this topic
Not started
Mark the topic complete when you have studied it and done the exercise.
Sign in to track your progress
Mark topics complete, continue where you left off and unlock specialisation exams.