ScreenshotNeo

BlogHow-to

How to Test Web UI with Selenium

Learn how to test a web UI with Selenium: choose stable locators, wait for the right state, assert user-visible results, and decide when to use Grid.

By the ScreenshotNeo team4 October 20269 min read

Selenium WebDriver tests a web UI by opening a real browser, interacting with the page through browser automation APIs, waiting for the state a user needs, and checking a meaningful visible result. A reliable test is more than a sequence of clicks: define one user outcome, use stable locators, synchronize on that outcome, and keep the test’s setup understandable.

This guide uses Python with pytest and Chrome. It also shows the Selenium-specific workflow in cURL and Node.js context, explains waits and locators, and covers Grid, failures, and screenshot-based debugging.

1. Choose a user outcome to test

Start with one behavior and its expected result. For example: submit a valid sign-in form and confirm that a dashboard heading appears. This gives the test a clear assertion and helps keep it independent of unrelated flows.

  1. Identify the user action, such as submitting a form.
  2. Identify the visible result that proves the action succeeded.
  3. Make the test’s initial state reproducible, such as using a dedicated test account or a resettable test record.

Selenium makes browser interaction possible; it does not design a maintainable test suite for you. Keep setup, action, and assertion clear, and avoid tests whose result depends on another test running first. See the Selenium Project’s test practices.

2. Set up a Python Selenium test

Install Selenium and pytest in your project’s virtual environment using your normal Python package workflow. The code below expects a locally available Chrome browser. Selenium’s overview explains that WebDriver communicates with browser-vendor automation APIs; browser and driver setup can depend on your environment, so consult the official Selenium overview and your browser’s current setup guidance.

Save this as test_login.py. Replace the example URL and selectors with those from your application. The example uses explicit waits and checks a visible result.

import os

import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait


@pytest.fixture
def driver():
    options = webdriver.ChromeOptions()
    if os.getenv("HEADLESS") == "1":
        options.add_argument("--headless")

    browser = webdriver.Chrome(options=options)
    browser.set_window_size(1365, 900)
    yield browser
    browser.quit()


def test_valid_login_shows_dashboard(driver):
    wait = WebDriverWait(driver, 10)
    driver.get("https://app.example.test/login")

    email = wait.until(
        EC.visibility_of_element_located((By.ID, "email"))
    )
    email.send_keys("qa@example.test")
    driver.find_element(By.ID, "password").send_keys("test-password")
    driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()

    heading = wait.until(
        EC.visibility_of_element_located((By.CSS_SELECTOR, "h1.dashboard-title"))
    )
    assert heading.text == "Dashboard"

Run it with pytest -q. For a headless run, set HEADLESS=1 in the environment before invoking pytest. Keep credentials out of source control; use environment variables or your CI secret store for test credentials. The sample uses placeholder values and a reserved example hostname, so it must be adapted to a test environment before it can pass.

What each part does

  • webdriver.Chrome() starts a browser session.
  • driver.get() navigates to the page.
  • By.ID and By.CSS_SELECTOR identify controls.
  • WebDriverWait polls for a specific condition before continuing.
  • The final assertion checks a user-visible heading rather than merely confirming that a click happened.
  • The fixture closes the browser even after a test failure.

3. Choose locators that survive UI changes

A locator connects a test to an element in the page. Prefer a unique, predictable ID when the application provides one. Otherwise use a compact, readable CSS selector. Use XPath when it expresses a relationship clearly, but avoid long paths tied to incidental DOM structure. Selenium’s locator guidance discusses readability and maintainability.

Locator Example When it fits
ID (By.ID, "email") A unique, stable ID exists.
CSS selector (By.CSS_SELECTOR, "button[type='submit']") A concise attribute or class selector expresses the target.
XPath (By.XPATH, "//label[normalize-space()='Email']/following::input[1]") A DOM relationship is the clearest available way to identify the control.

Avoid selectors based on generated class names, element position, or deep nesting when those details change during routine styling or layout work. If you can influence the application, add stable IDs or other purpose-built attributes to important controls.

4. Wait for the state the next step needs

A navigation reaching its configured readiness point does not guarantee that later JavaScript changes are complete. Wait for the actual condition needed by the next action: an element becoming visible, a button becoming clickable, text appearing, or a result route loading. Selenium’s waiting strategies describe explicit waits as polling for a condition until it succeeds or times out.

Explicit wait

wait = WebDriverWait(driver, 10)
submit = wait.until(
    EC.element_to_be_clickable((By.CSS_SELECTOR, "button[type='submit']"))
)
submit.click()

confirmation = wait.until(
    EC.visibility_of_element_located((By.ID, "confirmation"))
)

The timeout is a maximum, not a fixed pause: if the condition becomes true sooner, the test continues sooner. Choose a timeout appropriate for the application and environment, and let a timeout fail with enough context to diagnose the missing state.

Implicit wait versus explicit wait

Wait type What it does Good fit
Implicit Applies a global wait to element-location calls. A deliberately chosen global policy for a suite.
Explicit Polls for a particular condition at a particular step. A UI transition or precise state needed by the test.

For state-driven UI tests, explicit waits make the synchronization point visible in the test. Avoid mixing implicit and explicit waits casually: Selenium warns that doing so can produce unpredictable total wait times. Fixed sleeps can be too short on a slow run and waste time on a fast one; use them only when there is a specific reason a condition cannot be expressed.

5. Assert a result, not just an interaction

