Skip to content

Add click_element utility for robust element clicking in tests - #6968

Open
masenf wants to merge 3 commits into
mainfrom
claude/flaky-test-on-load-navigate-lw72ae
Open

Add click_element utility for robust element clicking in tests#6968
masenf wants to merge 3 commits into
mainfrom
claude/flaky-test-on-load-navigate-lw72ae

Conversation

@masenf

@masenf masenf commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Description

Fixes a StaleElementReferenceException flake in test_on_load_navigate_non_dynamic[prod], then applies the same fix to the other Selenium integration tests with the same shape.

Problem: poll_for_navigation returns as soon as driver.current_url changes, 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/x render the same component, so link_page_x existed 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 NoSuchElementException and StaleElementReferenceException. 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, including InvalidSelectorException (it derives from WebDriverException, not NoSuchElementException) and ElementClickInterceptedException.

Changes

  1. tests/integration/utils.py — add click_element(): a deadline loop that re-locates and clicks, raising TimeoutError with the locator and the last error chained as __cause__.
  2. tests/integration/test_dynamic_routes.py — all link clicks go through the helper: the link_page_x/link_index clicks in test_on_load_navigate_non_dynamic (the flaking test), the link_page_next loop and post-redirect click plus link_missing in test_on_load_navigate, and both next-page clicks in test_render_dynamic_arg. Cached element variables and their presence polls drop out, since the helper re-locates.
  3. tests/integration/test_navigation.pyexternal/external2, located immediately after driver.back() and clicked later with a window switch in between; internal for consistency.
  4. tests/integration/test_login_flow.pydoit, located as soon as the URL became /login; login and logout in the same flow.
  5. tests/integration/test_event_chain.pyunmount, located right after driver.get() and clicked once assert_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.py carries two TODO: drop after flakiness is resolved band-aids — driver.implicitly_wait(30) and an await asyncio.sleep(3) in test_render_dynamic_arg. Removing the implicit wait means converting every remaining bare find_element in 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 . and uv run pyright reflex tests are clean.
  • No new tests: this is a fix to test infrastructure, and the flake it removes is timing-dependent, so CI is the verification — integration-app-harness (Selenium, dev + prod) and integration-tests (Playwright) are green on the head commit.

Checklist

  • Changes follow the guidelines in CONTRIBUTING.md
  • Code is linted and formatted
  • Docstring follows Google style with Args/Raises sections
  • Existing tests pass with the changes

🤖 Generated with Claude Code

https://claude.ai/code/session_01W7hxgQtxcFzZC795xyftVS

`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
@masenf
masenf requested a review from a team as a code owner August 28, 2026 08:30
@greptile-apps

greptile-apps Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds a reusable Selenium helper that re-locates elements while retrying stale or temporarily absent references, then updates navigation-oriented integration tests to use it.

  • Introduces deadline-based element-click polling in tests/integration/utils.py.
  • Replaces direct Selenium clicks across dynamic-route, event-chain, login-flow, and navigation tests.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

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

Comment thread tests/integration/utils.py
@codspeed-hq

codspeed-hq Bot commented Aug 28, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 32 untouched benchmarks
⏩ 8 skipped benchmarks1


Comparing claude/flaky-test-on-load-navigate-lw72ae (a4be554) with main (9a26753)

Open in CodSpeed

Footnotes

  1. 8 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread tests/integration/utils.py
Comment thread tests/integration/utils.py
Comment thread tests/integration/utils.py Outdated
Comment thread tests/integration/utils.py
Comment thread tests/integration/utils.py Outdated
Comment thread tests/integration/utils.py Outdated
Comment thread tests/integration/test_dynamic_routes.py

@FarhanAliRaza FarhanAliRaza left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@masenf
masenf requested a review from FarhanAliRaza September 2, 2026 02:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants