Skip to content

Selenium โ€” Complete Interview Guide (5+ Yrs Automation Engineer)

Comprehensive Selenium reference grounded in your actual code at /Users/rohan/ROhan personal/automation/B2BProjectTest/ โ€” the Avysh B2B Java + Selenium 4 + TestNG + REST Assured + ExtentReports framework you built and ran in production for ~2 years. Every concept includes spoken-style interview answers and references real classes from your repo.

How this file is organised

Section Topic
1 Why Selenium + when to still pick it in 2026
2 Architecture & internals (W3C, drivers, sessions)
3 WebDriver lifecycle + DriverFactory pattern
4 Locators โ€” basics, XPath, CSS, Selenium 4 relative locators
5 The three waits โ€” implicit, explicit, fluent
6 Actions (click, sendKeys, Actions class)
7 JavaScriptExecutor
8 Frames, windows, tabs, alerts, cookies
9 Page Object Model โ€” POM, Page Factory, Helper/Facade
10 TestNG annotations + listeners + retry
11 Parallel execution + ThreadLocal driver
12 Selenium Grid (local + Docker)
13 Selenium 4 CDP โ€” network, geolocation, console
14 Headless mode + CI flags
15 Exceptions โ€” what each one really means
16 Reporting โ€” ExtentReports listener pattern
17 Tech debt in B2BProjectTest โ€” how I'd refactor
18 Selenium โ†’ Playwright migration (when and why)
19 35+ interview Q&A with full spoken answers
20 Capacity planning & scale โ€” scenario/system-design questions (100 tests in 1 min, Safari constraint, reduce suite time, data isolation at scale)

1. WHY SELENIUM

1.1 The honest 2026 framing

Selenium is no longer the default for new frameworks โ€” Playwright wins for greenfield work. But Selenium is still the right choice when:

Reason Why
Existing Java/TestNG codebase Migration cost outweighs Playwright's wins
Legacy browser support (IE 11, older Edge) Playwright doesn't target IE
Mature Selenium Grid + Docker fleet Re-tooling parallel infra is expensive
Polyglot teams Same Java API works for QA and backend devs
Compliance / banking Selenium 4 is W3C-standardised; some procurement processes only allow W3C tools

1.2 What Selenium 4 actually changed (memorize these โ€” top interview question)

Change What it gives you
W3C WebDriver protocol (replaces JSON Wire) More reliable, standardized, faster
Relative Locators (above, below, near, toLeftOf, toRightOf) Position-aware element finding
Native CDP support in Chrome / Edge Network mocking, geolocation, device emulation, console listening
New window/tab API โ€” driver.switchTo().newWindow(WindowType.TAB) Cleaner than the old window-handle dance
Optional Capabilities replaced by browser-specific Options (ChromeOptions, FirefoxOptions) Type-safe configuration
Improved Grid โ€” Docker-native, observability, distributed mode Easier scaling
Selenium Manager Auto-downloads matching driver โ€” no more chromedriver version-pinning hell

Spoken interview answer โ€” "What's new in Selenium 4?"

"Five things that actually matter in production. First, the protocol moved from JSON Wire to W3C WebDriver โ€” every browser now speaks the same standardized protocol, which made commands more reliable and slightly faster. Second, Relative Locators โ€” I can say aboveOf, below, near to find elements by position relative to another element, useful for forms where labels aren't formally associated. Third, native Chrome DevTools Protocol support โ€” I can throttle network, mock geolocation, intercept network requests directly without third-party plugins. Fourth, the new tab API โ€” driver.switchTo().newWindow(WindowType.TAB) is cleaner than juggling window handles. Fifth, Selenium Manager auto-downloads the right driver version, which killed an entire class of CI failures we used to debug weekly."


2. ARCHITECTURE & INTERNALS

2.1 The five layers

Your test code (Java/Python/JS/C#)
        โ†“
Selenium Client Library (language-specific bindings)
        โ†“
W3C WebDriver Protocol (HTTP + JSON)
        โ†“
Browser Driver (chromedriver / geckodriver / msedgedriver)
        โ†“
Browser (Chrome, Firefox, Edge, Safari)

2.2 What "session" means

When you call new ChromeDriver(): 1. Client starts the chromedriver executable as a local HTTP server (port 9515 by default) 2. Sends POST /session with desired capabilities 3. chromedriver launches Chrome + attaches via DevTools 4. Returns a session ID 5. Every subsequent command (click, navigate, etc.) is an HTTP request with that session ID

Why this matters in interviews

"Each command in Selenium is a separate HTTP request to the driver. So driver.findElement(...).click() is actually two requests โ€” one to resolve the locator, one to click. That's the architectural reason Selenium feels slower than Playwright's WebSocket โ€” at 200 commands per test, the per-request overhead adds up to seconds."

2.3 Why JSON Wire is gone

The old JSON Wire protocol was a Selenium-only spec. The W3C version standardised the wire format so browser vendors can implement it natively โ€” and they do. The endpoints and semantics are well-defined now, which made cross-browser tests behave more consistently.


3. WEBDRIVER LIFECYCLE + DRIVERFACTORY PATTERN

3.1 Your WebDriverUtils.java (real code, annotated)

public class WebDriverUtils {
    public WebDriver driver;
    public ThreadLocal<RemoteWebDriver> threadDriver = null;
    public String runParallel = "false";

    String driverPath = File.separator + "src" + File.separator + "main"
        + File.separator + "resources" + File.separator + "driver" + File.separator;

    public WebDriver getDriver() {
        if(threadDriver == null) {
            return driver;
        } else {
            return threadDriver.get();
        }
    }

    public void setDriverPath(String browserName) {
        switch (browserName) {
            case "Chrome":
                if(System.getProperty("os.name").toLowerCase().contains("mac")) {
                    this.driverPath = driverPath + "mac" + File.separator + "chromedriver";
                } else if(System.getProperty("os.name").toLowerCase().contains("windows")) {
                    this.driverPath = driverPath + "window" + File.separator + "chromedriver.exe";
                } else {
                    this.driverPath = driverPath + "ubuntu" + File.separator + "chromedriver";
                }
                break;
            // ... Firefox, IE, Edge, Safari ...
        }
    }

    public void initializeDriver(String browserName, String hubURL, String mode) {
        if(!runParallel.equalsIgnoreCase("true") && mode.equalsIgnoreCase("normal")) {
            switch (browserName) {
                case "chrome":
                    setDriverPath("Chrome");
                    System.setProperty("webdriver.chrome.driver",
                        System.getProperty("user.dir") + getDriverPath());
                    setDriver(new ChromeDriver());
                    break;
                // ... Firefox, IE, Edge, Safari ...
            }
        } else if(!runParallel.equalsIgnoreCase("true") && mode.equalsIgnoreCase("headless")) {
            // headless variant
            ChromeOptions option = new ChromeOptions();
            option.addArguments("--window-size=1920,1080");
            option.addArguments("--start-maximized");
            option.addArguments("--headless");
            option.addArguments("--disable-extensions");
            option.addArguments("--disable-gpu");           // legacy windows fix
            option.addArguments("--disable-dev-shm-usage"); // /dev/shm too small in Docker
            option.addArguments("--no-sandbox");            // Docker rootless workaround
            setDriver(new ChromeDriver(option));
        }
    }
}

Spoken walkthrough โ€” "Walk me through WebDriverUtils"

"WebDriverUtils is the driver factory in our framework. It holds both a regular WebDriver and a ThreadLocal<RemoteWebDriver> โ€” the ThreadLocal is what makes TestNG's parallel="classes" mode work without browser instances stealing each other. The setDriverPath method picks the right binary per OS โ€” mac, windows, ubuntu โ€” because we kept the drivers in src/main/resources/driver/{os}/. initializeDriver accepts a mode parameter โ€” normal or headless โ€” because we ran headless on Jenkins CI but headed locally for debugging. The four Chrome flags --disable-gpu, --disable-dev-shm-usage, --no-sandbox, --disable-extensions are the canonical CI quartet โ€” without them Chrome crashes in Docker. Each one has a specific reason: --no-sandbox is needed because Docker runs as root, --disable-dev-shm-usage is because /dev/shm is tiny by default, --disable-gpu is a legacy Windows fix that became habit, --disable-extensions shaves seconds off startup."

3.2 The modern alternative โ€” Selenium Manager + WebDriverManager

The setDriverPath + manual System.setProperty pattern is legacy now. Selenium 4.6+ ships with Selenium Manager โ€” auto-downloads the right driver. You can also use the Bonigarcia WebDriverManager library (which your pom.xml already includes):

import io.github.bonigarcia.wdm.WebDriverManager;

WebDriverManager.chromedriver().setup();
WebDriver driver = new ChromeDriver();
One line, no path setting, no OS detection.

Honest framing

"Our framework predates Selenium Manager โ€” when I built it in 2020, you really did have to ship per-OS chromedriver binaries in resources/driver/{os}/. Today I'd delete the entire OS-detection block and use Selenium Manager. The WebDriverManager library is also in our pom.xml, just unused โ€” I'd switch to it on the next refactor and remove about 80 lines of code from WebDriverUtils."

3.3 The canonical headless flags โ€” what each one does

Flag Why
--headless Run Chrome without a UI. --headless=new for Chrome 109+
--no-sandbox Bypass Chrome's user-namespace sandbox. Required when running as root in Docker.
--disable-dev-shm-usage Force Chrome to use /tmp for shared memory instead of /dev/shm (which is only 64MB in default Docker)
--disable-gpu Legacy Windows fix (Chrome on Windows tried to use GPU for headless rendering and crashed). Mostly harmless on Linux.
--window-size=1920,1080 Set a deterministic viewport for screenshots and responsive selectors
--start-maximized Maximizes window โ€” useful for layouts that depend on viewport size
--disable-extensions Faster startup, avoids extension interference

4. LOCATORS โ€” DEEP

4.1 The eight built-in locators

By.id("email");                                       // fastest, requires unique id
By.name("password");                                  // form fields
By.className("btn-primary");                          // CSS class
By.tagName("input");                                  // when only one tag matches
By.linkText("Sign in");                               // exact link text
By.partialLinkText("Sign");                           // substring link text
By.cssSelector("input[name='email']");                // fast, flexible
By.xpath("//input[@id='email']");                     // last resort

4.2 Priority order I follow

  1. ID โ€” fastest because browsers maintain an id โ†’ element map. Use if available.
  2. Name โ€” form fields often have stable name
  3. CSS selector โ€” fast, flexible, well-supported
  4. Link text โ€” for anchors only
  5. XPath โ€” last resort, but unavoidable when text-based lookup is needed

4.3 XPath cheatsheet (the parts that show up in interviews)

//input[@type='text']                       attribute equality
//button[text()='Login']                    exact text
//button[contains(text(),'Log')]            partial text
//div[@class='card' and @data-id='42']      multi-attribute
//div[contains(@class,'btn-primary')]       partial class (because @class is whole string!)
//a[starts-with(@href,'/user')]             starts-with
//div[@id='form']//input                    descendant
//div[@id='form']/input                     direct child
//input[1]                                  first matching
//input[last()]                             last matching
//tr[td[contains(.,'Pending')]]             rows containing td with 'Pending'
//label[normalize-space()='Email']/following-sibling::input    sibling navigation
//input[@id='email']/ancestor::form         walk up the tree

Real example from your HomePage.java

@FindBy(xpath="//*[@webix_tm_id='channelparent']")
WebElement linkManageChannels;

@FindBy(xpath="//*[@webix_tm_id='sidebar_link_LevelsTiers']")
WebElement linkManageChannelsLevels;

@FindBy(xpath="//*[@view_id='$template7']/div/div/span")
WebElement orderStatus;
Why XPath here was justified: the Avysh B2B app was built on Webix (a JavaScript UI framework). Webix renders custom HTML with webix_tm_id, view_id, radio_id attributes. These are stable per-feature, and By.cssSelector("[webix_tm_id='channelparent']") would work equally โ€” but the team standardized on XPath because the Webix tree often required parent/child navigation that's cleaner in XPath.

Spoken answer โ€” "When do you use XPath vs CSS?"

"CSS first when the locator is a simple attribute or descendant relationship โ€” it's faster and more readable. XPath when I need text-based matching (contains(text(), ...)) or navigation up the tree (ancestor::form) or by sibling (following-sibling::input). In our B2B framework we used XPath heavily because the underlying UI framework, Webix, exposed custom attributes โ€” webix_tm_id, view_id โ€” and the layouts had complex parent-child relationships. I never write absolute XPaths like /html/body/div[1]/... โ€” those break on the first DOM change."

4.4 Selenium 4 Relative Locators

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

WebElement emailLabel = driver.findElement(By.id("email-label"));
WebElement emailInput = driver.findElement(with(By.tagName("input")).toRightOf(emailLabel));

// Available relations: above, below, toLeftOf, toRightOf, near
When to use: forms where labels and inputs aren't formally associated, layouts where logical position is more reliable than DOM path. Not a magic bullet โ€” it does a render-position check, so dynamic re-layouts can break it.

4.5 PageFactory @FindBy (your B2BProjectTest pattern)

public class MyOrdersPage extends CommonUtils {
    @FindBy(xpath="//*[@class='viewOrder']")
    WebElement btnViewEye;

    @FindBy(xpath="//*[@view_id='orderAccept']/div/button")
    WebElement btnAccept;

    @FindBy(xpath="//*[@view_id='orderReject']/div/button")
    WebElement btnReject;

    public MyOrdersPage(WebDriver driver) {
        super(driver);
        PageFactory.initElements(driver, this);
    }
}

How @FindBy works: 1. PageFactory.initElements(driver, this) scans the class via reflection for @FindBy-annotated fields 2. For each field, it creates a proxy WebElement that captures the locator 3. The proxy doesn't resolve the element until you actually call a method on it (lazy) 4. Every call re-resolves โ€” so cached references don't go stale between actions

Spoken answer โ€” "What is PageFactory and how is it different from manual findElement?"

"PageFactory is a Selenium utility that uses reflection to wire up @FindBy-annotated fields with proxy WebElements. Two practical effects. First, the locator and its declaration sit next to the field name โ€” readability win. Second, the proxy is lazy and re-resolves on each call, so I rarely hit StaleElementReferenceException unless the parent DOM literally is replaced. The trade-off is that PageFactory's proxies can mask issues โ€” when the locator is wrong, you get NoSuchElementException only when you actually use it, not when the page object is constructed. For complex pages with hundreds of locators that's fine; for simple flows, manual driver.findElement is honestly easier to debug."


5. THE THREE WAITS โ€” IMPLICIT, EXPLICIT, FLUENT

This is the single most-asked Selenium topic. Memorize the differences.

5.1 Implicit wait

driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
- Sets a global timeout for every findElement call - If element not immediately found, polls until found or timeout - Once set, applies to every find for the driver's lifetime - Downside: applies to every find, which makes negative tests (asserting absence) slow

5.2 Explicit wait

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("submit")));
wait.until(ExpectedConditions.elementToBeClickable(By.id("submit")));
wait.until(ExpectedConditions.textToBePresentInElementLocated(
    By.id("status"), "Success"));