After the action, assert a result that matters to the user: confirmation text, a changed status, a new heading, a route, or an item in a list. A successful click alone does not prove that the application completed the intended behavior.

status = wait.until(
    EC.visibility_of_element_located((By.CSS_SELECTOR, "[role='status']"))
)
assert "Saved" in status.text

Make each test’s purpose narrow enough that a failure points to a useful behavior. Reuse setup where it improves clarity, but keep tests isolated so order and shared mutable state do not determine the result.

6. Run locally, then decide whether to use Grid

Local browser execution is a straightforward development loop for a small suite. Selenium Grid routes WebDriver commands to remote browser instances. It is useful when you need remote sessions, parallel runs, multiple browser versions, or cross-platform coverage. See the Selenium Grid documentation.

Need Starting point Trade-off to consider
Debug one test while developing Run locally in a visible browser. Coverage is limited to the local browser and platform.
Cover browser versions or operating systems Run sessions on Grid nodes that provide the target combinations. Remote browser infrastructure needs configuration and upkeep.
Reduce elapsed suite time through parallel runs Distribute independent tests across Grid sessions. Parallelism requires isolated test data and enough browser capacity.

There is no universal point at which Grid is necessary. Choose based on the browser and platform risks you need to cover, the value of parallel execution, and the operational work your team can support. Do not parallelize tests that share mutable data without first making their setup independent.

7. Capture evidence when a UI test fails

A screenshot can make a failure easier to inspect, especially when a test passes locally but fails in a CI browser. Save it at the point of failure, along with the test name and relevant logs. Selenium can save a screenshot from the current browser session:

driver.save_screenshot("failure.png")

For repeatable visual snapshots of a public page, an independent screenshot can also help compare what the browser rendered. It does not replace an interactive WebDriver test: a screenshot alone cannot establish that a form submission or other behavior worked.

Or skip the browser setup

If the task is to capture a page image rather than exercise its controls, ScreenshotNeo provides a website screenshot API and MCP server. A GET request returns an image or PDF. This example requests a WebP screenshot; see the ScreenshotNeo API documentation for request 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,
)
r.raise_for_status()
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 require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
  • Cookie banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
  • Bot checks, blank pages, timeouts, failed loads, and cache hits are never billed. Responses include page-verdict and billing headers.
  • An MCP server lets AI agents use screenshot, page-info, and PDF capture tools.
  • The free plan includes 1,000 screenshots a month with no card. Paid plans start at $5 for 3,000 screenshots.

Create a free ScreenshotNeo account and get 1,000 screenshots a month with no card.

Troubleshooting common Selenium failures

Symptom Likely cause Fix
NoSuchElementException The locator is wrong, the element has not appeared, or it is in another frame. Verify the selector in the current page, wait for the relevant condition, and switch to the correct frame if the UI uses one.
TimeoutException The awaited state did not occur before the deadline. Check whether the action succeeded, the selector matches the resulting UI, and the page has an error. Increase the timeout only when the state is expected to take longer.
ElementClickInterceptedException An overlay, animation, or another element covers the target. Wait for the overlay to disappear or the target to become clickable. Confirm the page is in the expected state before interacting.
StaleElementReferenceException The page re-rendered and replaced the element after it was located. Wait for the transition, then locate the element again instead of reusing the old reference.
Browser fails to start or session creation fails The browser is missing, incompatible with the available setup, or blocked by the runtime environment. Check browser installation and Selenium setup guidance for the environment. In containers or CI, verify that the browser can launch under that runner’s permissions and configuration.
Passes locally, fails in CI Timing, viewport, browser, data, or environment differs. Fix the viewport and test data, wait for conditions instead of sleeping, capture failure screenshots and logs, and reproduce the CI browser configuration locally where possible.
Test hangs after completion The browser session was not closed after the test. Use fixture teardown or a try/finally block to call quit(), including on failures.

Performance, reliability, and cost considerations

  • Runtime: Browser startup and page work take time. Explicit waits reduce needless waiting compared with fixed delays, while parallel Grid sessions can reduce elapsed suite time when tests are independent and capacity is available.
  • Reliability: Stable locators, state-based waits, isolated test data, and meaningful assertions address common sources of brittle tests. They cannot eliminate failures caused by application defects or unstable environments.
  • Debugging: Record the failing step and capture a screenshot or relevant browser logs. Keep enough context to distinguish a product regression from a setup or timing issue.
  • Cost: Local Selenium uses your own browser and compute resources. Grid adds infrastructure or service costs depending on how it is operated. The research sources do not establish a benchmark or universal cost figure, so measure your own suite and environment.
  • Scope: Selenium is appropriate for browser-level functional interaction. For a static screenshot requirement, a screenshot API can avoid maintaining browser automation code; for behavior, keep the WebDriver test and its assertions.

FAQ

Does Selenium test the application through a real browser?

WebDriver automates browsers through browser-vendor automation APIs, so the test interacts with the application through the browser.

Should every UI test use Selenium Grid?

No. Grid is useful when remote sessions, parallel execution, browser-version coverage, or cross-platform runs are requirements. A local browser is often enough for a focused development loop.

Can a screenshot prove that a UI flow works?

No. A screenshot records rendered output. Use WebDriver interactions and assertions to verify behavior such as submitting a form or changing application state.

Which locator should I try first?

Use a unique, predictable ID if available; otherwise choose a short CSS selector that clearly identifies the element.