Selenium Interview Questions and Answers

Last updated:

Check out 46 of the most common Selenium interview questions, then take an AI-powered practice interview

WebDriverTest AutomationGridPage Object ModelCross-browser
46+
Questions
19
Basic
18
Intermediate
9
Advanced
Q1

What changed between Selenium 3 and Selenium 4, and why does W3C WebDriver compliance matter in practice?

BasicFundamentals

Answer

Selenium 3 spoke the old JSON Wire Protocol and every command was translated by the driver binary into whatever each browser understood. Selenium 4 dropped that translation layer entirely and speaks the W3C WebDriver specification natively, which is the protocol Google, Mozilla, Apple and Microsoft implement in chromedriver, geckodriver, safaridriver and msedgedriver. The practical result is fewer cross-browser surprises: an element interaction that works in Chrome behaves the same way in Firefox because both drivers implement the same spec text for element interactability, pointer actions and error codes.

Beyond the protocol, Selenium 4 brought Selenium Manager for automatic driver resolution, relative locators, native tab and window creation with newWindow, element-level screenshots, getShadowRoot, wheel scroll actions, Duration-based timeout APIs, and a completely rewritten Grid. It also removed DesiredCapabilities in favour of browser-specific Options classes, so ChromeOptions, FirefoxOptions and EdgeOptions are now the only supported way to configure a session. Interviewers ask this because a large share of Indian candidates learned on Selenium 3 tutorials and still write new WebDriverWait(driver, 10) or set system properties for chromedriver, both of which are dead code in Selenium 4. Naming three or four concrete Selenium 4 features and one thing that was removed is enough to clear this question convincingly.

// Selenium 3 style (does not compile against Selenium 4)
// System.setProperty("webdriver.chrome.driver", "/usr/local/bin/chromedriver");
// DesiredCapabilities caps = DesiredCapabilities.chrome();
// WebDriverWait wait = new WebDriverWait(driver, 10);

// Selenium 4 style
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.time.Duration;

ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new", "--window-size=1920,1080");

// No system property: Selenium Manager resolves the driver
WebDriver driver = new ChromeDriver(options);
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

Key Points

  • JSON Wire Protocol replaced by native W3C WebDriver
  • DesiredCapabilities removed; use ChromeOptions / FirefoxOptions / EdgeOptions
  • Selenium Manager, relative locators, getShadowRoot, wheel actions added
  • Timeouts take java.time.Duration, not raw integers
  • Grid 4 is a full rewrite with Router, Distributor and Session Queue
💡 Pro Tip: If your answer mentions System.setProperty for chromedriver, the interviewer immediately knows your knowledge stopped at Selenium 3.
Q2

What is Selenium Manager and how has it replaced WebDriverManager and manual driver downloads?

BasicTooling

Answer

Selenium Manager is a small Rust binary shipped inside the Selenium client libraries since the 4.6 line. When you instantiate a driver and no explicit driver path is configured, Selenium detects the installed browser version, resolves the matching driver from the vendor endpoints (Chrome for Testing for Chrome and chromedriver, the Mozilla and Microsoft endpoints for the others), downloads it into a local cache, and starts the session. In recent 4.x releases it can also download the browser itself when the requested version is not installed locally, which is how you pin a build in CI without baking browsers into the image.

This killed two long-standing pain points. First, the classic error message 'session not created: This version of ChromeDriver only supports Chrome version 121' that used to break every suite the morning after Chrome auto-updated. Second, the need for Boni Garcia's WebDriverManager library, which most Indian Java suites still carry as a dependency out of habit even though Selenium now does the same job with zero code.

You can still override everything: set the driver path on the Service builder, point SE_CACHE_PATH at a shared directory so CI does not re-download on every run, and use SE_OFFLINE=true on air-gapped build agents that must use a pre-seeded cache. Interviewers use this question to check whether you have upgraded a real project recently or only read tutorials.

# Inspect what Selenium Manager would do, without running a test
selenium-manager --browser chrome --debug

# Pin a specific browser build in CI
selenium-manager --browser chrome --browser-version 130 --debug

# Share the driver cache across CI jobs so nothing re-downloads
export SE_CACHE_PATH=/opt/ci/selenium-cache

# Air-gapped agent: fail loudly instead of trying the network
export SE_OFFLINE=true

Key Points

  • Rust binary bundled with the client since Selenium 4.6
  • Auto-detects browser version and downloads the matching driver
  • Can download the browser itself in recent releases (Chrome for Testing)
  • Makes WebDriverManager and system properties redundant
  • SE_CACHE_PATH and SE_OFFLINE control caching and air-gapped builds
💡 Pro Tip: Cache SE_CACHE_PATH between CI runs. Otherwise every job pays a 30-60 second driver download and your pipeline breaks the day the vendor endpoint rate-limits you.
Q3

Which locator strategies does By support in Selenium 4, and how do you choose between them?

BasicLocators

Answer

By exposes eight strategies: id, name, className, tagName, linkText, partialLinkText, cssSelector and xpath. Under the W3C protocol only CSS selector, XPath, tag name and link text are true wire-level strategies; id, name and class name are convenience wrappers that Selenium rewrites into CSS before sending them, which is why By.className with a space in the value throws InvalidSelectorException instead of matching a multi-class element. Selection order in a healthy suite is: a dedicated test attribute first, then id, then a stable CSS selector, then XPath, and never linkText for anything user-visible in a product that supports Hindi or regional languages, because the moment marketing changes the copy or the app switches locale the locator dies.

The single highest-leverage practice is asking the development team for data-testid or data-qa attributes on interactive elements and locating on those. It costs a frontend engineer thirty seconds per component and eliminates the largest single category of flakiness in Indian enterprise suites, where the DOM is generated by a framework and class names are hashed at build time. Interviewers usually follow up by asking which is faster, CSS or XPath. The honest answer in 2026 is that on modern engines the difference is a fraction of a millisecond and irrelevant next to network and render time, so pick on readability and stability, not micro-benchmarks.

import org.openqa.selenium.By;

// Best: contract attribute owned by the test
driver.findElement(By.cssSelector("[data-testid='checkout-submit']"));

// Good: stable id
driver.findElement(By.id("pan-number"));

// Fragile: hashed framework class names
driver.findElement(By.className("Button_root__x7f2a"));

// Throws InvalidSelectorException: compound class name
// driver.findElement(By.className("btn btn-primary"));

// Correct form for a multi-class element
driver.findElement(By.cssSelector(".btn.btn-primary"));

Key Points

  • Eight strategies; id / name / className are rewritten to CSS internally
  • By.className rejects compound values with spaces
  • Prefer data-testid attributes over structural selectors
  • linkText breaks on copy changes and localisation
  • CSS vs XPath speed is irrelevant next to page render time
Q4

What is the difference between findElement and findElements, and what does each do when nothing matches?

BasicLocators

Answer

findElement returns a single WebElement, specifically the first match in document order, and throws NoSuchElementException when there is no match. findElements returns a List of WebElement and returns an empty list when there is no match, never throwing. That difference is the basis of the most common presence check in Selenium code: call findElements and test isEmpty, rather than wrapping findElement in a try-catch. Two behaviours surprise people.

First, both calls are affected by the implicit wait: if you have set a ten second implicit wait, findElements will block for the full ten seconds before returning an empty list, so a negative assertion written this way silently adds ten seconds to every run. In a suite with two hundred such checks that is over half an hour of pure waiting. Second, findElement can be chained off an element rather than the driver, and when you do that with an XPath starting with a forward slash the search escapes back to the document root.

You need a leading dot, so .//span rather than //span, otherwise nested searches quietly match the wrong element on pages with repeated components such as a product listing. Interviewers like this question because the answer reveals whether you have debugged a slow negative-assertion suite, which is a very common problem in inherited regression packs.

// Absence check without exceptions
boolean bannerGone = driver.findElements(By.cssSelector(".promo-banner")).isEmpty();

// Count matches
int rows = driver.findElements(By.cssSelector("table#orders tbody tr")).size();

WebElement card = driver.findElement(By.cssSelector("[data-testid='order-card']"));

// WRONG: escapes to document root, matches the first price on the page
WebElement wrong = card.findElement(By.xpath("//span[@class='price']"));

// RIGHT: scoped to the card
WebElement right = card.findElement(By.xpath(".//span[@class='price']"));

Key Points

  • findElement throws NoSuchElementException; findElements returns an empty list
  • findElements is the idiomatic absence check, not try-catch
  • Implicit wait applies to findElements too, so absence checks pay the full timeout
  • Chained XPath needs a leading dot: .//span, not //span
  • findElement always returns the first match in document order
💡 Pro Tip: Set implicit wait to zero if you use findElements for negative assertions, otherwise each absence check costs you the full implicit timeout.
Q5

What is the actual difference between driver.get(url) and driver.navigate().to(url)?

BasicNavigation

Answer

Functionally almost nothing: both issue the same W3C navigateTo command and both block until the page load strategy is satisfied. The difference is API surface. navigate() returns a Navigation object that also exposes back(), forward() and refresh(), so it is the entry point for history manipulation, while get() is a convenience shortcut for the single most common case. In Selenium 3 folklore you often hear that get waits for the page to load and navigate does not, which is simply untrue under the W3C protocol and a claim that a good interviewer will push back on.

What genuinely matters is the pageLoadStrategy capability, because that is what defines when either call returns. With the default 'normal' strategy the driver waits for document.readyState to reach 'complete', which on a page with a slow third-party analytics script or a chat widget can mean many seconds of dead time before your first assertion. Two more practical notes: navigating to the URL you are already on still triggers a full reload, and back() on a single page application often returns before the client-side router has finished rendering, so you still need an explicit wait on a page-specific element rather than trusting the navigation call. Treat every navigation as asynchronous with respect to the application, even though the WebDriver command itself is synchronous.

driver.get("https://example.in/orders");

// History control needs navigate()
driver.navigate().refresh();
driver.navigate().back();
driver.navigate().forward();

// Navigation is only half the job on an SPA
new WebDriverWait(driver, Duration.ofSeconds(15))
    .until(ExpectedConditions.visibilityOfElementLocated(
        By.cssSelector("[data-testid='orders-table']")));

Key Points

  • Both send the same W3C navigateTo command
  • navigate() additionally gives back(), forward(), refresh()
  • Return timing is governed by pageLoadStrategy, not by which method you call
  • back() on an SPA returns before the client router finishes rendering
  • Always follow navigation with an explicit wait on a page-specific element
Q6

What is the difference between driver.close() and driver.quit(), and what leaks when you get it wrong?

BasicSession Management

Answer

close() closes the current browsing context, meaning the focused window or tab, and leaves the WebDriver session alive. quit() terminates the session entirely: it closes every window, shuts down the browser process, and tells the driver to exit. If you close() the last remaining window, the session is technically still registered but has no context to act on, and the next command throws NoSuchWindowException, which is a confusing failure to debug. The leak matters most on shared infrastructure.

On Selenium Grid, a session that is never quit stays allocated until the node's session timeout expires, which defaults to several minutes. Run a hundred tests that forget quit() and your Grid slots are all consumed by dead sessions while new tests sit in the New Session Queue, and the whole pipeline appears to hang. Locally the symptom is different: orphaned chromedriver and chrome processes accumulate until the machine runs out of memory, which is why QA engineers on shared Windows VMs at large Indian service firms end up killing processes by hand.

The fix is structural, not disciplinary. Put quit() in a teardown hook that runs even when the test fails: an @AfterMethod with alwaysRun = true in TestNG, an @AfterEach in JUnit 5, a pytest fixture with yield, or a try-with-resources block since WebDriver implements AutoCloseable in Selenium 4.

// TestNG: runs even if the test threw
@AfterMethod(alwaysRun = true)
public void tearDown() {
    WebDriver driver = DriverFactory.get();
    if (driver != null) {
        driver.quit();
        DriverFactory.remove(); // clear the ThreadLocal too
    }
}

// Java 8+ alternative for a self-contained scenario
try (WebDriver driver = new ChromeDriver(options)) {
    driver.get("https://example.in");
} // quit() is invoked automatically

Key Points

  • close() closes one window; quit() ends the whole session
  • Closing the last window leaves a zombie session and NoSuchWindowException
  • Unquit Grid sessions occupy slots until the node session timeout
  • Locally you leak chromedriver and browser processes until OOM
  • WebDriver is AutoCloseable in Selenium 4, so try-with-resources works
💡 Pro Tip: Removing the ThreadLocal entry matters as much as calling quit(). A stale WebDriver reference held by a thread pool thread keeps the whole browser object graph alive.
Q7

How do you configure ChromeOptions for a reliable headless run on a CI agent in 2026?

BasicConfiguration

Answer

Use --headless=new, the Chromium headless mode that shares the same rendering path as headed Chrome. The old --headless implementation was a separate binary path with different behaviour around downloads, extensions, print, and some CSS, which is exactly why teams used to see tests that passed locally and failed headless. Beyond the mode flag, a CI profile needs an explicit window size because headless defaults to a small viewport and responsive layouts will render the mobile breakpoint, hiding the desktop navigation your test expects.

On Linux containers add --disable-dev-shm-usage so Chrome writes shared memory to /tmp instead of the default 64 MB /dev/shm, or raise the container's shm size instead. --no-sandbox is commonly added when running as root in Docker, though the better fix is running as a non-root user with the proper seccomp profile. Set a fixed user agent or a download directory through prefs when your assertions depend on them, and consider excluding the enable-automation switch when you are testing an app whose own bot detection interferes. One historical gotcha worth naming: Chrome 111 broke older Selenium clients until --remote-allow-origins=* was added, which is why some legacy Indian suites still carry that flag even though current Selenium versions do not need it.

ChromeOptions options = new ChromeOptions();
options.addArguments(
    "--headless=new",
    "--window-size=1920,1080",
    "--disable-dev-shm-usage",
    "--disable-gpu",
    "--lang=en-IN"
);

Map<String, Object> prefs = new HashMap<>();
prefs.put("download.default_directory", "/tmp/downloads");
prefs.put("download.prompt_for_download", false);
prefs.put("profile.default_content_setting_values.notifications", 2);
options.setExperimentalOption("prefs", prefs);

options.setExperimentalOption("excludeSwitches", List.of("enable-automation"));
options.setPageLoadStrategy(PageLoadStrategy.NORMAL);

WebDriver driver = new ChromeDriver(options);

Key Points

  • --headless=new shares the rendering path with headed Chrome
  • Always set --window-size, otherwise you test the mobile breakpoint
  • --disable-dev-shm-usage or a larger container shm for Docker
  • prefs configures download directory and file handling
  • --remote-allow-origins=* is a legacy workaround, not needed on current clients
Q8

Implicit wait, explicit wait and Thread.sleep: why is mixing implicit and explicit waits a documented problem?

BasicWaits

Answer

An implicit wait is a session-level setting that tells the driver to keep retrying element location for up to N seconds before throwing NoSuchElementException. It applies to every findElement and findElements call for the life of the session. An explicit wait is a WebDriverWait wrapped around a specific condition, polling until the condition is true or the timeout expires.

Thread.sleep is an unconditional pause that always costs its full duration and proves nothing about application state. The Selenium documentation explicitly warns against combining implicit and explicit waits because the timeouts compound unpredictably. The classic symptom: implicit wait is set to ten seconds, an explicit wait with a five second timeout polls every 500 ms, but each poll internally blocks for the ten second implicit retry, so a wait you configured for five seconds can take twenty or more and the failure message points at the explicit wait, which was never the culprit.

