ScreenshotNeo

BlogGuides

Selenium Tips and Tricks for Writing Better Automated Tests

Make Selenium 4 tests easier to trust and maintain with condition-based waits, stable locators, focused page objects, and practical debugging guidance.

By the ScreenshotNeo team4 October 202612 min read

Selenium tests are easier to maintain when they wait for the state an action actually needs, use locators that describe stable application elements, and keep page-specific interaction code in one place. In Selenium 4, use browser options classes to configure sessions, use explicit waits for specific conditions, and avoid mixing implicit and explicit waits. A page reaching document.readyState === "complete" does not guarantee that a JavaScript application has finished rendering the control your test needs.

This guide uses Java and Selenium 4 for its complete example, then covers synchronization, locators, page objects, browser options, interactions, and troubleshooting. Official references: Waiting Strategies, Browser Options, Page object models, and Expected Conditions.

1. Start with a condition-based wait

A browser and the application update asynchronously. If a test clicks a button and immediately searches for content the click triggers, either the application or the test may get there first. That race is a common source of flaky tests. Wait for the needed condition at the point where the test needs it: visibility before typing, clickability before clicking, or expected text before asserting.

Fixed sleeps make the test wait the same duration whether the page is ready quickly or slowly. Too short, and the test still fails; too long, and every run pays the delay. Use a bounded wait with a meaningful condition instead. A timeout is the limit for reporting a failure, not a way to disguise an unknown application state.

A complete Java example

This standalone Selenium 4 example opens Selenium’s dynamic-content demo, clicks its reveal button, waits for the field to become visible, enters text, and checks the result. It requires Java, Maven, and a browser supported by Selenium Manager. Save it as src/main/java/Example.java in a Maven project.

<!-- pom.xml dependency -->
<dependency>
  <groupId>org.seleniumhq.selenium</groupId>
  <artifactId>selenium-java</artifactId>
  <version>4.@@VERSION@@</version>
</dependency>

// src/main/java/Example.java
import java.time.Duration;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

public class Example {
  public static void main(String[] args) {
    WebDriver driver = new ChromeDriver();
    try {
      driver.manage().timeouts().implicitlyWait(Duration.ZERO);
      driver.get("https://www.selenium.dev/selenium/web/dynamic.html");

      driver.findElement(By.id("reveal")).click();
      WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
      WebElement field = wait.until(
          ExpectedConditions.visibilityOfElementLocated(By.id("revealed")));
      field.sendKeys("Ready when visible");

      if (!"Ready when visible".equals(field.getAttribute("value"))) {
        throw new AssertionError("The field did not contain the entered value");
      }
    } finally {
      driver.quit();
    }
  }
}

Replace 4.@@VERSION@@ with the Selenium 4 version used by your project. Keeping the implicit wait at zero makes the explicit wait’s timeout easier to reason about. In a test suite, put driver creation and cleanup in your framework’s setup and teardown hooks so cleanup still runs after assertion failures.

For other bindings, the same sequence applies: locate and click the trigger, wait for the resulting state, interact, then assert the outcome. Expected-condition APIs differ across languages and versions; Selenium’s documentation notes, for example, that Selenium 4 .NET does not provide the Expected Conditions class. Use the binding’s documented wait API when translating this example.

2. Choose the right wait and navigation strategy

Explicit and implicit waits

Approach What it waits for Good fit Trade-off
Explicit wait A condition you name, such as visibility or text Dynamic UI state before a specific action Requires identifying the condition at each transition
Implicit wait Element-location calls across the session A deliberately global element lookup policy Does not wait for visibility, clickability, or application-specific state
Fixed sleep A fixed duration, regardless of readiness Rarely, when a fixed delay itself is the behavior under test Can be too short or waste time

Selenium documents the implicit wait default as zero and warns against mixing implicit and explicit waits because observed timeouts can become unpredictable. Prefer explicit waits when the test needs a particular UI state. If a codebase has a nonzero implicit wait, make that choice consistent and account for it; do not casually add explicit waits on top.

Wait for the state that proves the operation completed

  • After a control becomes available, wait for visibility or clickability before interacting.
  • After submitting a form, wait for a success message, route change, or other observable completion signal.
  • After a loading indicator appears, wait for it to disappear only if disappearance means the target state is ready; otherwise wait for the resulting content directly.
  • After an action replaces a node, wait for the new node or expected text rather than reusing a stale element reference.

Choose the narrowest useful condition. “Element exists” may pass while the element is hidden. “Visible” may pass while an overlay still intercepts clicks. “Clickable” can help, but the application may still reject an action for a separate business-state reason. Assertions should verify the user-visible outcome after the action.

Page-load readiness is not application readiness