- Targets a specific element with a specific condition - Polls every 500ms by default - Throws TimeoutException if condition not met - This is what you should use in production code

Common ExpectedConditions

Condition Use when
visibilityOf(elem) Wait for element to be visible in DOM and have non-zero size
visibilityOfElementLocated(by) Same but with locator (re-resolves)
elementToBeClickable(elem) Visible AND enabled
presenceOfElementLocated(by) In DOM (may be hidden)
invisibilityOfElementLocated(by) Spinner gone
textToBePresentInElement(elem, text) Element contains text
urlContains(fragment) After navigation
titleContains(title) After navigation
alertIsPresent() Before driver.switchTo().alert()
numberOfElementsToBe(by, 5) Wait for list of 5
stalenessOf(elem) Wait for element to detach from DOM

5.3 Fluent wait

Wait<WebDriver> wait = new FluentWait<>(driver)
    .withTimeout(Duration.ofSeconds(30))
    .pollingEvery(Duration.ofSeconds(2))
    .ignoring(NoSuchElementException.class)
    .ignoring(StaleElementReferenceException.class);

WebElement el = wait.until(d -> d.findElement(By.id("data")));
- Same as explicit wait + custom polling interval + per-call ignored exception list - Use when you need to keep polling through transient exceptions

5.4 The Big Anti-Pattern โ€” mixing implicit and explicit

// DON'T DO THIS
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));
wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("x")));
When implicit + explicit are both active, Selenium can wait for the maximum of the two, then behaviour is implementation-specific. The safe rule: - Set implicit wait to 0 in your driver setup - Use explicit waits everywhere

Spoken answer โ€” "Walk me through the three waits and when to use each"

"Implicit wait is global โ€” set once, applies to every findElement call. The downside is it slows down negative tests because every 'element not found' check waits the full timeout. Explicit wait is targeted โ€” it waits for a specific condition on a specific element using WebDriverWait plus ExpectedConditions.elementToBeClickable or similar. This is what I use 95% of the time. Fluent wait is explicit wait plus a custom polling interval and the ability to ignore specific exceptions during polling โ€” useful when I expect transient StaleElementReferenceException during a DOM refresh. The cardinal rule is never mix implicit and explicit โ€” set implicit to zero and use explicit everywhere. Mixing them produces unpredictable timeouts."

5.5 Thread.sleep is not a wait

Your WebDriverUtils.waitForPageToCompleteState() uses Thread.sleep inside a loop:

public void waitForPageToCompleteState() throws InterruptedException {
    int counter = 0;
    int maxNoOfRetries = 10;
    while (maxNoOfRetries > 0 && (counter != maxNoOfRetries)) {
        Thread.sleep(2000);
        try {
            JavascriptExecutor js = (JavascriptExecutor) driver;
            if (js.executeScript("return document.readyState").toString().equals("complete")) {
                Thread.sleep(2000);
                break;
            }
        } catch (Exception e) {}
        counter++;
    }
}
Tech debt callout: this is checking document.readyState with Thread.sleep retries โ€” anti-pattern on three counts. First, Thread.sleep is a blocking pause, not a wait. Second, the Thread.sleep(2000) after the check is a magic delay that means the function always takes at least 2s even when the page was ready. Third, the outer Thread.sleep(2000) per loop means worst-case 20 seconds for a ready page that has a glitch in readyState.

Right pattern:

new WebDriverWait(driver, Duration.ofSeconds(20))
    .until(d -> ((JavascriptExecutor) d).executeScript("return document.readyState").equals("complete"));


6. ACTIONS โ€” CLICK, SENDKEYS, ACTIONS CLASS

6.1 Basic interactions

WebElement input = driver.findElement(By.id("email"));
input.clear();
input.sendKeys("a@x.com");

driver.findElement(By.id("submit")).click();
driver.findElement(By.id("country")).getAttribute("value");
driver.findElement(By.id("desc")).getText();

6.2 Your CommonUtils.click pattern (real code)

public void click(WebElement element) throws Exception {
    try {
        waitTillElementIsClickable(element, minTime);
        element.click();
    } catch (StaleElementReferenceException ex) {
        waitTillElementIsClickable(element, minTime);
        element.click();
    }
}

public void sendKeysTo(WebElement element, String str) throws InterruptedException {
    element.clear();
    element.sendKeys(str);
}
Why this is the right shape: waitTillElementIsClickable before every click means tests don't fail on transient timing. The Stale catch-and-retry handles the case where the DOM updates between the wait and the click โ€” rare but real on Angular apps that re-render on data load.

6.3 Select dropdowns

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

Select dropdown = new Select(driver.findElement(By.id("country")));
dropdown.selectByVisibleText("India");
dropdown.selectByValue("IN");
dropdown.selectByIndex(2);

// Read
String selected = dropdown.getFirstSelectedOption().getText();
List<WebElement> options = dropdown.getOptions();
Only works for native <select> elements. Custom div-based dropdowns (like Webix in your B2B app) need click-open + click-option.