The recommendation for any suite written today is to set the implicit wait to zero and use explicit waits everywhere. That is more verbose but every wait is then visible in the code, targeted at a real condition, and tunable per interaction. The one case for a short Thread.sleep is waiting out a fixed CSS animation on a modal where no DOM state changes, and even then a wait on an attribute or class change is better.

// Do this once, at driver creation
driver.manage().timeouts().implicitlyWait(Duration.ZERO);
driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(30));
driver.manage().timeouts().scriptTimeout(Duration.ofSeconds(20));

// Then wait explicitly, on a real condition
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
WebElement pay = wait.until(ExpectedConditions.elementToBeClickable(
    By.cssSelector("[data-testid='pay-now']")));
pay.click();

wait.until(ExpectedConditions.textToBePresentInElementLocated(
    By.cssSelector("[data-testid='status']"), "Payment successful"));

Key Points

  • Implicit wait is global to the session; explicit wait is per condition
  • Mixing them makes timeouts compound in a non-obvious way
  • Set implicit wait to zero and use explicit waits exclusively
  • Thread.sleep always burns the full duration and asserts nothing
  • Implicit wait also slows every findElements-based absence check
💡 Pro Tip: Grep any inherited suite for Thread.sleep. In most Indian regression packs, deleting sleeps and replacing them with explicit waits cuts total runtime by 30-50 percent.
Q9

How does WebDriverWait work internally, and which ExpectedConditions do you actually use?

BasicWaits

Answer

WebDriverWait extends FluentWait. It polls a supplied function every 500 milliseconds by default, ignores NoSuchElementException while polling, and returns the function's result as soon as it is neither null nor false. If the timeout expires it throws TimeoutException, and Selenium 4 attaches the last thrown exception as the cause, which is why reading the full stack trace rather than just the top line usually tells you what was really wrong.

The condition is any Function<WebDriver, T>, so ExpectedConditions is just a library of prebuilt ones. In real suites four cover most needs: elementToBeClickable before any click, visibilityOfElementLocated before reading text, invisibilityOfElementLocated to wait out a spinner or overlay, and textToBePresentInElementLocated for state transitions. presenceOfElementLocated is the one people misuse: it only checks the element exists in the DOM, not that it is rendered or has non-zero size, so it happily returns an element that is still behind a loading skeleton. Two design notes interviewers listen for.

Prefer the locator-based overloads over the element-based ones, because a WebDriverWait holding a WebElement reference across a re-render will throw StaleElementReferenceException, and by default WebDriverWait does not ignore that exception. And keep timeouts honest: a fifteen second wait on a locally fast page is fine, but a sixty second blanket timeout just converts fast failures into slow ones.

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));

// Wait out the spinner before touching anything
wait.until(ExpectedConditions.invisibilityOfElementLocated(
    By.cssSelector("[data-testid='loader']")));

// Custom condition: wait for a JS-driven counter to settle
wait.until(d -> ((JavascriptExecutor) d)
    .executeScript("return window.__pendingRequests === 0;").equals(true));

// Combine conditions
wait.until(ExpectedConditions.and(
    ExpectedConditions.urlContains("/summary"),
    ExpectedConditions.elementToBeClickable(By.id("confirm"))
));

Key Points

  • Polls every 500 ms, ignores NoSuchElementException, returns the first truthy result
  • TimeoutException in Selenium 4 carries the last underlying exception as cause
  • elementToBeClickable, visibilityOfElementLocated, invisibilityOfElementLocated, textToBePresentInElementLocated
  • presenceOfElementLocated does not imply visible or interactable
  • Prefer locator overloads; element overloads go stale on re-render
Q10

How do you handle dropdowns with the Select class, and what do you do when the widget is not a native select?

BasicInteractions

Answer

The Select helper in org.openqa.selenium.support.ui wraps a native HTML select element and gives you selectByVisibleText, selectByValue, selectByIndex, the matching deselect methods, getOptions, getFirstSelectedOption and isMultiple. Constructing Select on anything whose tag name is not select throws UnexpectedTagNameException immediately, which is the fastest way to discover that the pretty dropdown you are looking at is actually a div with role=listbox. That is the case for nearly every modern component library: React Select, Material UI, Ant Design and Radix all render a button plus a floating list, frequently portaled to the end of body rather than nested inside the form.

For those you drive the real interaction: click the trigger, wait for the listbox to be visible, then click the option by its accessible text or data attribute. Two failure modes to name. Portaled dropdowns break relative XPath written from the trigger, because the option list is not a descendant of the form, so locate it from the document root or by role.

And virtualised lists only render the visible window of options, so selecting the two-hundredth entry requires typing into the combobox filter rather than scrolling. Also remember deselect methods throw UnsupportedOperationException on a single-select element, a small detail interviewers occasionally use as a knowledge check.

import org.openqa.selenium.support.ui.Select;

// Native select
Select state = new Select(driver.findElement(By.id("state")));
state.selectByVisibleText("Karnataka");
String chosen = state.getFirstSelectedOption().getText();

// Custom listbox (React Select / MUI style, portaled to body)
driver.findElement(By.cssSelector("[data-testid='city-trigger']")).click();
new WebDriverWait(driver, Duration.ofSeconds(10))
    .until(ExpectedConditions.visibilityOfElementLocated(
        By.cssSelector("[role='listbox']")));
driver.findElement(By.xpath(
    "//div[@role='option'][normalize-space()='Bengaluru']")).click();

Key Points

  • Select works only on native <select>; otherwise UnexpectedTagNameException
  • selectByVisibleText / selectByValue / selectByIndex plus deselect variants
  • deselectAll throws UnsupportedOperationException on a single-select
  • Component-library dropdowns need click, wait, click on the option
  • Portaled listboxes are not descendants of the trigger; virtualised lists need filter typing
Q11

How do you work with iframes in Selenium, and what breaks when you forget to switch back?

BasicFrames

Answer

WebDriver commands only see the current browsing context. An iframe is a separate context, so any element inside it is invisible to findElement until you switch. Use driver.switchTo().frame() with an index, a name or id string, or, best of all, a located WebElement, because index positions shift when the page injects an extra hidden frame and names are often auto-generated.

To return, switchTo().defaultContent() jumps all the way back to the top-level document, while switchTo().parentFrame() goes up exactly one level, which matters with nested frames. The forgotten-switch failure is one of the most confusing in Selenium: after entering a frame, every subsequent locator that targets the main page throws NoSuchElementException even though you can plainly see the element in the browser, and engineers waste hours rewriting a perfectly good selector. In Indian enterprise work this comes up constantly because payment gateways, Salesforce Lightning, legacy banking portals and embedded reporting tools all render inside frames, and third-party card capture fields are frequently cross-origin so JavaScript injection cannot reach them either. Two robustness notes: wait with frameToBeAvailableAndSwitchToIt rather than switching immediately, because the frame element can exist before its document is loaded, and put the switch and the switch-back inside the page object method so no test ever leaks frame context to the next step.

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(20));

// Wait for the frame, then switch in one step
wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt(
    By.cssSelector("iframe[title='Secure card entry']")));

driver.findElement(By.name("cardnumber")).sendKeys("4111111111111111");
driver.findElement(By.name("exp-date")).sendKeys("12/29");

// Nested frame: go up exactly one level
driver.switchTo().parentFrame();

// Back to the host page before any further assertion
driver.switchTo().defaultContent();
driver.findElement(By.cssSelector("[data-testid='place-order']")).click();

Key Points

  • Elements inside an iframe are unreachable until you switch context
  • Prefer switching by WebElement over index or generated name
  • defaultContent() returns to the top document; parentFrame() goes up one level
  • Forgetting to switch back gives misleading NoSuchElementException
  • Use frameToBeAvailableAndSwitchToIt to avoid switching into an unloaded frame
💡 Pro Tip: Wrap frame entry and exit inside the page object method. A test that leaks frame context makes the next unrelated test fail, which is the worst kind of flake to diagnose.
Q12

How do you handle JavaScript alerts, confirms and prompts, and what is the unhandledPromptBehavior capability?

BasicAlerts

Answer

Native browser dialogs produced by window.alert, window.confirm and window.prompt are not DOM elements, so no locator will ever find them. You switch to them with driver.switchTo().alert(), then call accept() for OK, dismiss() for Cancel, getText() to read the message, and sendKeys() to type into a prompt. Calling switchTo().alert() when no dialog is open throws NoAlertPresentException, so in flows where the dialog is conditional, wait with ExpectedConditions.alertIsPresent() rather than guessing.

The subtle part interviewers probe is what happens when a dialog appears while you are doing something else. Under the W3C spec an open user prompt blocks other commands, and the driver's response is controlled by the unhandledPromptBehavior capability. The default is 'dismiss and notify', which auto-dismisses the dialog and throws UnhandledAlertException so you know it happened.

You can set it to 'accept', 'dismiss', 'accept and notify' or 'ignore'. Setting 'ignore' is the right choice when you intend to handle every dialog explicitly, because it stops the driver from silently clicking Cancel on a confirmation your test needed to accept. Two things Selenium cannot touch: HTTP basic auth prompts on older browsers and the native OS file picker, since neither is a JavaScript dialog. For file inputs you send the path to the input element instead of opening the picker at all.

ChromeOptions options = new ChromeOptions();
options.setCapability("unhandledPromptBehavior", "ignore");
WebDriver driver = new ChromeDriver(options);

driver.findElement(By.cssSelector("[data-testid='delete-account']")).click();

Alert alert = new WebDriverWait(driver, Duration.ofSeconds(10))
    .until(ExpectedConditions.alertIsPresent());

Assert.assertEquals(alert.getText(), "This action cannot be undone. Continue?");
alert.accept();

// Prompt dialog
driver.findElement(By.id("rename")).click();
Alert prompt = driver.switchTo().alert();
prompt.sendKeys("Q3 report");
prompt.accept();

Key Points

  • Alerts are not DOM elements; use switchTo().alert()
  • accept(), dismiss(), getText(), sendKeys() for prompts
  • Wait with ExpectedConditions.alertIsPresent() for conditional dialogs
  • unhandledPromptBehavior defaults to dismiss and notify
  • Native OS file pickers and basic auth prompts are not JS alerts
Q13

How do you handle multiple windows and tabs, and what did newWindow(WindowType.TAB) add in Selenium 4?

BasicWindows

Answer

Every browsing context has a window handle, an opaque string. getWindowHandle() returns the handle of the context you are currently driving, and getWindowHandles() returns a Set of all handles in the session. To act on a popup you capture the handle set before the action, perform the click that opens the new context, wait for the set size to change, and switch to the handle that was not there before. Selenium 4 added driver.switchTo().newWindow(WindowType.TAB) and WindowType.WINDOW, which create a fresh context and switch to it in one command without needing window.open through JavaScript.

That is genuinely useful for tests that need a second logged-in context, for example verifying that an admin action in one tab is reflected in a user dashboard in another. Practical warnings: handles are unordered because they live in a Set, so never assume insertion order or index positions. Closing a window with close() leaves your driver pointed at a dead handle, so always switch back to a valid handle immediately after. And on Grid, popups opened by target=_blank sometimes land behind the parent window in the virtual display; switching by handle still works because WebDriver does not require the window to be visually focused, but any Actions-based interaction that depends on real pointer focus can misbehave.

String parent = driver.getWindowHandle();
Set<String> before = driver.getWindowHandles();

driver.findElement(By.linkText("View invoice")).click();

new WebDriverWait(driver, Duration.ofSeconds(10))
    .until(ExpectedConditions.numberOfWindowsToBe(before.size() + 1));

String popup = driver.getWindowHandles().stream()
    .filter(h -> !before.contains(h))
    .findFirst()
    .orElseThrow();

driver.switchTo().window(popup);
Assert.assertTrue(driver.getTitle().contains("Invoice"));
driver.close();
driver.switchTo().window(parent);

// Selenium 4: open a clean second tab directly
driver.switchTo().newWindow(WindowType.TAB);
driver.get("https://example.in/admin");

Key Points

  • getWindowHandle() is singular current context; getWindowHandles() is the full Set
  • Diff the handle set before and after the click that opens the popup
  • Selenium 4 adds newWindow(WindowType.TAB / WindowType.WINDOW)
  • Handles are unordered; never index into them
  • After close(), immediately switchTo a valid handle
Q14

What is the difference between getText(), getAttribute(), getDomAttribute() and getDomProperty()?

BasicElement API

Answer

getText() returns the rendered, visible text of an element after CSS is applied, with whitespace normalised and hidden subtrees excluded. That last part trips people up: text hidden by display:none or by a collapsed parent returns an empty string even though it is present in the DOM, which is why a getText() assertion on an accordion panel fails until you expand it. getAttribute() is the legacy method and it is deliberately fuzzy: it first looks for a JavaScript property with that name and falls back to the HTML attribute, so getAttribute("value") on a text input returns what the user typed rather than the static value written in the markup, and getAttribute("checked") returns the live checked state. Selenium 4.5 introduced two explicit replacements. getDomAttribute() returns only the literal HTML attribute as authored in the markup, and getDomProperty() returns only the live JavaScript property from the DOM node.

New code should use the explicit pair because the intent is readable and the behaviour is not implementation-defined. The rule of thumb: current user input, checked, selected, disabled and className live states come from getDomProperty; static markup like data-testid, href as authored, or a custom attribute comes from getDomAttribute. If you need the raw markup including children, getAttribute("outerHTML") is still the pragmatic way to dump an element for debugging, and getCssValue() covers computed styles such as colour or display.

WebElement input = driver.findElement(By.id("gst-number"));
input.sendKeys("29ABCDE1234F1Z5");

// Authored markup: often empty or a default
String authored = input.getDomAttribute("value");

// Live DOM state: what the user actually typed
String typed = input.getDomProperty("value");

WebElement terms = driver.findElement(By.id("accept-terms"));
boolean isChecked = Boolean.parseBoolean(terms.getDomProperty("checked"));

WebElement badge = driver.findElement(By.cssSelector("[data-testid='status']"));
String visible = badge.getText();              // rendered text
String colour  = badge.getCssValue("color");   // computed style

Key Points

  • getText() returns visible rendered text only; hidden text comes back empty
  • getAttribute() guesses between property and attribute
  • getDomAttribute() is the authored HTML attribute (Selenium 4.5+)
  • getDomProperty() is the live DOM property
  • getCssValue() for computed styles, getAttribute('outerHTML') for debugging dumps
Q15

What is the Actions class for, and how do the wheel scroll actions added in Selenium 4.2 work?

BasicInteractions

Answer

Actions builds a W3C action sequence: a list of pointer, key and wheel inputs that the driver dispatches as low-level browser events rather than synthetic DOM events. You use it for anything a plain click or sendKeys cannot express: hover to reveal a menu, right-click for a context menu, double-click, click-and-hold drag and drop, keyboard chords like Ctrl plus A, and scrolling. Nothing is sent until you call perform(), so the fluent chain is just building the sequence.