WebDriver navigation waits according to the session’s page-load strategy. normal is the default and waits for document readiness complete; eager returns at interactive; none does not block on document readiness. These strategies apply across the session. A single-page application can fetch data and render controls after any of those milestones, so synchronize on the relevant application condition after navigation.

Consider eager or none only when waiting for all page resources is unnecessary and you have explicit synchronization after navigation. They may return control sooner, but they do not make the page’s dynamic state ready. Browser navigation caused by clicking or submitting a form also does not necessarily use the same navigation wait behavior as a direct URL navigation.

3. Make locators stable and understandable

A locator is part of the test’s knowledge of the application. Prefer an application-provided stable identifier or a concise selector tied to meaningful structure. Avoid selectors that depend on incidental styling classes, deep DOM nesting, or an element’s current position among unrelated matches.

  • Prefer unique IDs or stable, test-oriented attributes when the application provides them.
  • Use a label, accessible name, or other user-facing relationship when that best expresses the control.
  • Scope a lookup to a meaningful component when the page contains repeated controls.
  • Keep locator definitions near the page or component that owns the corresponding interaction.
  • When a locator matches multiple elements, make the intended scope and uniqueness explicit instead of selecting the first match by accident.

Use Selenium’s normal element APIs for ordinary user interactions. WebElement actions such as click and send keys are designed to interact with elements in a user-like way, including scrolling an off-screen element into view and checking interactability. If a click fails because an overlay covers the control, diagnose the overlay and wait for the right state instead of immediately bypassing it with JavaScript.

4. Use page objects to contain page knowledge

A page object provides a focused interface for operations on a page or component. It centralizes locators and repeated interaction sequences, so a UI change can often be handled in one place. Keep behavioral assertions in the test, where the expected outcome is visible to the reader. A page object may check that the expected page or critical elements loaded when it is constructed.

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.time.Duration;

public class DynamicPage {
  private final WebDriver driver;
  private final WebDriverWait wait;
  private final By revealButton = By.id("reveal");
  private final By revealedInput = By.id("revealed");

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

  public void open() {
    driver.get("https://www.selenium.dev/selenium/web/dynamic.html");
    wait.until(ExpectedConditions.visibilityOfElementLocated(revealButton));
  }

  public String revealAndEnter(String text) {
    wait.until(ExpectedConditions.elementToBeClickable(revealButton)).click();
    WebElement input = wait.until(
        ExpectedConditions.visibilityOfElementLocated(revealedInput));
    input.sendKeys(text);
    return input.getAttribute("value");
  }
}

// In test code: keep the behavioral assertion here.
DynamicPage page = new DynamicPage(driver);
page.open();
assertEquals("ready", page.revealAndEnter("ready"));

Do not turn a page object into a large repository of generic browser commands or put every assertion inside it. Small component objects can be clearer than one object for a long page with independent regions. The useful boundary is the one that keeps repeated page knowledge together without hiding what the test verifies.

5. Use Selenium 4 options and interactions deliberately

Selenium 4 configures sessions with browser-specific options classes. Remote sessions also need an options instance to select the browser. Use the current binding’s timeout APIs; in Java, timeout values use Duration.

import java.time.Duration;
import org.openqa.selenium.PageLoadStrategy;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;

ChromeOptions options = new ChromeOptions();
options.setPageLoadStrategy(PageLoadStrategy.NORMAL); // default behavior
ChromeDriver driver = new ChromeDriver(options);
driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(30));
driver.manage().timeouts().scriptTimeout(Duration.ofSeconds(10));
driver.manage().timeouts().implicitlyWait(Duration.ZERO);

Use timeout values that reflect your application and environment, and keep the failure bounded. A page-load timeout governs navigation; a script timeout governs asynchronous script execution; an implicit timeout governs element location. These do not replace explicit waits for a particular UI condition. For remote sessions, use the remote driver constructor with the remote endpoint and the browser options instance. Follow W3C capability names; vendor-specific capabilities need the vendor’s appropriate prefix.

Use the Actions API when a test specifically needs combined or lower-level input, such as a key sequence or pointer action. It is not a routine fix for a stale locator, an element obscured by an overlay, or an application state that was never awaited.

6. Make failures easier to diagnose

  1. Identify the failed transition. Find the last successful action and the next state the test assumed.
  2. Check the actual browser state. Capture a screenshot, current URL, page title, and relevant element text when the failure occurs.
  3. Check the locator. Confirm it matches the intended element and that the element is present in the current document or frame.
  4. Check synchronization. Replace a blind delay or immediate lookup with a wait for the actual state. Check that another wait type is not changing the timing.
  5. Check session and environment. Confirm browser startup, driver/browser compatibility, remote session availability, and cleanup.
  6. Keep the failure visible. Preserve the original exception and attach diagnostics; avoid catch-and-retry loops that silently turn a real failure into a pass.

