Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

If Selenium always opens or changes the first dropdown in a table, the usual cause is a page-wide locator. Locate the target tr first, then find and operate its descendant control. Also confirm whether the control is a native HTML <select>; Selenium’s Select helper does not operate custom dropdowns built from buttons, divs, or list items.

Why Selenium targets the first row

A call such as driver.find_element(By.CSS_SELECTOR, "select.status") searches the entire document and returns the first match. Repeating that call in a loop does not make it row-aware, so every iteration can modify the same control.

Tables also redraw after a selection, pagination, sorting, or an AJAX update. A previously stored row element may then be detached from the DOM, producing a stale-element error. Numeric row positions are similarly fragile: sorting or filtering can move a record to a different index.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The reliable mental model

  1. Identify the intended row with a stable key, such as data-row-id or a unique cell value.
  2. Find the dropdown relative to that row.
  3. Interact with the control according to its DOM type.
  4. Verify the changed value in that same row.
  5. After a redraw, locate the row again instead of reusing its old WebElement.

First determine whether it is a native select

Inspect the element in browser developer tools. A native control has a <select> element containing <option> elements. Selenium’s Select class only works with those HTML elements. A widget made from a button, input, div, or li is a JavaScript overlay and must be operated through ordinary element interactions.

DOM you find Correct approach Typical mistake
<select><option> Use Select after scoping to the row Using a global selector and changing the first match
Button plus popup/listbox Click the row’s trigger, then choose the visible option Passing the button or div to Select
Input with autocomplete Type, wait for suggestions, select the matching suggestion Assuming the suggestions are descendants of the input’s row

Fix a native select in each row (Python)

This example uses a stable row key and selects by visible text. Replace the selectors and values with those used by your page.

from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import Select, WebDriverWait

wait = WebDriverWait(driver, 10)

for wanted_id in ("42", "57", "83"):
    row = wait.until(lambda d: d.find_element(
        By.CSS_SELECTOR, f"tr[data-row-id='{wanted_id}']"
    ))
    select_el = row.find_element(By.CSS_SELECTOR, "select.status")
    wait.until(lambda d: select_el.is_enabled())

    Select(select_el).select_by_visible_text("Approved")

    selected = Select(select_el).first_selected_option.text.strip()
    assert selected == "Approved", (
        f"Row {wanted_id} selected {selected!r}, not 'Approved'"
    )

The important operation is row.find_element(...), not another call on driver. Selenium also supports selection by option value and zero-based index:

Select(select_el).select_by_value("approved")
Select(select_el).select_by_index(2)

Visible text is generally easiest to review, while a stable value is less affected by translated labels. Use an index only when the option order is a documented part of the page.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Multiple-select controls

If the element has the HTML multiple attribute, it can contain several selected options. Inspect all selected options rather than only first_selected_option:

selected = [option.text.strip()
            for option in Select(select_el).all_selected_options]
assert "Approved" in selected

The control’s size, placeholder behavior, and selected-option semantics come from HTML, so do not treat a multi-select as a single-choice menu.

Fix a custom JavaScript dropdown

Custom widgets often render their popup outside the table, commonly as a child of body. Locate the trigger in the intended row, wait until it is clickable, click it, and then locate an option in the active visible listbox.

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

wait = WebDriverWait(driver, 10)
row_key = "42"
row = wait.until(EC.presence_of_element_located(
    (By.CSS_SELECTOR, f"tr[data-row-id='{row_key}']")
))
trigger = row.find_element(
    By.CSS_SELECTOR, "button[aria-haspopup='listbox']"
)
wait.until(EC.element_to_be_clickable(trigger)).click()

option = wait.until(EC.element_to_be_clickable((
    By.XPATH,
    "//div[@role='listbox' and not(contains(@style,'display: none'))]"
    "//div[@role='option' and normalize-space()='Approved']"
)))
option.click()

Do not assume the nth option belongs to the nth row. If several listboxes exist, use an attribute that associates the open popup with the trigger (for example, an ARIA ownership or ID relationship), or restrict the XPath to a listbox that is actually visible. Some frameworks use li, button, or another role instead of div[role='option']; match the markup you inspected.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Verifying a custom selection

After the click, read the row’s displayed label, selected-value attribute, or accessible name and assert it changed. For a redraw-prone widget, wait for that row to show the expected label before continuing:

wait.until(lambda d: row.find_element(
    By.CSS_SELECTOR, "button[aria-haspopup='listbox']"
).text.strip() == "Approved")

If the selection replaces the row node, this wait can itself become stale. In that case, re-find the row by its stable key inside the wait callback.

Process rows safely when the table refreshes

Keep each iteration independent. Find the row immediately before acting, perform the selection, wait for the result, and only then move to the next key.