Selenium 4.2 added the wheel input source with scrollToElement, scrollByAmount, scrollFromOrigin with an element or viewport offset. Before that, everyone used JavascriptExecutor with scrollIntoView, which works but bypasses real scroll event handlers, so lazy-loaded content and infinite scroll listeners often did not fire. The wheel actions produce genuine wheel events, so virtualised tables and infinite feeds behave the way they do for a user.

Two production notes. Drag and drop with clickAndHold, moveByOffset and release fails on many HTML5 drag-and-drop implementations because they listen for dragstart rather than mousedown, and the reliable workaround is either a slower multi-step move sequence with intermediate offsets or a JavaScript dispatch of the real drag events. And moveToElement on an element that is partly off-screen throws MoveTargetOutOfBoundsException, so scroll first, then move.

Actions actions = new Actions(driver);

// Hover to open a mega menu, then click a child
WebElement menu = driver.findElement(By.cssSelector("[data-testid='nav-products']"));
actions.moveToElement(menu)
       .pause(Duration.ofMillis(200))
       .click(driver.findElement(By.linkText("Health Insurance")))
       .perform();

// Real wheel scroll: fires lazy-load listeners
WebElement footer = driver.findElement(By.tagName("footer"));
actions.scrollToElement(footer).perform();
actions.scrollByAmount(0, 600).perform();

// Keyboard chord
actions.keyDown(Keys.CONTROL).sendKeys("a").keyUp(Keys.CONTROL).perform();

// Drag and drop with intermediate steps (more reliable than dragAndDrop)
WebElement src = driver.findElement(By.id("card-1"));
WebElement dst = driver.findElement(By.id("column-done"));
actions.clickAndHold(src).moveByOffset(10, 10)
       .moveToElement(dst).pause(Duration.ofMillis(150))
       .release().perform();

Key Points

  • Actions emits W3C pointer, key and wheel input sequences
  • Nothing happens until perform() is called
  • Selenium 4.2 wheel actions: scrollToElement, scrollByAmount, scrollFromOrigin
  • Real wheel events trigger lazy loading that scrollIntoView misses
  • HTML5 drag and drop often needs multi-step moves or scripted drag events
Q16

How do you upload a file in Selenium, and what is LocalFileDetector for?

BasicFile Handling

Answer

You never automate the operating system file picker. Instead you call sendKeys with an absolute path directly on the input element whose type is file. The W3C spec defines this: the driver sets the file selection on the input and fires the change event, exactly as if the user had picked the file.

Three things go wrong in practice. First, many applications hide the real input with display:none or opacity:0 and show a styled button instead, and Selenium refuses to sendKeys to an element it considers not interactable. The correct fix is to locate the hidden input directly and send to it, since the spec explicitly allows file inputs to be non-visible when strictFileInteractability is left at its default of false.

Second, relative paths silently fail on some drivers, so always resolve to an absolute path from a test resources directory rather than hard-coding a Windows path that breaks on the Linux CI agent. Third, on Selenium Grid the file lives on your machine but the browser runs on a remote node, so sendKeys sends a path that does not exist there. That is what LocalFileDetector solves: set it on the RemoteWebDriver and Selenium zips the local file, transfers it to the node, and substitutes the remote path automatically. Multiple file inputs accept several paths separated by newline characters.

import org.openqa.selenium.remote.LocalFileDetector;
import org.openqa.selenium.remote.RemoteWebDriver;
import java.nio.file.Path;
import java.nio.file.Paths;

RemoteWebDriver driver = new RemoteWebDriver(gridUrl, new ChromeOptions());
driver.setFileDetector(new LocalFileDetector()); // required for Grid

Path resume = Paths.get("src/test/resources/fixtures/resume.pdf")
                   .toAbsolutePath();

// Target the hidden input, not the styled button
driver.findElement(By.cssSelector("input[type='file'][name='resume']"))
      .sendKeys(resume.toString());

new WebDriverWait(driver, Duration.ofSeconds(20))
    .until(ExpectedConditions.textToBePresentInElementLocated(
        By.cssSelector("[data-testid='upload-status']"), "resume.pdf"));

Key Points

  • sendKeys an absolute path to input[type=file]; never touch the OS picker
  • Hidden file inputs are legal targets; strictFileInteractability defaults to false
  • Resolve paths from test resources so Windows and Linux both work
  • RemoteWebDriver needs setFileDetector(new LocalFileDetector())
  • Multiple upload: newline-separated paths in one sendKeys call
💡 Pro Tip: If sendKeys throws ElementNotInteractableException on a file input, you are almost certainly targeting the styled label instead of the real input.
Q17

How do you capture screenshots, including element-level and full-page screenshots?

BasicDebugging

Answer

Cast the driver to TakesScreenshot and call getScreenshotAs with OutputType.FILE, OutputType.BYTES or OutputType.BASE64. FILE returns a temporary file that the JVM may clean up, so copy it somewhere durable immediately. Selenium 4 added the same capability on WebElement, so element.getScreenshotAs crops to that element's bounding box without any third-party library, which is far more useful in a failure report than a full page where the reviewer has to hunt for the broken control.

Full-page capture beyond the viewport is not part of the W3C spec for Chromium, so Chrome only gives you the visible viewport. Firefox exposes getFullPageScreenshotAs through FirefoxDriver, and for Chrome you either stitch scrolled captures with a library such as AShot or issue the CDP command Page.captureScreenshot with captureBeyondViewport set to true. In CI the useful pattern is not just capturing on failure but attaching the image to the report as base64, because build agents are ephemeral and a PNG written to the workspace disappears with the container.

Attach base64 into Allure or ExtentReports, and pair it with the page source and the browser console log, since a screenshot alone rarely explains why the click was intercepted. Capture the screenshot in a listener rather than inside the test so no assertion path can skip it.

// Whole viewport
File shot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
Files.copy(shot.toPath(), Paths.get("target/shots/" + testName + ".png"),
           StandardCopyOption.REPLACE_EXISTING);

// Just the element that failed (Selenium 4)
WebElement widget = driver.findElement(By.cssSelector("[data-testid='cart-total']"));
byte[] cropped = widget.getScreenshotAs(OutputType.BYTES);

// Attach to Allure so it survives an ephemeral agent
Allure.addAttachment("cart-total", new ByteArrayInputStream(cropped));

// Chrome full page via CDP
Map<String, Object> params = Map.of("captureBeyondViewport", true, "format", "png");
Map<String, Object> result =
    ((ChromiumDriver) driver).executeCdpCommand("Page.captureScreenshot", params);
byte[] fullPage = Base64.getDecoder().decode((String) result.get("data"));

Key Points

  • TakesScreenshot with OutputType.FILE, BYTES or BASE64
  • Selenium 4 supports element.getScreenshotAs for cropped captures
  • Chrome gives viewport only; Firefox has getFullPageScreenshotAs
  • Embed base64 in the report; workspace files vanish with the CI container
  • Capture in a TestNG or JUnit listener, not inside each test
Q18

What does the pageLoadStrategy capability control, and when would you set it to eager or none?

BasicConfiguration

Answer

pageLoadStrategy tells the driver how long a navigation command should block. The default, normal, waits for document.readyState to become complete, which means all subresources including images, fonts, iframes and third-party scripts have finished. eager returns as soon as readyState is interactive, that is once the HTML is parsed and the DOM is ready but before images and stylesheets finish. none returns immediately after the initial HTML response is received. The reason this matters commercially: many Indian consumer sites load analytics, chat widgets, ad pixels and payment SDKs from third-party domains, and a single slow beacon can add five to fifteen seconds to every driver.get with the normal strategy.

Across a thousand-test regression pack that is hours of pure waiting for resources your assertions never look at. Switching to eager usually recovers most of it with no behavioural change, because you should already be gating on explicit waits for the elements you care about. Use none only when you know exactly what you are doing, for example when you plan to intercept and block third-party requests yourself, because with none the DOM may be almost empty when the call returns and every single interaction needs its own explicit wait. Note the strategy is a session capability, not a per-navigation setting, so it applies to every page in that browser session.

ChromeOptions options = new ChromeOptions();
options.setPageLoadStrategy(PageLoadStrategy.EAGER);
WebDriver driver = new ChromeDriver(options);

driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(20));

driver.get("https://example.in/pricing");

// With EAGER you must still gate on the element you assert against
new WebDriverWait(driver, Duration.ofSeconds(15))
    .until(ExpectedConditions.visibilityOfElementLocated(
        By.cssSelector("[data-testid='plan-card']")));

Key Points

  • normal waits for readyState complete, eager for interactive, none returns immediately
  • Set once as a session capability, not per navigation
  • eager typically saves seconds per page on ad and analytics heavy sites
  • none requires an explicit wait before every single interaction
  • Pair a shorter pageLoadTimeout with eager so hangs still fail fast
💡 Pro Tip: Measure before and after. Run one representative journey with normal and with eager and compare wall clock. On ad-heavy pages the saving is usually large enough to justify the change on its own.
Q19

How do you set up Selenium with Python in 2026, and which APIs were removed in the 4.x line?

BasicLanguage Bindings

Answer

Install with pip install selenium and instantiate the driver directly. Selenium Manager handles the driver binary, so you do not need webdriver-manager and you do not pass an executable path. Several APIs that dominate old Indian tutorial content have been removed.

The find_element_by_* family (find_element_by_id, find_element_by_xpath and the rest) was removed in Selenium 4.3, replaced by find_element(By.ID, value) with By imported from selenium.webdriver.common.by. Passing executable_path or other configuration positionally to webdriver.Chrome was removed in the 4.10 line, so anything other than options and service must go through the Service object. options.headless as a boolean property was also dropped in favour of options.add_argument. Waits use WebDriverWait with expected_conditions imported as EC, and the condition takes a locator tuple rather than two arguments, which is the single most common syntax error for people moving from Java.

On the ecosystem side, pytest is the default runner: use a session or function scoped fixture that yields the driver and quits it in teardown, pytest-xdist with -n auto for parallelism, and pytest-html or Allure for reporting. Type hints landed across the Python bindings in Selenium 4, so a modern editor will flag the removed APIs before you ever run the suite.

import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC


@pytest.fixture
def driver():
    options = webdriver.ChromeOptions()
    options.add_argument('--headless=new')
    options.add_argument('--window-size=1920,1080')
    drv = webdriver.Chrome(options=options)  # Selenium Manager resolves the driver
    drv.implicitly_wait(0)
    yield drv
    drv.quit()


def test_search_returns_jobs(driver):
    driver.get('https://example.in/jobs')
    driver.find_element(By.CSS_SELECTOR, "[data-testid='q']").send_keys('sdet')
    driver.find_element(By.CSS_SELECTOR, "[data-testid='search']").click()

    results = WebDriverWait(driver, 15).until(
        EC.presence_of_all_elements_located(
            (By.CSS_SELECTOR, "[data-testid='job-card']"))
    )
    assert len(results) > 0

Key Points

  • find_element_by_* removed in 4.3; use find_element(By.ID, ...)
  • executable_path positional args removed in the 4.10 line; use Service
  • options.headless boolean gone; use add_argument('--headless=new')
  • expected_conditions take a locator tuple, not two arguments
  • pytest fixtures for lifecycle, pytest-xdist for parallel runs
Q20

What causes StaleElementReferenceException, and which fixes actually work?

IntermediateExceptions

Answer

A WebElement is not the element itself, it is a reference to a node in a specific document. The driver stores that reference in an internal element cache keyed by an id. When the referenced node is removed from the DOM, or the whole document is replaced by a navigation, the cache entry becomes invalid and the next command against it throws StaleElementReferenceException.

Two distinct causes produce it. Navigation: you located the element, the page navigated or reloaded, and you then clicked. React or Angular re-render: the element still looks identical on screen but the framework replaced the node during a state update, virtual DOM diff, or list re-key.

The fixes that work are structural. Re-locate immediately before interacting, so the gap between finding and acting is as small as possible. Prefer locator-based ExpectedConditions over holding a WebElement across a wait.

Never cache WebElement fields on a page object across actions, which is exactly why the Selenium team now discourages PageFactory: its lazy proxies re-find on first use but happily go stale afterwards. For genuinely racy widgets, use FluentWait with ignoring(StaleElementReferenceException.class) and a lambda that re-locates and acts inside the poll, so a stale hit simply causes another poll instead of a test failure. What does not work is a retry loop with sleeps, because it hides the underlying race and turns a two-second interaction into a ten-second one.

// Fails intermittently: the row is re-rendered between find and click
// WebElement row = driver.findElement(By.cssSelector("tr[data-id='4821']"));
// applyFilter();
// row.click();

// Robust: re-locate and act inside the polling function
By rowLocator = By.cssSelector("tr[data-id='4821'] [data-testid='open']");

new FluentWait<>(driver)
    .withTimeout(Duration.ofSeconds(15))
    .pollingEvery(Duration.ofMillis(250))
    .ignoring(StaleElementReferenceException.class)
    .ignoring(ElementClickInterceptedException.class)
    .until(d -> {
        d.findElement(rowLocator).click();
        return true;
    });

Key Points

  • WebElement is a cached reference to a specific DOM node, not a live query
  • Caused by navigation or by framework re-render replacing the node
  • Re-locate immediately before interacting; do not cache element fields
  • FluentWait ignoring StaleElementReferenceException with re-location inside the lambda
  • PageFactory proxies are a common source in legacy suites
💡 Pro Tip: If a page object stores WebElement fields, it will go stale on any SPA. Store By locators as fields and find inside the methods.
Q21

How do you diagnose ElementClickInterceptedException versus ElementNotInteractableException?

IntermediateExceptions

Answer

They are different failures with different fixes and the exception message tells you which. ElementClickInterceptedException means the element is present, visible and interactable, but at the exact pixel the driver aimed at, hit testing found a different element on top. The message names the obstructing element, which is the single most valuable line in the stack trace and the one most people skip.

In Indian consumer products the usual culprits are a sticky header that covers the top of the viewport after scroll, a cookie or app-download banner pinned to the bottom, a toast notification that appears for four seconds, or a modal backdrop that has faded out visually but has not yet been removed from the DOM. ElementNotInteractableException means the element itself cannot receive the interaction: it has zero size, is behind display:none, has visibility hidden, is disabled, or is present but detached from the render tree. The right fixes differ.

For interception, scroll the element into the centre of the viewport with a wheel action, wait for the obstructing element to disappear with invisibilityOfElementLocated, or dismiss the banner once in a global setup so it never interferes again. For non-interactable, wait for the enabling condition, expand the collapsed parent, or check whether you have located a wrapper instead of the real control. The lazy fix for both, a JavaScript click, is the wrong answer in an interview because it bypasses the very hit testing that would have caught a real user-facing regression.

// Read the message: it names the obstructing element
// org.openqa.selenium.ElementClickInterceptedException: element click intercepted:
// Element <button data-testid="pay"> is not clickable at point (640, 812).
// Other element would receive the click: <div class="cookie-banner">

// 1. Dismiss known global obstructions once, in setup
driver.findElements(By.cssSelector("[data-testid='cookie-accept']"))
      .stream().findFirst().ifPresent(WebElement::click);

// 2. Wait for transient overlays to actually leave the DOM
new WebDriverWait(driver, Duration.ofSeconds(10))
    .until(ExpectedConditions.invisibilityOfElementLocated(
        By.cssSelector(".modal-backdrop")));