For repeated failures, record enough context to distinguish a slow transition from a broken locator, an intercepted click, a stale reference, or a browser/session problem. Retrying the entire test can conceal state leakage or a real product defect; first make the failing condition and captured evidence understandable.

7. Common Selenium problems and fixes

Symptom Likely cause Useful fix
NoSuchElementException The element is not present yet, the locator is wrong, or the test is in the wrong frame or page. Verify the locator and browsing context; wait for presence or visibility if the page adds it asynchronously.
TimeoutException The awaited condition never became true within its bound, or navigation/script exceeded its timeout. Check the condition, selector, page state, and timeout category. Do not simply increase every timeout.
StaleElementReferenceException The DOM node was replaced after the element was located. Wait for the replacement state and locate the element again; avoid holding references across rerenders.
ElementClickInterceptedException An overlay, animation, or another element covers the target. Wait for the overlay or transition to finish, then use a normal element click and verify the result.
ElementNotInteractableException The element exists but is hidden, disabled, or otherwise not ready for interaction. Wait for visibility or enabled/clickable state; confirm the test selected the interactive control.
Test continues before dynamic content appears Navigation readiness was mistaken for application readiness. Wait for the specific element, text, or state needed after navigation.
Waits last longer than their configured timeout Implicit and explicit waits are mixed, or another command is blocking. Use a consistent wait policy and inspect navigation, script, and remote command timeouts separately.
Browser fails to start or session creation fails Browser installation, driver setup, options, or remote configuration is incorrect. Check browser availability and Selenium Manager output; review Selenium 4 options and remote capabilities.

8. Performance, reliability, and cost considerations

Performance: condition-based waits return as soon as their condition succeeds, while fixed sleeps always spend the full delay. An explicit wait that polls too aggressively can add needless commands, especially over a remote connection; use a sensible polling interval and a bounded timeout. Faster navigation strategies can save waiting on assets irrelevant to the test, but require reliable synchronization after navigation.

Reliability: no wait or page-object pattern guarantees a test will never be flaky. Tests can still fail because of product defects, environment differences, shared state, or unstable test data. Make setup and teardown predictable, isolate state where possible, and report diagnostics that show what the browser saw at failure time.

Cost: Selenium itself is an open-source browser automation project. Execution costs come from the infrastructure you choose, such as local machines, CI capacity, or remote browser sessions. Reduce unnecessary waits and repeated browser work while retaining checks that protect user-visible behavior; do not trade away useful coverage just to shorten a run.

9. Capture a browser result without maintaining a Selenium session

If the job is to save a page image or PDF for documentation, review, or an AI workflow, a browser automation script may be more setup than needed. You can still use Selenium when you need to interact with the page and verify behavior. For a direct website capture, ScreenshotNeo is a website screenshot API and MCP server for developers.

Do it yourself with Selenium

For a one-off screenshot with Selenium, open the target and save the browser viewport. This Java example uses the same Selenium 4 dependency shown above:

import org.openqa.selenium.OutputType;
import org.openqa.selenium.chrome.ChromeDriver;

ChromeDriver driver = new ChromeDriver();
try {
  driver.get("https://stripe.com");
  byte[] png = driver.getScreenshotAs(OutputType.BYTES);
  java.nio.file.Files.write(java.nio.file.Path.of("shot.png"), png);
} finally {
  driver.quit();
}

This captures the current viewport. Full-page capture, element screenshots, browser state setup, and cleanup may need additional code or browser-specific handling. Add explicit waits if the page content you need renders after navigation.

Or skip the browser setup

ScreenshotNeo takes a screenshot or PDF with one GET request. See the API documentation for options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));

Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed; response headers report the page verdict and billing status. An MCP server lets AI agents use the take_screenshot, get_page_info, and capture_pdf tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for free and get 1,000 screenshots a month with no card.

10. Frequently asked questions

Should every test use a page object?

No. A short test with no repeated page knowledge may be clearer directly. Add a page or component object when it makes shared locators and operations easier to update without hiding assertions.

Should I increase the timeout when a test fails?

Only if evidence shows the correct condition sometimes takes longer in the target environment. First check that the condition and locator are right and that the failure is actually a timing issue.

Is Selenium’s Actions API the fix for intercepted clicks?

Usually not. First find what covers the target and wait for the intended interaction state. Use Actions when the behavior itself requires a sequence of pointer or keyboard input.

Can I use readyState to know when a single-page application is ready?

It tells you about document loading, not whether later application data and controls have reached the state your test needs. Wait for that state directly.