6.4 Actions class โ€” mouse, keyboard, drag

import org.openqa.selenium.interactions.Actions;

Actions actions = new Actions(driver);

// Hover
actions.moveToElement(menu).perform();

// Right-click
actions.contextClick(elem).perform();

// Double-click
actions.doubleClick(elem).perform();

// Drag and drop
actions.dragAndDrop(source, target).perform();

// Manual drag (for HTML5 dragstart events)
actions.clickAndHold(source).moveByOffset(100, 0).release().perform();

// Keyboard
actions.keyDown(Keys.CONTROL).click(link).keyUp(Keys.CONTROL).perform();  // Ctrl+click
actions.sendKeys(Keys.chord(Keys.CONTROL, "a")).perform();                 // Ctrl+A

6.5 File upload

driver.findElement(By.id("upload")).sendKeys("/absolute/path/to/file.png");
Works because Selenium types the path into the hidden <input type="file">. No native dialog interaction.

6.6 Real-world clear quirk

element.clear() doesn't always fire input / change events that React or Angular listen on. If clear doesn't trigger the form's dirty state, do:

element.sendKeys(Keys.chord(Keys.CONTROL, "a"));
element.sendKeys(Keys.DELETE);


7. JAVASCRIPTEXECUTOR

When WebDriver can't do it natively, drop down to JS.

7.1 Common uses

JavascriptExecutor js = (JavascriptExecutor) driver;

// Scroll
js.executeScript("window.scrollTo(0, document.body.scrollHeight);");
js.executeScript("arguments[0].scrollIntoView({block: 'center'});", element);

// Click when an overlay intercepts
js.executeScript("arguments[0].click();", element);

// Read storage
String token = (String) js.executeScript("return localStorage.getItem('access_token');");

// Set a hidden input bypassing oninput handlers
js.executeScript("arguments[0].value = arguments[1];", input, "hello");

// Set viewport for screenshot fidelity
js.executeScript("window.resizeTo(1920, 1080);");

// Get page title via DOM (more reliable than driver.getTitle on some apps)
String title = (String) js.executeScript("return document.title;");

// Wait for jQuery / Angular to be idle
Boolean done = (Boolean) js.executeScript("return jQuery.active === 0;");

7.2 When to use vs not use

  • Use when overlay intercepts a click, when scroll is needed for a fixed-header layout, when reading browser storage
  • Don't use for things WebDriver supports natively โ€” JS-click bypasses Selenium's actionability checks and can mask real bugs (e.g., the button was disabled but JS click still triggered the handler)

Spoken answer โ€” "When would you use JavascriptExecutor?"

"Three real scenarios. First, when an overlay intercepts a click โ€” arguments[0].click() via JS bypasses the click interceptor. Second, when I need to scroll an element into the visible viewport, especially with sticky headers, scrollIntoView with block: 'center' is more reliable than Selenium's auto-scroll. Third, reading browser state that WebDriver doesn't expose โ€” localStorage tokens, performance.now() timings, anything in window. I avoid using it as a workaround for click failures, because JS click skips Selenium's actionability checks โ€” visibility, enabled, hit-testing โ€” which can mask bugs like 'button is disabled but the handler still fires.'"


8. FRAMES, WINDOWS, TABS, ALERTS, COOKIES

8.1 iframes

// By index, name/id, or element
driver.switchTo().frame(0);
driver.switchTo().frame("paymentFrame");
driver.switchTo().frame(driver.findElement(By.tagName("iframe")));

driver.switchTo().parentFrame();        // back one level
driver.switchTo().defaultContent();      // back to top
Gotcha: every locator inside a frame searches only that frame. Forget to switchTo().defaultContent() and your next locator silently fails.

8.2 Multiple windows / tabs (the old way)

String original = driver.getWindowHandle();
// click something that opens a new tab
for (String handle : driver.getWindowHandles()) {
    if (!handle.equals(original)) {
        driver.switchTo().window(handle);
        break;
    }
}
// ... work in new tab
driver.close();                          // closes current window
driver.switchTo().window(original);

8.3 New tab/window โ€” Selenium 4 native

driver.switchTo().newWindow(WindowType.TAB);
driver.switchTo().newWindow(WindowType.WINDOW);
// Driver now points at the new window โ€” no handle juggling

8.4 Alerts (browser-native popup)

Alert alert = driver.switchTo().alert();
String text = alert.getText();
alert.accept();              // OK
alert.dismiss();             // Cancel
alert.sendKeys("answer");    // for prompts

8.5 Cookies โ€” manipulate without UI

// Save
File f = new File("cookies.json");
new ObjectMapper().writeValue(f, driver.manage().getCookies());

// Load
driver.get("https://example.com");  // must be on the domain first
List<Cookie> saved = new ObjectMapper().readValue(f, new TypeReference<>(){});
saved.forEach(c -> driver.manage().addCookie(c));
driver.navigate().refresh();

// One-off
driver.manage().addCookie(new Cookie("session", "abc123"));
driver.manage().deleteCookieNamed("session");
driver.manage().deleteAllCookies();
Use case: poor man's storageState โ€” login once, save cookies, reuse to skip the login form in subsequent tests.


9. PAGE OBJECT MODEL โ€” POM, PAGE FACTORY, HELPER/FACADE

9.1 Plain POM

public class LoginPage {
    private final WebDriver driver;

    private final By emailInput = By.id("email");
    private final By passwordInput = By.id("password");
    private final By loginBtn = By.id("login");

    public LoginPage(WebDriver driver) { this.driver = driver; }

    public void login(String email, String password) {
        driver.findElement(emailInput).sendKeys(email);
        driver.findElement(passwordInput).sendKeys(password);
        driver.findElement(loginBtn).click();
    }
}

9.2 Page Factory POM (your B2BProjectTest pattern)

public class HomePage extends CommonUtils {
    public WebDriver driver;

    @FindBy(xpath="//*[@webix_tm_id='channelparent']")
    WebElement linkManageChannels;

    @FindBy(xpath="//*[@webix_tm_id='ordersparent']")
    WebElement linkOrders;

    public HomePage(WebDriver driver) {
        super(driver);
        this.driver = driver;
        PageFactory.initElements(driver, this);
    }
}

9.3 Your three-tier pattern โ€” Page + Helper + Test

You're using a sophisticated layering โ€” Page Objects expose elements, Helper classes wrap user-facing business actions, Tests call Helpers.

// PAGE (locators + atomic actions)
public class HomePage extends CommonUtils { @FindBy(...) WebElement linkOrders; }

// HELPER (business workflows)
public class HomeHelper {
    HomePage homePage;
    public HomeHelper(WebDriver driver) { homePage = new HomePage(driver); }
    public void selectBrand(String brand, String subBrand) {
        homePage.clickBrandDropdown();
        homePage.selectBrand(brand);
        // ...
    }
}

// TEST (orchestrates helpers)
public class RegionsTest {
    @Test
    public void verifyCreateAndDeleteRegions() throws Exception {
        homeHelper.selectBrand(testData.get("brandName"), testData.get("subBrandName"));
        regionsHelper.createRegions(testData.get("countryName"), testData.get("cityName"));
        softAssertions.assertThat(regionsHelper.verifyCreateRegions(testData.get("cityName")))
            .describedAs("Regions Created Successfully")
            .isEqualTo(true);
    }
}

Spoken answer โ€” "Why both Page and Helper layers?"

"The Page layer owns the DOM โ€” locators and atomic actions. The Helper layer owns the user journey โ€” selectBrand, createRegion, placeOrder. Tests orchestrate the helpers. The win is that tests read like English โ€” regionsHelper.createRegions(country, city) โ€” and changes to the page structure only ripple to the Page class, not every test. The Helper layer also lets multiple tests share a workflow without duplicating it. The trade-off is one more layer to navigate, which is fine for a 40+ page application like ours but overkill for a 5-page app."

9.4 Inheritance pattern โ€” extending a CommonUtils base

Your pages extend CommonUtils which holds driver, click(), sendKeysTo(), findTheElement(), alertPopUp() etc. This is the Base Page pattern โ€” shared helpers in one place.

public abstract class CommonUtils {
    public WebDriver driver;
    public CommonUtils(WebDriver driver) { this.driver = driver; }

    public void click(WebElement element) throws Exception {
        try {
            waitTillElementIsClickable(element, minTime);
            element.click();
        } catch (StaleElementReferenceException ex) {
            waitTillElementIsClickable(element, minTime);
            element.click();
        }
    }
    public void sendKeysTo(WebElement element, String str) { ... }
    public void waitForNGToLoad() { ... }
    public void alertPopUp() { ... }
}
Why this is correct: every page now inherits a single, tested set of robust click/sendKeys/wait helpers โ€” no per-page reinvention.


10. TESTNG ANNOTATIONS + LISTENERS + RETRY

10.1 The annotation order โ€” memorize this

@BeforeSuite     โ†’ once before all tests
  @BeforeTest    โ†’ before each <test> in testng.xml
    @BeforeClass โ†’ once per test class
      @BeforeMethod โ†’ before each @Test
        @Test       โ†’ the test
      @AfterMethod  โ†’ after each @Test
    @AfterClass    โ†’ once per test class
  @AfterTest     โ†’ after each <test>
@AfterSuite      โ†’ once after all tests

10.2 The full set of @Test attributes

@Test(
    description = "Create and Delete a Region",       // shows in reports
    groups = {"Smoke_Test"},                          // for selective runs
    testName = "Regions-1.1",                         // human-readable name
    priority = 1,                                     // lower runs first
    enabled = true,                                   // skip if false
    invocationCount = 3,                              // run N times
    threadPoolSize = 2,                                // run invocations in parallel
    timeOut = 60_000,                                  // ms per test
    dependsOnMethods = "verifyLogin",
    dependsOnGroups = "auth",
    alwaysRun = true,                                  // run even if dependencies failed
    dataProvider = "users",
    expectedExceptions = NoSuchElementException.class
)
public void verifyCreateAndDeleteRegions() throws Exception { ... }

Your real test (annotated)

@Test(description = "Create and Delete a Region",
      groups = {"Smoke_Test"},
      testName = "Regions-1.1")