// 3. Scroll the target to the centre, away from sticky chrome
WebElement pay = driver.findElement(By.cssSelector("[data-testid='pay']"));
new Actions(driver).scrollToElement(pay).perform();
new Actions(driver).scrollByAmount(0, -120).perform();
pay.click();

Key Points

  • Intercepted: something is on top at the click point; the message names it
  • NotInteractable: the element itself is hidden, sized zero, or disabled
  • Common interceptors: sticky headers, cookie banners, toasts, lingering modal backdrops
  • Fix by scrolling to centre or waiting for the obstruction to go, not by JS click
  • A JavaScript click hides real usability regressions from your suite
Q22

Explain the Page Object Model, and why does the Selenium team now discourage PageFactory in new code?

IntermediateDesign Patterns

Answer

Page Object Model puts the locators and the interaction vocabulary for one screen into a class, so tests read as business steps rather than as CSS selectors. When the login form changes, one file changes and fifty tests keep passing. The rules that make it work: page objects expose intent-revealing methods, not raw WebElements; navigation methods return the next page object so flows chain naturally; and page objects contain no assertions, because a page object that asserts becomes a test and can no longer be reused by a scenario that expects failure.

Beyond POM, mature suites layer a component object for repeated widgets such as a data grid or a date picker, so you are not duplicating table parsing in twelve pages. PageFactory, the @FindBy plus initElements mechanism, was a Selenium 2 idea and is discouraged now for concrete reasons. It creates a dynamic proxy per field that re-finds the element on first use but caches nothing sensibly across a re-render, which produces StaleElementReferenceException in single page applications.

It hides the timing of location, so you cannot see in the code when the driver actually queries the DOM. It only supports the locator strategies expressible as annotations, which pushes people toward brittle XPath. And it does not compose with explicit waits cleanly. Modern practice is plain By constants as private fields and a small helper that finds and waits inside each method, which is more explicit and, importantly, debuggable.

public class CheckoutPage {
    private final WebDriver driver;
    private final WebDriverWait wait;

    private static final By UPI_TAB   = By.cssSelector("[data-testid='pm-upi']");
    private static final By VPA_INPUT = By.cssSelector("[data-testid='vpa']");
    private static final By PAY_BTN   = By.cssSelector("[data-testid='pay-now']");

    public CheckoutPage(WebDriver driver) {
        this.driver = driver;
        this.wait = new WebDriverWait(driver, Duration.ofSeconds(15));
    }

    public CheckoutPage payWithUpi(String vpa) {
        wait.until(ExpectedConditions.elementToBeClickable(UPI_TAB)).click();
        wait.until(ExpectedConditions.visibilityOfElementLocated(VPA_INPUT))
            .sendKeys(vpa);
        return this;
    }

    public OrderConfirmationPage submit() {
        wait.until(ExpectedConditions.elementToBeClickable(PAY_BTN)).click();
        return new OrderConfirmationPage(driver);
    }
}

Key Points

  • Page objects expose intent methods, not WebElements
  • Navigation methods return the next page object; no assertions inside page objects
  • Component objects for repeated widgets prevent duplication
  • PageFactory proxies go stale on SPA re-renders and hide when location happens
  • Prefer By constants plus explicit waits inside methods
Q23

WebDriver is not thread-safe. How do you run tests in parallel without cross-talk?

IntermediateConcurrency

Answer

A WebDriver instance maps to exactly one browser session, and neither the client object nor the session is safe to share across threads. If two threads issue commands on the same driver, they interleave on a single HTTP connection to one browser, and you get symptoms that look like application bugs: a click landing on the wrong page, an assertion reading another test's data, or a NoSuchWindowException when one thread quits while the other is mid-command. The standard solution is one driver per thread stored in a ThreadLocal, wrapped in a small factory.

The test never constructs a driver directly, it asks the factory, and the teardown hook calls both quit() and remove(). The remove() call is not optional: TestNG and JUnit reuse pool threads across tests, so an entry left behind means the next test on that thread inherits a dead driver reference, and in a long run the retained browser object graphs are a real memory leak on the JVM side. Shared mutable state elsewhere is the second trap.

A static WebDriverWait, a shared page object instance, a mutable test-data singleton, or a non-thread-safe Extent report node will all corrupt results under parallel execution even when the drivers themselves are isolated. Make anything shared either immutable or ThreadLocal, and prefer data that is generated per test with a unique suffix so two threads never fight over the same account or order id.

public final class DriverFactory {
    private static final ThreadLocal<WebDriver> TL = new ThreadLocal<>();

    public static WebDriver get() {
        return TL.get();
    }

    public static void create(String browser) {
        WebDriver driver = switch (browser) {
            case "firefox" -> new FirefoxDriver(firefoxOptions());
            case "edge"    -> new EdgeDriver(edgeOptions());
            default        -> new ChromeDriver(chromeOptions());
        };
        driver.manage().timeouts().implicitlyWait(Duration.ZERO);
        TL.set(driver);
    }

    public static void destroy() {
        WebDriver driver = TL.get();
        if (driver != null) {
            driver.quit();
        }
        TL.remove(); // critical: pool threads are reused
    }
}

Key Points

  • One WebDriver per thread; never share an instance
  • ThreadLocal factory plus quit() and remove() in teardown
  • Forgetting remove() leaks drivers and poisons the next test on that pool thread
  • Shared page objects, waits or report nodes break parallelism too
  • Generate unique test data per thread to avoid data collisions
💡 Pro Tip: If your suite passes serially and fails in parallel, look for static state before you look at the application. It is shared mutable state ninety percent of the time.
Q24

How do you configure parallel execution in TestNG and Maven Surefire, and how many browsers fit on one machine?

IntermediateConcurrency

Answer

TestNG controls parallelism from the suite XML with the parallel attribute set to methods, classes, tests or instances, plus thread-count. parallel="methods" gives the highest utilisation but demands that every test method be fully independent, including its data. parallel="classes" is the pragmatic default for suites that share setup within a class. Maven Surefire has its own parallel and threadCount settings plus forkCount for separate JVMs, and the classic mistake is configuring both TestNG and Surefire, which multiplies rather than caps your thread count and instantly exhausts the machine. Pick one layer and leave the other at its default.

Sizing is the part interviewers care about. A headless Chrome session under load realistically wants about one vCPU and 700 MB to 1 GB of RAM once the page has a heavy JavaScript bundle. So an 8 vCPU, 16 GB build agent runs six to eight parallel sessions comfortably and starts thrashing at twelve, where you see timeouts that look like application slowness but are actually CPU starvation.

The diagnostic signature is telling: failure rate rises with thread count, failures scatter across unrelated tests, and re-running a failed test alone passes. Measure with a small matrix, four, eight, twelve threads, and plot pass rate against wall clock before committing a number to your pipeline config.

<!-- testng.xml -->
<suite name="regression" parallel="classes" thread-count="6"
       data-provider-thread-count="4" verbose="1">
  <listeners>
    <listener class-name="framework.listeners.ScreenshotListener"/>
    <listener class-name="framework.listeners.RetryTransformer"/>
  </listeners>
  <test name="chrome-suite">
    <parameter name="browser" value="chrome"/>
    <classes>
      <class name="tests.CheckoutTests"/>
      <class name="tests.SearchTests"/>
    </classes>
  </test>
</suite>

<!-- pom.xml: let TestNG own parallelism, keep Surefire single-forked -->
<plugin>
  <artifactId>maven-surefire-plugin</artifactId>
  <configuration>
    <forkCount>1</forkCount>
    <suiteXmlFiles><suiteXmlFile>testng.xml</suiteXmlFile></suiteXmlFiles>
  </configuration>
</plugin>

Key Points

  • TestNG: parallel=methods|classes|tests|instances plus thread-count
  • Surefire: parallel, threadCount, forkCount; do not configure both layers
  • Budget roughly 1 vCPU and about 1 GB RAM per headless Chrome session
  • Over-subscription shows up as scattered timeouts that pass on re-run alone
  • Tune empirically: plot pass rate against wall clock for 4, 8 and 12 threads
Q25

When do you reach for FluentWait instead of WebDriverWait, and how do you write a custom condition?

IntermediateWaits

Answer

WebDriverWait is a preconfigured FluentWait: 500 millisecond polling, ignoring NoSuchElementException only. You drop to FluentWait when you need to change any of that. The three levers are pollingEvery, which you lower to 100 milliseconds for a fast-flipping UI state or raise to two seconds when each poll is expensive such as a JavaScript execution or an API call; ignoring, where you add StaleElementReferenceException, ElementClickInterceptedException or a domain exception so a transient failure causes another poll rather than aborting; and withMessage, which supplies a Supplier<String> evaluated at failure time so your TimeoutException says what was actually being waited for instead of a generic timeout.

Custom conditions are just functions from WebDriver to a value, and they are where real suites get their reliability. Useful ones in production: wait until a table's row count stabilises across two consecutive polls, which handles progressive rendering; wait until jQuery.active is zero or a framework-specific idle flag is set; wait until an element's bounding box stops moving, which defeats animated modals that intercept clicks mid-transition; and wait until a downloaded file appears on disk with a non-growing size. Interviewers ask this to separate people who only call ExpectedConditions from people who have had to stabilise a genuinely difficult page, so bring a concrete example from your own suite.

// Wait until the results grid stops growing, then assert
Function<WebDriver, Integer> stableRowCount = new Function<>() {
    private int previous = -1;

    @Override
    public Integer apply(WebDriver d) {
        int now = d.findElements(By.cssSelector("tbody tr")).size();
        boolean stable = now > 0 && now == previous;
        previous = now;
        return stable ? now : null;
    }
};

int rows = new FluentWait<>(driver)
    .withTimeout(Duration.ofSeconds(20))
    .pollingEvery(Duration.ofMillis(400))
    .ignoring(StaleElementReferenceException.class)
    .withMessage(() -> "Result grid never stabilised on " + driver.getCurrentUrl())
    .until(stableRowCount);

Assert.assertEquals(rows, 25);

Key Points

  • WebDriverWait is FluentWait with fixed 500 ms polling
  • pollingEvery, ignoring and withMessage are the levers
  • Custom condition is any Function<WebDriver, T> returning non-null and non-false
  • Stabilisation conditions (count steady across two polls) beat fixed sleeps
  • withMessage turns a useless TimeoutException into a diagnosable one
Q26

How is Selenium Grid 4 structured, and what do the Router, Distributor, Session Map and New Session Queue each do?

IntermediateGrid

Answer

Grid 4 is not the old hub-and-node code with a new version number, it is a rewrite into five cooperating components. The Router is the single public endpoint on port 4444: it inspects an incoming request, pushes new session requests onto the New Session Queue and forwards commands for an existing session straight to the owning Node using the Session Map. The New Session Queue holds pending requests with its own request timeout and retry interval, which is what gives you backpressure instead of instant failures when every slot is busy.

The Distributor watches that queue, finds a Node whose declared stereotype matches the requested capabilities, and creates the session. The Session Map stores session id to Node URL. The Event Bus carries registration and heartbeat traffic between components.

You can run all of this three ways: standalone, where one process plays every role and is what most people use locally; hub and node, the familiar two-role split; and fully distributed, where each component runs separately so you can scale and restart them independently. The failure most teams hit is capability matching: a test asks for browserVersion 130, no Node declares that version in its stereotype, and the request sits in the queue until it times out with an error about not finding a matching slot. It looks like the Grid is down when it is simply refusing an impossible request. The other one is node sizing: max-sessions defaults to the CPU count and is capped at eight unless you pass override-max-sessions, so adding vCPUs does not automatically raise concurrency.

# node.toml: declare exactly what this node can serve
[node]
max-sessions = 6
override-max-sessions = true
session-timeout = 300
drain-after-session-count = 200

[[node.driver-configuration]]
display-name = "chrome"
stereotype = '{"browserName":"chrome","platformName":"LINUX"}'
max-sessions = 4

[[node.driver-configuration]]
display-name = "firefox"
stereotype = '{"browserName":"firefox","platformName":"LINUX"}'
max-sessions = 2

# Start the node against a running hub
java -jar selenium-server.jar node --config node.toml --hub http://grid-hub:4442

# Tune the queue on the hub side
java -jar selenium-server.jar hub \
  --session-request-timeout 300 \
  --session-retry-interval 5

# Stop a node taking new work before a deploy, without killing live sessions
curl -X POST http://node-3:5555/se/grid/node/drain \
  -H "X-REGISTRATION-SECRET: $GRID_SECRET"

Key Points

  • Router, Distributor, Session Map, New Session Queue, Event Bus, Node
  • Standalone, hub-and-node, and fully distributed deployment modes
  • Queue provides backpressure; requests wait rather than failing instantly
  • Unmatched capabilities look like an outage but are a stereotype mismatch
  • max-sessions caps at 8 unless override-max-sessions is set
💡 Pro Tip: Open /status on the Router and read the stereotypes. Ninety percent of 'the Grid is stuck' tickets are a capability the Grid was never configured to serve.
Q27

How do you point a suite at a remote Grid with RemoteWebDriver, and what are the se: prefixed capabilities for?

IntermediateGrid

Answer

You construct a RemoteWebDriver with the Grid URL and an Options object. The Options carry the W3C capabilities the Grid matches on (browserName, browserVersion, platformName) plus vendor extensions such as goog:chromeOptions. Anything the Grid itself should act on goes under the se: namespace, which the Node strips before starting the browser: se:name labels the session in the Grid UI and logs, se:recordVideo and se:screenResolution control the video sidecar in the Docker images, se:timeZone sets the container timezone which matters when you assert on IST dates, and se:downloadsEnabled turns on the managed download directory so you can pull files back off the Node.

Three things bite teams moving from local to Grid. Uploads stop working because the file is on your machine and the browser is on a Node, which is exactly what setFileDetector with LocalFileDetector fixes. Screenshots and DevTools access can disappear because the remote object is a proxy, so you wrap it with Augmenter to get the interface implementations back.

And client-side timeouts start mattering: the default HTTP read timeout can fire before a genuinely slow page load, so set it explicitly through ClientConfig rather than debugging phantom connection resets. Always log getSessionId() on failure. That single value is what lets you find the corresponding Node log line, video file and trace when a test failed at two in the morning on a Grid running fifty parallel sessions, and interviewers running large suites will ask specifically how you correlate a red test with Grid-side evidence.

ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new", "--window-size=1920,1080");

// Consumed by the Grid, never passed to Chrome
options.setCapability("se:name", "checkout-upi-happy-path");
options.setCapability("se:timeZone", "Asia/Kolkata");
options.setCapability("se:screenResolution", "1920x1080");
options.setCapability("se:recordVideo", true);
options.setCapability("se:downloadsEnabled", true);

URL gridUrl = new URL("http://grid.internal:4444");
RemoteWebDriver driver = new RemoteWebDriver(gridUrl, options);

// Local file, remote browser
driver.setFileDetector(new LocalFileDetector());

// This id is your only link to the Node log, video and trace
logger.info("grid session {}", driver.getSessionId());

// Remote proxies need augmenting for extra interfaces
WebDriver augmented = new Augmenter().augment(driver);
DevTools devTools = ((HasDevTools) augmented).getDevTools();

