Skip to content

Unpin selenium to fix CI flakiness against current Chrome - #3955

Open
T4rk1n wants to merge 11 commits into
devfrom
fix/unpin-selenium
Open

Unpin selenium to fix CI flakiness against current Chrome#3955
T4rk1n wants to merge 11 commits into
devfrom
fix/unpin-selenium

Conversation

@T4rk1n

@T4rk1n T4rk1n commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Problem

Recent pushes and PRs have been going red intermittently across the browser-based integration tests. The failures are scattered across unrelated Selenium tests (test_persistence, test_csp, test_multi_output, test_derived_props, async callbacks, table server tests…), a different subset each run, all surfacing as StaleElementReferenceException / TimeoutException. That pattern is environmental flakiness, not a bad merge (which would fail the same test deterministically).

Root cause

  • requirements/testing.txt pinned selenium>=3.141.0,<=4.2.0 (selenium 4.2.0 is from 2022).
  • Every CI browser job uses browser-actions/setup-chrome@v1 with chrome-version: stableunpinned — so CI now installs Chrome 151.
  • Selenium 4.2 predates Selenium Manager (added in 4.6), so it cannot reliably provision/drive current Chrome, producing timing/staleness flakiness.

Two amplifiers: the dependabot pip bump that would have raised selenium never landed on dev, and the recent React 18/19 test matrix roughly doubled the browser shards, so a single flake reddens the whole run more often.

Fix

Bump the pin to selenium>=4.11.0,<=4.46.0. The >=4.11.0 floor guarantees a mature Selenium Manager that auto-provisions a chromedriver matching whatever stable Chrome CI installs (this is why install-chromedriver: false in the setup step remains correct).

Compatibility checks

  • No removed find_element_by_* APIs anywhere in dash/ (those were dropped in selenium 4.3).
  • Driver construction already uses the modern API: webdriver.Chrome(options=...) / webdriver.Remote(command_executor=..., options=...).
  • Verified the full API surface dash/testing/browser.py uses, plus Selenium Manager availability, against selenium 4.46.0.

Follow-ups (not in this PR)

  • A few tests exhaust all 3 reruns (e.g. test_async_cbsc001_simple_callback) and may be genuinely broken rather than flaky — worth a targeted look once this settles the noise.
  • Optionally pin Chrome for fully reproducible runs; modern selenium tracks stable fine either way.

T4rk1n added 2 commits August 18, 2026 16:39
The testing requirements capped selenium at <=4.2.0 (2022), which predates
Selenium Manager. CI installs the current stable Chrome (now 151) via an
unpinned browser-actions/setup-chrome, and selenium 4.2 cannot reliably
provision or drive it, producing scattered StaleElementReferenceException /
TimeoutException failures across unrelated browser integration tests on
every push and PR.

Require selenium>=4.11.0 (mature Selenium Manager auto-provisions a matching
chromedriver) up to the current latest 4.46.0.
Unpinning selenium exposed two deterministic breaks the 4.2.0 cap had hidden:

- browser.py set the 'marionette' Firefox capability, which modern
  selenium/geckodriver reject with InvalidArgumentException (marionette is
  the implicit, only protocol now). Removed it.
- Three test modules used the find_element(s)_by_* helper methods that
  selenium removed in 4.3. Migrated them to find_element(s)(By.*, ...).
@T4rk1n

T4rk1n commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up commit 1845c32: the first CI run surfaced two deterministic breaks that the old selenium<=4.2.0 cap had been masking (both now fixed):

  1. dash/testing/browser.py set the Firefox marionette capability, which modern selenium/geckodriver reject (InvalidArgumentException: marionette is not the name of a known capability). It's the implicit protocol now — removed. This is what failed the Lint & Unit jobs (test_browser_smoke[Firefox]).
  2. Three test modules used the find_element(s)_by_* helper methods that selenium removed in 4.3 (AttributeError: 'WebElement' object has no attribute 'find_element_by_tag_name'). Migrated to find_element(s)(By.*, ...). This failed a Main Dash Chrome group.

Remaining StaleElementReferenceException flakes in that first run should be resolved by the selenium bump itself (matching driver for Chrome 151). Re-running CI to confirm.

T4rk1n added 4 commits August 19, 2026 10:57
selenium 4.3 changed move_to_element_with_offset to measure the offset from
the element's center instead of its top-left corner. The dash_duo drag/click
helpers (click_at_coord_fractions, zoom_in_graph_by_ratio) and the dcc page
object helpers passed top-left-based fractional offsets (width*fx, height*fy),
so under modern selenium they overshot past the element edge and raised
MoveTargetOutOfBoundsException — failing the slider drag/step tests and the
graph tooltip center-hover test.

