Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e99dd68
Unpin selenium to fix CI flakiness against current Chrome
T4rk1n Aug 18, 2026
1845c32
Migrate testing helpers off selenium APIs removed in 4.3+
T4rk1n Aug 18, 2026
bec3549
Fix ActionChains offsets for selenium 4.3+ center-origin change
T4rk1n Aug 19, 2026
8722853
Read selenium TimeoutException message via .msg, not .args[0]
T4rk1n Aug 19, 2026
a5a474f
Stop Xvfb from hanging the Setup virtual display CI step
T4rk1n Aug 19, 2026
a96074b
Drop apt-get from Setup virtual display; Xvfb is preinstalled
T4rk1n Aug 19, 2026
28873e0
Fix flaky callback-count tests with deterministic keystroke gating
T4rk1n Aug 19, 2026
057eacb
Prevent hung tests from wedging CI: bound teardown join + per-test ti…
T4rk1n Aug 19, 2026
35129c2
update deps
T4rk1n Aug 20, 2026
9f5b6d6
extract setup-virtual-display
T4rk1n Aug 20, 2026
cf32d18
Fix stale-element flake in tdrp004 by polling derived-prop cells
T4rk1n Aug 20, 2026
7090bd8
Fix flaky grbs007 clickData race; mark dvcv003 flaky
T4rk1n Aug 20, 2026
3dbf7fb
Fix rdcap003 race: poll for option re-render after click
T4rk1n Aug 20, 2026
2cbf447
Fix races in msmh003 and msps001 DCC tests
T4rk1n Aug 20, 2026
43ca43b
Trigger clean CI run for Percy snapshot upload
T4rk1n Aug 20, 2026
89ffecd
Fix arb008 stale-element-on-click race
T4rk1n Aug 20, 2026
e7cad53
Update CHANGELOG.md
T4rk1n Aug 21, 2026
ee1f4e0
Fix races in dveh002 (click count) and msps001 (dropdown intercept)
T4rk1n Aug 21, 2026
d0b6cbf
Harden empt001 clear and msps001 dropdownsingle selection
T4rk1n Aug 21, 2026
8e86f96
Force-exit background/async CI steps after pytest to stop shutdown hangs
T4rk1n Aug 21, 2026
36e699f
Fix ddso002 dropped-keystroke race in dropdown keyboard nav
T4rk1n Aug 21, 2026
fe9a029
Revert ddso002 key-delivery change (mark flaky); gate rdmo002 keystrokes
T4rk1n Aug 21, 2026
b5287df
Fix rdps008 read-after-click race on delete-column toggle
T4rk1n Aug 21, 2026
918ba3c
CI stability check (build 1 of 2)
T4rk1n Aug 21, 2026
0141dc1
Mark dropdown a11y keyboard-nav tests flaky (focus-timing)
T4rk1n Aug 21, 2026
481649c
Skip occupied ports when starting the threaded test server
T4rk1n Aug 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/actions/setup-virtual-display/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: 'Setup virtual display'
description: 'Start an Xvfb virtual display and export DISPLAY for headed browser tests'

runs:
using: 'composite'
steps:
- name: Setup virtual display
shell: bash
run: |
sudo Xvfb :99 -ac -screen 0 1280x1024x24 >/dev/null 2>&1 &
disown
echo "DISPLAY=:99" >> $GITHUB_ENV
34 changes: 9 additions & 25 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,7 @@ jobs:
run: npm ci

- name: Setup virtual display
run: |
sudo apt-get update
sudo apt-get install -y xvfb
sudo Xvfb :99 -ac -screen 0 1280x1024x24 &
echo "DISPLAY=:99" >> $GITHUB_ENV
uses: ./.github/actions/setup-virtual-display