Key Points

  • Options carry W3C matching capabilities; se: keys are consumed by the Grid
  • se:name, se:timeZone, se:screenResolution, se:recordVideo, se:downloadsEnabled
  • LocalFileDetector is mandatory for uploads against a remote Node
  • Augmenter restores TakesScreenshot and HasDevTools on the remote proxy
  • Log getSessionId() to correlate a failure with Node logs and video
Q28

How do you reach elements inside Shadow DOM with getShadowRoot(), and what stays unreachable?

IntermediateShadow DOM

Answer

Selenium 4 added WebElement.getShadowRoot(), which returns a SearchContext representing the element's open shadow root. From there you call findElement again, and you keep chaining one hop per nesting level. Three constraints define what is possible.

First, only open roots work: if the component was created with attachShadow({ mode: 'closed' }) the call throws NoSuchShadowRootException and there is no supported way in, because the browser deliberately hides the subtree. Second, inside a shadow root you can use CSS selectors only. XPath evaluation is rooted at the document and a shadow root is a DocumentFragment, so By.xpath fails there, which surprises the large number of Indian enterprise suites written almost entirely in XPath.

Third, a CSS selector never pierces a shadow boundary on its own: there is no descendant selector that crosses from the host into the root, so you cannot write one long selector and must chain. This comes up constantly in real work because Salesforce Lightning Web Components, Vaadin, Ionic, Polymer-era portals and Chrome's own internal pages such as the downloads and PDF viewer UI all use shadow DOM. The pragmatic escalation path is to ask the component team to keep roots open in non-production builds, or to expose a small test hook object on the custom element that returns the state you need, which is far cheaper than fighting the boundary. The older JavascriptExecutor trick of returning arguments[0].shadowRoot still works and is the fallback on very old client versions, but it has the same open-root restriction.

// Host element lives in the light DOM
WebElement host = driver.findElement(By.cssSelector("order-summary-card"));
SearchContext shadow = host.getShadowRoot();

// CSS only from here down
WebElement total = shadow.findElement(
    By.cssSelector("[data-testid='grand-total']"));
Assert.assertEquals(total.getText(), "Rs 1,24,500");

// Nested roots: one hop at a time, no combined selector exists
SearchContext inner = shadow
    .findElement(By.cssSelector("payment-widget"))
    .getShadowRoot();
inner.findElement(By.cssSelector("button.pay")).click();

// Closed root throws NoSuchShadowRootException.
// Negotiate a test hook instead of fighting the boundary:
Object token = ((JavascriptExecutor) driver).executeScript(
    "return document.querySelector('payment-widget').__testHooks.state;");

Key Points

  • getShadowRoot() returns a SearchContext; chain one call per nesting level
  • Closed roots throw NoSuchShadowRootException with no workaround
  • CSS only inside a shadow root; By.xpath does not work there
  • No selector pierces the boundary, so one long CSS string cannot work
  • Common in Salesforce LWC, Vaadin, Ionic and Chrome internal pages
💡 Pro Tip: If your framework is XPath-first, shadow DOM will force a CSS rewrite of those page objects. Budget that work rather than discovering it mid-sprint.
Q29

What is the difference between executeScript and executeAsyncScript, and when is a JavaScript click defensible?

IntermediateJavaScript Execution

Answer

executeScript runs a synchronous snippet in the page and returns immediately with the result. Return values are marshalled by the protocol: an HTMLElement becomes a WebElement, a JavaScript number becomes a Long or Double, an array becomes a List, a plain object becomes a Map, and undefined or null becomes null. Arguments you pass are exposed as arguments[0], arguments[1] and so on, and a WebElement argument arrives in the page as the real DOM node. executeAsyncScript is different: Selenium appends a callback as the final argument, and the command blocks until your script invokes that callback or the script timeout expires, at which point you get ScriptTimeoutException.

That is the correct tool for waiting on an application event, a promise, or a setTimeout-driven transition, and the timeout is configured with driver.manage().timeouts().scriptTimeout(Duration). Legitimate uses of the executor are the ones where no user-facing surface exists for the state you need: reading window.dataLayer to assert an analytics event fired, pulling performance.getEntriesByType('navigation') for a page timing guardrail, checking a feature flag object, or setting up state before a scenario. The illegitimate one, and the reason interviewers ask, is arguments[0].click() used to escape ElementClickInterceptedException.

A scripted click dispatches a DOM event directly and skips hit testing, pointer-events, disabled state, and the overlay that was in the way, so your suite passes on a button no real user can reach. That is a suppressed regression, not a fix. Use the executor to read and to set up, and use real interactions to act.

JavascriptExecutor js = (JavascriptExecutor) driver;

// Read state the UI never renders
Long ttfb = (Long) js.executeScript(
    "const n = performance.getEntriesByType('navigation')[0];" +
    "return Math.round(n.responseStart - n.requestStart);");
Assert.assertTrue(ttfb < 800, "TTFB regression: " + ttfb + " ms");

// Objects come back as Map, arrays as List
@SuppressWarnings("unchecked")
Map<String, Object> event = (Map<String, Object>) js.executeScript(
    "return window.dataLayer.find(e => e.event === 'purchase');");
Assert.assertEquals(event.get("currency"), "INR");

// Async: wait for an application event, not a sleep
driver.manage().timeouts().scriptTimeout(Duration.ofSeconds(20));
Long count = (Long) js.executeAsyncScript(
    "const done = arguments[arguments.length - 1];" +
    "window.addEventListener('app:orders-loaded'," +
    " e => done(e.detail.count), { once: true });");

// Do not do this to dodge an intercepted click:
// js.executeScript("arguments[0].click();", payButton);

Key Points

  • executeScript is synchronous; executeAsyncScript blocks on an injected callback
  • Callback is always arguments[arguments.length - 1]
  • scriptTimeout governs the async call and throws ScriptTimeoutException
  • Element to WebElement, object to Map, array to List on the return path
  • JS click bypasses hit testing and hides real usability regressions
Q30

How do you build data-driven Selenium tests with TestNG @DataProvider, and what breaks under parallel execution?

IntermediateTest Frameworks

Answer

A @DataProvider is a method returning Object[][] or Iterator<Object[]>, referenced by name from @Test(dataProvider = "..."). Setting parallel = true on the provider lets its rows run concurrently, and the concurrency is governed by data-provider-thread-count in the suite XML, which is a separate pool from thread-count. Confusing the two is a standard interview trap: raising thread-count does nothing for a parallel data provider.

Return Iterator<Object[]> rather than Object[][] whenever the source is large, because the array form is fully materialised on the heap before the first test runs, and a fifteen thousand row Excel sheet loaded through Apache POI's XSSFWorkbook will happily exhaust a 2 GB CI heap before a single browser starts. POI's Workbook is also not thread-safe, so read it once in a @BeforeSuite and hand out immutable row objects rather than letting each thread touch the workbook. The bigger parallel failure is data collision.

Two threads that both register vendor@example.in produce a duplicate-account error that looks exactly like a product defect, and teams lose days to it. Generate a unique key per row at runtime and never rely on the fixture file for uniqueness. Finally, fix your reporting: without ITest or a meaningful toString on the parameter object, every row shows up in the report under the same method name and a failure tells you nothing about which input broke. Interviewers ask this to see whether you have run data-driven suites at real volume or only demonstrated the annotation.

@DataProvider(name = "vendorRows", parallel = true)
public Iterator<Object[]> vendorRows() {
    // Lazy: never materialise 15k rows before the first browser starts
    return VendorFixtures.stream("src/test/resources/vendors.csv")
        .map(row -> new Object[] { row })
        .iterator();
}

@Test(dataProvider = "vendorRows")
public void registersVendor(VendorRow row) {
    // Uniqueness comes from the test, not from the fixture file
    String email = "qa+" + row.id() + "-" + UUID.randomUUID() + "@example.in";

    new SignupPage(DriverFactory.get())
        .enterGstin(row.gstin())
        .enterEmail(email)
        .submit()
        .assertRegistered();
}

Key Points

  • data-provider-thread-count is separate from suite thread-count
  • Iterator<Object[]> streams; Object[][] materialises the whole set on heap
  • Apache POI Workbook is not thread-safe: read once, share immutable rows
  • Generate unique data per row or parallel threads collide on the same account
  • Implement ITest or a real toString so report rows are distinguishable
💡 Pro Tip: In testng.xml: parallel="methods" thread-count="6" data-provider-thread-count="4". They are different pools and both need setting.
Q31

How do you implement IRetryAnalyzer for flaky tests, and when is retrying the wrong answer?

IntermediateFlakiness

Answer

IRetryAnalyzer has one method, retry(ITestResult), and returning true makes TestNG run the method again. You attach it per test with @Test(retryAnalyzer = ...), or globally through an IAnnotationTransformer listener so you never touch an annotation. The design decisions matter more than the code.

Retry only on a whitelist of infrastructure exceptions such as TimeoutException, SessionNotCreatedException, StaleElementReferenceException and raw WebDriverException, and explicitly refuse to retry AssertionError, because retrying an assertion failure is how a real regression reaches production while the dashboard stays green. Cap at one retry, since a test that needs three attempts is not flaky, it is broken. Create a fresh driver for the retry rather than reusing a session that may already be in a bad state.

And record the fact that a retry happened, because TestNG's default report marks the first failed attempt as skipped, which silently inflates your pass rate. This is the governance question underneath the technical one, and Indian service-company panels ask it directly: a retry analyzer bolted on to lift a client-facing pass rate from seventy percent to ninety-five percent is not test engineering, it is hiding data. The honest setup pairs retries with a flake metric, so any test that passes only on retry twice in a rolling week gets quarantined out of the gating suite and assigned to someone to fix. Retry buys you a stable pipeline signal for a week or two, not permanently.

public class RetryAnalyzer implements IRetryAnalyzer {
    private static final int MAX_RETRIES = 1;
    private int attempts = 0;

    private static final List<Class<?>> INFRA = List.of(
        TimeoutException.class,
        SessionNotCreatedException.class,
        StaleElementReferenceException.class,
        WebDriverException.class);

    @Override
    public boolean retry(ITestResult result) {
        Throwable t = result.getThrowable();
        if (t instanceof AssertionError) return false;  // a real failure
        if (attempts >= MAX_RETRIES) return false;
        boolean infra = t != null && INFRA.stream().anyMatch(c -> c.isInstance(t));
        if (!infra) return false;

        attempts++;
        FlakeMetrics.record(result.getMethod().getQualifiedName(), t);
        return true;
    }
}

// Attach everywhere without editing a single @Test annotation
public class RetryTransformer implements IAnnotationTransformer {
    @Override
    public void transform(ITestAnnotation a, Class<?> c,
                          Constructor<?> ctor, Method m) {
        a.setRetryAnalyzer(RetryAnalyzer.class);
    }
}

Key Points

  • IRetryAnalyzer.retry(ITestResult) plus IAnnotationTransformer for global attachment
  • Whitelist infrastructure exceptions; never retry AssertionError
  • One retry maximum, with a fresh driver for the second attempt
  • TestNG reports the failed first attempt as skipped, inflating pass rate
  • Pair retries with a flake metric and a quarantine rule, or you are hiding defects
Q32

How do you skip UI login with driver.manage().addCookie() or a localStorage write, and what does that cost you?

IntermediateSession Management

Answer

Driving the login form in every test is the single largest waste in most regression suites: eight to fifteen seconds per test, multiplied by hundreds of tests, plus a dependency on an auth service that has nothing to do with what you are verifying. The faster pattern is to obtain a session from the API, then hand it to the browser. For cookie-based auth, call the login endpoint with an HTTP client, then use driver.manage().addCookie() with a Cookie.Builder.

The hard rule is that you must already be on the target domain before adding the cookie, otherwise you get InvalidCookieDomainException, so you navigate to a cheap page on that origin first and then reload after injection. Cookie.Builder in Selenium 4 exposes sameSite alongside domain, path, expiry, isSecure and isHttpOnly, and WebDriver can set HttpOnly cookies even though page JavaScript cannot. For token-in-storage apps you use the JavascriptExecutor to write localStorage or sessionStorage and then refresh, since storage writes before navigation are discarded.

Two gotchas. SameSite=None requires Secure, so on plain http a browser drops the cookie silently and your test just looks logged out. And many applications pair the session with a CSRF token rendered into a meta tag or a device fingerprint, so injecting only the session cookie gives you a page that renders fine and then returns 403 on the first POST. The cost is that you stop exercising the login journey, so keep a small dedicated set of tests that authenticate through the UI, including the OTP path, and let everything else inject.

// 1. Get a session from the API, not from the login form
String token = AuthApi.login("qa-bot@example.in", System.getenv("QA_PASSWORD"));

// 2. Be on the origin before touching the cookie jar
driver.get("https://app.example.in/favicon.ico");

driver.manage().addCookie(new Cookie.Builder("sid", token)
    .domain(".example.in")
    .path("/")
    .isSecure(true)
    .isHttpOnly(true)
    .sameSite("Lax")
    .expiresOn(Date.from(Instant.now().plus(Duration.ofHours(2))))
    .build());

// 3. Token-in-storage apps: write, then reload
((JavascriptExecutor) driver).executeScript(
    "window.localStorage.setItem('access_token', arguments[0]);", token);
driver.navigate().refresh();

driver.get("https://app.example.in/dashboard");
new WebDriverWait(driver, Duration.ofSeconds(15))
    .until(ExpectedConditions.visibilityOfElementLocated(
        By.cssSelector("[data-testid='user-menu']")));

Key Points

  • Navigate to the origin first or addCookie throws InvalidCookieDomainException
  • Cookie.Builder supports sameSite, secure, httpOnly, domain, path, expiry
  • localStorage writes need a refresh to take effect
  • SameSite=None without Secure is dropped silently over http
  • Keep a few real UI login tests; injection skips that coverage
💡 Pro Tip: Measure it before you argue for it. Cutting UI login from 400 tests at 10 seconds each removes over an hour of serial runtime.
Q33

How do you verify file downloads deterministically, including when the browser runs on a remote Grid node?

IntermediateFile Handling

Answer

Locally you point the browser at a directory you control and then poll it. In Chrome that means the prefs download.default_directory and download.prompt_for_download set to false, plus plugins.always_open_pdf_externally set to true when the artefact is a PDF, otherwise Chrome opens its internal viewer and nothing lands on disk. Firefox needs browser.download.folderList set to 2, browser.download.dir, and browser.helperApps.neverAsk.saveToDisk carrying the exact MIME type.

The polling has to be smarter than checking existence, because Chrome writes a partial file with a .crdownload suffix first: wait until the target file exists, no .crdownload sibling remains, and the size is unchanged across two consecutive polls. A fixed Thread.sleep here is the classic source of a suite that passes on a fast laptop and fails on a loaded CI agent. On a Grid the download lands on the node's filesystem, not yours.

Selenium 4 solves this properly: start the Grid with managed downloads enabled, request se:downloadsEnabled in your capabilities, and use the HasDownloads interface to list what the session produced and pull a named file back to a local directory. Finally, assert on content, not on the filename. A zero-byte CSV or an HTML error page saved as report.xlsx both satisfy a filename check, and that is exactly the bug an interviewer will describe to see whether your verification is real.

// Grid-side: --enable-managed-downloads true
ChromeOptions options = new ChromeOptions();
options.setCapability("se:downloadsEnabled", true);
RemoteWebDriver driver = new RemoteWebDriver(gridUrl, options);