public void verifyCreateAndDeleteRegions() throws Exception {
    try {
        softAssertions = new SoftAssertions();
        Map<String, String> testData = new ReadTestData()
            .readJsonElementInOrder("regions/regions.json", "verifyCreateAndDeleteRegions");

        homeHelper.selectBrand(testData.get("brandName"), testData.get("subBrandName"));
        regionsHelper.createRegions(testData.get("countryName"), testData.get("cityName"));
        softAssertions.assertThat(regionsHelper.verifyCreateRegions(testData.get("cityName")))
            .describedAs("Regions Created Successfully")
            .isEqualTo(true);
        regionsHelper.deleteRegions(testData.get("cityName"));
        softAssertions.assertThat(regionsHelper.verifyCreateRegions(testData.get("cityName")))
            .describedAs("Regions Created Successfully")
            .isEqualTo(false);
        softAssertions.assertAll();
    } catch (Exception e) {
        webDriverUtils.goToHome(properties.getProperty("homeUrl"));
        throw new CustomException(e, driver);
    }
}
What's good: AssertJ SoftAssertions with .describedAs for descriptive failure messages. Custom exception wrapper that takes a screenshot and includes context.

10.3 DataProvider

@DataProvider(name = "loginData")
public Object[][] loginData() {
    return new Object[][] {
        { "valid@x.com", "Pass@123", true },
        { "invalid", "Pass@123", false },
        { "", "", false },
    };
}

@Test(dataProvider = "loginData")
public void testLogin(String email, String pwd, boolean expectedSuccess) { ... }

Parallel DataProvider

@DataProvider(name = "users", parallel = true)
public Object[][] users() { ... }
// All rows run in parallel โ€” each in its own thread

10.4 Listeners โ€” your ExtentReporterNG and Retry

ExtentReporterNG implements IReporter

public class ExtentReporterNG implements IReporter {
    private ExtentReports extent;

    public void generateReport(List<XmlSuite> xmlSuites, List<ISuite> suites,
                                String outputDirectory) {
        extent = new ExtentReports(outputDirectory + File.separator
            + "AutomationResult" + Instant.now().toEpochMilli() + ".html", true);

        for (ISuite suite : suites) {
            Map<String, ISuiteResult> result = suite.getResults();
            for (ISuiteResult r : result.values()) {
                ITestContext context = r.getTestContext();
                buildTestNodes(context.getPassedTests(), LogStatus.PASS);
                buildTestNodes(context.getFailedTests(), LogStatus.FAIL);
                buildTestNodes(context.getSkippedTests(), LogStatus.SKIP);
            }
        }
        extent.flush();
        extent.close();
    }
}
What this does: IReporter is a TestNG interface that fires once at the end of the suite. We iterate all results and write to an HTML report.

IReporter vs ITestListener โ€” important interview distinction

Interface Fires when Use for
IReporter Once at suite end with full results Batch report generation
ITestListener On every event (test start/pass/fail/skip) Per-test screenshots, live logging
ISuiteListener Suite start / finish Suite-level setup
IInvokedMethodListener Around every method (including @Before/After) Method-level logging
IAnnotationTransformer Once per class at startup, modifying annotations Auto-applying retry analyzers

Your Retry implements IAnnotationTransformer

public class Retry implements IAnnotationTransformer {
    public void transform(ITestAnnotation testannotation, Class testClass,
                          Constructor testConstructor, Method testMethod) {
        IRetryAnalyzer retry = testannotation.getRetryAnalyzer();
        if (retry == null) {
            testannotation.setRetryAnalyzer(RetryAnalyzer.class);
        }
    }
}
What this achieves: TestNG calls transform() for every @Test annotation during loading. We set RetryAnalyzer.class on any test that doesn't already have one. Result: every test in the suite gets retry-on-failure automatically โ€” no per-test opt-in.

The companion RetryAnalyzer

public class RetryAnalyzer implements IRetryAnalyzer {
    int counter = 0;
    int retryLimit = 1;
    public boolean retry(ITestResult result) {
        if (counter < retryLimit) { counter++; return true; }
        return false;
    }
}
One retry. Conservative โ€” catches transient flakiness without masking real bugs.

Spoken answer โ€” "How does your retry mechanism work?"

"Two-class pattern. RetryAnalyzer implements IRetryAnalyzer with a counter and a retry limit of one. The Retry listener implements IAnnotationTransformer โ€” TestNG calls its transform method for every @Test annotation during class loading. We check if the test already has a retry analyzer; if not, we wire in RetryAnalyzer.class. Result is that every test in the suite gets one automatic retry on failure without devs having to opt in test-by-test. We register the Retry listener in the suite XML once, and it covers all 200+ tests. The single retry is intentional โ€” anything that fails twice in a row is a real bug, not flakiness."

10.5 Suite XML structure (your orderAPI.xml)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="Avysh B2B Product Test_Suite" verbose="2"
       parallel="classes" thread-count="2">
    <parameter name="runParallel" value="true" />
    <parameter name="environment" value="config.properties" />
    <parameter name="browser" value="Chrome" />
    <parameter name="hubURL" value="http://localhost:4444/wd/hub" />

    <listeners>
        <listener class-name="com.avysh.qa.extentreport.ExtentReporterNG" />
    </listeners>

    <test name="Avysh B2B Smoke Product Test">
        <groups>
            <run>
                <include name="Order-APIs" />
            </run>
        </groups>
        <classes>
            <class name="com.avysh.qa.module.orders.OrdersAPITest"/>
        </classes>
    </test>
</suite>
Element Effect
parallel="classes" Each class runs in its own thread
parallel="methods" Each @Test method runs in its own thread
parallel="tests" Each <test> tag runs in its own thread
thread-count Concurrency cap
<parameter> Available to @Parameters in test classes
<groups><run><include> Filter by @Test(groups=...)

11. PARALLEL EXECUTION + THREADLOCAL DRIVER

11.1 The ThreadLocal driver pattern

With parallel="classes" thread-count="4", TestNG runs four test classes simultaneously. Each needs its own browser. If you used a single static WebDriver driver, all four threads would fight for the same driver โ€” chaos.

The fix: ThreadLocal<WebDriver>.

public class WebDriverUtils {
    public ThreadLocal<RemoteWebDriver> threadDriver = null;

    public void setDriver(ThreadLocal<RemoteWebDriver> threadDriver) {
        this.threadDriver = threadDriver;
    }

    public WebDriver getDriver() {
        if (threadDriver == null) return driver;     // fallback for non-parallel
        return threadDriver.get();                    // each thread sees its own
    }
}

ThreadLocal<T> is a Java class โ€” each thread sees its own value via .get()/.set(). Different threads can't read each other's values. So per-thread driver isolation comes free.

Spoken answer โ€” "How does parallel execution work in your framework?"

"Two layers. At the suite level, TestNG's parallel='classes' thread-count='4' runs each test class in its own thread, up to four concurrent. At the driver layer, WebDriverUtils holds a ThreadLocal<RemoteWebDriver> โ€” when each test class calls getDriver(), it gets a thread-local driver instance. So thread 1 has its Chrome window, thread 2 has its Chrome window, no cross-talk. The whole pattern works because TestNG creates a fresh thread per class, and ThreadLocal partitions state by thread identity. Without ThreadLocal, all four classes would write to the same driver field and overwrite each other's browser handles every few seconds."

11.2 Important โ€” don't forget to clean up ThreadLocal

After test class ends, call threadDriver.remove() โ€” otherwise the thread keeps a reference to the dead WebDriver and you leak memory in long CI runs.

@AfterClass(alwaysRun = true)
public void tearDown() {
    driver.quit();
    threadDriver.remove();   // <โ€” important
}

12. SELENIUM GRID

12.1 What it is

Distributed Selenium โ€” one hub routes test sessions to multiple nodes (machines or containers running a browser).

Test โ†’ Hub (router)
        โ”œโ†’ Node 1: Chrome on Mac
        โ”œโ†’ Node 2: Firefox on Windows
        โ””โ†’ Node 3: Edge on Linux

12.2 Connecting from test code

RemoteWebDriver driver = new RemoteWebDriver(
    new URL("http://hub:4444/wd/hub"),
    new ChromeOptions()
);

Your WebDriverUtils.initializeDriver() takes a hubURL parameter โ€” when set, you route via Grid; when empty, you run locally.

12.3 Selenium 4 Grid modes

  • Standalone โ€” single process, hub + node combined (for dev)
  • Hub-node โ€” separate hub + nodes (classic)
  • Distributed โ€” separate hub, session-queue, distributor, router (for scale)
  • Docker โ€” official images, easy to compose

12.4 Docker compose example

services:
  hub:
    image: selenium/hub:4
    ports: ["4444:4444"]
  chrome:
    image: selenium/node-chrome:4
    shm_size: 2g
    depends_on: [hub]
    environment:
      - SE_EVENT_BUS_HOST=hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
      - SE_NODE_MAX_SESSIONS=5
  firefox:
    image: selenium/node-firefox:4
    depends_on: [hub]
    environment:
      - SE_EVENT_BUS_HOST=hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
docker-compose up, then point your tests at http://localhost:4444/wd/hub.


13. SELENIUM 4 CDP โ€” NETWORK, GEOLOCATION, CONSOLE

This is the killer Selenium 4 feature. Chrome DevTools Protocol โ€” direct access to Chrome internals.

13.1 Setup

ChromeDriver chrome = (ChromeDriver) driver;
DevTools devTools = chrome.getDevTools();
devTools.createSession();

13.2 Network throttling

devTools.send(Network.enable(Optional.empty(), Optional.empty(), Optional.empty()));
devTools.send(Network.emulateNetworkConditions(
    false,                  // offline?
    100,                    // latency ms
    10_000,                 // download bytes/s (10 Mbps)
    5_000,                  // upload bytes/s
    Optional.empty()
));

13.3 Capture network requests

devTools.addListener(Network.requestWillBeSent(), req -> {
    System.out.println(req.getRequest().getMethod() + " " + req.getRequest().getUrl());
});

