Code-first learning
Interactive Selenium code walkthroughs
Step through each snippet one note at a time — the explained lines highlight as you go — then copy the whole thing ready to run.
Explicit wait, line by line
The canonical WebDriverWait setup: why the timeout lives on the wait, what until() actually polls, and how the returned element is used.
Step 1 of 8
1WebDriver driver = new ChromeDriver();2driver.manage().timeouts().implicitlyWait(Duration.ZERO);3 4WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10), Duration.ofMillis(250));5 6driver.get("https://example.com/login");7 8WebElement submit = wait.until(9 ExpectedConditions.elementToBeClickable(By.cssSelector("[data-test='submit']")));10 11submit.click();12 13wait.until(ExpectedConditions.urlContains("/dashboard"));14 15driver.quit();Line 1 · Create the session
Selenium Manager resolves the matching chromedriver automatically since 4.6 — no WebDriverManager and no PATH setup required.
Run it: Runs as-is with selenium-java 4.x on the classpath and Chrome installed. Wrap in @Test and move quit() into @AfterMethod for real suites.
A page object worth defending in an interview
Locators as constants, no assertions inside the page, fluent returns for navigation, and the wait owned by the page.
Step 1 of 5
1public class LoginPage {2 3 private final WebDriver driver;4 private final WebDriverWait wait;5 6 private static final By EMAIL = By.cssSelector("[data-test='email']");7 private static final By PASSWORD = By.cssSelector("[data-test='password']");8 private static final By SUBMIT = By.cssSelector("[data-test='submit']");9 private static final By ERROR = By.cssSelector("[data-test='form-error']");10 11 public LoginPage(WebDriver driver) {12 this.driver = driver;13 this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));14 }15 16 public LoginPage enterCredentials(String email, String password) {17 wait.until(ExpectedConditions.visibilityOfElementLocated(EMAIL)).sendKeys(email);18 driver.findElement(PASSWORD).sendKeys(password);19 return this;20 }21 22 public DashboardPage submitExpectingSuccess() {23 driver.findElement(SUBMIT).click();24 return new DashboardPage(driver).waitUntilLoaded();25 }26 27 public String errorMessage() {28 return wait.until(ExpectedConditions.visibilityOfElementLocated(ERROR)).getText();29 }30}Line 3–4 · Injected driver, page-owned wait
The page never creates a driver — that belongs to the factory. Owning the wait means every method in this page shares one timeout policy.
Run it: Drop into src/test/java/pages. Pair with a DashboardPage exposing waitUntilLoaded() as shown in the Scenario Lab wait task.
A retry wrapper for StaleElementReferenceException
How to survive React re-renders without sleeps: re-locate inside the retry, keep the timeout bounded, and never swallow real failures.
Step 1 of 5
1public static void clickWithRetry(WebDriver driver, By locator, Duration timeout) {2 Instant deadline = Instant.now().plus(timeout);3 StaleElementReferenceException last = null;4 5 while (Instant.now().isBefore(deadline)) {6 try {7 WebElement element = new WebDriverWait(driver, Duration.ofSeconds(2))8 .until(ExpectedConditions.elementToBeClickable(locator));9 element.click();10 return;11 } catch (StaleElementReferenceException e) {12 last = e;13 }14 }15 16 throw new IllegalStateException("Element stayed stale for " + timeout, last);17}Line 1 · Take a locator, not an element
This is the whole trick. A WebElement parameter would already be stale; a By lets the helper look the element up again on every attempt.
Run it: Put it in a SafeActions utility and call it from page objects only where the DOM genuinely re-renders — not as a blanket wrapper for every click.
Reaching into Shadow DOM
getShadowRoot() for open shadow roots, why XPath cannot cross the boundary, and the JS fallback for nested roots.
Step 1 of 5
1WebElement host = driver.findElement(By.cssSelector("app-card"));2 3SearchContext shadow = host.getShadowRoot();4WebElement title = shadow.findElement(By.cssSelector(".title"));5System.out.println(title.getText());6 7// Nested shadow roots: chain via JavascriptExecutor.8WebElement deep = (WebElement) ((JavascriptExecutor) driver).executeScript(9 "return arguments[0].shadowRoot"10 + " .querySelector('app-row').shadowRoot"11 + " .querySelector('button.confirm')", host);12 13deep.click();Line 1 · Find the host in the light DOM
The custom element itself is a normal node. Only its internals are encapsulated.
Run it: Requires Selenium 4.x and Chrome/Edge. For closed shadow roots, ask developers to expose a data-test hook instead of hacking around it.
Choosing locators under pressure
The same button located five ways, ranked from most to least maintainable, with the exact failure mode of each weak option.
Step 1 of 5
1// 1. Best — an agreed test contract.2driver.findElement(By.cssSelector("[data-test='checkout-submit']"));3 4// 2. Good — a stable, semantic id.5driver.findElement(By.id("checkout-submit"));6 7// 3. Acceptable — structure scoped to a stable ancestor.8driver.findElement(By.cssSelector("form#checkout button[type='submit']"));9 10// 4. Fragile — visible text.11driver.findElement(By.xpath("//button[normalize-space()='Place order']"));12 13// 5. Never — absolute XPath from devtools.14driver.findElement(By.xpath("/html/body/div[3]/div/div[2]/form/button[1]"));Line 1–2 · data-test survives refactors
It exists only for tests, so designers and framework upgrades do not touch it. Agreeing this contract with developers is the answer interviewers want.
Run it: Use as a review checklist: any new locator that lands at level 4 or 5 needs a comment explaining why nothing better exists.
Wiring RemoteWebDriver to Grid
Same suite, local or distributed: options built from a parameter, ThreadLocal storage, and clean teardown.
Step 1 of 5
1@BeforeMethod2@Parameters({"browser", "gridUrl"})3public void startDriver(@Optional("chrome") String browser,4 @Optional("") String gridUrl) throws Exception {5 MutableCapabilities options = switch (browser) {6 case "firefox" -> new FirefoxOptions();7 case "edge" -> new EdgeOptions();8 default -> new ChromeOptions();9 };10 11 WebDriver driver = gridUrl.isBlank()12 ? new ChromeDriver((ChromeOptions) options)13 : new RemoteWebDriver(new URL(gridUrl), options);14 15 driver.manage().timeouts().implicitlyWait(Duration.ZERO);16 DriverFactory.set(driver);17}18 19@AfterMethod(alwaysRun = true)20public void stopDriver() {21 DriverFactory.stop();22}Line 1–4 · Per-method lifecycle
@BeforeMethod gives every test its own session, which is what makes parallel='methods' safe. @Parameters lets testng.xml choose browser and Grid URL.
Run it: Start a hub with `docker run -p 4444:4444 selenium/standalone-chrome`, then pass gridUrl=http://localhost:4444/wd/hub in testng.xml.