driver.findElement(By.cssSelector("[data-testid='export-gstr1']")).click();

HasDownloads downloads = (HasDownloads) driver;
String name = new FluentWait<>(driver)
    .withTimeout(Duration.ofSeconds(60))
    .pollingEvery(Duration.ofSeconds(2))
    .withMessage(() -> "No export appeared in the node download dir")
    .until(d -> downloads.getDownloadableFiles().stream()
        .filter(f -> f.endsWith(".csv"))
        .findFirst().orElse(null));

downloads.downloadFile(name, Path.of("target/downloads"));
downloads.deleteDownloadableFiles();  // keep the node clean for the next session

// Verify content, not the filename
List<String> rows = Files.readAllLines(Path.of("target/downloads", name));
Assert.assertEquals(rows.get(0), "gstin,invoice_no,taxable_value,igst");
Assert.assertTrue(rows.size() > 1, "Export downloaded but contained no data rows");

Key Points

  • Chrome prefs: download.default_directory, prompt_for_download, always_open_pdf_externally
  • Firefox needs folderList=2, download.dir and neverAsk.saveToDisk MIME types
  • Poll for the file, absence of .crdownload, and a stable size
  • Grid: enable managed downloads plus se:downloadsEnabled, then use HasDownloads
  • Assert on parsed content; a filename check passes on a zero-byte file
Q34

How do you drive Chrome DevTools Protocol from Selenium, and why is the project moving away from it?

IntermediateDevTools

Answer

Two entry points exist. The blunt one is ChromiumDriver.executeCdpCommand(String, Map), which sends a raw CDP command and returns the raw response, useful for one-shot calls. The typed one is getDevTools() from the HasDevTools interface, then createSession(), then send() for commands and addListener() for events, using the versioned classes under org.openqa.selenium.devtools.v1xx.

Typical production uses are things WebDriver alone cannot do: throttling the connection with Network.emulateNetworkConditions to see how a page behaves on a slow 3G link, which is a real requirement for Indian consumer apps where a large share of traffic is on mid-tier Android over patchy mobile data; overriding geolocation with Emulation.setGeolocationOverride to test city-based pricing; injecting an auth or feature-flag header with Network.setExtraHTTPHeaders; and stubbing an unreliable third-party dependency through the Fetch domain so your checkout test does not fail because a partner sandbox is down. The reason the Selenium project is steering away from CDP is that it is a Chromium implementation detail with no compatibility guarantee. It changes every Chrome release, Selenium can only ship a handful of versioned bindings at a time, and when Chrome moves past them you first see a warning about falling back to the closest available version and eventually a hard break.

It is also Chromium only, so every CDP-dependent test silently drops your Firefox and Safari coverage, which undermines the exact reason most teams chose Selenium. WebDriver BiDi is the standardised replacement.

DevTools devTools = ((HasDevTools) driver).getDevTools();
devTools.createSession();

// Slow 3G: what a Tier-2 city user on mobile data actually sees
devTools.send(Network.enable(Optional.empty(), Optional.empty(), Optional.empty()));
devTools.send(Network.emulateNetworkConditions(
    false,           // offline
    150,             // latency ms
    750 * 1024 / 8,  // download bytes/sec
    250 * 1024 / 8,  // upload bytes/sec
    Optional.of(ConnectionType.CELLULAR3G)));

// City-based pricing without a VPN
devTools.send(Emulation.setGeolocationOverride(
    Optional.of(12.9716), Optional.of(77.5946), Optional.of(1)));

// Feature flag header on every request
devTools.send(Network.setExtraHTTPHeaders(
    new Headers(Map.of("x-feature-new-checkout", "on"))));

// Raw form, no versioned bindings needed
((ChromiumDriver) driver).executeCdpCommand("Emulation.setCPUThrottlingRate",
    Map.of("rate", 4));

Key Points

  • executeCdpCommand for raw calls; getDevTools() for typed commands and events
  • Network throttling, geolocation override, extra headers, Fetch-based stubbing
  • Versioned org.openqa.selenium.devtools.v1xx packages track Chrome releases
  • Version drift produces fallback warnings and eventually hard failures
  • Chromium only, so CDP tests do not run on Firefox or Safari
💡 Pro Tip: Pin your Chrome version in CI if you depend on CDP. An unattended browser auto-update is the usual cause of a suite that broke overnight with nobody having merged anything.
Q35

How do you collect browser console and performance logs from a Selenium session, and what are the limits?

IntermediateDebugging

Answer

For Chromium you request logging up front with the goog:loggingPrefs capability, built from a LoggingPreferences object, then read entries with driver.manage().logs().get(LogType.BROWSER). Each LogEntry carries a level, a timestamp and a message string. Setting LogType.PERFORMANCE gives you a stream of CDP Network and Timeline events serialised as JSON, which is how teams read response status codes and request timing before BiDi existed.

Three limits matter. The buffer is drained on read, so calling get() twice returns the second call empty and any code that reads logs in two places will lose half of them. Read once per step and accumulate into your own list.

Firefox and geckodriver do not implement the log endpoint, so the same call throws and any cross-browser framework needs a guard rather than a try-catch that swallows everything. And log capture must be configured at session creation, so you cannot decide to turn it on after a test has already failed. The reason to bother is that console errors catch a whole class of defects that pass a purely visual assertion: a Content Security Policy violation, a 404 on a hashed JavaScript chunk after a bad deploy, an uncaught TypeError in an event handler where the page still renders correctly.

A gate that fails any test producing a SEVERE console entry, with an allowlist for known third-party noise, finds real regressions cheaply. Attach the captured entries to the failure report next to the screenshot, because a picture rarely explains why a click did nothing.

LoggingPreferences logs = new LoggingPreferences();
logs.enable(LogType.BROWSER, Level.ALL);
logs.enable(LogType.PERFORMANCE, Level.ALL);

ChromeOptions options = new ChromeOptions();
options.setCapability("goog:loggingPrefs", logs);
WebDriver driver = new ChromeDriver(options);

// ... run the journey ...

List<String> allowlist = List.of("googletagmanager", "clarity.ms", "favicon.ico");

List<String> severe = driver.manage().logs().get(LogType.BROWSER)
    .getAll().stream()
    .filter(e -> e.getLevel() == Level.SEVERE)
    .map(LogEntry::getMessage)
    .filter(m -> allowlist.stream().noneMatch(m::contains))
    .toList();

Assert.assertTrue(severe.isEmpty(),
    "Console errors on " + driver.getCurrentUrl() + ":\n"
        + String.join("\n", severe));

Key Points

  • goog:loggingPrefs with LoggingPreferences must be set at session creation
  • LogType.BROWSER for console, LogType.PERFORMANCE for CDP network events
  • The log buffer is drained on read: read once and accumulate
  • geckodriver does not implement the log endpoint at all
  • Gate on SEVERE console entries with an allowlist for third-party noise
Q36

How do you wire a TestNG ITestListener so that a red build is diagnosable without re-running it?

IntermediateReporting

Answer

A listener implementing ITestListener gives you onTestStart, onTestSuccess, onTestFailure and onTestSkipped, and it is the correct place for artefact capture because it runs on every failure path including ones where an assertion short-circuited the test body. Inside onTestFailure you want five things: an element or viewport screenshot attached as base64 rather than a file, since CI containers are ephemeral and a PNG in the workspace disappears; the current URL, which alone resolves a surprising share of failures; the page source; the browser console log; and the Grid session id, which is the join key to the node log and video. Register the listener in testng.xml, with @Listeners on a base class, or through the ServiceLoader file META-INF/services/org.testng.ITestNGListener so it applies to everything without configuration.

Two traps come up in interviews. The listener must fetch the driver from the ThreadLocal factory, never from a static field, or under parallel execution you will attach thread A's screenshot to thread B's failure and produce reports that actively mislead. And ITestListener does not fire for failures inside @BeforeMethod: those surface as skipped tests with no artefact at all, which is why frameworks add an IInvokedMethodListener to cover configuration methods too. On the reporting side, ExtentReports node objects are not safe to share across threads, so hold the ExtentTest in a ThreadLocal, and Allure's @Step annotations only produce output when the AspectJ weaver javaagent is actually on the command line, which is the usual explanation for empty step trees.

public class FailureArtifactListener implements ITestListener {

    @Override
    public void onTestFailure(ITestResult result) {
        WebDriver driver = DriverFactory.get();   // ThreadLocal, not static
        if (driver == null) return;

        byte[] png = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
        Allure.getLifecycle().addAttachment(
            "screenshot", "image/png", "png", png);

        Allure.addAttachment("url", driver.getCurrentUrl());
        Allure.addAttachment("dom", "text/html", driver.getPageSource(), ".html");

        if (driver instanceof RemoteWebDriver remote) {
            Allure.addAttachment("grid-session", remote.getSessionId().toString());
        }

        try {
            String console = driver.manage().logs().get(LogType.BROWSER)
                .getAll().stream().map(LogEntry::toString)
                .collect(Collectors.joining("\n"));
            Allure.addAttachment("console", console);
        } catch (WebDriverException unsupported) {
            // geckodriver has no log endpoint
        }
    }
}

Key Points

  • Capture screenshot, URL, page source, console log and Grid session id on failure
  • Attach base64 to the report; workspace files die with the container
  • Pull the driver from ThreadLocal, never a static field, under parallel runs
  • ITestListener misses @BeforeMethod failures; add IInvokedMethodListener
  • ExtentTest needs ThreadLocal; Allure @Step needs the AspectJ javaagent
Q37

How do relative locators compute proximity, and where do above(), near() and toRightOf() fall down?

IntermediateLocators

Answer

RelativeLocator, reached through the static with(By) helper, builds a locator described in spatial terms: above, below, toLeftOf, toRightOf and near, where near defaults to a fifty pixel radius and takes an explicit distance overload. It is not a new wire protocol strategy. Selenium injects JavaScript that gathers candidate elements matching the base By, calls getBoundingClientRect on each and on the anchor, filters by the geometric relationship, and for near sorts by centre-to-centre distance.

Understanding that implementation tells you every limitation. Only rendered elements participate, since a display:none element has a zero rect, so relative locators cannot find something that is present but hidden. The result depends on the current viewport and scroll position, which is why a locator that works at 1920 by 1080 quietly matches a different element at 1366 by 768 or in a headless session that defaulted to a small window.

It is slower than a plain CSS selector because it evaluates a candidate set inside the page on every call. And it says nothing about DOM structure, so overlapping elements, absolutely positioned overlays and right-to-left layouts all produce surprising matches. Where it genuinely earns its place is a DOM you do not control: a legacy table where the only stable anchor is the row label, a form field next to a label with no for attribute, or a delete button in a card grid identified by the product name beside it. Treat it as a documented fallback with a pinned window size, not as your default strategy, and interviewers will be satisfied that you know why rather than just that it exists.

import static org.openqa.selenium.support.locators.RelativeLocator.with;

// Pin the viewport: relative locators are geometry, not structure
driver.manage().window().setSize(new Dimension(1920, 1080));

// Legacy table with no per-row test ids: anchor on the label
WebElement gstinCell = driver.findElement(By.xpath(
    "//td[normalize-space()='29ABCDE1234F1Z5']"));

WebElement statusCell = driver.findElement(
    with(By.tagName("td")).toRightOf(gstinCell));

WebElement deleteBtn = driver.findElement(
    with(By.cssSelector("button")).near(gstinCell, 120));

// Label with no 'for' attribute, input rendered underneath it
WebElement label = driver.findElement(By.xpath(
    "//label[normalize-space()='PAN number']"));
driver.findElement(with(By.tagName("input")).below(label))
      .sendKeys("ABCDE1234F");

Assert.assertEquals(statusCell.getText(), "Filed");

Key Points

  • Implemented as injected JS over getBoundingClientRect, not a wire strategy
  • above, below, toLeftOf, toRightOf, near (50px default, distance overload)
  • Hidden or zero-size elements have no rect and never match
  • Results shift with viewport width and scroll position
  • Best as a fallback on a DOM you cannot change, with a pinned window size
💡 Pro Tip: If a relative locator only fails in headless, check your window size. Headless defaults to a small viewport and the whole geometry changes.
Q38

What is WebDriver BiDi, which Selenium modules expose it, and what does it give you that classic WebDriver cannot?

AdvancedBiDi

Answer

Classic WebDriver is strictly request and response: your client asks, the driver answers, and the browser has no way to push anything back. That is why Selenium could never natively tell you a console error had appeared, intercept an in-flight request, or hand you an authentication challenge as it happened. WebDriver BiDi is the W3C standard that fixes this by adding a WebSocket channel alongside the existing HTTP commands, and critically it is implemented by both chromedriver and geckodriver, so the capability is cross-browser rather than Chromium only like CDP.

You opt in by setting the webSocketUrl capability to true, after which Selenium exposes modules for the protocol areas. In the Java bindings that means LogInspector for console entries and uncaught JavaScript exceptions delivered as events, BrowsingContext for creating, navigating and capturing contexts including a genuine full-page screenshot in Chrome, Network for adding intercepts and supplying responses or credentials, and Script for preloading scripts into every new document. The strategic point for an interview is the direction of travel: Selenium's CDP-backed helpers are being reimplemented on BiDi, so the fragile versioned devtools packages stop being a maintenance liability and your network stubbing finally runs on Firefox too. Practical cautions: the surface implemented differs by browser and by Selenium release, so verify against your version rather than a blog post; an unquit session now leaks a WebSocket as well as a browser; and Grid forwards the socket through the Router, so any ingress or corporate proxy that refuses connection upgrades breaks BiDi in a way that presents as an unexplained hang.

ChromeOptions options = new ChromeOptions();
options.setCapability("webSocketUrl", true);   // opt in to BiDi
WebDriver driver = new ChromeDriver(options);

List<String> jsErrors = Collections.synchronizedList(new ArrayList<>());

try (LogInspector inspector = new LogInspector(driver)) {
    // Pushed events, not a polled buffer that drains on read
    inspector.onConsoleEntry(entry ->
        logger.warn("console {}: {}", entry.getLevel(), entry.getText()));
    inspector.onJavaScriptException(ex -> jsErrors.add(ex.getText()));

    BrowsingContext context =
        new BrowsingContext(driver, driver.getWindowHandle());
    context.navigate("https://app.example.in/checkout", ReadinessState.COMPLETE);

    // Full page, not just the viewport, and not a CDP call
    String base64 = context.captureScreenshot();
    Allure.addAttachment("full-page", "image/png",
        new ByteArrayInputStream(Base64.getDecoder().decode(base64)), ".png");
}

Assert.assertTrue(jsErrors.isEmpty(),
    "Uncaught JS during checkout: " + jsErrors);
driver.quit();

Key Points

  • WebSocket channel on top of classic WebDriver; standardised by the W3C
  • Enable with the webSocketUrl capability set to true
  • LogInspector, BrowsingContext, Network and Script modules in the Java bindings
  • Implemented by chromedriver and geckodriver, unlike Chromium-only CDP
  • Ingress or proxies that block WebSocket upgrade break BiDi through Grid
💡 Pro Tip: Say out loud that BiDi is the replacement for CDP, not an addition to it. That single sentence separates candidates tracking the project from candidates repeating 2022 material.
Q39

How do you run Selenium Grid on Kubernetes with autoscaling, and what tuning stops a large suite from collapsing during scale-up?