devTools.addListener(Network.responseReceived(), resp -> {
    if (resp.getResponse().getStatus() >= 400) {
        System.out.println("BAD: " + resp.getResponse().getUrl());
    }
});

13.4 Block requests (e.g., images for speed)

devTools.send(Network.setBlockedURLs(List.of("*.png", "*.jpg", "*.woff2")));
devTools.send(Network.enable(Optional.empty(), Optional.empty(), Optional.empty()));

13.5 Override geolocation, timezone, user agent

devTools.send(Emulation.setGeolocationOverride(
    Optional.of(12.97), Optional.of(77.59), Optional.of(1.0)));   // Bangalore

devTools.send(Emulation.setTimezoneOverride("Asia/Kolkata"));
devTools.send(Network.setUserAgentOverride("MyTestUA/1.0", Optional.empty(),
    Optional.empty(), Optional.empty()));

13.6 Capture console logs

devTools.send(Log.enable());
devTools.addListener(Log.entryAdded(), entry -> {
    System.out.println("[" + entry.getLevel() + "] " + entry.getText());
});

Spoken answer โ€” "What's Selenium 4 CDP and have you used it?"

"CDP is the Chrome DevTools Protocol โ€” the same protocol Chrome's own DevTools uses. Selenium 4 exposes it directly via devTools.send(...) and devTools.addListener(...). Practical uses: throttle the network to simulate 3G, mock geolocation for location-sensitive tests, override user agent, capture every network request and assert no 4xx hits, block images and analytics requests to speed up tests, listen to browser console for JS errors. In the Avysh framework I didn't use it heavily โ€” we were on Selenium 4 alpha-5 from 2020 which had limited CDP support โ€” but for a fresh Selenium 4 framework today, it would be a major part of the testing strategy. Playwright has all of this built into its core API, which is one reason it's preferred for new work."


14. HEADLESS MODE + CI FLAGS

ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new");           // Chrome 109+ new headless
options.addArguments("--no-sandbox");              // Docker as-root workaround
options.addArguments("--disable-dev-shm-usage");   // /dev/shm too small in Docker
options.addArguments("--disable-gpu");             // legacy Windows headless fix
options.addArguments("--window-size=1920,1080");   // viewport for screenshots
options.addArguments("--disable-extensions");      // faster startup
options.addArguments("--disable-blink-features=AutomationControlled");  // hide automation flag from bot-detection

WebDriver driver = new ChromeDriver(options);

When tests behave differently headless vs headed

  • Different default viewport โ€” set --window-size explicitly
  • No GPU โ€” canvas / WebGL behaviour differs
  • No font fallback to system โ€” install fonts in the Docker image
  • Bot detection sometimes fires on headless โ€” the --disable-blink-features=AutomationControlled flag helps

15. EXCEPTIONS โ€” WHAT EACH ONE MEANS

Exception Cause Fix
NoSuchElementException Locator didn't resolve Verify locator; add explicit wait; check parent frame
StaleElementReferenceException Reference held to detached DOM node Re-find inside a fluent wait that ignores it
ElementNotInteractableException Element exists but is hidden / disabled / covered Wait for elementToBeClickable; check overlay
ElementClickInterceptedException Another element is intercepting clicks Scroll into view; close the intercepting element; JS click as last resort
TimeoutException Explicit wait timed out Increase timeout, fix condition, check selector
WebDriverException Generic driver error Check driver/browser version compatibility
NoSuchWindowException switchTo().window() with invalid handle Re-fetch handles
NoSuchFrameException switchTo().frame() invalid index/name Verify frame exists; switch to defaultContent first
NoAlertPresentException switchTo().alert() with no alert Wait for alertIsPresent first
InvalidSelectorException XPath or CSS syntax error Validate locator in DevTools
SessionNotCreatedException Driver init failed Driver / browser version mismatch; outdated chromedriver
UnreachableBrowserException Browser died mid-test Memory; OOMKilled in Docker; bumped Chrome version
JavascriptException executeScript JS error Debug the JS โ€” wrap in try/catch

Spoken answer โ€” "What's the most common Selenium exception and how do you handle it?"

"StaleElementReferenceException by a wide margin. It happens when I hold a WebElement reference, the page re-renders that part of the DOM, and the reference now points to a detached node. There are three fixes depending on the cause. First, if the re-render is predictable โ€” like after clicking 'Save' and the list reloads โ€” I re-fetch the element after the action. Second, for timing-driven re-renders, I wrap in a FluentWait that ignores StaleElement and retries the action. Third, for pages that constantly re-render โ€” like analytics dashboards with live data โ€” I move from WebElement references to By locators, so every access re-resolves. The CommonUtils.click in our framework does the second approach โ€” try, catch StaleElement, wait for clickable, retry."


16. REPORTING โ€” EXTENTREPORTS LISTENER PATTERN

16.1 Your ExtentReporterNG (real code, condensed)

public class ExtentReporterNG implements IReporter {
    private ExtentReports extent;

    public void generateReport(List<XmlSuite> xmlSuites, List<ISuite> suites,
                                String outputDirectory) {
        extent = new ExtentReports(outputDirectory + File.separator
            + "AutomationResult" + Instant.now().toEpochMilli() + ".html", true);

        for (ISuite suite : suites) {
            Map<String, ISuiteResult> result = suite.getResults();
            for (ISuiteResult r : result.values()) {
                ITestContext context = r.getTestContext();
                buildTestNodes(context.getPassedTests(), LogStatus.PASS);
                buildTestNodes(context.getFailedTests(), LogStatus.FAIL);
                buildTestNodes(context.getSkippedTests(), LogStatus.SKIP);
            }
        }
        extent.flush();
        extent.close();
    }

    private void buildTestNodes(IResultMap tests, LogStatus status) {
        if (tests.size() > 0) {
            for (ITestResult result : tests.getAllResults()) {
                ExtentTest test = extent.startTest(result.getMethod().getMethodName());
                test.setStartedTime(getTime(result.getStartMillis()));
                test.setEndedTime(getTime(result.getEndMillis()));
                for (String group : result.getMethod().getGroups()) {
                    test.assignCategory(group);
                }
                if (result.getThrowable() != null) {
                    test.log(status, result.getThrowable());
                } else {
                    test.log(status, "Test " + status.toString().toLowerCase() + "ed");
                }
                extent.endTest(test);
            }
        }
    }
}

16.2 The two reporting patterns โ€” which to use when

Pattern When Trade-offs
IReporter (your pattern) At end of suite, batch-process all results Misses real-time logging; no per-action screenshots
ITestListener Hook every test event individually Per-test screenshots, real-time logging, slightly more complex

The modern shape is ITestListener because you can: - Take a screenshot at the moment of failure (vs only end-of-test) - Capture step-by-step logs inside the test - Stream progress to a live dashboard - Hook ExtentReports 5+ which uses ExtentTest per test

Honest tech-debt callout โ€” "Your ExtentReporter is the old IReporter pattern. What would you change?"

"Yes โ€” IReporter batch-processes at the end, which means I miss real-time progress and can only attach the exception, not screenshots at the failing step. The modern pattern is ITestListener plus ExtentReports 5.x. In onTestFailure, I'd capture a screenshot via TakesScreenshot on the driver, attach it to the ExtentTest node, and log the throwable. I'd also add ExtentReports' built-in ExtentSparkReporter for the HTML output, which has dark mode, search, filter, and trend history. The library version we have is com.relevantcodes 2.41 from 2016 โ€” eight years stale. Migration to com.aventstack 5.x is one day of work."


17. TECH DEBT IN B2BPROJECTTEST โ€” HONEST AUDIT

This is the seniority signal. Call out your own debts before the interviewer finds them.

Tech debt Why it's a problem What I'd do
Selenium 4 alpha-5 (from 2020) Pre-release; missing features, fewer bug fixes Bump to Selenium 4.20+ stable
ExtentReports 2.41.1 (com.relevantcodes, 2016) Old API, no modern reporter (Spark), no dark mode Migrate to ExtentReports 5.x + ExtentSparkReporter
Java 1.8 source target Misses var, records, sealed classes, switch expressions Move to Java 17 LTS
RestAssured.baseURI = uri; as static mutation (in OrdersAPI) Two parallel API tests overwrite each other's base URI Use instance-scoped given().baseUri(uri).when()...
Assertions inside API helpers (Assert.assertEquals(statusCode, 200) in OrdersAPI.getOrdersOnStatus) Makes helpers unreusable for negative tests Helpers return Response; tests assert
Driver binaries in resources/driver/{os}/ Manual per-OS distribution; brittle Use Selenium Manager / WebDriverManager (already in pom.xml but unused)
Thread.sleep in waitForPageToCompleteState Worst-case 20s for a healthy page; magic 2s post-delay Replace with WebDriverWait polling document.readyState
@FindBy(xpath="//*[@webix_tm_id='...']") everywhere XPath is slower than CSS for simple attribute checks Use By.cssSelector("[webix_tm_id='...']") for these
IReporter instead of ITestListener No per-test screenshots; can't capture progress live Migrate to ITestListener
WebDriverUtils.loadPropertyFile uses stack-trace inspection to get package name Fragile, surprising at distance Pass package/path explicitly or use Spring-style config
All locators are XPath even when CSS would do Performance and maintainability Convert simple attribute selectors to CSS
Hard-coded API key in OrdersAPI.getOrdersOnStatus: "517f73c431514a562da1af6851h8q6851" Secret in source Move to env var or properties
Browser config split between properties and ChromeOptions in code One change requires touching both Centralize in a BrowserOptionsFactory

The spoken answer

"Several. The biggest: Selenium 4 alpha-5 from 2020 โ€” I'd bump to 4.20 stable, which gives me Selenium Manager and proper CDP support. ExtentReports 2.41 from 2016 โ€” I'd migrate to 5.x and switch from IReporter to ITestListener so I can attach per-test screenshots. Java 1.8 to Java 17 LTS for var, records, switch expressions. In the OrdersAPI helper, RestAssured.baseURI = uri is a static mutation that breaks parallel tests โ€” instance-scoped given().baseUri(uri) fixes it. Same helper has Assert.assertEquals(statusCode, 200) inside โ€” assertions in API helpers make them unreusable for negative tests; helpers should return responses, tests should assert. The driver binaries in resources/driver/{os}/ are a 2020 pattern โ€” Selenium Manager handles this in one line now. And the waitForPageToCompleteState uses Thread.sleep retries โ€” I'd replace it with a proper WebDriverWait polling document.readyState. None of these are urgent on a stable test suite, but they're the list for the next refactor sprint."