row_ids = ["42", "57", "83"]
for row_id in row_ids:
    def current_row(d):
        return d.find_element(
            By.CSS_SELECTOR, f"tr[data-row-id='{row_id}']"
        )

    row = wait.until(current_row)
    select_el = row.find_element(By.CSS_SELECTOR, "select.status")
    Select(select_el).select_by_value("approved")

    wait.until(lambda d: d.find_element(
        By.CSS_SELECTOR,
        f"tr[data-row-id='{row_id}'] select.status option:checked"
    ).get_attribute("value") == "approved")

If the table paginates, first navigate to the page containing the key and wait for the rows to finish loading. If it sorts after an update, never infer the next target from the old row index.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Handling click interception and interactability

Selenium scrolls an element into view and checks whether it can receive the click. If another element covers the click center, the click can fail with an element-click-intercepted error. Common causes include a loading mask, cookie banner, sticky header, animation, or a row positioned partly outside the viewport.

  • Wait for the page’s loading mask or overlay to become invisible.
  • Wait for the row-local trigger to be clickable, not merely present.
  • Scroll the row into a useful position before clicking.
  • Close or accept an obstructing banner when that is part of the test’s intended behavior.
  • Use a JavaScript click only as a last resort, after understanding the widget’s event handling; it bypasses normal hit testing and can hide a real layout defect.
from selenium.webdriver.common.action_chains import ActionChains

row = wait.until(EC.presence_of_element_located(
    (By.CSS_SELECTOR, "tr[data-row-id='42']")
))
trigger = row.find_element(By.CSS_SELECTOR, "button.status-menu")
driver.execute_script(
    "arguments[0].scrollIntoView({block: 'center'});", trigger
)
wait.until(EC.element_to_be_clickable(trigger))
ActionChains(driver).move_to_element(trigger).click().perform()

Locator and synchronization checklist

  • Prefer data-row-id, a record ID, or another stable key over tr:nth-child(3).
  • Scope every repeated control to its target row.
  • Use explicit waits for presence, enabled state, visibility, and clickability.
  • Wait for network-driven redraws or for the selected label/value to change.
  • Re-locate rows after sorting, filtering, pagination, or AJAX replacement.
  • Assert the final value in the same row that was edited.
  • Capture the row key and HTML in failure logs so a wrong-row defect is diagnosable.

Common failures and their fixes

Every loop iteration changes row one

Cause: the dropdown is found from driver. Fix: find the target row first and call row.find_element.

UnexpectedTagNameException from Select

Cause: the widget is not a native select. Fix: inspect its trigger and popup, then use click-and-option interactions.

ElementClickInterceptedException

Cause: an overlay or another element covers the click point. Fix: wait for the obstruction to disappear, scroll into view, and use a normal clickable wait.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

StaleElementReferenceException

Cause: the table replaced the row after selection. Fix: discard the old WebElement and re-find the row by its stable key.

The menu opens but the wrong option is chosen

Cause: options are rendered globally and the test selects an arbitrary matching or nth option. Fix: target the currently visible popup or its trigger-associated listbox, and match normalized text or a stable option value.

Timeout while waiting for a row

Cause: the row is on another page, filtered out, or still loading. Fix: navigate or adjust filters first, then wait for the specific key instead of increasing the timeout blindly.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Performance and reliability practices

Use one driver session and explicit waits rather than fixed sleeps. Stable keys prevent retries caused by row reordering. For large tables, process only the required records and avoid collecting every row repeatedly; locate the key directly when the page provides an ID selector. If a selection triggers a request, wait for a deterministic UI change rather than sleeping for an assumed network duration.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Keep tests deterministic by resetting filters and sort order, and record the requested row key, desired value, actual value, and visible errors on failure. This distinguishes a locator bug from an application defect.

Or skip the browser setup

If your goal is a clean image or PDF of a page rather than interactive form testing, ScreenshotNeo provides a website screenshot API and MCP server. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result.

One request is enough:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo API documentation for all options, including row-relevant custom JavaScript, CSS selectors, waits, cookies, headers, device presets, full-page capture, and PDF output. It also offers an MCP server with take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 shots each month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

FAQ

Should I use a row index if the table is static?

Only when the markup and ordering are guaranteed by the application. A stable record key remains safer and continues to work after sorting, filtering, or pagination.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Can I select an option by visible text in a custom widget?

Yes, provided the lookup is limited to the active popup and the text is normalized. A page-wide text match can still select an option from another open or hidden widget.

Why does a JavaScript click sometimes pass while Selenium click fails?

JavaScript bypasses Selenium’s hit-testing and therefore can ignore an overlay or layout problem. Prefer fixing the obstruction; reserve JavaScript for a widget whose event model you understand.

How can I prove the test changed the intended row?

Read back the selected value or label from the same stable-keyed row and assert it. Include that key and both values in failure output.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.