AdvancedGrid

Answer

Deploy the distributed topology: Router, Distributor, Session Queue, Session Map and Event Bus as separate Deployments, and each browser as its own Node Deployment. The cleanest model for autoscaling is one session per pod, achieved by setting the Node's max-sessions to one and drain-after-session-count to one, so a pod serves a single session and then exits and is replaced with a clean browser. That removes an entire class of cross-test contamination from leftover profile state.

Autoscaling is done with KEDA's Selenium Grid scaler, which queries the Router's GraphQL endpoint for queued session requests matching a browser name and version and scales that Node Deployment accordingly. The tuning that decides whether this works under load is mostly about time. The Session Queue's request timeout must exceed the worst case cold start, which is pod scheduling plus image pull plus browser launch, otherwise the queue starts rejecting requests at precisely the moment the suite is asking for capacity and your run fails during scale-up rather than because of a defect.

Set terminationGracePeriodSeconds above your longest test so Kubernetes does not send SIGKILL into a live session. Give each pod roughly one CPU and one to two gigabytes, and mount an emptyDir with medium Memory at /dev/shm sized around 2Gi, because a memory-limited container with the default 64 MB shm produces tab crashes that read as application errors. Pre-pull browser images with a DaemonSet to avoid an image pull storm when fifty pods start at once, and think hard before putting Nodes on spot capacity, since a reclaimed instance kills live sessions and shows up as random flakiness.

# Node deployment: one clean browser per session
spec:
  terminationGracePeriodSeconds: 300
  containers:
    - name: chrome-node
      image: selenium/node-chrome
      env:
        - name: SE_NODE_MAX_SESSIONS
          value: "1"
        - name: SE_NODE_OVERRIDE_MAX_SESSIONS
          value: "true"
        - name: SE_DRAIN_AFTER_SESSION_COUNT
          value: "1"
        - name: SE_NODE_SESSION_TIMEOUT
          value: "300"
      resources:
        requests: { cpu: "1", memory: "1Gi" }
        limits:   { cpu: "2", memory: "2Gi" }
      volumeMounts:
        - { name: dshm, mountPath: /dev/shm }
  volumes:
    - name: dshm
      emptyDir: { medium: Memory, sizeLimit: 2Gi }
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata: { name: chrome-node-scaler }
spec:
  scaleTargetRef: { name: chrome-node }
  minReplicaCount: 2
  maxReplicaCount: 60
  cooldownPeriod: 60
  triggers:
    - type: selenium-grid
      metadata:
        url: http://selenium-router:4444/graphql
        browserName: chrome

Key Points

  • Distributed topology plus one-session-per-pod Nodes for clean autoscaling
  • KEDA Selenium Grid scaler reads queued requests from the Router GraphQL endpoint
  • session-request-timeout must exceed pod schedule plus image pull plus browser start
  • terminationGracePeriodSeconds longer than the longest test, or K8s kills live sessions
  • emptyDir Memory at /dev/shm; default 64 MB causes tab crashes under memory limits
Q40

A nightly Selenium suite gets slower and then OOMs on the build agent. How do you find and fix the leak?

AdvancedPerformance

Answer

Separate three distinct leaks before you touch anything, because the fixes are unrelated. The first is operating system processes. Every session that is not quit leaves a chromedriver and a browser process tree behind, and on a long-lived agent these accumulate until the kernel OOM killer starts choosing victims, which is often the JVM rather than the browsers.

Confirm it by counting chromedriver processes over time during a run. Fix it with a teardown that always runs, and add a reaper step in the pipeline as insurance rather than as the primary fix. The second is JVM heap.

ThreadLocal entries that are never removed keep the whole RemoteWebDriver object graph reachable through the pool thread's ThreadLocalMap for the life of the executor, and reporting frameworks that hold page source plus base64 screenshots per test will consume hundreds of megabytes across a two thousand test run. Run with heap dump on out of memory, open the dump, and look at what is retaining the driver instances. The third is browser side.

A driver reused across three hundred navigations accumulates detached DOM nodes, event listeners and service worker caches, and the signature is that the last tests in a class are noticeably slower than the first ones even though they do less work. Recycle the driver per class or every N tests instead of per suite. One diagnostic worth naming: a container with a memory limit but the default 64 MB /dev/shm produces the message about the session being deleted because the page crashed, or a tab crash, which teams routinely misfile as an application defect when it is purely a shared memory ceiling.

// 1. Recycle the session so browser-side memory cannot accumulate
public abstract class BaseTest {
    private static final int SESSIONS_BEFORE_RECYCLE = 25;
    private static final ThreadLocal<Integer> USED = ThreadLocal.withInitial(() -> 0);

    @BeforeMethod(alwaysRun = true)
    public void ensureDriver() {
        if (DriverFactory.get() == null || USED.get() >= SESSIONS_BEFORE_RECYCLE) {
            DriverFactory.destroy();   // quit() + ThreadLocal.remove()
            DriverFactory.create(System.getProperty("browser", "chrome"));
            USED.set(0);
        }
        USED.set(USED.get() + 1);
    }
}

// 2. Watch the real numbers while the suite runs
// watch -n 10 'pgrep -c chromedriver; free -m | head -2'

// 3. Make an OOM produce evidence instead of a mystery
// MAVEN_OPTS="-Xmx2g -XX:+HeapDumpOnOutOfMemoryError \
//   -XX:HeapDumpPath=target/heap.hprof"

// 4. Belt and braces in CI, never the primary fix
// pkill -f chromedriver || true

Key Points

  • Three separate leaks: OS processes, JVM heap, and in-browser memory
  • Unremoved ThreadLocal entries retain the whole driver graph on pool threads
  • Reporting attachments held in memory dominate heap on large suites
  • Slowdown within a class points at a reused session, not at the application
  • Small /dev/shm under a container memory limit shows up as a page crash
💡 Pro Tip: If the suite is fine for the first hundred tests and degrades after, that is almost always session reuse or an unremoved ThreadLocal, not the application under test.
Q41

How do you measure flakiness in a Selenium suite and drive it down, rather than retrying it away?

AdvancedFlakiness

Answer

Start with a definition you can compute: a flaky result is a test that failed and then passed against an unchanged commit. To compute it you need every result persisted with the commit SHA, test id, outcome, attempt number, duration, browser and node id. Teams that skip this step argue about flakiness from anecdote and never converge.

Once you have the data, rank tests by flake rate over a rolling two week window and triage by cause class, because the classes are finite. Timing: a missing explicit wait, an animation that intercepts the click, a race with an async request. Data: two tests sharing an account, a fixture that is not unique, state left over from a previous run.

Environment: an oversubscribed Grid, a third-party sandbox that rate limits, DNS. Application non-determinism: a genuine race in the product, and these are the most valuable finds because the suite has caught a real defect that would have reached users. The structural fixes are the same every time: implicit waits at zero, no sleeps, unique data generated per test, third-party calls stubbed, a fresh browser context per test, and assertions on state you control rather than on a shared environment.

Then set a quarantine policy with teeth: a test that flakes twice in the window leaves the gating suite for a nightly job, gets a named owner and a two week deadline, and is deleted if nobody fixes it. An unowned quarantined test is noise, not coverage. Interviewers at product companies press on this because they have seen a retry analyzer lift a dashboard to ninety-five percent while regressions shipped underneath it.

// Emit one row per attempt so flake rate is computable, not anecdotal
public class ResultRecorder implements ITestListener {

    @Override
    public void onFinish(ITestContext ctx) {
        String sha = System.getenv("GIT_COMMIT");
        Stream.of(ctx.getPassedTests(), ctx.getFailedTests(), ctx.getSkippedTests())
            .flatMap(r -> r.getAllResults().stream())
            .map(r -> Map.of(
                "commit", sha,
                "test", r.getMethod().getQualifiedName(),
                "status", statusName(r.getStatus()),
                "attempt", RetryAnalyzer.attemptsFor(r),
                "duration_ms", r.getEndMillis() - r.getStartMillis(),
                "browser", System.getProperty("browser", "chrome"),
                "node", System.getenv("HOSTNAME")))
            .forEach(TestResultsSink::write);
    }
}

-- Rolling 14-day flake rate, worst offenders first
-- SELECT test,
--        SUM(CASE WHEN attempt > 1 AND status = 'PASS' THEN 1 ELSE 0 END)
--          / COUNT(*)::float AS flake_rate
-- FROM test_results
-- WHERE run_at > now() - interval '14 days'
-- GROUP BY test HAVING COUNT(*) > 20
-- ORDER BY flake_rate DESC LIMIT 20;

Key Points

  • Flake = failed then passed on an unchanged commit; persist every result to compute it
  • Triage by cause: timing, data, environment, or real application races
  • Application races found by flaky tests are genuine defects, not test bugs
  • Structural fixes: zero implicit wait, no sleeps, unique data, stubbed third parties
  • Quarantine with an owner and a deadline; delete what nobody fixes
Q42

A 2,000-test Selenium regression pack takes four hours. Walk through getting it under fifteen minutes.

AdvancedCI/CD

Answer

Work the levers in order of payoff, and do the arithmetic out loud because that is what the interviewer is listening for. First, cut work. A two thousand test browser suite is usually six hundred journeys worth keeping plus fourteen hundred assertions about validation rules, calculations and permissions that belong in API or unit tests where they run in milliseconds and do not need a browser.

Deleting duplicated coverage is the cheapest speedup available and nobody wants to do it. Second, cut per test cost: inject the session instead of driving login, switch pageLoadStrategy to eager on ad-heavy pages, delete every Thread.sleep, and set implicit wait to zero so negative assertions stop paying a full timeout each. Third, parallelise inside the job with TestNG thread count sized to roughly one vCPU and one gigabyte per headless session.

Fourth, shard across jobs, and shard by recorded duration rather than by class name, because alphabetical splitting reliably produces one shard that takes forty minutes and nine that take four. Store per test durations from the last green run and bin pack greedily, longest test first into the currently lightest shard. Now the arithmetic: if the surviving suite averages twenty five seconds per test across twelve hundred tests, that is thirty thousand test seconds, roughly eight and a half hours of work.

Fifteen minutes of wall clock therefore needs about thirty four concurrent sessions, which is six shards of six threads on eight vCPU agents with headroom. Then run a two minute smoke shard fail fast on every pull request and keep the full cross browser matrix nightly.

#!/usr/bin/env python3
# Split by measured duration, not by class name.
import json, sys

SHARDS = int(sys.argv[1])
durations = json.load(open("target/last-green-durations.json"))  # {test: seconds}

# Longest first into the currently lightest shard
shards = [[] for _ in range(SHARDS)]
load = [0.0] * SHARDS
for test, secs in sorted(durations.items(), key=lambda kv: -kv[1]):
    i = load.index(min(load))
    shards[i].append(test)
    load[i] += secs

for i, tests in enumerate(shards):
    with open(f"target/testng-shard-{i}.xml", "w") as fh:
        fh.write('<suite name="shard-%d" parallel="methods" thread-count="6">\n'
                 '  <test name="t"><methods>\n' % i)
        for t in tests:
            fh.write('    <include name="%s"/>\n' % t)
        fh.write('  </methods></test>\n</suite>\n')
    print(f"shard {i}: {len(tests)} tests, {load[i]:.0f}s projected")

Key Points

  • Delete coverage that belongs in API or unit tests before optimising anything
  • Session injection, eager page load, zero implicit wait, no sleeps
  • Budget about 1 vCPU and 1 GB per headless session when sizing threads
  • Shard by recorded duration with greedy bin packing, never alphabetically
  • Total test-seconds divided by target wall clock gives required concurrency
💡 Pro Tip: Publish projected shard times in the pipeline log. The moment one shard drifts past the others, the split is stale and the whole job inherits its runtime.
Q43

How do you automate login flows that involve OTP, TOTP and captcha without weakening the product?

AdvancedAuthentication

Answer

Phone OTP is the primary auth path for most Indian consumer products, so this is a load-bearing question here rather than an edge case. There are four legitimate techniques and one that will fail a security review. First, fetch the code out of band: a test-only endpoint in lower environments that returns the latest OTP for a whitelisted number, the SMS provider's sandbox API, or a direct read from the messages table in a non-production database.

Poll it with a FluentWait that ignores the not-yet-delivered case, never a fixed sleep, because delivery latency varies wildly. Second, for a whitelisted range of test numbers, allow a fixed OTP behind an environment flag that is impossible to enable in production, which is a product decision the security team signs off on rather than something QA does quietly. Third, TOTP-based MFA needs no bypass at all: your test holds the shared secret and computes the current code with a TOTP library, exactly as an authenticator app would.

Fourth, for reCAPTCHA or hCaptcha, use the vendor's documented test keys in non-production so the challenge always passes. Routing through a captcha solving service is the answer that ends an interview badly: it violates the vendor terms and it means your suite depends on a third party being up. Two Selenium specific details worth naming.

OTP screens are usually six single-character inputs, so one sendKeys with the whole string fills only the first box, and auto-advance focus handlers can reorder characters when you type too quickly, which is a genuine flake source. Send per box and assert the assembled value before submitting. And for everything that is not testing login itself, inject the session instead.

// 1. Trigger the OTP
new LoginPage(driver).enterMobile("9199" + rand6()).requestOtp();

// 2. Poll a test-only channel, ignoring not-yet-delivered
String otp = new FluentWait<>(driver)
    .withTimeout(Duration.ofSeconds(45))
    .pollingEvery(Duration.ofSeconds(2))
    .ignoring(OtpNotDeliveredException.class)
    .withMessage(() -> "No OTP delivered for the test number")
    .until(d -> OtpTestApi.latestFor(mobile));   // 6 digits

// 3. Six separate inputs: one combined sendKeys fills only the first box
List<WebElement> boxes = driver.findElements(
    By.cssSelector("[data-testid^='otp-digit-']"));
for (int i = 0; i < boxes.size(); i++) {
    boxes.get(i).sendKeys(String.valueOf(otp.charAt(i)));
}

// Auto-advance handlers can reorder fast input: verify before submitting
String assembled = boxes.stream()
    .map(b -> b.getDomProperty("value")).collect(Collectors.joining());
Assert.assertEquals(assembled, otp, "OTP boxes filled out of order");

// 4. TOTP MFA needs no bypass at all
String code = new Totp(System.getenv("QA_TOTP_SECRET")).now();
driver.findElement(By.cssSelector("[data-testid='mfa-code']")).sendKeys(code);

Key Points

  • Fetch the OTP from a test endpoint, provider sandbox or lower-environment DB
  • Fixed OTP only for whitelisted test numbers behind a non-production flag
  • TOTP MFA needs no bypass: compute the code from the shared secret
  • Use vendor captcha test keys; never a solving service
  • Six-box OTP inputs need per-element sendKeys, not one combined string
💡 Pro Tip: Never let a test read production SMS. If the only way to get the OTP is a real device, that is a product observability gap worth raising, not a scripting problem.
Q44

How do you use the OpenTelemetry tracing built into Grid 4 to prove whether a slow test is the app or the infrastructure?

AdvancedObservability

Answer