18. SELENIUM โ†’ PLAYWRIGHT MIGRATION

18.1 The pitch I'd make to my team

"Three wins, one risk. Wins: auto-wait removes 80% of our WebDriverWait + ExpectedConditions boilerplate, which is where most of our flakiness lived. Parallel execution is built into the config โ€” no Selenium Grid to maintain. WebKit support means we catch Safari bugs without needing a Mac. Risk: it's a rewrite, not a port. The Locator API, action API, and the async model are different enough that we can't sed-replace through. Migration path: keep Selenium suite running during the bridge, port one user flow per week starting with the smoke suite, retire the Selenium equivalent once Playwright is green for two sprints. We learn by doing, not by reading docs."

18.2 The mapping cheat-sheet

Selenium (B2BProjectTest pattern) Playwright equivalent
driver.findElement(By.xpath(...)) page.locator('xpath=...') or page.getByRole(...)
WebDriverWait + ExpectedConditions await expect(loc).toBeVisible() (auto-retry)
@FindBy Page Factory Lazy locator fields in POM
ThreadLocal<RemoteWebDriver> Worker = own browser, automatic
TestNG parallel="classes" fullyParallel: true
RetryAnalyzer + IAnnotationTransformer retries: isCI ? 2 : 0
ExtentReporterNG IReporter Playwright HTML reporter (built-in)
JavascriptExecutor.executeScript page.evaluate(() => {...})
Actions.dragAndDrop await page.dragAndDrop(src, target)
Selenium Grid Workers + sharding (built-in)
driver.switchTo().frame page.frameLocator('#iframe')
driver.switchTo().alert() page.on('dialog', d => d.accept())
driver.manage().getCookies() await context.cookies()
CDP via DevTools.send(...) page.route('**/api/*', handler) โ€” first-class

19. INTERVIEW QUESTIONS โ€” 35+ Q&A

19.1 Architecture & basics

Q1. Walk me through Selenium's architecture.

Short: Test โ†’ Client library โ†’ W3C protocol over HTTP โ†’ Browser driver โ†’ Browser. Long:

"Five layers. My test code uses the Selenium client library, in our case the Java bindings. The client serializes commands into the W3C WebDriver protocol โ€” used to be the JSON Wire Protocol before Selenium 4 โ€” and sends them over HTTP to a local browser driver like chromedriver. The driver knows how to control its specific browser via native APIs โ€” Chrome's DevTools Protocol, Firefox's Marionette, Safari's WebDriver. The driver translates each command, executes it, and returns the result. Every command is one HTTP request, which is the architectural reason Selenium is slower than Playwright's persistent WebSocket โ€” over 200 commands per test, the HTTP overhead adds up to real seconds."

Q2. What changed in Selenium 4?

[See section 1.2 for the full answer]

Q3. Difference between findElement and findElements?

"findElement returns the first match and throws NoSuchElementException if none found. findElements returns a List<WebElement> โ€” empty list if no match, no exception. Use findElement when the element must exist; findElements when you're counting, iterating, or checking absence."

19.2 Waits

Q4. Walk me through the three waits and when to use each.

[See section 5.4 for the full answer]

Q5. What happens if you mix implicit and explicit wait?

"Behavior becomes implementation-specific. Selenium can wait for the maximum of the two, or behave unpredictably. The safe rule: set implicit wait to zero, use explicit waits everywhere. I enforce this in the driver setup."

Q6. What's the difference between visibilityOf and visibilityOfElementLocated?

"visibilityOf(elem) takes an existing WebElement โ€” useful when you already have the reference. If the underlying DOM node is replaced, this wait throws StaleElement and you have to refetch. visibilityOfElementLocated(by) takes a locator and re-resolves it on each poll, so it survives DOM updates. Generally prefer the locator version for re-renders, the element version when you know the element is stable."

19.3 Locators

Q7. Difference between absolute and relative XPath?

"Absolute XPath starts at root โ€” /html/body/div[1]/form/input โ€” and breaks on the first DOM change. Relative XPath starts with // and matches anywhere in the tree โ€” //input[@id='email']. I never write absolute XPaths in production. Relative XPaths with attribute filters are stable across most layout changes."

Q8. When do you use XPath vs CSS?

[See section 4.3 for the full answer]

Q9. What are Selenium 4 Relative Locators?

"A new locator API in Selenium 4 โ€” with(By.tagName('input')).toRightOf(label). The relations are above, below, toLeftOf, toRightOf, near. It works by checking actual rendered position of elements. Useful when there's no formal label-input association in the DOM. Trade-off: it's a render-position check, so it's slower and can break on layout reflows. I'd use it for label-input forms where the existing locators are fragile, not as a default."

Q10. What is PageFactory and how does @FindBy work?

[See section 9.2 + 4.5 for the full answer]

19.4 Actions

Q11. How would you hover over an element and then click a submenu?

"Actions class. new Actions(driver).moveToElement(menuRoot).perform(); then submenuItem.click();. Sometimes the hover-then-click needs to be chained: actions.moveToElement(menuRoot).pause(500).click(submenuItem).perform();. The pause gives time for the CSS hover transition to complete."

Q12. How do you handle a file upload?

"If the page uses a standard <input type='file'>, just sendKeys the absolute file path: driver.findElement(By.id('upload')).sendKeys('/path/to/file.pdf'). This works because WebDriver writes to the input's value directly, bypassing the native OS file dialog. If the app uses a custom drag-drop uploader without a real input, you have to use JS โ€” read the file in JavaScript, create a File object, dispatch a drop event."

Q13. What's the difference between clear() and sendKeys(Keys.CONTROL + 'a') followed by Keys.DELETE?

"clear() calls the DOM clear method, which sets value to empty. Some modern apps with controlled React inputs don't react to this because no input event fires. The Ctrl+A + Delete pattern simulates real keyboard input, which fires keydown, keyup, and input events, so React re-validates. I default to clear() but fall back to the keyboard pattern when forms don't react."

19.5 JS executor

Q14. When would you use JavascriptExecutor?

[See section 7.2 for the full answer]

Q15. How do you scroll an element into the visible viewport?

"JS executor: js.executeScript("arguments[0].scrollIntoView({block: 'center'});", element). The block: 'center' keeps it from being hidden by sticky headers. Selenium's built-in scrolling sometimes lands the element flush against a header, leading to ElementClickIntercepted."

19.6 Frames, windows, cookies

Q16. How do you handle multiple browser windows?

"Old way: getWindowHandles(), iterate, switch to the one that's not the original. Selenium 4: driver.switchTo().newWindow(WindowType.TAB) opens a new tab and switches automatically. To go back, save the original handle before opening anything new and switchTo().window(original)."

Q17. How do you persist login across tests in Selenium?

"Two approaches. The light approach: save cookies after login, reuse them in subsequent tests via driver.manage().addCookie. Need to be on the same domain first. The deeper approach: use Chrome's --user-data-dir=/path/to/profile flag to point at a persistent profile โ€” login stays in the profile. The cookie approach is closer to what Playwright's storageState does."

19.7 POM and frameworks

Q18. Walk me through your Page Object Model.

[See section 9.3 for the full answer โ€” Page + Helper + Test]

Q19. POM vs Page Factory?

"POM is the design pattern โ€” encapsulate page elements and actions in a class. Page Factory is Selenium's implementation helper that uses @FindBy annotations and reflection. Page Factory's value is locators sit next to fields and elements re-resolve lazily โ€” so no stale element issues. The trade-off is that errors come at use-time instead of construction-time, which can be confusing when debugging."

Q20. How do you organize 40+ pages?

"By feature module. In our B2BProjectTest, pages live under com.avysh.qa.pages.* and tests under com.avysh.qa.module.{regions, products, orders, manageteam, ...}.*. The Helper layer sits between โ€” pages own DOM, helpers own user workflows, tests orchestrate helpers. Suite XMLs split by module โ€” Smoke, brandPIM, sellerPIM, orderAPI, integration โ€” so CI can run only what's relevant per change."

19.8 TestNG

Q21. Order of TestNG annotations.

"BeforeSuite, BeforeTest, BeforeClass, BeforeMethod, @Test, AfterMethod, AfterClass, AfterTest, AfterSuite. Suite-level annotations fire once for the whole run. Test-tag annotations fire per <test> in the suite XML. Class-level fire per test class. Method-level fire per @Test."

Q22. Difference between dependsOnMethods and priority?

"priority is a number โ€” lower runs first, but it's a hint not an order guarantee, especially in parallel. dependsOnMethods is a hard dependency โ€” if the method this depends on fails, the dependent test is skipped, not failed. I avoid dependsOnMethods for normal flow; it's for cases like 'logout test only runs if login passed' where the dependency is functional, not convenience ordering."

Q23. Soft Assert vs Hard Assert?

"Hard assert โ€” Assert.assertEquals(actual, expected) โ€” stops the test on first failure. Soft assert from AssertJ (which our framework uses) โ€” softAssertions.assertThat(actual).isEqualTo(expected) โ€” collects failures and reports them all when you call softAssertions.assertAll(). Without assertAll(), failures silently pass. I use soft for validating multiple fields on an object โ€” like asserting 12 fields of an order response โ€” so I see all mismatches in one run. Hard for invariants where any failure should abort."

Q24. How does your retry mechanism work?

[See section 10.4 for the full answer]

Q25. What's the difference between IReporter and ITestListener?

[See section 10.4 + section 16.2 for the comparison]

19.9 Parallel

Q26. How does parallel execution work in your framework?

[See section 11.1 for the full answer]

Q27. What's a ThreadLocal and why is it needed?

