Selenium Waits and Locators
Locator Strategies Overview
Selenium provides eight built-in locator strategies, each suited to different situations. The right choice depends on the page structure, the availability of stable identifiers, and how resilient you need the locator to be against future markup changes. No single strategy is universally best, and experienced Selenium users switch between strategies based on what each page offers.
ID and Name Locators
Locating by ID (By.ID) is the fastest and most reliable strategy when the target element has a unique id attribute. Browsers maintain an internal hash map of element IDs, so ID lookups are O(1) operations. The limitation is that not all elements have IDs, and some frameworks generate dynamic IDs that change on each page load, making them useless for automation.
Name locators (By.NAME) target the name attribute, which is most common on form inputs. Name values tend to be more stable than dynamically generated IDs because they are tied to form processing logic. However, name is not guaranteed to be unique on a page, so find_element() returns the first match while find_elements() returns all matches.
CSS Selectors
CSS selectors (By.CSS_SELECTOR) are the most versatile general-purpose locator strategy. They support matching by ID (#elementId), class (.className), attribute ([data-testid="value"]), tag name (button), hierarchy (div.container > p), pseudo-classes (:first-child, :nth-of-type(2)), and combinations of all of these.
CSS selectors are faster than XPath in most browsers because browsers have optimized CSS selector engines for their rendering pipeline. They are also more readable for developers who work with CSS daily. The main limitation is that CSS selectors cannot traverse upward in the DOM tree (from child to parent) or match elements based on text content, which are both capabilities that XPath provides.
Attribute selectors are particularly useful for automation. [data-testid="login-button"] targets a custom test attribute that is decoupled from visual styling and resistant to CSS refactoring. Many teams add data-testid attributes specifically for automation, creating a contract between the application code and the test code that survives visual redesigns. This practice is recommended by the Selenium documentation and is the default locator strategy in Playwright.
XPath Locators
XPath (By.XPATH) is the most powerful locator strategy, capable of matching any element based on any combination of attributes, text content, position, and document structure. XPath can traverse both downward and upward through the DOM tree, making it the only strategy that can locate a parent element based on a child's properties.
Common XPath patterns include attribute matching (//input[@type='email']), text content matching (//button[text()='Submit']), partial text matching (//a[contains(text(), 'View')]), parent traversal (//span[@class='price']/parent::div), and sibling traversal (//label[text()='Email']/following-sibling::input). The // prefix searches the entire document, while ./ searches within the current element context.
XPath's power comes with tradeoffs. It is slightly slower than CSS selectors in most browsers, and complex XPath expressions can be difficult to read and maintain. Position-based XPath like //div[3]/ul/li[2] is particularly fragile because it breaks when the page structure changes even slightly. When using XPath, prefer attribute and text-based matching over positional matching.
XPath functions extend matching capabilities. contains() matches partial attribute or text values, starts-with() matches attribute prefixes, normalize-space() handles whitespace variations, and not() inverts conditions. Combining these functions creates precise selectors: //div[contains(@class, 'active') and not(contains(@class, 'hidden'))].
Other Built-in Locators
Class name (By.CLASS_NAME) finds elements by their CSS class attribute. It matches a single class name, not compound selectors or multiple classes. For elements with multiple classes, use CSS selector syntax instead: By.CSS_SELECTOR, ".class1.class2".
Tag name (By.TAG_NAME) finds elements by their HTML tag. It is rarely used for finding specific elements because most pages contain many elements of the same tag. It is more useful with find_elements() to collect all elements of a type, such as all <a> tags for link auditing or all <img> tags for image verification.
Link text (By.LINK_TEXT) and partial link text (By.PARTIAL_LINK_TEXT) find <a> elements by their visible text content. Link text requires an exact match while partial link text matches a substring. These are convenient for navigation testing but only work with anchor elements, not buttons or other clickable elements.
Relative Locators
Selenium 4 introduced relative locators that find elements based on their visual position relative to another element. Available relative methods are above(), below(), toLeftOf(), toRightOf(), and near(). In Python, use the locate_with() function; in Java, use RelativeLocator.with().
Relative locators are useful when elements lack unique identifiers but have consistent spatial relationships. For example, finding the input field above a specific label, or the edit button to the right of a particular table row. The proximity is calculated based on the elements' rendered positions on screen, which means relative locators can behave differently at different viewport sizes.
Use relative locators as a supplement to traditional strategies, not a replacement. They are less precise than ID or CSS selectors and depend on visual layout that can change with responsive design breakpoints. They are most valuable in forms and data tables where elements follow a predictable grid layout.
Understanding Selenium Waits
Web pages load content asynchronously. The browser receives the initial HTML, then starts downloading and executing CSS, JavaScript, images, and data from APIs. Content that depends on JavaScript execution or API responses may not exist in the DOM when your script tries to find it. Waits bridge this timing gap by pausing execution until the content is ready.
Implicit Waits
An implicit wait sets a global timeout that applies to every find_element() call. If the element is not immediately present, Selenium polls the DOM repeatedly until the element appears or the timeout expires. Set an implicit wait once after creating the driver: driver.implicitly_wait(10) in Python or driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10)) in Java.
The appeal of implicit waits is simplicity: one line of code handles all timing issues. The problem is that implicit waits apply uniformly to every element search, even when you want immediate feedback (like checking whether an element does not exist). They also interact poorly with explicit waits, creating unpredictable timeout behavior when both are used. Most Selenium experts recommend avoiding implicit waits entirely in favor of explicit waits.
Explicit Waits
Explicit waits are the recommended approach for handling timing in Selenium. They wait for a specific condition on a specific element, providing precise control over what you are waiting for and how long you are willing to wait.
The core class is WebDriverWait, which accepts the driver and a timeout. Call .until() with an expected condition to wait for. The condition is checked repeatedly (by default every 500 milliseconds) until it returns a truthy value or the timeout expires. When the condition is met, the wait returns the result. When the timeout expires, a TimeoutException is raised.
The expected_conditions module provides pre-built conditions for common scenarios. presence_of_element_located checks that an element exists in the DOM. visibility_of_element_located checks that an element is both present and visible. element_to_be_clickable verifies that an element is visible and enabled for interaction. text_to_be_present_in_element waits for specific text content. staleness_of waits for an element to become detached from the DOM, which is useful for detecting page transitions.
The distinction between presence and visibility matters. An element can be present in the DOM but hidden with CSS (display: none or visibility: hidden). If you need to read its text or click it, wait for visibility or clickability, not just presence. Waiting for presence is appropriate when you need to verify that the DOM has been updated, even if the element is not visible.
Fluent Waits
Fluent waits extend explicit waits with additional configuration options. You can customize the polling interval (how frequently the condition is checked), specify which exceptions to ignore during polling, and set a custom message for the timeout exception.
In Java, FluentWait is a separate class that provides these options through a builder pattern. Set the timeout with withTimeout(), the polling interval with pollingEvery(), and exception ignoring with ignoring(). In Python, WebDriverWait accepts an optional poll_frequency parameter and ignored_exceptions parameter that provide the same functionality.
A common use case is waiting for elements inside an iframe that is still loading. The find_element() call inside the wait condition may throw a NoSuchFrameException or NoSuchElementException while the iframe is loading. Configuring the wait to ignore these exceptions lets the polling continue until the frame and element are both available.
Best Practices
Use explicit waits exclusively and avoid implicit waits. Explicit waits give you precise control and clear intent in your code. Each wait states exactly what it is waiting for and how long it will wait, making the code self-documenting and predictable.
Choose the most stable locator available. Prefer IDs and data-testid attributes when available, CSS selectors for structural matching, and XPath only when you need text matching or upward DOM traversal. Avoid position-based locators that break when the page layout changes.
Wait for the right condition. If you need to click an element, wait for element_to_be_clickable, not just presence_of_element_located. If you need to read text, wait for visibility_of_element_located. Waiting for the correct condition prevents flaky tests caused by interacting with elements that are present but not yet ready for the intended action.
Never use time.sleep() or Thread.sleep() as a wait strategy. Fixed sleeps always wait the full duration even when the element is ready immediately, and they fail when the page takes longer than expected. The only legitimate use of fixed sleeps is throttling requests during web scraping to respect rate limits.
Test your locators in the browser developer tools before using them in code. Chrome and Firefox both support testing CSS selectors with document.querySelector() and XPath with document.evaluate() or $x() in the console. Verifying locators interactively catches errors before they become debugging sessions in your test suite.
Reliable Selenium automation depends on two things: stable locators that find the right elements regardless of minor page changes, and explicit waits that synchronize your script with the browser's asynchronous loading behavior. Master CSS selectors and XPath for element location, use WebDriverWait with expected conditions for timing, and you will eliminate the vast majority of test flakiness.