02 · Selenium Advanced¶
Level 1 gave you findElement, click and sendKeys — enough for a form.
Real applications add spinners, iframes, native alerts, drag-and-drop, new
tabs and elements that only appear on hover. This module covers the Selenium
APIs for all of it, and settles the wait strategy question properly, because
timing is the cause of roughly every flaky UI test ever written.
1. Wait strategy — pick one and commit¶
| Wait | Scope | Waits for | Verdict |
|---|---|---|---|
Thread.sleep |
One line | Nothing — it just stops | Never |
| Implicit | Every findElement, globally |
Presence in the DOM only | Baseline only |
Explicit (WebDriverWait) |
One call | A named condition | Default choice |
FluentWait |
One call | A condition, with tuning | For polling odd things |
Do not mix implicit and explicit waits
The implicit wait applies inside WebDriverWait's polling loop, so a 10s
explicit wait layered on a 10s implicit wait can take far longer than 10
seconds — and in some driver versions produces genuinely unpredictable
timeouts. The professional setting is implicit wait = 0, explicit
waits everywhere:
FluentWait¶
WebDriverWait is a FluentWait<WebDriver> with defaults. Build your own
when you need a different polling interval or extra ignored exceptions.
package com.example.tests;
import org.openqa.selenium.*;
import org.openqa.selenium.support.ui.FluentWait;
import org.openqa.selenium.support.ui.Wait;
import java.time.Duration;
import java.util.function.Function;
public final class Waits {
public static WebElement waitForElement(WebDriver driver, By locator) {
Wait<WebDriver> wait = new FluentWait<>(driver)
.withTimeout(Duration.ofSeconds(30))
.pollingEvery(Duration.ofMillis(500))
.ignoring(NoSuchElementException.class)
.ignoring(StaleElementReferenceException.class)
.withMessage("Element never became visible: " + locator);
return wait.until((Function<WebDriver, WebElement>) d -> {
WebElement el = d.findElement(locator);
return el.isDisplayed() ? el : null;
});
}
}
A FluentWait condition succeeds when the function returns non-null (or
true). Returning null means "not yet, poll again".
Custom expected conditions¶
import org.openqa.selenium.support.ui.ExpectedCondition;
public static ExpectedCondition<Boolean> spinnerGone(By spinner) {
return driver -> driver.findElements(spinner).stream()
.noneMatch(WebElement::isDisplayed);
}
// usage
new WebDriverWait(driver, Duration.ofSeconds(20)).until(spinnerGone(By.cssSelector(".loading")));
Wait for the application's signal that it is ready — a spinner disappearing, a row count settling — not for a fixed number of seconds.
2. Alerts, confirms and prompts¶
JavaScript dialogs are browser chrome, not DOM. findElement cannot see them.
package com.example.tests;
import org.junit.jupiter.api.*;
import org.openqa.selenium.*;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.time.Duration;
import static org.junit.jupiter.api.Assertions.*;
class AlertTest {
private WebDriver driver;
private WebDriverWait wait;
@BeforeEach
void setUp() {
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new", "--window-size=1920,1080");
driver = new ChromeDriver(options);
driver.manage().timeouts().implicitlyWait(Duration.ZERO);
wait = new WebDriverWait(driver, Duration.ofSeconds(10));
driver.get("https://the-internet.herokuapp.com/javascript_alerts");
}
@Test
@DisplayName("Accepting a confirm dialog is reported on the page")
void acceptConfirm() {
driver.findElement(By.cssSelector("button[onclick='jsConfirm()']")).click();
Alert alert = wait.until(ExpectedConditions.alertIsPresent());
assertEquals("I am a JS Confirm", alert.getText());
alert.accept();
assertEquals("You clicked: Ok",
driver.findElement(By.id("result")).getText());
}
@Test
@DisplayName("A prompt returns the typed text")
void answerPrompt() {
driver.findElement(By.cssSelector("button[onclick='jsPrompt()']")).click();
Alert alert = wait.until(ExpectedConditions.alertIsPresent());
alert.sendKeys("QA Engineer");
alert.accept();
assertEquals("You entered: QA Engineer",
driver.findElement(By.id("result")).getText());
}
@AfterEach
void tearDown() {
if (driver != null) driver.quit();
}
}
| Method | Effect |
|---|---|
alert.accept() |
OK |
alert.dismiss() |
Cancel |
alert.getText() |
The dialog message |
alert.sendKeys(text) |
Types into a prompt only |
An unhandled alert makes the next command throw
UnhandledAlertException — a confusing failure far from its cause.
3. Frames¶
An iframe is a separate document. You must switch into it and back out.
@Test
void typeInsideAnIframe() {
driver.get("https://the-internet.herokuapp.com/iframe");
driver.switchTo().frame("mce_0_ifr"); // by name or id
WebElement body = driver.findElement(By.id("tinymce"));
body.clear();
body.sendKeys("Text typed inside the editor frame");
assertEquals("Text typed inside the editor frame", body.getText());
driver.switchTo().defaultContent(); // back to the main document
assertTrue(driver.findElement(By.id("page-footer")).isDisplayed());
}
| Call | Switches to |
|---|---|
switchTo().frame("name-or-id") |
Named frame |
switchTo().frame(0) |
Frame by index |
switchTo().frame(webElement) |
Frame located as an element |
switchTo().parentFrame() |
One level up |
switchTo().defaultContent() |
The top document |
4. Windows and tabs¶
@Test
void handleNewWindow() {
driver.get("https://the-internet.herokuapp.com/windows");
String original = driver.getWindowHandle();
driver.findElement(By.linkText("Click Here")).click();
wait.until(ExpectedConditions.numberOfWindowsToBe(2));
for (String handle : driver.getWindowHandles()) {
if (!handle.equals(original)) {
driver.switchTo().window(handle);
break;
}
}
assertEquals("New Window", driver.getTitle());
driver.close(); // close the new window only
driver.switchTo().window(original); // ALWAYS switch back after close()
assertEquals("The Internet", driver.getTitle());
}
Selenium 4 can also open one itself:
After close() the driver points at a dead window
Every subsequent command throws NoSuchWindowException until you call
switchTo().window(...). Make "close then switch" a single habit.
5. The Actions API¶
For hovers, drag-and-drop, right-clicks, double-clicks and key combinations.
import org.openqa.selenium.interactions.Actions;
@Test
void hoverRevealsCaption() {
driver.get("https://the-internet.herokuapp.com/hovers");
WebElement avatar = driver.findElement(By.cssSelector(".figure:nth-child(3) img"));
new Actions(driver).moveToElement(avatar).perform();
WebElement caption = driver.findElement(By.cssSelector(".figure:nth-child(3) h5"));
assertTrue(caption.isDisplayed(), "Caption should appear on hover");
assertEquals("name: user1", caption.getText());
}
@Test
void dragAndDrop() {
driver.get("https://the-internet.herokuapp.com/drag_and_drop");
WebElement columnA = driver.findElement(By.id("column-a"));
WebElement columnB = driver.findElement(By.id("column-b"));
new Actions(driver).dragAndDrop(columnA, columnB).perform();
assertEquals("B", driver.findElement(By.id("column-a")).getText());
}
| Action | Code |
|---|---|
| Hover | .moveToElement(el) |
| Right-click | .contextClick(el) |
| Double-click | .doubleClick(el) |
| Drag and drop | .dragAndDrop(src, target) |
| Manual drag | .clickAndHold(src).moveToElement(t).release() |
| Ctrl+A | .keyDown(Keys.CONTROL).sendKeys("a").keyUp(Keys.CONTROL) |
| Scroll to element | .scrollToElement(el) (Selenium 4) |
HTML5 drag-and-drop often ignores dragAndDrop
Selenium's synthetic events do not always trigger HTML5 dragstart
listeners. When dragAndDrop silently does nothing, fall back to
clickAndHold(...).moveByOffset(10, 0).moveToElement(target).release(),
or dispatch the drag events with a JavaScript snippet. This is a known
limitation, not a bug in your test.
6. JavaScriptExecutor — the escape hatch¶
import org.openqa.selenium.JavascriptExecutor;
JavascriptExecutor js = (JavascriptExecutor) driver;
js.executeScript("arguments[0].scrollIntoView({block:'center'});", element);
js.executeScript("window.scrollTo(0, document.body.scrollHeight);");
js.executeScript("arguments[0].click();", element); // last resort
js.executeScript("arguments[0].value='typed';", inputElement); // last resort
String state = (String) js.executeScript("return document.readyState;");
A JS click is a test that no longer tests the user
js.executeScript("arguments[0].click()") fires the handler even when
the element is invisible, disabled or covered by a modal — exactly the
conditions a real user could not click through. Using it to "fix" a
failing test converts a genuine bug into a green build. Use it only for
scrolling and diagnostics; if a normal click() fails, find out why.
7. Other things you will meet¶
// File upload -- send the absolute path to the <input type="file">
driver.findElement(By.id("file-upload"))
.sendKeys(Path.of("src/test/resources/upload.txt").toAbsolutePath().toString());
// Shadow DOM (Selenium 4)
SearchContext shadow = driver.findElement(By.id("host")).getShadowRoot();
shadow.findElement(By.cssSelector(".inner")).click();
// Relative locators (Selenium 4)
import static org.openqa.selenium.support.locators.RelativeLocator.with;
driver.findElement(with(By.tagName("input")).below(By.id("email-label")));
// Element screenshot, not whole page
File png = driver.findElement(By.id("chart")).getScreenshotAs(OutputType.FILE);
// Cookies -- skip the login UI entirely
driver.manage().addCookie(new Cookie("session", "abc123"));
8. Testing traps¶
Trap 1 — isDisplayed() on a missing element throws
It throws NoSuchElementException, not false. Use
!driver.findElements(by).isEmpty() to test for absence.
Trap 2 — stale elements after a re-render
A WebElement is a handle to one DOM node. Any framework re-render
invalidates it. Re-find after every action that changes the page, and
never hold an element across a navigation.
Trap 3 — window size changes what exists
Headless Chrome defaults to a small viewport where responsive sites hide
the desktop nav entirely. Always set --window-size=1920,1080 so headless
and headed runs see the same DOM.
Trap 4 — waiting for presence when you need interactability
presenceOfElementLocated returns while the element is still hidden
behind a fade-in, and the click then throws
ElementClickInterceptedException. For clicks, wait for
elementToBeClickable.
Cheat sheet¶
| Task | Code |
|---|---|
| Disable implicit wait | driver.manage().timeouts().implicitlyWait(Duration.ZERO) |
| Explicit wait | new WebDriverWait(driver, Duration.ofSeconds(10)).until(...) |
| Fluent wait | new FluentWait<>(driver).withTimeout(...).pollingEvery(...).ignoring(...) |
| Alert | wait.until(ExpectedConditions.alertIsPresent()).accept() |
| Enter frame | driver.switchTo().frame("id") |
| Leave frame | driver.switchTo().defaultContent() |
| Switch window | driver.switchTo().window(handle) |
| New tab | driver.switchTo().newWindow(WindowType.TAB) |
| Hover | new Actions(driver).moveToElement(el).perform() |
| Drag | new Actions(driver).dragAndDrop(a, b).perform() |
| Scroll into view | js.executeScript("arguments[0].scrollIntoView(true);", el) |
| Upload | input.sendKeys(absolutePath) |
| Shadow root | el.getShadowRoot() |
How It Actually Works¶
FluentWait/WebDriverWait are not event listeners — Selenium has no way
to be notified when the DOM changes, because it's a separate process
talking over HTTP. They're polling loops, plainly: internally,
Wait.until(condition) runs a loop that calls condition.apply(driver),
catches any exception in the ignoring(...) list and treats it as "not yet
ready," sleeps for pollingEvery, and repeats — until either the condition
returns a truthy/non-null value or the elapsed time exceeds withTimeout,
at which point it throws TimeoutException. ExpectedConditions.visibilityOfElementLocated(by)
is just a pre-built Function<WebDriver, WebElement> that internally calls
driver.findElement(by) and returns null (or lets NoSuchElementException
propagate) if the element isn't visible yet — the "condition" is nothing
more than a function that keeps getting re-invoked until it stops returning
falsy/error.
This is exactly why mixing implicit and explicit waits compounds instead of
overriding: the implicit wait is configured at the chromedriver session
level and applies inside every findElement call, including the one
buried inside your ExpectedConditions function — so each of FluentWait's
polling iterations can itself block for up to the implicit-wait duration
before returning "not found," multiplying the two timeouts together rather
than one superseding the other. Setting implicit wait to zero removes that
inner blocking call entirely, leaving FluentWait's own loop as the only
timing mechanism in play — which is what makes its timeout value actually
mean what it says.
Exercise¶
All targets are on https://the-internet.herokuapp.com/.
- Set the implicit wait to zero across your suite and fix everything that breaks with explicit waits. Record how many tests needed a change.
- On
/dynamic_loading/2, replaceWebDriverWaitwith aFluentWaitpolling every 250 ms for 20 s, ignoringNoSuchElementException. Add awithMessage(...), force a timeout, and confirm your message appears in the failure. - On
/javascript_alerts, write three tests: accept an alert, dismiss a confirm, and answer a prompt — asserting#resulteach time. - On
/iframe, type into the TinyMCE body, switch back out, and assert the footer is visible. Then deliberately omitdefaultContent()and record the exception you get. - On
/windows, open the new window, assert its title, close it, switch back, and assert the original title. Then omit the switch-back and record the exception. - On
/hovers, assert that all three captions are hidden initially and that hovering the second one revealsname: user2. - On
/dynamic_controls, click Remove, wait for the checkbox to disappear, assert the "It's gone!" message, then click Add and wait for it to return. Write a customExpectedConditionfor one of the two directions. - Write two sentences on when a
JavascriptExecutorclick is legitimate, and one example where using it would have hidden a real accessibility bug.