Add click_element utility for robust element clicking in tests - #6968
Add click_element utility for robust element clicking in tests#6968masenf wants to merge 3 commits into
Conversation
`poll_for_navigation` returns as soon as the URL changes, but the client side router swaps the route component after that, replacing the DOM nodes. Since index and /static/x render the same component, a link located right after navigating back to index could go stale before the click was dispatched, raising StaleElementReferenceException. Add a `click_element` helper that re-locates the element on every attempt and use it for the link clicks in the dynamic route navigation tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7hxgQtxcFzZC795xyftVS
Greptile SummaryAdds a reusable Selenium helper that re-locates elements while retrying stale or temporarily absent references, then updates navigation-oriented integration tests to use it.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| tests/integration/utils.py | Adds click_element, which retries missing or stale element references until its polling deadline. |
| tests/integration/test_dynamic_routes.py | Replaces cached element references with fresh lookups through click_element during client-side navigation. |
| tests/integration/test_event_chain.py | Uses the new helper to locate and click the unmount button. |
| tests/integration/test_login_flow.py | Uses fresh element lookup and clicking for login, action, and logout controls. |
| tests/integration/test_navigation.py | Uses the new helper for internal and external navigation links. |
Reviews (3): Last reviewed commit: "Use click_element for the other stale-pr..." | Re-trigger Greptile
Merging this PR will not alter performance
Comparing Footnotes
|
There was a problem hiding this comment.
All reported issues were addressed across 2 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
FarhanAliRaza
left a comment
There was a problem hiding this comment.
I ran tests/integration/test_dynamic_routes.py in a worktree at this branch, in two configurations.
First, a baseline. I reverted both changed files to their pre-PR content and ran the module repeatedly. The flake reproduced twice in 16 runs. Both failures were test_on_load_navigate_non_dynamic[prod], and both raised StaleElementReferenceException from the link.click() that this PR replaces. This confirms the diagnosis in the description.
Second, the branch as submitted. The module passes.
I also checked the rest of the file for the same locate-then-click shape, and ran ruff check, pyright, and a McCabe complexity check at max-complexity=4 on both changed files. All three are clean, so complexity is not a concern here.
The change fixes a real fault. My concerns are with the shape of the new helper and with how completely it is applied. See the inline comments.
Review feedback: retrying on any exception could re-dispatch a click that had already landed (the exact-count event assertions would then flake) and made a bad locator wait out the full timeout. Retry only on StaleElementReferenceException, which chromedriver raises before the click reaches the browser, and let every other error propagate immediately. Retrying NoSuchElementException in particular would be costly here: this module sets a 30s implicit wait, so a missing element already blocks that long per lookup. Poll in a plain loop instead of AppHarness._poll_for, which suppresses all exceptions and required carrying the last one out via nonlocal. Also convert the remaining find-then-click-across-navigation sites in test_dynamic_routes.py, which have the same stale-reference shape. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7hxgQtxcFzZC795xyftVS
Same failure shape as the dynamic route test: an element located right after a navigation, clicked once the route component has been swapped. - test_navigation: `external`/`external2`, located immediately after `driver.back()` and clicked later, with a window switch in between. - test_login_flow: `doit`, located as soon as the URL became /login; `login` and `logout` for consistency in the same flow. - test_event_chain: `unmount`, located right after `driver.get()` and clicked after `assert_token()` waits out hydration. These modules set no implicit wait and relied on a presence poll before the click, so click_element now retries NoSuchElementException as well as StaleElementReferenceException — both are raised while locating or validating the reference, never after the click is dispatched, so the click still cannot fire twice. InvalidSelectorException derives from WebDriverException rather than NoSuchElementException, so a bad locator still fails immediately. Left alone: single-expression `find_element(...).click()` calls (no window between locating and clicking) and clicks that never cross a navigation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7hxgQtxcFzZC795xyftVS
Type of change
Description
Fixes a
StaleElementReferenceExceptionflake intest_on_load_navigate_non_dynamic[prod], then applies the same fix to the other Selenium integration tests with the same shape.Problem:
poll_for_navigationreturns as soon asdriver.current_urlchanges, which happens when the router pushes history — before React swaps the route component. An element located in that window belongs to the outgoing tree and is unmounted before the click is dispatched. In the observed failure,/and/static/xrender the same component, solink_page_xexisted on both pages and was located on the page being navigated away from. Prod mode widens the window because the route chunk is fetched lazily.Solution:
click_element(driver, by, value, timeout)re-locates the element on every attempt, so it clicks whichever node is currently rendered.Retries are confined to
NoSuchElementExceptionandStaleElementReferenceException. Both are raised while locating or validating the element reference, never after the click reaches the browser, so a click cannot be dispatched twice — which matters because these tests assert exact event counts. Everything else propagates on the first attempt, includingInvalidSelectorException(it derives fromWebDriverException, notNoSuchElementException) andElementClickInterceptedException.Changes
tests/integration/utils.py— addclick_element(): a deadline loop that re-locates and clicks, raisingTimeoutErrorwith the locator and the last error chained as__cause__.tests/integration/test_dynamic_routes.py— all link clicks go through the helper: thelink_page_x/link_indexclicks intest_on_load_navigate_non_dynamic(the flaking test), thelink_page_nextloop and post-redirect click pluslink_missingintest_on_load_navigate, and bothnext-pageclicks intest_render_dynamic_arg. Cached element variables and their presence polls drop out, since the helper re-locates.tests/integration/test_navigation.py—external/external2, located immediately afterdriver.back()and clicked later with a window switch in between;internalfor consistency.tests/integration/test_login_flow.py—doit, located as soon as the URL became/login;loginandlogoutin the same flow.tests/integration/test_event_chain.py—unmount, located right afterdriver.get()and clicked onceassert_token()has waited out hydration (where the StrictMode remount lands).Deliberately unchanged: single-expression
find_element(...).click()calls, which have no window between locating and clicking (test_auto_memo.py,test_deploy_url.py), and clicks that never cross a navigation (test_client_storage.py,test_connection_banner.py,test_upload.py— those already re-locate after each refresh).Follow-up work (not in this PR)
test_dynamic_routes.pycarries twoTODO: drop after flakiness is resolvedband-aids —driver.implicitly_wait(30)and anawait asyncio.sleep(3)intest_render_dynamic_arg. Removing the implicit wait means converting every remaining barefind_elementin that module to a polling lookup, and neither removal can be demonstrated except by repeated CI runs, so both are left for a later change.Testing
uv run ruff check .,uv run ruff format .anduv run pyright reflex testsare clean.integration-app-harness(Selenium, dev + prod) andintegration-tests(Playwright) are green on the head commit.Checklist
🤖 Generated with Claude Code
https://claude.ai/code/session_01W7hxgQtxcFzZC795xyftVS