Convert the proportional offsets to center-relative (width*(fx-0.5)) and cast
to int (W3C actions require integer pixels). Small fixed-pixel offsets (5, 8)
are left as-is: they stay within any element regardless of origin.
dash_duo's _wait_for helpers raise selenium's TimeoutException(str(message)).
Modern selenium's WebDriverException.__init__ calls super().__init__() with no
args, so the message lives on .msg and .args is empty — test_duo's
err.value.args[0] assertions raised IndexError. Read .msg, selenium's stable
message accessor.
The step backgrounded Xvfb with a bare '&', so it inherited the step's
stdout/stderr pipe to the Actions runner. Xvfb never exits, so that pipe never
reached EOF and the runner blocked on the step indefinitely (intermittent
'Setup virtual display' hangs across the browser-test jobs). Redirect Xvfb's
output to /dev/null and disown it so the step's pipe closes and the step
completes immediately.
The redirect/disown alone did not stop the hang: the real culprit is
'apt-get update && apt-get install -y xvfb', which intermittently blocks on the
runner's dpkg/apt lock (apt-daily / unattended-upgrades). xvfb is already
preinstalled on the GitHub Ubuntu runners ('xvfb is already the newest
version'), so the install is pure risk. Just start the preinstalled Xvfb; if it
were ever absent the step fails fast instead of hanging.
@T4rk1n

T4rk1n commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

CI verified — systemic flakiness fixed

Full run + targeted re-runs of the flaky jobs. The two structural causes of the cross-PR flakiness are resolved:

  • selenium/Chrome mismatch — the pinned selenium 4.2.0 couldn't drive the current stable Chrome (151); bumped to >=4.11.0 (Selenium Manager auto-provisions a matching driver). Exposed and fixed four deterministic selenium-4 breaks along the way: marionette capability, removed find_element_by_*, move_to_element_with_offset center-origin, and TimeoutException.args[0].msg.
  • "Setup virtual display" hang — the step ran apt-get install -y xvfb, which intermittently blocked on the runner's dpkg lock. xvfb is preinstalled on the runners, so the apt-get calls were removed; the step now just starts Xvfb. 26 display-step instances ran with zero hangs.

Remaining reds are pre-existing flakes, not regressions

On re-run, the DCC and Main Dash failures (test_rdcap003, test_msmh003 markdown re-render race, test_inni004) all passed — confirmed flaky. The one stubborn failure, test_async_cbsc001_simple_callback (AssertionError: initial count + each key stroke), is already failing on dev HEAD (run 32175191617), so it predates this branch.

Recommend merging this PR (it strictly improves CI) and tracking the async-callback test failure + the racy per-test flakes in a separate issue.

T4rk1n added 2 commits August 19, 2026 13:15
test_(async_)cbsc001/cbsc008 assert an exact one-callback-per-keystroke count,
but the renderer coalesces same-identity callbacks still queued in its
'requested' state (requestedCallbacks.ts) into a single request. Two keystrokes
landing in that batching window collapse into one invocation, so the count
undershoots. The Lock choreography the tests used to serialize typing no longer
holds now that async callbacks execute concurrently, and React 19's more
aggressive event batching plus faster Chrome typing pushed the failure rate to
~90% locally — routinely exhausting the flaky retries.

Gate each keystroke on the previous callback having executed (wait until the
counter reflects it) so a keystroke's callback always leaves the 'requested'
queue before the next is sent and can never be coalesced. This makes the
exact-count assertion correct by construction; drop the Lock, the per-keystroke
sleeps, and the @flaky retries.
…meout

Two changes so a stuck test/server can no longer hang a whole CI step (the
'Run Async Callback Tests' step was wedging for the full job timeout):

- ThreadedRunner.stop() Flask path called self.thread.join() with no timeout.
  If the injected SystemExit fails to unwind a worker stuck in a C call, that
  join blocks teardown forever. Bound it with stop_timeout (FastAPI and Quart
  paths already join with a timeout); the following until_not then fails fast
  instead of hanging.
- Add pytest-timeout (requirements/ci.txt, installed via the [ci] extra in
  every test job) and set a 180s per-test cap in pytest.ini. Any remaining hang
  now fails with a full thread stack dump naming the test, instead of stalling
  the step until the job-level timeout.
@T4rk1n

T4rk1n commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

✅ Full green run

Run 32283425251 passed with zero failed jobs after the anti-hang + gating fixes.

The two systemic problems are gone:

  • No more hangs. The 'Run Async Callback Tests' step was wedging until the job timeout. Root cause: ThreadedRunner.stop()'s Flask path did an unbounded self.thread.join(); if the SystemExit thread-kill fails to unwind a worker, teardown blocks forever. Bounded it (FastAPI/Quart already were). Added pytest-timeout (180s/test, via the [ci] extra) so any future hang fails with a stack dump naming the test instead of stalling the step.
  • Callback-count flakes fixed. cbsc001/cbsc008 (async + sync) asserted an exact one-callback-per-keystroke count the renderer never guaranteed — it coalesces same-identity callbacks still queued in requested state. Rewrote them to gate each keystroke on the previous callback having executed (until(...)), so coalescing can't happen; dropped the Lock/sleep/@flaky. Async step now 26 passed, background 36 passed.

Remaining occasional reds (test_lcbc017, test_persistence, test_rdcap003, test_dveh002) are the pre-existing low-frequency flake tail — they cleared on re-run and predate this branch.

Optional follow-up (not in this PR): add if: ${{ !cancelled() }} to the Async step so a Background flake stops skipping it, and the graceful-Flask-shutdown change to cut the teardown-kill noise at the source.

T4rk1n added 3 commits August 20, 2026 11:48
test_tdrp004_navigate_selected_cells read the derived-prop display cells
with one-shot find_element().get_attribute() while keystrokes were still
firing. props_container re-renders wholesale on every table-prop change,
so the element went stale between find and read, failing Table Group 1
consistently once selenium was unpinned.

Add a wait_prop() helper that re-finds the element each poll and waits for
the value to settle, and use it for the tab-navigation assertions.
@sonarqubecloud

Copy link
Copy Markdown

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.

1 participant