- name: Run lint
env:
Expand Down Expand Up @@ -271,6 +267,10 @@ jobs:
# Set REDIS_URL for your application/tests
# The service 'redis' will be available on localhost (or redis) at port 6379
REDIS_URL: redis://localhost:6379
# These suites can leave non-daemon workers alive after pytest finishes,
# hanging the step at interpreter shutdown; hard-exit once the session is
# done (see pytest_sessionfinish in tests/conftest.py).
DASH_TEST_FORCE_EXIT: "1"
steps:
- name: Checkout repository
uses: actions/checkout@v4
Expand Down Expand Up @@ -672,11 +672,7 @@ jobs:
chrome-version: stable

- name: Setup virtual display
run: |
sudo apt-get update
sudo apt-get install -y xvfb
sudo Xvfb :99 -ac -screen 0 1280x1024x24 &
echo "DISPLAY=:99" >> $GITHUB_ENV
uses: ./.github/actions/setup-virtual-display

- name: Build/Setup test components
run: npm run setup-tests.py
Expand Down Expand Up @@ -777,11 +773,7 @@ jobs:
chrome-version: stable

- name: Setup virtual display
run: |
sudo apt-get update
sudo apt-get install -y xvfb
sudo Xvfb :99 -ac -screen 0 1280x1024x24 &
echo "DISPLAY=:99" >> $GITHUB_ENV
uses: ./.github/actions/setup-virtual-display

- name: Install HTML components dependencies
working-directory: components/dash-html-components
Expand Down Expand Up @@ -974,11 +966,7 @@ jobs:
chrome-version: stable

- name: Setup virtual display
run: |
sudo apt-get update
sudo apt-get install -y xvfb
sudo Xvfb :99 -ac -screen 0 1280x1024x24 &
echo "DISPLAY=:99" >> $GITHUB_ENV
uses: ./.github/actions/setup-virtual-display

- name: Remove DCC Python package and run tests
run: |
Expand Down Expand Up @@ -1058,11 +1046,7 @@ jobs:
chrome-version: stable

- name: Setup virtual display
run: |
sudo apt-get update
sudo apt-get install -y xvfb
sudo Xvfb :99 -ac -screen 0 1280x1024x24 &
echo "DISPLAY=:99" >> $GITHUB_ENV
uses: ./.github/actions/setup-virtual-display