Grid 4 ships instrumented with OpenTelemetry, and every component emits spans, so a single session produces a trace covering time waiting in the New Session Queue, the Distributor's slot decision, session creation on the Node, and each subsequent command. You turn it into something usable by putting an exporter on the classpath and setting the standard OpenTelemetry properties: the OTLP traces exporter, an endpoint pointing at your collector, and a distinct service name per component so the trace is readable. Tracing is on by default and can be disabled with the tracing flag, which people occasionally do for performance and then wonder why they are blind.

The real value is decomposition. A test that intermittently takes ninety seconds has four candidate explanations and the trace separates them cleanly: long queue time means you are out of capacity and need more Nodes or fewer threads, long session creation means image pull or browser startup, high per command latency means network distance between your runner and the Grid, and everything else is the application. That third one is underrated.

It is common for a team to run the Grid in one cloud region and the CI runners in another, and once you add fifty milliseconds of round trip to each of five hundred commands in a test you have paid twenty five seconds per test in pure network time that no profiler on the app will ever show. To make traces actionable, log the Grid session id and the trace id together in the test report so a red test links directly to its trace, and set se:name to the test name so the Grid UI and the trace agree on what you are looking at.

# Every Grid component, same collector, distinct service names
export OTEL_TRACES_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc

OTEL_SERVICE_NAME=selenium-router \
  java -jar selenium-server.jar router --tracing true \
    --sessions http://sessions:5556 --distributor http://distributor:5553 \
    --sessionqueue http://queue:5559

OTEL_SERVICE_NAME=selenium-node \
  java -jar selenium-server.jar node --config node.toml

Key Points

  • Router, Distributor, Queue and Node all emit spans for a session lifecycle
  • Configure the OTLP exporter endpoint and a service name per component
  • Trace separates queue time, session creation, command latency and app time
  • Cross-region runner and Grid adds RTT to every one of hundreds of commands
  • Log session id and trace id in the report so a red test links to its trace
💡 Pro Tip: Before blaming the application for a slow suite, look at one trace. Queue time and command RTT explain more slow Selenium runs than page performance does.
Q45

How do you run Cucumber with Selenium in parallel without the glue layer corrupting state?

AdvancedBDD

Answer

Cucumber's glue is global by design: step definitions are matched by expression across the whole glue package, and Cucumber creates a fresh instance of each step definition class per scenario. The standard mistake follows directly from that. Because several step classes need the same driver, people put it in a static field so everyone can reach it, and the suite then works perfectly in serial and produces nonsense in parallel.

The correct mechanism is scenario scoped dependency injection. Add cucumber-picocontainer, declare a plain TestContext class holding the driver and any per scenario state, and take it as a constructor parameter in every step class. PicoContainer builds one TestContext per scenario and injects the same instance into every step class in that scenario, which gives you exactly the isolation you need with no static anywhere.

Parallelism then comes from the runner: with the JUnit Platform engine you set the parallel execution properties in junit-platform.properties, and with TestNG you extend AbstractTestNGCucumberTests and override the scenarios data provider with parallel enabled. Hooks do the lifecycle: a tagged @Before creates the driver only for scenarios tagged as UI, so API only scenarios never pay for a browser, and an ordered @After quits it and calls scenario.attach with the screenshot bytes so the artefact lands in the Cucumber report rather than in a file nobody opens. Two further traps: each Scenario Outline example runs as an independent scenario and therefore needs its own unique data, and overlapping glue packages produce ambiguous step definition errors that only appear once both are on the path. Worth saying honestly in an interview: if no one outside QA reads the feature files, the Gherkin layer is cost without benefit.

// Scenario-scoped, injected by PicoContainer. No statics anywhere.
public class TestContext {
    public WebDriver driver;
    public String orderId;
}

public class Hooks {
    private final TestContext ctx;
    public Hooks(TestContext ctx) { this.ctx = ctx; }

    @Before("@ui")
    public void startBrowser() {
        ctx.driver = DriverFactory.build(System.getProperty("browser", "chrome"));
    }

    @After(order = 100)
    public void capture(Scenario scenario) {
        if (ctx.driver == null) return;
        if (scenario.isFailed()) {
            scenario.attach(((TakesScreenshot) ctx.driver)
                .getScreenshotAs(OutputType.BYTES), "image/png", scenario.getName());
        }
        ctx.driver.quit();
    }
}

public class CheckoutSteps {
    private final TestContext ctx;
    public CheckoutSteps(TestContext ctx) { this.ctx = ctx; }   // same instance

    @When("the buyer pays with UPI {string}")
    public void paysWithUpi(String vpa) {
        new CheckoutPage(ctx.driver).payWithUpi(vpa).submit();
    }
}

// junit-platform.properties
// cucumber.execution.parallel.enabled=true
// cucumber.execution.parallel.config.strategy=fixed
// cucumber.execution.parallel.config.fixed.parallelism=6

Key Points

  • Step definition classes are instantiated per scenario; a static driver breaks parallel
  • cucumber-picocontainer gives scenario-scoped constructor injection of a TestContext
  • Parallelism via junit-platform.properties or a parallel TestNG scenarios provider
  • Tagged @Before avoids launching a browser for API-only scenarios
  • Scenario Outline examples are separate scenarios and need unique data each
Q46

When would you keep Selenium rather than migrate to Playwright, and what does a migration actually cost?

AdvancedTooling

Answer

Answer this on engineering grounds, because a candidate who says Selenium is dead and a candidate who says Selenium is always better both fail. Selenium's durable advantages are structural. W3C WebDriver is a standard the browser vendors implement themselves, so safaridriver drives real Safari on real macOS and the same protocol underpins Appium, which means one framework and one skill set across web and mobile.

Grid is a mature self-hosted distribution layer, and for BFSI and government clients in India whose data cannot leave the network, an on-premise Grid is often the only option a security review will approve. Six official language bindings mean a Java or C# organisation keeps TestNG, Maven, Allure and an existing asset of thousands of tests. Playwright's real advantages are equally concrete: actionability checks built into every action so you are not hand-writing waits, browser contexts that give an isolated session in milliseconds instead of a fresh browser process, a bundled runner with fixtures and sharding, and a trace viewer.

Selenium has deliberately closed two gaps recently: Selenium Manager removed driver management entirely, and WebDriver BiDi brings cross-browser network interception and event streams that previously needed Chromium-only CDP. Two gaps stay open by design, because Selenium is a browser automation library and not a test framework: there is no runner and no auto-waiting, so your framework quality is entirely your own work. The migration cost is not the page objects, which port fairly mechanically.

It is every explicit wait, every CDP call, the TestNG listeners, the Grid infrastructure, the reporting pipeline, the CI shape, and retraining the team. Greenfield web-only in a TypeScript shop, take Playwright. Large existing Java asset, regulated on-premise, Safari or mobile in scope, keep Selenium and spend the money on the framework instead.

// The gap in one place: Selenium gives you no actionability check,
// so a serious framework writes one and uses it everywhere.
public final class Interact {

    public static void click(WebDriver driver, By locator, Duration timeout) {
        new FluentWait<>(driver)
            .withTimeout(timeout)
            .pollingEvery(Duration.ofMillis(200))
            .ignoring(StaleElementReferenceException.class)
            .ignoring(ElementClickInterceptedException.class)
            .ignoring(ElementNotInteractableException.class)
            .withMessage(() -> "Never became clickable: " + locator
                + " on " + driver.getCurrentUrl())
            .until(d -> {
                WebElement el = d.findElement(locator);
                if (!el.isDisplayed() || !el.isEnabled()) return false;
                new Actions(d).scrollToElement(el).perform();
                el.click();
                return true;
            });
    }
}

// Playwright equivalent: page.click(selector). The check is built in.
// That single difference is most of what a migration is actually buying.

Key Points

  • W3C WebDriver is vendor-implemented: real Safari, and shared with Appium for mobile
  • Self-hosted Grid clears on-premise security reviews that SaaS runners do not
  • Selenium Manager and BiDi closed the driver-management and interception gaps
  • No bundled runner and no auto-waiting: framework quality is your responsibility
  • Migration cost is waits, CDP calls, listeners, Grid, reporting and retraining
💡 Pro Tip: Frame the choice by constraint, not by preference: browser matrix, hosting rules, existing asset size, and team language. Interviewers are testing judgement, not loyalty.

Companies Hiring Selenium

TCS
Infosys
Wipro
Accenture
Cognizant
HCLTech
LTIMindtree
Flipkart

Salary Insights

Average in India
₹5-18 LPA

Frequently Asked Questions

What salary can a Selenium automation engineer expect in India in 2026?

Entry level QA automation roles at service organisations such as TCS, Infosys, Wipro, Cognizant and LTIMindtree typically start around ₹3.5-6 LPA, and a manual tester moving into Selenium internally often lands ₹4.5-7 LPA. With three to five years and a framework you genuinely built rather than maintained, the band is roughly ₹8-14 LPA, and SDET titles at product companies pull higher than QA Engineer titles for identical work, which is worth knowing before you negotiate. Senior SDET and automation architect roles at product companies in Bengaluru, Hyderabad, Pune and Gurugram run ₹18-32 LPA, with the top of that range going to people who own CI infrastructure and Grid at scale rather than only writing tests. Two multipliers matter more than years of experience: being able to work in the application's own language so you can read the code under test, and owning the pipeline, meaning Docker, Kubernetes, Grid and the reporting stack. Selenium alone plateaus around the middle of the band; Selenium plus API testing plus CI ownership is what breaks past it.

How long does it take to prepare for a Selenium interview if I am currently a manual tester?

Four to eight weeks of consistent evening effort is realistic if you already understand HTML, CSS selectors and basic programming. Spend the first two weeks on locators and waits until explicit waits and the difference between presence, visibility and clickability are automatic, because that is where most interviews start and where most candidates leak marks. Weeks three and four go on building one real framework end to end: page objects with By constants, a ThreadLocal driver factory, TestNG with parallel execution, a failure listener that captures screenshots, and a Maven build. Weeks five and six cover the failure modes that senior questions live in: StaleElementReferenceException, ElementClickInterceptedException, iframes, shadow DOM, file uploads and downloads. If you are targeting product companies, add two more weeks for Grid, Docker and CI. The single highest-return activity is automating a real public site with genuinely difficult behaviour rather than a practice demo site, because that is what produces the specific war stories interviewers reward.

What do interviewers expect from a fresher versus someone with five years of Selenium experience?

A fresher is assessed on fundamentals and reasoning: locator strategies, why explicit waits beat Thread.sleep, findElement versus findElements, handling frames, alerts and windows, plus enough Java or Python to write a clean loop and a small class. Getting the concept right matters more than perfect syntax, and being able to explain why your locator is stable earns more than reciting eight By strategies. At five years the questions move entirely to judgement and failure modes. Expect to be asked why your suite is flaky and what you measured, how you sized parallelism and what broke when you got it wrong, how you structured page objects and why you did or did not use PageFactory, how you handled a Grid running out of slots, and what you would delete from your current suite. Nobody at that level is impressed by knowing that Actions exists; they want the production incident where a sticky header intercepted a click across two hundred tests and what you changed. Bring two or three specific stories with numbers you actually collected.

Is Selenium still worth learning in 2026 when Playwright is growing so fast?

Yes, for reasons that are about the Indian job market rather than about which tool is nicer. The volume of open automation roles here is dominated by service organisations maintaining large existing Java suites for banking, insurance, telecom and retail clients, and those suites are Selenium, will remain Selenium for years, and are contractually tied to a browser matrix that includes real Safari. Self-hosted Grid also clears on-premise security reviews that a SaaS test runner does not, which matters for BFSI and government work. Technically Selenium has closed real gaps recently: Selenium Manager removed driver version management, and WebDriver BiDi brings the cross-browser event and network interception that previously required Chromium-only CDP. The honest positioning is that Selenium is the safer bet for employability across the whole Indian market and Playwright is the better default for greenfield web-only projects in TypeScript shops. Learning Selenium properly, especially the W3C protocol and Grid, transfers almost entirely, so it is not a dead end either way.

Should I learn Selenium, Playwright or Cypress first for the best career outcome?

Match the tool to the job market you are targeting. If you want maximum interview volume in India, particularly at service organisations and enterprise product teams, learn Selenium with Java plus TestNG and Maven, because that is what the majority of postings ask for by name. If you are aiming at product startups with JavaScript or TypeScript stacks, Playwright with its own runner is the faster path and its trace viewer makes you productive quickly. Cypress remains common in frontend-owned test suites but its architecture constrains multi-tab and multi-origin scenarios, so it is the narrowest of the three. The strategic answer, and the one that pays, is that all three are variations on the same skills: locating elements, reasoning about asynchrony, designing page objects, keeping tests independent, and running them in CI. Learn one deeply enough to have opinions, then pick up the second in a fortnight. What actually separates candidates in an interview is API testing ability, CI ownership and the capacity to explain why a suite is flaky, none of which is tool specific.

Do Selenium certifications help, and what should I show instead?

Certifications carry little weight for automation roles in India. ISTQB Foundation still gets asked for in some service-organisation HR filters and is cheap to clear, so treat it as a checkbox rather than as evidence of skill, and the ISTQB Test Automation Engineer extension is only worth the time if a specific employer has named it. No Selenium certification is recognised by the Selenium project itself. What moves an interview is a public repository containing one framework you built: page objects with By constants and no PageFactory, a ThreadLocal driver factory, TestNG parallel execution, a failure listener producing screenshots and console logs, a docker-compose Grid, a GitHub Actions workflow that runs it, and a README explaining the design decisions and their trade-offs. Add a short section on what you measured, such as suite runtime before and after removing sleeps. One repository like that plus two concrete debugging stories outperforms any certificate, because it demonstrates exactly the judgement the technical rounds are testing for.

Introduction

Selenium is the oldest browser automation stack still winning production work, and in 2026 that is because of the W3C WebDriver specification rather than in spite of it. Selenium 4 speaks pure W3C WebDriver, so the same test code drives Chrome, Edge, Firefox and Safari through vendor-maintained drivers, and the same protocol underpins Appium for mobile. In India that matters commercially: service organisations such as TCS, Infosys, Wipro, Accenture and LTIMindtree run enormous regression suites for banking, insurance and telecom clients where the browser matrix is contractual, the language is Java, and the suite has to keep running for a decade.

Interviews for these roles are far less about knowing that findElement exists and far more about the failure modes. Panels probe why your suite is flaky, whether you understand that WebDriver is not thread-safe, how you size a Grid node, what actually causes StaleElementReferenceException, and whether you can explain the shift from Chrome DevTools Protocol hooks to the standardised WebDriver BiDi transport. Product companies like Flipkart add scale questions: parallel execution in Kubernetes, container memory limits, session queue backpressure, and how you keep a two-thousand-test suite under fifteen minutes of wall clock in CI.

This guide works through 46 Selenium interview questions asked in 2026, ordered basic first and then intermediate and advanced. Each answer explains the real behaviour, the production gotcha that bites teams in month three, and what the interviewer is actually checking. Every question carries a snippet using current Selenium 4 APIs including Selenium Manager, Duration-based waits, relative locators, getShadowRoot, wheel actions and BiDi. Work the basic block until the wait and locator semantics are automatic, then spend your remaining prep time on the concurrency, Grid and observability sections that decide senior offers.

Ready to practice Selenium interviews?

Don't just read, practice these Selenium questions live with an AI interviewer that asks follow-ups and scores your answers.

AI-powered practice
Instant feedback
Free to start
Start Free Mock Interview