"ThreadLocal is a Java class โ€” each thread sees its own value via .get()/.set(), separate from other threads. In Selenium parallel execution, every test class runs in its own thread but uses the same WebDriverUtils.getDriver() call. Without ThreadLocal, all threads would share a single driver field, so thread 1's click could land on thread 2's window. With ThreadLocal<WebDriver>, each thread gets its own driver reference, perfect isolation."

Q28. What's the difference between parallel='classes' and parallel='methods' in testng.xml?

"parallel='classes' โ€” each test class runs in its own thread, methods within a class run serially. parallel='methods' โ€” each @Test method runs in its own thread regardless of class. Classes is safer when tests in the same class share state via @BeforeMethod setup. Methods gives more parallelism but requires every @Test to be fully self-contained โ€” no shared instance state, fresh driver per method. We use classes in our framework because our @BeforeClass does the login and we want that login shared across that class's tests."

19.10 Grid

Q29. What is Selenium Grid and when would you use it?

"Distributed Selenium. A hub routes test sessions to one or more nodes, each running a specific browser/OS combination. Three real use cases. First, run tests across multiple browser-OS matrices in parallel for cross-browser coverage. Second, scale beyond a single machine โ€” 100 parallel tests need 100 browser processes, which takes more RAM than one box has. Third, dedicated machines for fragile tests โ€” Safari only runs on Mac, IE only on Windows. With Playwright's built-in workers we don't need Grid for single-machine scaling, but for matrix testing across OS-browser combinations, Grid is still useful."

Q30. How would you set up Selenium Grid in Docker?

"Use the official selenium/hub:4 and selenium/node-chrome:4, selenium/node-firefox:4 images. Docker Compose them โ€” hub on port 4444, nodes pointing at the hub via SE_EVENT_BUS_HOST. Set shm_size: 2g on the Chrome node โ€” Chrome crashes with 64MB shm. Set SE_NODE_MAX_SESSIONS to control concurrency per node. Then point your RemoteWebDriver at http://hub:4444/wd/hub. The new mode in Selenium 4 also supports auto-scaling via Docker, which means new nodes spin up on demand."

19.11 Exceptions + debugging

Q31. What's the most common Selenium exception and how do you handle it?

[See section 15 for the full answer]

Q32. How do you debug a flaky Selenium test?

"Six-step process I follow. First, run it 50 times locally โ€” if it fails even once, it's flaky. Second, look at the failure mode โ€” what exception, at what line. Third, examine waits โ€” is it Thread.sleep or implicit wait? Replace with explicit wait on the specific condition. Fourth, check for shared state โ€” does this test depend on data left over from another test? Make it create its own data. Fifth, run in CI conditions โ€” headless, same parallelism โ€” some failures only reproduce in headless. Sixth, if nothing reproduces, add screenshot capture at the failing step via TestListener onTestFailure so the next failure has evidence. Last resort, take a video of the run with FFmpeg or a Selenium 4 BiDi recording."

Q33. How do you take a screenshot on test failure?

"Implement ITestListener.onTestFailure(ITestResult result). Inside, cast the driver: File src = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE); then copy via FileUtils. Attach the screenshot path to the ExtentReports test node. We need to fetch the driver from the test context โ€” (WebDriver) result.getTestContext().getAttribute('driver') โ€” which means the test class must set it via context.setAttribute('driver', driver) in @BeforeMethod."

19.12 Practical patterns

Q34. How do you handle dynamic content that loads slowly?

"Layered. First, explicit wait on a stable element that signals 'load complete' โ€” usually a non-spinner like the first data row or a success indicator. Second, if the load is API-driven, wait for the API response via Selenium 4 CDP Network.responseReceived listener โ€” explicit, not magic timeouts. Third, for Angular apps, the NgWebDriver pattern in our framework โ€” ngDriver.waitForAngularRequestsToFinish() โ€” hooks into Angular's zone to detect when all pending HTTP requests finish. I never use Thread.sleep as the primary wait."

Q35. How do you handle a captcha?

"Three approaches in order of preference. First, ask backend devs for a test-mode bypass โ€” most apps support a 'skip captcha for whitelisted test accounts' mode in dev/stage. Second, use captcha-bypass tokens supplied by the dev team โ€” pass a known-valid token in the request. Third, only as last resort, mock the captcha endpoint via Selenium 4 CDP Fetch.requestPaused listener and respond 'valid' to it. Never try to programmatically solve captchas โ€” they're designed to defeat automation."

Q36. Tell me about a real bug you found with Selenium.

"On the Avysh B2B app, we had a flow where the Order Status dropdown would silently not update if the user clicked too quickly after the page loaded. UI showed the old value, backend stored the new one โ€” so users would think nothing happened, click again, double-save. The Selenium tests caught it because our explicit waits had a timing margin โ€” our tests would set status to 'Fulfilled', then immediately read the displayed status, and the assertion would fail intermittently. Root cause was a missing await in the Angular event handler. The dev fixed it; I added a regression test that explicitly clicks-then-reads-immediately to keep it from coming back."

Q37. How would you build a Selenium framework from scratch tomorrow?

"Same shape as B2BProjectTest but with the modernizations. Java 17, Selenium 4.20 stable, TestNG 7. Selenium Manager instead of per-OS driver binaries. ExtentReports 5 with ITestListener instead of 2.41 with IReporter. Page Factory POM with Helper facade layer for business workflows. RestAssured for backend API tests integrated into the same suite. TestNG parallel='classes' with ThreadLocal driver. Docker-based Selenium Grid for cross-browser. CI: GitHub Actions matrix per browser. CDP for network throttling and request interception. The big change from B2BProjectTest: I'd default to CSS selectors over XPath, instance-scoped REST Assured, no static mutations, no assertions inside helpers, no Thread.sleep anywhere."


20. CAPACITY PLANNING & SCALE โ€” SCENARIO QUESTIONS

These are the "system-design for SDET" questions that separate a mid from a senior. They're rarely about a single API โ€” they test whether you can reason about throughput, infrastructure, cost, and where to push back on the requirement. Panels in 2026 increasingly ask "how would you run 10,000 tests a day" style questions, so keep the formula and the challenge-the-premise move ready.

20.1 The master formula (memorize this)

Every "run N tests in T minutes" question reduces to one equation:

Required concurrency  N = (Tests ร— Browsers ร— Avg_test_time) / Target_time

That's just Little's Law for tests: throughput you need = total work รท time budget. Everything else โ€” Grid, Docker, cloud, sharding โ€” is how you buy that concurrency. Say the formula out loud first; it signals you think in throughput, not tools.

The four levers (once you have N, every optimization is one of these): 1. โ†‘ Concurrency โ€” more parallel sessions (Grid nodes, cloud slots, CI shards). 2. โ†“ Avg test time โ€” faster tests (API login, seeded data, headless, no Thread.sleep). 3. โ†“ Test count โ€” push logic down the pyramid to API/unit; run only affected tests. 4. โ†“ Browser matrix โ€” full run on one browser, targeted subset on the rest.


Q38. "100 tests, 3 browsers (Chrome, Safari, Firefox), must finish under 1 minute (~10s per test). Plan it." โญ

This is the flagship scenario. Answer in five moves.

Move 1 โ€” Clarify assumptions out loud