- name: Install Table test dependencies
working-directory: components/dash-table
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
- [#3646](https://github.com/plotly/dash/pull/3646) Remove React 16 support (`16.14.0` is no longer an accepted value for `REACT_VERSION` / `_set_react_version`).

### Fixed
- [#3955](https://github.com/plotly/dash/pull/3955) Unpin `selenium` in the testing requirements (was capped at `<=4.2.0`, from 2022) and require `>=4.11.0`. The old cap predated Selenium Manager, so the pinned selenium could not drive the current stable Chrome (151+) that CI installs, producing widespread `StaleElementReferenceException`/`TimeoutException` flakiness across the browser-based integration tests. Modern selenium auto-provisions a matching chromedriver, restoring stable CI runs.
- [#3941](https://github.com/plotly/dash/pull/3941) Fix the FastAPI and Quart backends opening a WebSocket connection on every page load, even for apps with no WebSocket callbacks. The renderer keyed the connection on the mere presence of WebSocket infrastructure (always advertised by these backends) rather than on whether it was needed. The socket now opens eagerly only when `websocket_callbacks=True`; with just per-callback `websocket=True` it opens lazily on the first such callback dispatch, and an app with no WebSocket callbacks never opens one. Fixes [#3939](https://github.com/plotly/dash/issues/3939).
- [#3916](https://github.com/plotly/dash/pull/3916) Fixed a regression where dragging multiple files into `dcc.Upload` would upload only the first file when `multiple=True`
- [#3922](https://github.com/plotly/dash/pull/3922) Fix `dcc.Input(type="number")` stepper behavior when only `min` is set.
Expand Down
46 changes: 23 additions & 23 deletions components/dash-core-components/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 12 additions & 4 deletions components/dash-core-components/tests/dash_core_components_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,18 @@ def click_and_hold_at_coord_fractions(self, elem_or_selector, fx, fy):
elem = self._get_element(elem_or_selector)

ActionChains(self.driver).move_to_element_with_offset(
elem, elem.size["width"] * fx, elem.size["height"] * fy
elem,
int(elem.size["width"] * (fx - 0.5)),
int(elem.size["height"] * (fy - 0.5)),
).click_and_hold().perform()

def move_to_coord_fractions(self, elem_or_selector, fx, fy):
elem = self._get_element(elem_or_selector)

ActionChains(self.driver).click_and_hold().move_to_element_with_offset(
elem, elem.size["width"] * fx, elem.size["height"] * fy
elem,
int(elem.size["width"] * (fx - 0.5)),
int(elem.size["height"] * (fy - 0.5)),
).perform()

def release(self):
Expand All @@ -120,7 +124,11 @@ def click_and_drag_at_coord_fractions(self, elem_or_selector, fx1, fy1, fx2, fy2
elem = self._get_element(elem_or_selector)

ActionChains(self.driver).move_to_element_with_offset(
elem, elem.size["width"] * fx1, elem.size["height"] * fy1
elem,
int(elem.size["width"] * (fx1 - 0.5)),
int(elem.size["height"] * (fy1 - 0.5)),
).click_and_hold().move_to_element_with_offset(
elem, elem.size["width"] * fx2, elem.size["height"] * fy2
elem,
int(elem.size["width"] * (fx2 - 0.5)),
int(elem.size["height"] * (fy2 - 0.5)),
).release().perform()
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest
from flaky import flaky
from dash import Dash, Input, Output
from dash.dcc import Dropdown
from dash.html import Div, Label, P, Span
Expand Down Expand Up @@ -260,6 +261,9 @@ def test_a11y005_selection_visibility_multi(dash_duo):
assert dash_duo.get_logs() == []


# ActionChains keyboard nav depends on the menu input having focus, which can
# lag menu-open under CI load and drop a keystroke; retry the test when it does.
@flaky(max_runs=3)
def test_a11y006_multi_select_keyboard_focus_retention(dash_duo):
def send_keys(key):
actions = ActionChains(dash_duo.driver)
Expand Down Expand Up @@ -396,6 +400,7 @@ def update_output(value):
assert dash_duo.get_logs() == []


@flaky(max_runs=3)
def test_a11y008_home_end_pageup_pagedown_navigation(dash_duo):
def send_keys(key):
actions = ActionChains(dash_duo.driver)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from dash import Dash, html, dcc, Input, Output
from flaky import flaky
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
from time import sleep
Expand Down Expand Up @@ -38,6 +39,10 @@ def test_ddso001_search_preserves_custom_order(dash_duo):
assert dash_duo.get_logs() == []


# Keyboard nav here goes through ActionChains (document.activeElement); the
# selection depends on the menu input having focus, which can lag menu-open under
# CI load and drop a keystroke. Retry the whole test when that happens.
@flaky(max_runs=3)
def test_ddso002_multi_search_preserves_custom_order(dash_duo):
def send_keys(key):
ActionChains(dash_duo.driver).send_keys(key).perform()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,16 @@ def handleClick(clickData):

dash_dcc.find_elements("g .xy")[0].click()

data = dash_dcc.wait_for_element("#test-text-area").get_attribute("value")
# The clickData callback also fires once on load with clickData=None (value
# "null"); wait for the click's real payload before parsing, otherwise we
# race that initial value and json.loads returns None.
wait.until(
lambda: (dash_dcc.find_element("#test-text-area").get_attribute("value") or "")
not in ("", "null"),
timeout=3,
)

data = dash_dcc.find_element("#test-text-area").get_attribute("value")
assert data != "", "graph clickData must contain data"

data = json.loads(data)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# -*- coding: utf-8 -*-

from selenium.webdriver.common.by import By

import dash.testing.wait as wait
from dash import Dash, Input, Output, dcc, html


Expand Down Expand Up @@ -71,11 +74,21 @@ def update_md(nclicks):

# a highlighted node will have <span> children which are what color the text
code = dash_dcc.wait_for_element("code[data-highlighted='yes']")
assert len(code.find_elements_by_tag_name("span")) == 2
assert len(code.find_elements(By.TAG_NAME, "span")) == 2

dash_dcc.find_element("#md-trigger").click()

code = dash_dcc.wait_for_element("code[data-highlighted='yes']")
assert len(code.find_elements_by_tag_name("span")) == 3
# The click swaps the markdown via a callback and the block is re-highlighted
# asynchronously; poll for the new span count instead of reading it right
# after the click, which races the re-render and sees the old content.
wait.until(
lambda: len(
dash_dcc.find_element("code[data-highlighted='yes']").find_elements(
By.TAG_NAME, "span"
)
)
== 3,
timeout=4,
)

assert dash_dcc.get_logs() == []
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,14 @@ def make_output(*args):

dash_dcc.find_element("#dropdownsingle").click()
dash_dcc.find_element(".dash-dropdown-content .dash-dropdown-search").send_keys(
"one" + Keys.ENTER
"one"
)
# Wait for the list to filter, then click the option to select it and close
# the menu. Pressing Enter did not reliably close the menu under load, and
# the lingering overlay then intercepted the next dropdown's click.
dash_dcc.wait_for_element(".dash-dropdown-content .dash-dropdown-option")
dash_dcc.find_element(".dash-dropdown-content .dash-dropdown-option").click()
dash_dcc.wait_for_no_elements(".dash-dropdown-content")

dash_dcc.find_element("#dropdownmulti").click()
dash_dcc.find_element(".dash-dropdown-content .dash-dropdown-search").send_keys(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,10 @@ def update_tooltip_content(hoverData):
elem = dash_dcc.find_element("#graph .nsewdrag")

with lock:
# hover on the center of the graph
# hover on the center of the graph (offset is measured from the
# element center in selenium >= 4.3, so 0, 0 is the center)
ActionChains(dash_dcc.driver).move_to_element_with_offset(
elem, elem.size["width"] / 2, elem.size["height"] / 2
elem, 0, 0
).click().perform()
dash_dcc.wait_for_text_to_equal("#graph-tooltip", loading_text)

Expand All @@ -83,7 +84,7 @@ def update_tooltip_content(hoverData):
elem = dash_dcc.find_element("#graph .nsewdrag")

ActionChains(dash_dcc.driver).move_to_element_with_offset(
elem, 5, elem.size["height"] - 5
elem, int(5 - elem.size["width"] / 2), int(elem.size["height"] / 2 - 5)
).perform()

until(lambda: not dash_dcc.find_element("#graph-tooltip").is_displayed(), 3)
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import time
from selenium.webdriver.common.by import By
from dash import Dash, dcc, html


Expand Down Expand Up @@ -41,16 +42,16 @@ def test_upca001_upload_children_gallery(dash_dcc):
time.sleep(0.5)
dash_dcc.percy_snapshot("upca001 children gallery")

first_child = dash_dcc.find_element("#upload").find_element_by_css_selector(
":first-child"
first_child = dash_dcc.find_element("#upload").find_element(
By.CSS_SELECTOR, ":first-child"
)
# Check that there is no default style since className is specified
style = first_child.get_attribute("style")
assert "opacity: 0.5" not in style

first_child = dash_dcc.find_element(
"#upload-no-className"
).find_element_by_css_selector(":first-child")
first_child = dash_dcc.find_element("#upload-no-className").find_element(
By.CSS_SELECTOR, ":first-child"
)

# Check that there is default style since no className is specified
style = first_child.get_attribute("style")
Expand Down
Loading
Loading