This page is for testers and automation engineers facing a Selenium round, from a first QA job to a lead who owns the framework. Most rounds start with how WebDriver talks to the browser and how you pick locators, then test waits, alerts, frames and windows, and the exceptions everyone hits. Mid and senior rounds move to page objects, TestNG, parallel runs on Grid and why tests go flaky. Each question shows what the interviewer is really checking, the shape of a strong answer and a short answer you can say out loud. Examples are in Java, the most common pairing. Practise saying them, then swap in your own stories.
Search all questions by round, difficulty and level, or save the ones you want to practise.
Client: the language binding turns each call into a command following the W3C WebDriver standard.
Driver: a browser-specific program such as chromedriver or geckodriver receives it over HTTP.
Browser: the driver uses the browser's own automation support to do the action and sends back a response.
"My test code uses the Selenium client library. When I call findElement or click, the library turns that into a command defined by the W3C WebDriver standard and sends it as an HTTP request to a driver program, like chromedriver for Chrome or geckodriver for Firefox. The driver is made by or for that browser, so it knows how to control it natively. It performs the action and sends a JSON response back, either the result or an error like no such element. That's why every browser needs its matching driver, and why a version mismatch between browser and driver breaks things. It's also why Grid works so easily: the test just sends the same commands to a remote address instead of a local driver. In recent Selenium 4 releases, Selenium Manager can find or download the right driver for you."
Saying Selenium injects JavaScript into the page to drive it, or not knowing a separate driver program exists.
close(): closes only the window or tab that currently has focus.
quit(): closes every window and ends the WebDriver session.
Teardown: quit() in an after hook that always runs, even when the test fails.
"close closes just the window or tab the driver is focused on. If other windows are still open, the session carries on, but I have to switch to one of them before the next command or I'll get a NoSuchWindowException. quit closes all the windows and ends the session, which also lets the driver process shut down. So quit is what goes in teardown, in an AfterMethod or AfterEach that runs whether the test passed or failed. I learned why that matters when a suite on our CI box kept slowing down: tests that failed early never reached a quit call at the end of the test body, and dozens of orphan Chrome processes piled up. Moving quit into the after hook fixed it."
Saying they are the same, or putting quit at the end of the test method where a failed assertion skips it.
findElement: returns the first match or throws NoSuchElementException.
findElements: returns a list, empty when nothing matches; it never throws for no match.
Use: findElements to check absence or count items, bearing in mind the implicit wait.
"findElement returns the first element that matches. If nothing matches, it throws NoSuchElementException, after waiting for the implicit wait if one is set. findElements returns a list of every match, and if nothing matches it just returns an empty list, no exception. So I use findElements when I want to count things, like the number of search results, or when I want to check that something is not there, like an error banner after a valid login, by asserting the list is empty. One catch: if an implicit wait is set, findElements waits the full timeout before giving up on an empty result, so an absence check can quietly add seconds to every test. That's one reason I keep the implicit wait at zero and use explicit waits instead."
Wrapping findElement in try-catch to test absence, or saying findElements throws when empty.
The eight: id, name, className, tagName, linkText, partialLinkText, cssSelector and xpath.
Order of preference: a dedicated test attribute or unique id, then name, then a short CSS selector, then XPath when you need text or axes.
Avoid: long absolute paths, index positions and styling classes that change with a redesign.
"The By class gives eight strategies: id, name, class name, tag name, link text, partial link text, CSS selector and XPath. I pick based on what's least likely to change. First choice is a dedicated test attribute like data-testid, or an id that's unique and not generated. Then name, which is usually stable on form fields. Then a short CSS selector. I use XPath when I need to match on visible text or move relative to another element. What I avoid is anything tied to layout or styling: absolute paths from the html tag, index numbers like the third div, or class names that designers rename. Before committing a locator, I check in the browser console that it matches exactly one element. And where the app has no stable hooks, I ask the developers to add test ids, which is cheap for them and saves us hours."
Saying XPath is always best, or relying on absolute XPaths and index positions copied from dev tools.
CSS: short and readable for ids, classes, attributes and child relationships; my default.
XPath: can match visible text and move in any direction with axes like ancestor and following-sibling.
Absolute vs relative: always relative XPath starting with //, never a full path from /html.
"My default is CSS because it's short and easy to read. Things like an id, a data attribute, or an input inside a form are one line in CSS. I switch to XPath for two things CSS can't do well. First, matching on visible text, like a button that says Place order, using normalize-space or contains on text. Second, moving around the tree with axes: going up to an ancestor row, or across to a following sibling cell. That's common in tables where the only stable thing is some text in a neighbouring cell. People often say CSS is faster, but in modern browsers the difference is small, so I choose on readability and stability. With XPath I always write relative paths starting with double slash. An absolute path from the html tag breaks the moment someone adds a wrapper div."
By saveCss = By.cssSelector("form#profile button[type='submit']");
By saveXpath = By.xpath("//button[normalize-space()='Save changes']");
By priceCell = By.xpath("//td[normalize-space()='Blue mug']/following-sibling::td[1]");
Only saying one is faster, or defending absolute XPath because it is what the browser copied.
Anchor: find the cell with the known email text.
Walk: go to its row, then down to that row's Edit button.
Parameterise: build the locator from the input instead of hard-coding one user.
"The only stable thing in that row is the email, so I anchor on it. I find the row that contains a cell whose trimmed text equals the email, then look inside that same row for the button whose text is Edit. The condition in square brackets on tr keeps me inside one row, so I can't accidentally click the Edit button of the row above. I wrap it in a method that takes the email, so the test reads like editUser with an address. I'd avoid positional locators like the fourth row, because sorting or a new user changes the order. If the email could contain a single quote I'd handle escaping, but for normal test data this is enough. Before using it I check in dev tools that it matches exactly one button."
public void editUser(String email) {
By edit = By.xpath("//table[@id='users']//tr[td[normalize-space()='" + email + "']]"
+ "//button[normalize-space()='Edit']");
wait.until(ExpectedConditions.elementToBeClickable(edit)).click();
}
Using a fixed row index, or a locator that matches every Edit button on the page.
Other attributes: name, aria-label, a data attribute or visible text that stays the same.
Partial match: the stable prefix with ^= in CSS or starts-with() in XPath, checked to be unique.
Relative position: anchor on a stable neighbour, or use Selenium 4 relative locators; ask for a test id long term.
"First I look past the id. Most elements have something stable: a name, an aria-label, a data attribute, or the visible text of a button. If none of those exist but the id has a fixed prefix, I can match on the prefix, with the ^= operator in CSS or the starts-with function in XPath, and check that it's still unique on the page. Another option is to anchor on something stable nearby, like the label text, and move to the element from there. Selenium 4 also has relative locators, like the input below the email field, which I use sparingly because layout changes can move things. The real fix is to ask developers to add a data-testid, because a generated id is telling you the framework owns that value, not the team."
By byPrefix = By.cssSelector("button[id^='btn-']");
By byLabel = By.cssSelector("button[aria-label='Download report']");
By byNeighbour = RelativeLocator.with(By.tagName("input")).below(By.id("email"));
Recording the current id and hoping, or reaching straight for an index-based absolute XPath.
Implicit: a global setting; every find polls for presence up to the timeout.
Explicit: WebDriverWait waits for a specific condition, like clickable or text present, at one spot.
Fluent: an explicit wait where you also set polling interval and which exceptions to ignore; do not mix with implicit.
"An implicit wait is set once on the driver, and from then on every findElement keeps polling until the element is present or the timeout runs out. It only checks presence, not whether the element is visible or clickable. An explicit wait, WebDriverWait, waits for a specific condition at one place in the code, like element to be clickable or text to be present. A fluent wait is the configurable form of that: I choose the timeout, how often it polls, and which exceptions to ignore while polling. I use explicit waits by default and keep the implicit wait at zero. Mixing them is a known trap: the two timeouts can stack in ways that are hard to predict, so a wait you expected to take ten seconds can take much longer. And Thread.sleep only ever goes in as a last resort with a comment explaining why."
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.elementToBeClickable(By.id("save"))).click();
Wait<WebDriver> fluent = new FluentWait<>(driver)
.withTimeout(Duration.ofSeconds(30))
.pollingEvery(Duration.ofMillis(500))
.ignoring(NoSuchElementException.class);
WebElement status = fluent.until(d -> d.findElement(By.id("report-ready")));
Saying Thread.sleep is fine, or that an implicit wait makes an element clickable.
Cause: the element you found earlier was removed or re-rendered, so your reference points at nothing.
Typical triggers: page refresh, navigation, a list re-rendered after a filter, frameworks that replace nodes.
Fix: locate again after the change, wait for the old element to go stale, keep By locators instead of cached elements.
"A WebElement is a reference to one specific node in the DOM. If the page removes or replaces that node, even with one that looks identical, my reference is stale and any action on it throws this exception. It happens a lot with modern front ends: I find a list of results, apply a filter, the framework re-renders the list, and my saved elements are all dead. The proper fix is to find elements again after anything that changes the page, instead of storing them in fields for later. In page objects I keep By locators and look them up inside each method. When I know an action will re-render something, I wait for the old element to go stale with ExpectedConditions.stalenessOf, then find the new one. A small retry can cover rare races, but if I need it everywhere, the design is wrong."
Saying it means the element was never found, or wrapping every action in a blind retry loop.
Wait and switch: wait for alertIsPresent, which also switches to it.
Act: accept, dismiss, getText, or sendKeys for a prompt.
Know the difference: an HTML modal is just page elements; the Alert API only covers native JavaScript dialogs.
"Native JavaScript dialogs aren't part of the page, so I can't find them with a locator. I wait for the alert with ExpectedConditions.alertIsPresent, which returns the Alert object. Then I can read its message with getText, click OK with accept, click Cancel with dismiss, or type into a prompt with sendKeys before accepting. While an alert is open the page is blocked, so any other command throws an unhandled alert error, and if I try to switch when there's no alert I get NoAlertPresentException. The thing I check first is whether it's really a native alert. Most modern apps use HTML modals styled to look like dialogs, and those I handle with normal locators and waits. The browser's own login pop-ups and download dialogs aren't JavaScript alerts either."
Alert alert = wait.until(ExpectedConditions.alertIsPresent());
assertEquals(alert.getText(), "Delete this invoice?");
alert.accept();
Trying to locate the alert's OK button with XPath, or not knowing an HTML modal is handled differently.
Why: the driver only searches the current document; an iframe holds a separate one.
Switch in: by index, name or id, or a WebElement; best with frameToBeAvailableAndSwitchToIt.
Switch out: parentFrame for one level up, defaultContent back to the top page.
"An iframe loads a separate document, and the driver only searches whichever document it's currently focused on. So the locator is fine, I'm just looking in the wrong place. I switch into the frame first. I can switch by index, by name or id, or by passing the frame's WebElement, and I prefer the WebElement or a stable name over an index, since indexes change when another frame is added. Even better, I wait with frameToBeAvailableAndSwitchToIt, because payment widgets and editors often load their frame late. For nested frames I switch one level at a time. When I'm done, parentFrame takes me up one level and defaultContent takes me back to the main page. Forgetting to switch back is the most common bug here: the next step fails on an element that's plainly visible on the main page."
wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt(By.cssSelector("iframe[title='Card details']")));
driver.findElement(By.name("cardnumber")).sendKeys("4111111111111111");
driver.switchTo().defaultContent();
driver.findElement(By.id("pay")).click();
Blaming the locator or adding waits, without realising the element is in another document.
Remember: save the original handle before the click.
Wait and pick: wait for the window count, then pick the handle that is not the original.
Return: close the new tab if done, then switch back to the saved handle.
"Before clicking, I save the current window handle. Then I click the link and wait until the number of windows is two, because the new tab may take a moment to appear. getWindowHandles gives me a set of handles, and a set has no guaranteed order, so I don't assume the new one is last. I loop through and switch to the handle that isn't the original. Then I do my checks there, like the invoice title. When I'm done I close that tab and switch back to the original handle. Important point: Selenium doesn't follow focus automatically. Even though the browser shows the new tab, the driver stays on the old one until I switch. In Selenium 4 I can also open a fresh tab myself with switchTo().newWindow."
String original = driver.getWindowHandle();
driver.findElement(By.linkText("View invoice")).click();
wait.until(ExpectedConditions.numberOfWindowsToBe(2));
for (String handle : driver.getWindowHandles()) {
if (!handle.equals(original)) {
driver.switchTo().window(handle);
break;
}
}
assertTrue(driver.getTitle().contains("Invoice"));
driver.close();
driver.switchTo().window(original);
Assuming the driver moves to the new tab on its own, or picking a handle by its position in the set.
Native select: wrap it in the Select class; choose by visible text, value or index.
Checks: getFirstSelectedOption, getOptions, isMultiple; deselect only on multi-selects.
Custom dropdown: click to open, wait for the option list, click the option by its text.
"If it's a real select tag, I wrap the element in Selenium's Select class. Then I can pick by visible text, by the value attribute, or by index. I prefer visible text or value, because index breaks when an option is added. To verify, getFirstSelectedOption tells me what's chosen and getOptions gives me the full list, which is handy for checking a country list or sort options. Deselect methods only work on a multi-select. But many apps today build dropdowns out of divs and list items. If I pass one of those to Select, it throws UnexpectedTagNameException. For those I treat it like a user would: click the control to open it, wait for the options to be visible, then click the option that has my text. Sometimes typing into the search box inside it and pressing Enter is more reliable."
Select country = new Select(driver.findElement(By.id("country")));
country.selectByVisibleText("Canada");
assertEquals(country.getFirstSelectedOption().getText(), "Canada");
Using the Select class on a div-based dropdown, or always selecting by index.
Why: a shadow root is a separate tree; normal locators from the page do not cross into it.
Selenium 4: find the host element, call getShadowRoot, then search inside with CSS selectors.
Limits: chain one root at a time for nested components; closed shadow roots cannot be reached this way.
"Web components can attach a shadow root, which is a separate DOM tree hidden behind a host element. Normal locators from the document stop at the host, so the field looks invisible to Selenium even though I can see it. In Selenium 4 I find the host element, call getShadowRoot, and get back a search context I can find elements in. Inside a shadow root I stick to CSS selectors, because XPath doesn't work there in Chrome and CSS is the safe choice everywhere. If components are nested, I repeat it: find the inner host inside the outer root, then get its shadow root. This only works for open shadow roots. If the component uses a closed one, I can't get in from outside, so I'd talk to the developers about exposing a test hook or cover that logic in component tests instead."
WebElement host = driver.findElement(By.cssSelector("app-login"));
SearchContext root = host.getShadowRoot();
root.findElement(By.cssSelector("input[name='username']")).sendKeys("qa_user");
root.findElement(By.cssSelector("button[type='submit']")).click();
Trying ever longer XPaths from the document root, or saying Selenium simply cannot test web components.
Meaning: another element, like an overlay, spinner, sticky header or cookie banner, would receive the click.
Fix the timing: wait for the overlay to disappear, dismiss the banner, scroll the element into view.
Last resorts: Actions for hover or complex gestures; a JavaScript click only with a note, since it skips real user checks.
"That exception means the element was found and is visible, but something else is sitting on top of the point Selenium would click. The error message usually names the element that would get the click, so I read that first. Most of the time it's a loading overlay, a sticky header, a toast or a cookie banner. The fix is to deal with that: wait for the overlay to become invisible, close the banner in setup, or scroll the element into the middle of the view so the header doesn't cover it. If the element only appears on hover, I use the Actions class to move to the menu first. A JavaScript click will almost always succeed, but it bypasses the checks a real user would hit, so it can pass a test on a button nobody can actually press. I only use it with a comment explaining why."
wait.until(ExpectedConditions.invisibilityOfElementLocated(By.cssSelector(".loading-overlay")));
new Actions(driver).moveToElement(driver.findElement(By.id("account-menu"))).perform();
wait.until(ExpectedConditions.elementToBeClickable(By.linkText("Sign out"))).click();
Switching every failing click to a JavaScript click without finding what was covering the element.
What: cast the driver to JavascriptExecutor and call executeScript; arguments arrive as arguments[0] and so on, and return values come back to your code.
Good uses: scrolling an element into view, reading properties the UI does not show, checking document.readyState, clearing storage between tests.
Caution: a JavaScript click or value change skips visibility and overlay checks and may not fire the app's events.
"JavascriptExecutor lets me run a script inside the page. I cast the driver to it and call executeScript, passing elements or values as arguments, and whatever the script returns comes back to my Java code. An element comes back as a WebElement, a whole number as a Long. I use it for things WebDriver doesn't do directly: scrolling an element into the centre of the view, reading a property like whether an input is valid, checking document.readyState, or clearing local storage between tests. What I avoid is using it to act for the user. A JavaScript click, or setting a field's value directly, skips the checks WebDriver makes, like whether the element is visible or covered, and it may not fire the events the app listens to. So the test can pass on a form a real user couldn't submit. When I do use it for an action, I leave a comment saying why."
JavascriptExecutor js = (JavascriptExecutor) driver;
WebElement terms = driver.findElement(By.id("terms"));
js.executeScript("arguments[0].scrollIntoView({block: 'center'});", terms);
Boolean valid = (Boolean) js.executeScript("return arguments[0].checkValidity();", driver.findElement(By.id("email")));
String state = (String) js.executeScript("return document.readyState;");
Switching to a JavaScript click whenever a normal click fails, or not knowing it skips the checks a real user would hit.
Local: find the input of type file and sendKeys the absolute path; never click the button that opens the system dialog.
Hidden inputs: styled upload buttons often hide the real input; by default drivers still accept a path sent to it.
Remote: on Grid, set a LocalFileDetector so the file is copied to the node before the path is typed.
"WebDriver can't control the operating system's file dialog, so I never click the Upload button that opens it. Instead I find the input element with type file and call sendKeys with the absolute path to the file. The browser treats that as if the user picked the file. Many apps hide the real input behind a styled button, but by default drivers still accept a path sent to a hidden file input, so I just locate the input itself. For several files on an input that allows multiple, I join the paths with a newline. On Grid there's a catch: the path is on my machine, but the browser runs on a node that doesn't have that file. So I set a LocalFileDetector on the RemoteWebDriver, and Selenium sends the file to the node first. I keep test files in the project's resources folder so the path works on every machine."
((RemoteWebDriver) driver).setFileDetector(new LocalFileDetector()); // Grid runs only
String path = Paths.get("src/test/resources/files/invoice.pdf").toAbsolutePath().toString();
driver.findElement(By.cssSelector("input[type='file']")).sendKeys(path);
Clicking the upload button and trying to drive the system file dialog, or hard-coding a path that only exists on your own laptop.
API: cast the driver to TakesScreenshot and call getScreenshotAs.
Hook: a TestNG listener's onTestFailure, or a JUnit 5 extension, so every test gets it for free.
Useful output: name the file after the test, attach it to the report, and grab the URL and page source too.
"The basic call is to cast the driver to TakesScreenshot and ask for the screenshot as a file, bytes or Base64. But I don't want every test to remember that, so I put it in the runner. With TestNG I write a listener that implements ITestListener and overrides onTestFailure. It gets the driver for the current thread, takes the screenshot and saves it with the test name, so parallel runs don't overwrite each other. I register the listener once in testng.xml. In a real framework I'd also attach it to the HTML report and save the current URL and page source, because a screenshot alone often doesn't show why a locator failed. Selenium 4 can also screenshot a single element, which is handy for visual checks on one widget."
public class FailureListener implements ITestListener {
@Override
public void onTestFailure(ITestResult result) {
WebDriver driver = DriverManager.getDriver();
File src = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
Path target = Paths.get("screenshots", result.getName() + ".png");
try {
Files.createDirectories(target.getParent());
Files.copy(src.toPath(), target, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
Adding a screenshot call inside every catch block in every test.
Idea: one class per page or component holds its locators and the actions a user can take there.
Inside: private locators, methods like loginAs, return the next page object for navigation.
Outside: assertions and test data belong in tests; page objects expose state for tests to check.
"Page Object Model means each page, or each reusable part like a header or a date picker, gets a class. The class holds the locators, kept private, and public methods for what a user does there, like loginAs with an email and password, or searchFor a term. If an action takes you to another page, the method returns that page object, so tests read like a story. The big win is maintenance: when the login button changes, I fix one locator in one class instead of forty tests. What I keep out is assertions and test data. The page object tells you things, like getErrorMessage, and the test decides what's right. I also avoid one giant class per page for complex screens; I split them into components. It makes tests shorter and much easier to review."
public class LoginPage {
private final WebDriver driver;
private final By email = By.id("email");
private final By password = By.id("password");
private final By submit = By.cssSelector("button[type='submit']");
public LoginPage(WebDriver driver) { this.driver = driver; }
public DashboardPage loginAs(String user, String pass) {
driver.findElement(email).sendKeys(user);
driver.findElement(password).sendKeys(pass);
driver.findElement(submit).click();
return new DashboardPage(driver);
}
}
Putting assertions and test flow inside page objects, or describing POM as just a folder of locators.
Order: BeforeSuite, BeforeTest, BeforeClass, BeforeMethod, then Test, then the matching After ones in reverse.
Trap: BeforeTest refers to the test tag in testng.xml, not each test method.
Browser: usually BeforeMethod and AfterMethod, so every test gets a clean session.
"The order is BeforeSuite, BeforeTest, BeforeClass, BeforeMethod, then the Test method, and then the After ones mirror it: AfterMethod, AfterClass, AfterTest, AfterSuite. A common mix-up is BeforeTest. It doesn't run before each test method, it runs before each test tag in testng.xml. BeforeMethod is the one that runs before every test method. I normally start the browser in BeforeMethod and quit it in AfterMethod, so each test gets a fresh session, no leftover cookies or state, and it's safe to run in parallel. Heavier one-time work, like reading config or seeding data through an API, goes in BeforeSuite or BeforeClass. On the Test annotation itself I use groups for smoke and regression, and I avoid priority and dependsOnMethods for ordering, because tests that depend on each other fail together."
Saying BeforeTest runs before every test method, or opening one browser for the whole suite by default.
TestNG: a DataProvider method returns rows; the test takes them as parameters.
Source: inline for a few rows; a CSV, JSON or spreadsheet file for many, read in the provider.
Report: each row is a separate test result, so one bad row does not hide the others.
"In TestNG I'd use a DataProvider. It's a method that returns rows of data, usually a two-dimensional Object array, and each row becomes one run of the test with those values passed in as parameters. I include the expected outcome in each row, so one test covers valid logins, wrong passwords and empty fields. Each row shows up as its own result in the report, so if row seven fails I see exactly which data broke. For a handful of rows I keep them inline. For larger sets, or when testers who don't code maintain the data, I read a CSV or JSON file inside the provider. A DataProvider can also run its rows in parallel if the test is independent. In JUnit 5 the same idea is a parameterized test with a method or CSV source."
@DataProvider(name = "logins")
public Object[][] logins() {
return new Object[][] {
{"valid@shop.test", "Right#123", true},
{"valid@shop.test", "wrong", false},
{"", "Right#123", false}
};
}
@Test(dataProvider = "logins")
public void login(String email, String password, boolean shouldPass) {
DashboardPage page = new LoginPage(driver).loginAs(email, password);
assertEquals(page.isLoaded(), shouldPass);
}
Looping over the data inside one test method, so the first failure stops the rest and the report shows one result.
Layers: driver factory with ThreadLocal, config per environment, page and component objects, test data builders, tests.
Running: TestNG or JUnit with groups for smoke and regression, parallel on Grid or containers, triggered from CI on every merge.
Speed and trust: API calls for setup, screenshots and logs on failure, a readable report, a flaky-test policy and review standards.
"I'd start with what the team needs: fast feedback on each change, and tests any of the five can read and fix. The layers would be a driver factory that reads browser and environment from config and holds drivers in a ThreadLocal, so parallel works from day one. Then page and component objects, test data builders, and thin tests that read like user journeys. Setup goes through APIs, like creating a user and logging in with a token, so the UI tests only check the UI part. TestNG groups split smoke and regression. The smoke set runs on every merge, the full set nightly on Grid in containers. Every failure saves a screenshot, page source and console log into the report. And I'd write down the rules: locator conventions, no sleeps, and flaky tests get quarantined and fixed within a week."
Listing tools with no structure, or designing everything to run through the UI including setup.
Before: the concrete pain, like run time, flakiness or maintenance cost.
Changes: the two or three decisions that mattered most and why.
After: measurable results and what you would still do differently.
"When I joined my last team, the regression suite was about four hundred scripts written in a record-and-play style, with locators copied into every test and sleeps everywhere. It took around three hours and failed often enough that nobody trusted it. I made three changes over a couple of months. First, page objects, so each locator lived in one place. Second, a driver manager with ThreadLocal so we could run in parallel on a small Grid. Third, moving setup like creating users and orders to API calls, which cut minutes from each test. We also deleted tests that duplicated each other. By the end the suite ran in about forty minutes and red builds were real bugs most of the time. If I did it again, I'd bring the developers in earlier to add test ids."
Describing only the tools used, with no problem, trade-off or result.
Purpose: run tests on remote machines and many browser types at once from one entry point.
Grid 4 parts: router, new session queue, distributor, session map and nodes; run as standalone, hub and node, or fully distributed.
Code: RemoteWebDriver with the Grid URL and browser options instead of a local driver.
"Grid lets my tests run on other machines and across browsers in parallel, through one address. In Grid 4 the main parts are a router that receives every request, a queue that holds new session requests, a distributor that picks a node with a matching browser, a session map that remembers which node owns which session, and the nodes that actually run browsers. For a small setup I run it standalone, all in one process, or as a hub with a few nodes. Many teams run the nodes as containers so they're easy to scale and throw away. In the test code the only change is creating a RemoteWebDriver with the Grid URL and the browser options, like ChromeOptions or FirefoxOptions. I keep that behind a driver factory, so a config flag switches between local and Grid without touching tests."
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new");
WebDriver driver = new RemoteWebDriver(URI.create("http://grid.internal:4444").toURL(), options);
Thinking Grid makes tests parallel on its own, without knowing the runner must also be set to run in parallel.
Cause: a shared or static WebDriver field used by several threads at once.
Fix: a ThreadLocal driver, created in BeforeMethod and quit and removed in AfterMethod.
Also isolate: test data, user accounts and output file names per test.
"That's almost always a shared driver, usually a static WebDriver field in a base class. With parallel methods, two threads read the same field, so one test's sendKeys lands in the other test's browser. WebDriver isn't meant to be shared across threads. The fix is to give each thread its own driver through a ThreadLocal. BeforeMethod creates a driver and sets it, page objects get it through a getter, and AfterMethod quits it and calls remove, so a reused thread doesn't pick up a dead driver. Then I check the other shared things. Two tests logging in as the same user can log each other out, so each test gets its own account or creates its data through an API. Screenshots and downloads get unique names. Then I set parallel and thread-count in testng.xml."
public final class DriverManager {
private static final ThreadLocal<WebDriver> DRIVER = new ThreadLocal<>();
public static void start() { DRIVER.set(new ChromeDriver()); }
public static WebDriver getDriver() { return DRIVER.get(); }
public static void stop() {
WebDriver d = DRIVER.get();
if (d != null) {
d.quit();
DRIVER.remove();
}
}
}
Adding synchronized to driver calls, which removes the parallelism, or not noticing shared test data.
What: the browser runs without a visible window; same engine, same page.
Why: CI servers have no display, it uses fewer resources, and it is easy to run many at once.
Gotchas: a smaller default window size, responsive layouts that hide elements, and sites that treat headless browsers differently.
"Headless means the browser runs without drawing a window on screen. It's the same browser engine, so the page behaves the same way, and screenshots still work. I use it in CI because build agents usually have no display, and it's lighter, so I can run more browsers in parallel. For Chrome I pass the headless=new argument in ChromeOptions, and Firefox has its own headless flag. The most common reason a test fails only headless is window size. The default headless window is smaller than my laptop screen, so a responsive site shows the mobile menu and the desktop link I'm clicking doesn't exist. I always set the window size explicitly. Other causes are file downloads needing a set folder, and some sites behaving differently when they detect a headless browser."
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new", "--window-size=1920,1080");
WebDriver driver = new ChromeDriver(options);
Saying headless skips rendering so layout-related bugs cannot happen, or not setting a window size.
Measure: find the slowest tests and where time goes, like logins, sleeps and setup through the UI.
Trim and move: remove duplicates, push checks down to API or unit tests, set up data through APIs.
Split and scale: a fast smoke set on every pull request, full suite nightly, parallel runs on Grid.
"First I'd measure. I'd sort tests by duration and look at where the time goes. Usually a few patterns dominate: every test logging in through the UI, fixed sleeps, and setup like creating orders by clicking through forms. Replacing UI setup with API calls and a logged-in session often cuts a big chunk on its own. Next I'd look at coverage: some tests check validation rules or calculations that belong in API or unit tests, and some duplicate each other. Then I'd split the suite. A smoke set of the most important journeys, targeted at around ten to fifteen minutes, runs on every pull request. The full suite runs nightly and before releases. Finally I'd make everything parallel-safe and run on Grid. More machines helps, but if the tests aren't independent it just gives you faster flakiness."
Only saying add more Grid nodes, without touching test design, data setup or what belongs at the UI layer.
Timing: missing or wrong waits, animations, overlays, stale elements.
State: shared accounts or data, order dependence, leftovers from a previous test, parallel collisions.
Environment and design: slow or unstable test servers, third-party calls, dates and time zones, brittle locators; find it with reruns, artifacts and failure history.
"I sort causes into three buckets. Timing is the biggest: clicking before an overlay clears, reading text before an API call finishes, stale elements after a re-render, animations. State is next: tests sharing a user or a cart, depending on the order they run in, or colliding when run in parallel. Then environment and design: a slow test server, third-party widgets, dates that break at midnight or across time zones, and locators tied to layout. To tell which one, I rerun the test in a loop, alone and then in the full suite in parallel. If it only fails in the suite, it's state. If it fails alone too, it's timing or the app. Screenshots, page source and browser console logs from failed runs usually point straight at the cause. Retries are a temporary label, not a fix."
Answering only with Thread.sleep and a retry analyzer, or blaming Selenium itself for every flaky failure.
Situation: which test, how often it failed and why it mattered.
Investigation: what evidence you gathered and what you ruled out.
Fix and result: the root-cause change and how you showed it held.
"At my last company our checkout test failed about once in every ten CI runs but never on my laptop, and people had started ignoring red builds. I set it to run fifty times in a row on the CI agent and saved a screenshot and the browser console log on each failure. Every failed screenshot showed the same thing: a Payment processing overlay still fading out when we clicked Place order. Locally the machine was faster, so the overlay was gone in time. Someone had put a two-second sleep there earlier, which was usually enough on CI but not always. I replaced it with a wait for the overlay to become invisible, then ran it another hundred times with no failures. I also added a helper so other tests could reuse that wait, and removed three similar sleeps."
A story where the fix was a longer sleep or a retry, with no evidence of the cause.
Triage fast: check the failure artifacts; is it the test or a real intermittent bug?
Decide on risk: if it is the test, quarantine it visibly and cover the flow another way for this release.
Follow through: a ticket with an owner and a date, and the test back in the suite soon after.
"I wouldn't say yes or no straight away. I'd spend thirty minutes on the evidence: the failure screenshots, logs and when it started failing. If it began failing right after a recent change, it might be a real intermittent bug, and disabling the test hides exactly the thing we'd ship. If it's clearly the test, like a timing issue, I'm fine taking it out of the release gate, but not deleting it. I'd move it to a quarantine group that still runs and reports, and make sure that flow gets checked another way for this release, even a quick manual pass. Then I'd raise a ticket with an owner and a date to fix it, and tell the team it's quarantined so nobody thinks it's covered. Disabling quietly is how coverage disappears."
Disabling it quietly with no ticket, or blocking the release without looking at the evidence.
The miss: what broke and why the suite did not catch it.
Root cause in testing: a weak assertion, missing case, wrong layer or unrealistic data.
Change: the targeted fix, and a habit that prevents the same kind of gap.
"At my last company a discount code stopped applying for customers with saved addresses, and it reached production even though our checkout tests passed. When I looked, our test used a fresh user every time, so it never had a saved address. Worse, the assertion only checked that the order confirmation page appeared, not the total. So the suite was green while the price was wrong. I fixed the test to check the final amount, and added a data variation for a returning customer. The bigger change was a review habit: every test must assert the outcome the business cares about, not just that a page loaded. I went through our top twenty checkout and payment tests with that lens and found four more with weak assertions."
Blaming the developers or the environment, or saying the answer is to automate every possible case.
Agree on the goal: faster releases, fewer escaped bugs, less manual regression.
Prioritise: stable, high-risk, frequently run flows first; skip one-off, visual or fast-changing ones.
Right layer: move logic checks to API or unit tests; show a plan with milestones.
"I'd start by agreeing on the goal, because automating two hundred cases isn't the goal, less manual regression and fewer escaped bugs are. Then I'd sort the cases. First to automate are stable, high-risk flows that we run every release, like sign-up, login, checkout and payments. Next, cases with lots of data variations, because data-driven tests pay off quickly there. Some cases I'd push down a layer, like validation rules or calculations, which are faster and more reliable as API tests. And some I'd leave manual: one-off checks, screens that change every sprint, and visual judgement. I'd come back with a plan showing which cases go where, the order, and a realistic number for the quarter, with the most valuable ones done first so the team sees benefit within weeks."
Promising all two hundred through the UI without questioning value, or refusing without offering a plan.
ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your resume and notes are never stored on our servers. It stays out of screen share on every plan; only you can see it.