PRACTICAL GUIDE / Selenium TypeScript capabilities factory
Stop capability state from leaking between Selenium sessions
Build fresh, typed Selenium browser options in TypeScript, diagnose mismatched sessions, and stop capability state from leaking across tests.
In this guide6 sections
What you will learn
- Why a shared options object poisons later sessions
- Build a fresh request from typed intent
- Diagnose the request before blaming Grid
- Tell capability defects from their near misses
A Chrome test passes alone but opens headed when the suite runs in parallel. Another test added an argument to a shared options object, and the next session inherited a request nobody intended. The browser is behaving consistently; the framework has lost control of its own input.
That bug is easy to dismiss as a Grid problem because it appears during session creation. It is also easy to hide with a retry because a fresh worker may build a different request. A useful capabilities factory does one narrow job: it converts validated, immutable test intent into a new Selenium request for one session.
Why a shared options object poisons later sessions
Capabilities take part in the new-session handshake. The local Selenium binding sends requested values such as browserName, browserVersion, platformName, and browser-specific options. A local driver, Grid, or cloud endpoint processes that request and returns the capabilities of the session it created. Those two views are related, but they are not interchangeable. The request describes what the test asked for. The returned capabilities describe what the remote end says it supplied.
In the JavaScript binding, Chrome and Firefox options are objects with mutating methods. Calling addArguments() changes the instance. Builder methods also retain configuration until build() uses it. A module-level singleton therefore turns configuration into shared state. The first caller may add headless mode, the second may add a proxy, and a third may receive both. Nothing in TypeScript's type system makes a mutable object safe merely because it was declared with const; const prevents reassignment of the variable, not mutation of the object it references.
The timing makes this failure deceptive. If every test configures the singleton during module loading and sessions start sequentially, the final state may look stable. Parallel workers, conditional setup, or a test that adds an argument only on CI expose the leak. The session that fails is often not the session that performed the mutation. Its stack trace points at build(), while the cause sits in an earlier test or helper.
A factory removes that temporal coupling only if it creates every mutable Selenium object inside the call. Returning the same chrome.Options from a cache under a new wrapper does not help. Cloning a shallow JavaScript object is also insufficient when it contains nested arrays or provider option objects. The safest default is to keep the input profile made of primitives and readonly collections, then translate it into fresh Selenium classes at the last responsible moment.
Browser options are not a bag of universal flags. --headless=new is a Chromium launch argument. Firefox accepts its own arguments. A Grid matching key such as platformName belongs to the WebDriver request, while a cloud vendor's settings belong under that vendor's namespaced capability. Treating all of them as arbitrary strings makes unsupported combinations representable. It also makes a typo compile cleanly and fail much later at the remote endpoint.
The factory should not promise that a request will be accepted. Grid capacity, installed browsers, driver compatibility, and provider policy remain outside it. Its contract is smaller and testable: reject invalid application configuration, choose the right browser option type, make a new request object per call, and expose enough sanitized intent to diagnose what was sent.
Build a fresh request from typed intent
A discriminated union gives the compiler useful information. When browser is chrome, Chrome-only fields are available. When it is firefox, the factory cannot accidentally read them. Keeping the remote URL outside the browser profile also separates transport from browser behavior.
The following factory supports two explicit profiles. It does not accept a generic capability map. That is deliberate. New settings enter through code review, validation, and a browser-specific translation rather than through an unexamined JSON blob.
import { Browser, Builder, WebDriver } from "selenium-webdriver";
import * as chrome from "selenium-webdriver/chrome";
import * as firefox from "selenium-webdriver/firefox";
type WindowSize = Readonly<{ width: number; height: number }>;
export type BrowserProfile =
| Readonly<{
browser: "chrome";
headless: boolean;
window: WindowSize;
binaryPath?: string;
}>
| Readonly<{
browser: "firefox";
headless: boolean;
window: WindowSize;
binaryPath?: string;
}>;
function checkedRemoteUrl(value: string | undefined): string | undefined {
if (value === undefined || value.trim() === "") return undefined;
const url = new URL(value);
if (url.protocol !== "http:" && url.protocol !== "https:") {
throw new Error("SELENIUM_REMOTE_URL must use http or https");
}
return url.toString();
}
export function makeBuilder(
profile: BrowserProfile,
remoteUrl = checkedRemoteUrl(process.env.SELENIUM_REMOTE_URL),
): Builder {
if (profile.window.width < 320 || profile.window.height < 240) {
throw new Error("Browser window is too small for this suite");
}
let builder: Builder;
switch (profile.browser) {
case "chrome": {
const options = new chrome.Options();
options.addArguments(
`--window-size=${profile.window.width},${profile.window.height}`,
);
if (profile.headless) options.addArguments("--headless=new");
if (profile.binaryPath) options.setChromeBinaryPath(profile.binaryPath);
builder = new Builder()
.disableEnvironmentOverrides()
.forBrowser(Browser.CHROME)
.setChromeOptions(options);
break;
}
case "firefox": {
const options = new firefox.Options().windowSize(profile.window);
if (profile.headless) options.addArguments("-headless");
if (profile.binaryPath) options.setBinary(profile.binaryPath);
builder = new Builder()
.disableEnvironmentOverrides()
.forBrowser(Browser.FIREFOX)
.setFirefoxOptions(options);
break;
}
default: {
const unhandled: never = profile;
throw new Error(
`No builder branch for browser profile: ${JSON.stringify(unhandled)}`,
);
}
}
return remoteUrl ? builder.usingServer(remoteUrl) : builder;
}
export async function startDriver(
profile: BrowserProfile,
remoteUrl?: string,
): Promise<WebDriver> {
return makeBuilder(profile, checkedRemoteUrl(remoteUrl)).build();
}Notice what the code refuses to do. It does not read BROWSER in several helpers. It does not combine Chrome and Firefox options and hope the selected browser ignores the irrelevant half. It does not copy arbitrary environment variables into capabilities. It does not log the URL, which may contain credentials in some environments. Those omissions keep the factory's authority understandable.
Parsing the environment should be another small boundary. Convert strings to a known profile once, fail on unknown values, and pass the result down. A truthy check is not enough for booleans because the string "false" is truthy in JavaScript. Accept a tiny vocabulary and reject everything else.
import type { BrowserProfile } from "./capabilities-factory";
function booleanSetting(name: string, value: string | undefined): boolean {
if (value === "true") return true;
if (value === "false") return false;
throw new Error(`${name} must be exactly "true" or "false"`);
}
const SUPPORTED_BROWSERS: Record<BrowserProfile["browser"], true> = {
chrome: true,
firefox: true,
};
function isSupportedBrowser(
value: string | undefined,
): value is BrowserProfile["browser"] {
return value !== undefined && Object.hasOwn(SUPPORTED_BROWSERS, value);
}
export function profileFromEnv(
env: NodeJS.ProcessEnv,
): BrowserProfile {
const browser = env.E2E_BROWSER;
if (!isSupportedBrowser(browser)) {
throw new Error(
`E2E_BROWSER must be one of: ${Object.keys(SUPPORTED_BROWSERS).join(", ")}`,
);
}
const headless = booleanSetting("E2E_HEADLESS", env.E2E_HEADLESS);
const window = { width: 1440, height: 900 } as const;
const binaryPath = env.E2E_BROWSER_BINARY?.trim() || undefined;
return browser === "chrome"
? { browser, headless, window, binaryPath }
: { browser, headless, window, binaryPath };
}There is some duplication in the return expression, and that is acceptable. Browser configuration is a place where explicit branches age better than clever generic merging.
Two constructs, and only those two, make the compiler enforce that. The first is the const unhandled: never = profile; line in the factory's default arm. Once every discriminant value has a case, TypeScript narrows profile to never there and the assignment is legal; add a member and the narrowed type is that new member, which is not assignable to never. The second is Record<BrowserProfile["browser"], true>, which forces the environment parser's allow list to name every browser the union declares. Adding an edge member to the union and running the same npx tsc --noEmit the CI job runs produces exactly two diagnostics: Type 'Readonly<{ browser: "edge"; ... }>' is not assignable to type 'never' in the factory, and Property 'edge' is missing in type '{ chrome: true; firefox: true; }' in the parser.
Neither is optional decoration, and the earlier if (profile.browser === "chrome") { ... } else { ... } shape provided neither. With an if and a bare else, the else branch happily accepts every non-Chrome member, so adding edge compiles with zero diagnostics and makeBuilder({ browser: "edge", ... }) returns a builder whose requested browserName is firefox, with Firefox options attached and Chrome options null. That is the exact failure this section attributes to a loose dictionary: a new label accepted silently with its semantics quietly borrowed from an unrelated branch. An exhaustiveness arm is what turns the discriminated union from documentation into a check.
Diagnose the request before blaming Grid
Start with three identities: the test attempt, the requested profile, and the created session. The test runner already has an attempt or worker identifier. Log the profile before build(), then log a whitelist of returned capabilities after it resolves. If build() rejects, there is no session and no returned capability set; preserve the sanitized request and the error instead of inventing a session record.
Do not dump every returned capability. Provider extensions can contain account metadata, proxy details, debugger addresses, or paths that have no place in a public CI artifact. Four standard fields usually answer the first diagnostic question.
import type { WebDriver } from "selenium-webdriver";
import {
startDriver,
type BrowserProfile,
} from "./capabilities-factory";
export function requestedProfile(profile: BrowserProfile) {
return {
browser: profile.browser,
headless: profile.headless,
window: profile.window,
hasCustomBinary: profile.binaryPath !== undefined,
};
}
export async function returnedSession(driver: WebDriver) {
const caps = await driver.getCapabilities();
return {
browserName: caps.getBrowserName(),
browserVersion: caps.getBrowserVersion(),
platformName: caps.getPlatform(),
pageLoadStrategy: caps.getPageLoadStrategy(),
};
}
export async function startWithEvidence(
profile: BrowserProfile,
): Promise<WebDriver> {
const attemptId = process.env.TEST_ATTEMPT_ID ?? "local";
console.info(JSON.stringify({
event: "webdriver.request",
attemptId,
profile: requestedProfile(profile),
}));
const driver = await startDriver(profile, process.env.SELENIUM_REMOTE_URL);
console.info(JSON.stringify({
event: "webdriver.session.started",
attemptId,
session: await returnedSession(driver),
}));
return driver;
}Suppose a test requests Firefox headless but the runner fails with a display-related browser startup error. The request log proves whether the parser selected Firefox and whether headless was true. If both are correct, inspect the factory branch and driver service log. If the request says Chrome, the defect is upstream in environment selection. If Firefox starts and the returned capabilities identify Firefox, the capabilities factory did its job; investigate the browser process or container display configuration next.
Consider a second case: Grid cannot create a session after the team pins platformName or browserVersion. Do not infer a matcher defect from the client exception alone. Compare the requested standard capabilities with the Grid node stereotypes and available slots. The WebDriver specification allows the remote end to match a requested browser version using an implementation-defined comparison algorithm, so a framework should not fabricate its own claim about which version strings a particular Grid will accept. Record the exact requested value and use the Grid's own configuration and logs as evidence.
A third case looks like leakage but is really process configuration. Two CI jobs use the same test code, yet only one opens headed. If each job runs in a separate process, they cannot share an in-memory options instance. Compare E2E_HEADLESS, the selected workflow matrix value, and the request event. An absent variable that falls back to a local default is different from a mutated object. The evidence diverges before Selenium creates a session.
Unit tests can catch genuine object reuse without opening a browser. The oracle must observe behavior that would change if the factory regressed. This test mutates one result and proves that another result does not acquire the mutation. It also proves that the browser branches construct different option types.
import assert from "node:assert/strict";
import test from "node:test";
import { makeBuilder } from "../src/capabilities-factory";
const window = { width: 1440, height: 900 } as const;
test("each Chrome request owns its options", () => {
const first = makeBuilder({ browser: "chrome", headless: false, window });
const second = makeBuilder({ browser: "chrome", headless: false, window });
const firstOptions = first.getChromeOptions();
const secondOptions = second.getChromeOptions();
assert.ok(firstOptions);
assert.ok(secondOptions);
assert.notStrictEqual(firstOptions, secondOptions);
firstOptions.addArguments("--incognito");
const secondChromeConfig = secondOptions.get("goog:chromeOptions") as
| { args?: string[] }
| undefined;
assert.equal(secondChromeConfig?.args?.includes("--incognito") ?? false, false);
});
test("Firefox requests never carry Chrome options", () => {
const builder = makeBuilder({ browser: "firefox", headless: true, window });
assert.equal(builder.getChromeOptions(), null);
assert.ok(builder.getFirefoxOptions());
});Depending on the binding version, an unset builder option may be represented as null or undefined in its declarations. If your installed type definition says undefined, assert that value instead. The important check is not a memorized sentinel. It is that the Firefox branch did not construct or retain Chrome options.
Here is how the shared-state failure usually unfolds in a real suite. A smoke project imports defaultChromeOptions and adds headless mode during setup. A visual project imports the same object and expects a visible browser. Import order decides whether the visual project inherits headless mode. When a runner changes file discovery order or splits tests across workers, the symptom moves. Put a temporary stack trace beside each mutation of the singleton, and the earlier caller becomes visible. If no mutation trace appears in the failing process, stop pursuing the shared-object theory and compare worker configuration instead.
That distinction matters because parallel test runners isolate state differently. Worker threads can share module objects within a process only when the runner arranges that sharing. Separate operating-system processes have separate heaps. Separate CI jobs cannot mutate one another's JavaScript objects at all. They can still consume the same incorrect environment variable, cache artifact, or Grid endpoint. Identify the process and worker IDs before claiming that an object crossed a boundary it physically could not cross.
A custom browser binary creates another useful worked example. The Chrome option API accepts a binary path, but that path is resolved on the machine that launches Chrome. With a local driver, /opt/chrome/chrome refers to the test machine. With a remote Grid, the same string is consumed on the node. A developer can confirm that the file exists locally and still send a path that does not exist in the container. The request log will show hasCustomBinary: true; the Grid node log, not the client filesystem, is where the startup failure must be investigated.
The corrective action is not to copy the local binary path into every environment. Make binaryPath an environment-specific deployment choice and omit it when the node's normal browser discovery is authoritative. This costs some uniformity: local and remote profiles may no longer be textually identical. It gains a more important property, which is that each path is meaningful in the filesystem where it is used.
Version pinning has a different evidence pattern. Imagine the profile asks for a specific browser version and build() never returns. There is no returned session to inspect. The sanitized request contains the requested version, while Grid status and node configuration show whether matching capacity exists. If the request reaches an external provider, use that provider's job or request identifier from its supported reporting surface. Do not substitute a screenshot from a retried session; it belongs to a different negotiation.
When a session does start, compare requested intent with returned facts without demanding string equality for fields the remote end owns. A request may omit a version and receive a concrete installed version. That is normal information, not drift. If a request includes a value that matters to coverage, evaluate it according to the endpoint's documented matching rules and your own policy. The factory can report both sides, but it should not quietly rewrite a returned version to resemble the request.
Nested provider options deserve the same isolation test as browser options. A common leak starts with a module-level object holding a build name and test name. One test changes the name, then concurrent sessions report under the same provider job. If you support such metadata, create the nested object inside the adapter call and derive the test name from the current runner identity. A shallow spread of the outer capability object does not protect a nested object that all calls still reference.
Be careful with snapshots of options internals. They are useful as focused contract tests, but keys such as goog:chromeOptions expose the binding's serialized representation. A Selenium upgrade can legitimately alter nonessential details. Assert only the behavior your framework owns: separate instances, required arguments, selected browser, and validated inputs. Let the smoke test cover the complete wire representation against a real endpoint.
The test above has an oracle that can fail for the intended regression. If makeBuilder() is changed to return one cached options instance, notStrictEqual fails. If a later refactor shallow-copies the wrapper while retaining the nested argument list, mutating the first result makes the second assertion fail. That is stronger than asserting that a hard-coded profile contains the same browser literal used to create it, which would tell you nothing about the factory.
Tell capability defects from their near misses
A session creation error groups several boundaries into one stack trace. The factory may have produced an invalid value. Grid may have no matching slot. The driver executable may not support the installed browser. A browser process may start and exit. Authentication to a remote provider may fail. Treating all of these as “bad capabilities” creates random edits to otherwise correct code.
Use the last boundary that produced trustworthy evidence. If validation rejects E2E_BROWSER=safari before build(), the factory found a configuration defect. If Grid records the request but reports no matching capacity, compare only the matching fields and stereotypes. If a node accepts the session and then ChromeDriver reports that Chrome exited, capability matching already happened; inspect the node's browser logs, container resources, binary path, and startup arguments.
An application navigation failure is later still. Once driver.get() runs, a page timeout, DNS failure, TLS problem, or application error does not become a capability issue merely because headless mode is involved. Capture the current URL, page title when available, browser console evidence your binding supports, and network or server logs appropriate to the application. Keep that evidence attached to the same attempt ID as the session record.
Returned capabilities are useful, but they do not prove that every requested launch argument took effect. Standard capabilities have defined response fields. Browser-specific response data varies. If a headless-specific rendering difference matters, assert a product-visible condition or collect browser-level evidence. Do not claim that seeing browserName: chrome proves a particular Chromium command-line switch was honored.
Watch for accidental environment overrides as well. Selenium's JavaScript Builder supports environment-based configuration, including browser and remote URL settings. A team that combines explicit builder calls with ambient variables can create two sources of truth. Decide whether your framework reads and validates the variables itself or delegates to Selenium's Builder behavior. Explicit parsing is usually easier to audit because an unexpected process variable cannot silently change the selected transport.
Provider capability namespaces form another near miss. A cloud service may require a namespaced object for build labels, tunnel selection, or device settings. Selenium cannot validate the provider's private schema for you. Keep that object behind a provider-specific adapter, validate the fields your team uses, and redact it in logs. Do not add unknown keys to the common profile until every local run carries cloud-only vocabulary.
Roll the change through an existing suite
Replacing a shared singleton in one commit can expose callers that have been mutating it as an undocumented extension point. Begin by locating every import of the old options object and every call to a mutating method. Classify each mutation as a suite default, a browser-specific default, or genuine per-test intent. Per-test browser launch changes are expensive because they require a separate session; many belong in a separate project or CI matrix entry rather than inside an individual test.
Introduce the typed profile while the old path still exists, but route one small CI job through the new factory. Log both the validated profile and returned standard capabilities. Do not create two browser sessions per test merely to compare factories. That doubles load and can change scheduling behavior. Compare configuration at the request-construction level, then run a representative smoke slice with real sessions.
Next, move suite defaults into the factory and delete caller mutations as each area migrates. Make the old singleton inaccessible rather than marking it deprecated indefinitely. A deprecation comment does not stop an urgent test fix from calling addArguments() on it. Compile errors are a useful migration tool here.
The CI matrix should make browser intent visible. This job assumes typescript, tsx, and selenium-webdriver are locked in package-lock.json, and that the unit test and smoke test paths shown exist. The Grid service is health-checked before the smoke test, so a container that never became ready is not mislabeled as a test failure.
name: webdriver-capability-contract
on: [pull_request]
jobs:
capability-contract:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
browser: [chrome, firefox]
services:
selenium:
image: selenium/standalone-${{ matrix.browser }}:4.44.0-20260505
ports:
- 4444:4444
options: >-
--shm-size=2g
--health-cmd "/opt/bin/check-grid.sh --host 0.0.0.0 --port 4444"
--health-interval 5s
--health-timeout 3s
--health-retries 20
env:
E2E_BROWSER: ${{ matrix.browser }}
E2E_HEADLESS: "true"
SELENIUM_REMOTE_URL: http://localhost:4444
TEST_ATTEMPT_ID: ${{ github.run_id }}-${{ matrix.browser }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx tsc --noEmit
- run: node --import tsx --test test/capabilities-factory.test.ts
- run: node --import tsx test/session-smoke.tsPin the Selenium image to a version your team has qualified, then update it deliberately. A floating image tag turns a capability contract job into an uncontrolled dependency probe. The cost is maintenance: someone must schedule updates and inspect release notes. The benefit is that an unrelated image publication cannot rewrite the conditions of an old commit.
The factory itself also adds code and review friction. Every supported setting needs a type, validation rule, translation, and test. That is the price of rejecting ambiguous requests early. Avoid paying it for values that tests never vary. A ten-field profile with two meaningful fields is bureaucracy, not safety.
Real browser smoke tests cost queue time and infrastructure capacity. Keep most factory tests browser-free, then use a small session test for each supported browser and transport. Unit tests prove construction and isolation. The smoke slice proves the installed binding, Grid, driver, and browser can negotiate a session together. Neither replaces the other.
Rollout also needs a removal condition. Set a date or milestone after which imports of the old helper fail linting or compilation, and name the owner of provider-specific adapters. Without that line, both paths survive, new tests copy whichever example they find first, and incident evidence remains inconsistent. The temporary dual path should reduce migration risk, not become the permanent architecture.
Monitor outcomes that the change can plausibly affect. Count rejected configuration inputs, session-creation failures grouped by browser and endpoint, and retries that succeed with an unchanged profile. Do not claim the factory improved all test stability. Locator races, application latency, and data collisions sit elsewhere. A narrower claim makes a regression easier to notice and keeps the new abstraction accountable.
Know when a factory is the wrong abstraction
A suite with one browser, one launch configuration, and a single local process may be clearer with a ten-line fixture that constructs ChromeOptions directly. Adding a public factory, profile hierarchy, and provider adapters would create more places to look without removing meaningful variation. Extract the abstraction when configuration has at least two real consumers or when shared mutation has already become a risk.
Do not use a capability factory to switch browser state that Selenium can change after session creation. Window navigation, cookies, local storage, and application login belong to fixtures or domain helpers with their own cleanup. Restarting the browser for every small state change makes tests slower and confuses launch configuration with test setup.
Avoid a universal Record<string, unknown> escape hatch. It feels flexible, but it restores every failure the typed boundary was meant to prevent. If a provider needs private capabilities, create a provider-specific type and adapter. If a one-off experiment truly requires an unknown key, keep it in an isolated test project until the team understands its contract.
A factory is also the wrong place to conceal Grid scarcity. Removing browserVersion until any node accepts the request may get a build moving, but it changes coverage. Queue time, unavailable nodes, and mismatched stereotypes should be visible as infrastructure signals. Change the requested coverage only through an explicit test policy decision.
Finally, do not add a retry inside startDriver() without exposing attempts. A second request may land on another node and pass, erasing the first request and its error. If session creation retries are part of policy, the caller should assign a new attempt ID, preserve each sanitized request and failure, and apply a bounded rule. A green retry is evidence that one attempt succeeded, not proof that capability construction was correct on the first attempt.
// FIELD DISPATCH
Get the QA Field Notes
Weekly QA battles, AI testing guides, and interview drills. Free on Substack.
// LIVE COURSE / THE TESTING ACADEMY
Playwright Automation Mastery
Go beyond Selenium. Master Playwright with JS/TS in 90 days.
From the instructor behind this guide.
Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.
PRIMARY REFERENCES
Verify the details at the source
QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.
- 01Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
- 02Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
- 03Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
- 04Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
FAQ / QUICK ANSWERS
Questions testers ask
Can I reuse one Chrome Options object for every Selenium test?
Reusing one is unsafe. Options expose mutating methods, so a later caller can change what another session will request. Create and configure a new options instance for each call instead.
How do I compare requested capabilities with the session Selenium created?
Log the validated input before build(), then read the returned values with driver.getCapabilities() after the session starts. Keep the log to a safe whitelist so provider tokens and proxy credentials never enter test artifacts.
Why does headless mode work in Chrome but fail in Firefox?
Browser launch arguments are not portable WebDriver capabilities. Translate one typed headless setting into the browser-specific option for each supported browser, and test both branches independently.
Should a capability factory return a WebDriver or a Builder?
Either boundary can work, but returning a Builder makes request construction easy to test without starting a browser. Returning a driver gives the factory clearer ownership of session creation, so choose one boundary and document who must call quit().
Where should Selenium environment variables be validated?
Parse them once at the process boundary and reject unknown browser names, malformed URLs, and contradictory values before a session request is sent. Test code should receive a validated profile rather than read process.env throughout the suite.
RELATED GUIDES
Continue the learning route
GUIDE 01
Advanced Selenium Java: Typed Driver Factories and JUnit Extension Lifecycles
Build typed Selenium Java driver factories with test-scoped JUnit extensions, parallel-safe stores, failure capture, and guaranteed WebDriver cleanup.
GUIDE 02
Selenium TypeScript ESM Setup for WebDriver
Learn Selenium TypeScript ESM setup with working code, failure cases, debugging steps, and CI evidence for reliable QA automation in practice.
GUIDE 03
Migrate Selenium 3 Capabilities and Grid Configuration to Selenium 4
Migrate Selenium 3 suites to Selenium 4 with W3C capability names, typed browser options, side-by-side Grid rollout, compatibility checks, and rollback.
GUIDE 04
Build a Selenium Grid Capacity Dashboard with GraphQL
Build a Selenium Grid GraphQL dashboard for node health, sessions, queue depth, compatible slot capacity, stale-data handling, and actionable alerts.
GUIDE 05
Selenium Java Grid Session Factory Architecture
A practical guide to Selenium Java grid session architecture, covering design, implementation, debugging, scale, measurable release gates, and senior interview scenarios.