The answer swings entirely on these, so state them back before you compute (they've handed us ~10s/test): - Average test duration โ€” given as ~10s. (If it weren't, I'd ask โ€” 5s vs 20s doubles the infra.) - Are the tests independent / parallel-safe? (own data, no shared static state) - Do all 100 genuinely need all 3 browsers? (almost never โ€” Move 4) - Self-hosted or cloud grid budget?

Move 2 โ€” Do the math

100 tests ร— 3 browsers = 300 executions. At 10s each, serial = 3000s = 50 minutes. Target = 60s:

N = (100 ร— 3 ร— 10) / 60 = 50 concurrent sessions

Add ~20% headroom for setup/teardown, queueing, stragglers โ†’ plan for ~60 (~17 per browser).

Move 3 โ€” The Safari constraint (this is the differentiator โ€” lead with it)

  • Safari only runs on macOS. No Linux, no selenium/node-safari Docker image โ€” you can't containerize it like Chrome/Firefox.
  • safaridriver allows exactly ONE session per machine. Hard Apple limit, not a config knob. You physically cannot run 2 concurrent Safari sessions on one Mac.

So the concurrency math that works for Chrome collapses for Safari (each browser needs ~17 concurrent):

Browser Runs on ~17 concurrent sessions means Feasible?
Chrome Linux/Docker nodes ~1โ€“2 ร— 16-vCPU containers โœ… Easy, autoscale
Firefox Linux/Docker nodes ~1โ€“2 ร— 16-vCPU containers โœ… Easy
Safari macOS only, 1 session/Mac ~17 physical Macs โŒ Absurd to self-host

Even at this small scale, Safari is the wall: 17 idle Macs sitting in a rack for a 1-minute job makes no sense.

Move 4 โ€” The senior move: challenge "3 browsers ร— 100"

Cross-browser risk is mostly rendering/CSS/WebKit quirks, not business logic โ€” and business logic is already fully covered by the 100 on Chrome. So: - Run the full 100 on Chrome (primary) + full 100 on Firefox โ€” both cheap on Linux, ~17 sessions each. - Run only a ~20-test rendering-critical subset on Safari via a cloud Mac farm (BrowserStack/Sauce/LambdaTest โ€” they own the Macs; you rent parallel slots). - Safari concurrency drops: 20 ร— 10 / 60 = ~4 cloud Mac sessions โ€” trivial and cheap.

Revised totals:

Browser Tests Concurrency Infra
Chrome 100 ~17 Self-hosted Grid/Selenoid (1โ€“2 nodes)
Firefox 100 ~17 Self-hosted Grid/Selenoid (1โ€“2 nodes)
Safari ~20 (critical) ~4 Cloud Mac farm
Total ~38 concurrent only ~4 are the expensive Macs

Move 5 โ€” Make it real

  • Infra: at this scale a small Grid/Selenoid fleet โ€” 2โ€“3 Docker nodes for Chrome+Firefox โ€” is plenty; no need for heavy autoscaling. Buy the ~4 Safari slots from a cloud grid.
  • Parallel-safe tests: ThreadLocal<WebDriver>, parallel="methods", no static mutation (RestAssured.baseURI = ... breaks under load), each test seeds its own data.
  • Shard across CI runners if you want it even faster โ€” e.g. a few GitHub Actions runners splitting the 100, --shard i/n.
  • Shrink per-test time โ€” inject session via storageState/cookie instead of UI login; seed data via API; headless with the CI flag quartet. (Getting 10s โ†’ 6s would let you hit the target with even fewer sessions.)

Spoken answer

"First the formula: 100 tests ร— 3 browsers is 300 runs; at ~10s each that's 50 minutes serial, so to hit 1 minute I need concurrency of 300ร—10/60 = 50 parallel sessions, plus headroom, call it 60 โ€” about 17 per browser. Chrome and Firefox I'd run the full 100 each on a small Grid or Selenoid fleet โ€” one or two Docker nodes each, easy on Linux. But the moment Safari's in the mix the plan changes: Safari only runs on macOS and safaridriver allows one session per machine, so 17 parallel Safari would mean 17 physical Macs for a one-minute job. I wouldn't do that. Instead I'd run only the ~20 rendering-critical tests on Safari via a cloud Mac farm โ€” about 4 parallel sessions โ€” because Safari bugs are WebKit rendering quirks, not business logic, and the logic is already covered on Chrome. That lands the whole thing under a minute at ~38 total concurrency, only 4 of which are the expensive Macs. Tests have to be parallel-safe โ€” ThreadLocal drivers, no static state, own data."

Variant A โ€” the 3 browsers are Chromium, Firefox, Edge (all Linux-capable) โ€” the "happy path"

The Safari wall disappears: Chromium, Firefox, and Edge all run headless on Linux and all have official Docker node images (selenium/node-chromium, selenium/node-firefox, selenium/node-edge). No macOS, no cloud Mac farm, no one-session-per-machine limit โ€” the matrix is fully symmetric and entirely self-hosted.

The math is identical: 300 runs โ†’ 50 concurrent (~17/browser) โ†’ ~3โ€“4 ร— 16-vCPU Docker nodes behind one Grid. Point RemoteWebDriver at the hub, parallel="methods" + ThreadLocal, done in ~1 minute.

The clever bit here isn't an infra limit โ€” it's engine redundancy. Chromium and Edge are both Blink (Edge has been Chromium-based since 2020); Firefox is Gecko โ€” the only distinct engine in this set. So running the full 100 on both Chromium and Edge is near-redundant at the rendering level (Edge's real differences are IE-mode/SmartScreen/enterprise-policy, not layout). If the matrix were large I'd run full on Chromium + Firefox and only a ~20-test Edge-specific subset on Edge (300 โ†’ ~220 runs, ~37 concurrency). At 100 tests in a minute it's cheap enough to just run all three fully โ€” optimize the matrix only when scale demands it.

Variant B โ€” the 3 browsers use 3 different engines โ€” how the calc changes

The formula never changes โ€” N = (tests ร— browsers ร— avg_time) / target is engine-agnostic. What engines change is how much of the matrix you're allowed to trim โ€” the effective browser multiplier.

There are only three engines in the world: Blink (Chrome/Chromium/Edge/Opera/Brave), Gecko (Firefox), WebKit (Safari). So "3 different engines" = Chrome + Firefox + Safari, and:

No rendering redundancy โ†’ you cannot shrink the matrix on engine grounds โ†’ the multiplier stays at full 3.

3 distinct engines (Chrome+Firefox+Safari): every browser is a unique signal
   โ†’ run full 100 on all 3 โ†’ 300 executions โ†’ N = 50  (~17/browser)

Shared engine (Chromium+Edge+Firefox = Blink+Blink+Gecko): one is redundant
   โ†’ full 100 + full 100 + ~20 subset โ†’ ~220 executions โ†’ N โ‰ˆ 37

The sting: if the third engine is WebKit, that's Safari โ†’ the macOS / one-session-per-machine / cloud-Mac-farm wall returns for that browser. So 3 distinct engines is the case where you can't trim and the one that reintroduces the Safari infra problem.

One-liner: "Count engines, not browsers. Same-engine browsers are near-redundant, so I trim the extra to a subset; distinct engines each carry unique rendering risk, so I run them fully โ€” and if one of those engines is WebKit, Safari's macOS constraint drives the infra."


Q39. "Your regression suite takes 4 hours. Get it under 30 minutes." (reduce execution time)

"8ร— speedup โ€” I'd attack it on four fronts, biggest ROI first. One, parallelize: if it's serial today, parallel='methods' + ThreadLocal + a Grid/Selenoid fleet gives near-linear speedup up to my node count โ€” that alone is often 4โ€“6ร—. Two, cut per-test time: most Selenium suites bleed time on UI login and Thread.sleep โ€” inject the session via cookie/storageState, seed data via API, replace every sleep with explicit waits, run headless. Three, right-size the pyramid: a 4-hour UI suite usually means logic that belongs in fast API/unit tests โ€” I'd move contract checks down to RestAssured and keep UI for true journeys. Four, run only what changed: test-impact analysis / selection so a PR runs the affected slice, and the full suite runs nightly. I'd measure first โ€” find the slowest 20% of tests, they're usually 80% of the wall-clock."

Q40. "Design test infrastructure to run 10,000 tests a day across a team." (system design)

"I'd think in four layers. Execution grid: autoscaling Selenium Grid or Selenoid/Moon on Kubernetes, nodes spun on demand from the session queue so we pay only for what runs; cloud farm for Safari/real devices. Test-data layer: each run provisions isolated data โ€” either per-worker namespaces or an API that mints fresh accounts โ€” so 100 parallel workers never collide; this is the #1 thing that breaks at scale. Orchestration: CI (GitHub Actions/Jenkins) shards the suite across runners, triggers on PR + nightly, with test-selection so PRs run fast. Observability: central reporting (ExtentReports/Allure), per-test video/trace on failure, flaky-test tracking with quarantine, and dashboards for pass-rate and duration trends. The themes interviewers want: isolation, elasticity, and failure debuggability in a distributed system."

Q41. "How do you generate/isolate test data across hundreds of parallel workers?"

"Never share mutable data across parallel tests โ€” that's the classic flaky-at-scale trap. Three patterns, best first. Data factories / API seeding: each test creates exactly what it needs via an API call in setup and tears it down after โ€” fully isolated, no collisions. Per-worker namespacing: prefix data with the worker/thread ID (user_${workerId}@test.com) so workers can't touch each other's rows. Dedicated pools: a pool of pre-created accounts leased per test and returned. I avoid a shared golden dataset โ€” the moment two parallel tests mutate the same record you get non-deterministic failures that only appear under load."

Q42. "How would you set up a dynamic/auto-scaling Grid so you're not paying for idle nodes?"

"Selenium 4 Grid on Kubernetes with the queue-driven autoscaler, or Selenoid/Moon which spin one container per session and destroy it after. Scale signal is session-queue depth โ€” KEDA watches the queue and adds Chrome/Firefox pods when tests are waiting, scales to zero when idle. Set SE_NODE_MAX_SESSIONS to ~1 per vCPU and shm_size: 2g on Chrome nodes. For Safari/real devices I don't self-host โ€” I burst to a cloud farm and pay per parallel slot. The point is elasticity: full fleet during the nightly regression, near-zero cost at 3am when nothing's running."

Q43. "How much parallelism is 'safe' on one machine? How do you size a node?"

"Rule of thumb: ~1 browser session per vCPU and ~1โ€“1.5 GB RAM per Chrome. So a 16-vCPU / 24 GB node runs ~16 Chrome sessions comfortably โ€” push past that and context-switching and OOM kills make tests slower and flakier, not faster. I tune SE_NODE_MAX_SESSIONS to the vCPU count, watch for UnreachableBrowserException/OOMKilled as the signal I've overcommitted, and scale out (more nodes) rather than up (more sessions per node) past that ceiling. Chrome needs shm_size: 2g or it crashes regardless of RAM."

Q44. "Cross-browser matrix is exploding your CI time โ€” how do you keep coverage without running everything everywhere?"

"I don't run the full suite on every browser โ€” that's quadratic waste. Full logic coverage on one primary browser (Chrome), then a curated cross-browser subset on the others โ€” the rendering-sensitive and browser-quirk-prone flows only. Cross-browser bugs are overwhelmingly CSS/layout/WebKit issues, so the subset targets exactly those. In CI I model it as a matrix but with different test tags per browser: @smoke @cross-browser runs everywhere, the full regression only on Chrome. Cuts matrix cost 60โ€“70% with negligible risk to real coverage."


QUICK CHEAT SHEET

Most-used commands

mvn clean test                                    # all tests
mvn test -Dtest=RegionsTest                       # one class
mvn test -Dgroups=Smoke_Test                       # by group
mvn test -DsuiteXmlFile=Smoke_Testng.xml          # specific suite
mvn test -Dbrowser=chrome -Dmode=headless          # via properties

Five lines that signal seniority in interview

  1. "Don't mix implicit and explicit waits โ€” set implicit to zero and use explicit everywhere."
  2. "ThreadLocal is what makes parallel TestNG work without driver collisions."
  3. "Page Object Model is the pattern, Page Factory is Selenium's implementation โ€” the lazy proxy is the win."
  4. "@AfterMethod onTestFailure plus ITestListener for per-test screenshots โ€” IReporter is end-of-suite only."
  5. "RestAssured.baseURI = uri is a static mutation that breaks parallel API tests โ€” use given().baseUri(uri).when() instead."
  6. "Any 'run N tests in T minutes' question is just concurrency = (tests ร— browsers ร— avg_time) / target โ€” then buy that concurrency and challenge the browser matrix."
  7. "Safari can't be containerized โ€” macOS-only, one session per machine โ€” so you burst it to a cloud Mac farm and run only the rendering-critical subset there."

Owner: Rohan Dsouza | Grounded in: Avysh B2BProjectTest (Java + Selenium 4 + TestNG + REST Assured + ExtentReports) | Updated: 2026