Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
86d4fe0
migrate circleci to github workflow
T4rk1n Mar 11, 2026
becf1c6
sync package-lock.json's
T4rk1n Mar 11, 2026
b86c2c6
fix linting and testing
T4rk1n Mar 11, 2026
c065cc7
fix windows
T4rk1n Mar 11, 2026
02a0d0f
lint fix
T4rk1n Mar 11, 2026
3e15f71
fix test iframe
T4rk1n Mar 11, 2026
0bdda92
ci fixes
T4rk1n Mar 11, 2026
588d6a3
fix linting path
T4rk1n Mar 12, 2026
5359af8
fix table focus tests.
T4rk1n Mar 12, 2026
7a3c1ab
fix linting path
T4rk1n Mar 12, 2026
1914764
fix table percy finalize
T4rk1n Mar 12, 2026
173f4ac
add consolidated test report
T4rk1n Mar 12, 2026
b50ccf3
try fix background timeout
T4rk1n Mar 12, 2026
04b2cb5
fix report
T4rk1n Mar 12, 2026
d5432ed
add test durations for smarter splitting
T4rk1n Mar 12, 2026
601c8ba
fix chrome download for modern chrome
T4rk1n Mar 12, 2026
57c6495
lint-fix
T4rk1n Mar 12, 2026
f5e6461
lint fix
T4rk1n Mar 12, 2026
a0e2130
skip bad sizing tests
T4rk1n Mar 12, 2026
bd1bb13
improved grrs001 timing
T4rk1n Mar 12, 2026
45c4717
use same browser for all tests during session
T4rk1n Mar 12, 2026
4add6c6
lint fix
T4rk1n Mar 12, 2026
847b46f
fix tests or run fresh browser
T4rk1n Mar 12, 2026
d566263
fixes
T4rk1n Mar 13, 2026
f214b90
more wait for finished background callback test
T4rk1n Mar 19, 2026
6c7802e
more fresh browser for dcc tests
T4rk1n Mar 19, 2026
08994ff
more fresh browsers
T4rk1n Mar 19, 2026
c2200d6
more fresh browsers
T4rk1n Mar 19, 2026
37c5a62
report only failed test in summary
T4rk1n Mar 19, 2026
b103240
clear logs after tests
T4rk1n Mar 19, 2026
bf3f1f7
clear log
T4rk1n Mar 19, 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
507 changes: 0 additions & 507 deletions .circleci/config.yml

This file was deleted.

770 changes: 659 additions & 111 deletions .github/workflows/testing.yml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
lts/iron
24
333 changes: 333 additions & 0 deletions .test_durations

Large diffs are not rendered by default.

428 changes: 428 additions & 0 deletions components/dash-core-components/.test_durations

Large diffs are not rendered by default.

690 changes: 368 additions & 322 deletions components/dash-core-components/package-lock.json

Large diffs are not rendered by default.

259 changes: 258 additions & 1 deletion components/dash-core-components/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,265 @@ def start_server(self, app, **kwargs):
self.server_url = self.server.url


class _ReusableDashCoreComponentsComposite(DashCoreComponentsMixin):
"""DCC composite that reuses an existing browser instance."""

def __init__(self, server, browser_instance):
self.server = server
self._browser_instance = browser_instance
self._driver = browser_instance._driver
self._browser = browser_instance._browser
self._headless = browser_instance._headless
self._wait_timeout = browser_instance._wait_timeout
self._percy_run = browser_instance._percy_run
self._percy_finalize = browser_instance._percy_finalize
self._pause = browser_instance._pause
self._wd_wait = browser_instance._wd_wait
self._download_path = browser_instance._download_path
self._last_ts = 0
self._url = ""
self._window_idx = 0

def __getattr__(self, name):
# Delegate any missing attributes/methods to the browser instance
return getattr(self._browser_instance, name)

@property
def driver(self):
return self._driver

@property
def wait_timeout(self):
return self._wait_timeout

def start_server(self, app, **kwargs):
"""start the local server with app"""
# Ensure browser is on blank page before starting new server
self._ensure_blank_page()
self.server(app, **kwargs)
self.server_url = self.server.url

def _ensure_blank_page(self):
"""Ensure browser is on a blank page with no stale content."""
try:
current_url = self.driver.current_url
if current_url != "about:blank":
self.driver.get("about:blank")
# Wait for blank page to fully load
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

WebDriverWait(self.driver, 2).until(EC.url_to_be("about:blank"))
except Exception:
pass

@property
def server_url(self):
return self._url

@server_url.setter
def server_url(self, value):
self._url = value
self.wait_for_page()

def wait_for_page(self, url=None, timeout=10):
from selenium.common.exceptions import (
TimeoutException,
StaleElementReferenceException,
)
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.common.by import By
from dash.testing.errors import DashAppLoadingError

target_url = self._url if url is None else url

# Navigate to the target URL
self.driver.get(target_url)

try:
# Wait for URL to match (handles redirects)
WebDriverWait(self.driver, timeout).until(
lambda d: target_url in d.current_url
)

# Wait for react entry point with staleness check
def fresh_react_entry(driver):
try:
elem = driver.find_element(By.CSS_SELECTOR, "#react-entry-point")
# Verify element is interactive (not stale)
_ = elem.is_displayed()
return elem
except StaleElementReferenceException:
return False

WebDriverWait(self.driver, timeout).until(fresh_react_entry)

except TimeoutException as exc:
raise DashAppLoadingError("Dash app failed to load") from exc

def wait_for_element_by_css_selector(self, selector, timeout=None):
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By

wait = WebDriverWait(self.driver, timeout or self._wait_timeout)
return wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, selector)))

def wait_for_element_by_id(self, element_id, timeout=None):
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By

wait = WebDriverWait(self.driver, timeout or self._wait_timeout)
return wait.until(EC.presence_of_element_located((By.ID, element_id)))

def find_element(self, selector, attribute="CSS_SELECTOR"):
from selenium.webdriver.common.by import By

return self.driver.find_element(getattr(By, attribute.upper()), selector)

def find_elements(self, selector, attribute="CSS_SELECTOR"):
from selenium.webdriver.common.by import By

return self.driver.find_elements(getattr(By, attribute.upper()), selector)

def wait_for_element(self, selector, timeout=None):
return self.wait_for_element_by_css_selector(selector, timeout)

def wait_for_text_to_equal(self, selector, text, timeout=None):
from dash.testing.wait import text_to_equal

return self._wait_for(
text_to_equal(selector, text, timeout or self._wait_timeout), timeout
)

def _wait_for(self, method, timeout):
from selenium.webdriver.support.wait import WebDriverWait
from selenium.common.exceptions import TimeoutException

wait = WebDriverWait(self.driver, timeout or self._wait_timeout)
try:
return wait.until(method)
except TimeoutException:
raise

def wait_for_style_to_equal(self, selector, style, val, timeout=None):
from dash.testing.wait import style_to_equal

return self._wait_for(style_to_equal(selector, style, val), timeout)

def percy_snapshot(
self, name="", wait_for_callbacks=False, convert_canvases=False, widths=None
):
# Delegate to browser instance's percy_snapshot
self._browser_instance.percy_snapshot(
name, wait_for_callbacks, convert_canvases, widths
)

def clear_input(self, elem_or_selector):
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains

elem = (
self.find_element(elem_or_selector)
if isinstance(elem_or_selector, str)
else elem_or_selector
)
(
ActionChains(self.driver)
.move_to_element(elem)
.pause(0.2)
.click(elem)
.send_keys(Keys.END)
.key_down(Keys.SHIFT)
.send_keys(Keys.HOME)
.key_up(Keys.SHIFT)
.send_keys(Keys.DELETE)
).perform()

def clear_storage(self):
self.driver.execute_script("window.localStorage.clear()")
self.driver.execute_script("window.sessionStorage.clear()")

def get_logs(self):
if self._browser == "chrome":
return [
entry
for entry in self.driver.get_log("browser")
if entry["timestamp"] > self._last_ts
]
return None

def _reset_browser_state(self):
"""Clear browser state between tests."""
try:
# Stop any running JavaScript
self.driver.execute_script("window.stop();")
except Exception:
pass

try:
self.driver.delete_all_cookies()
except Exception:
pass

try:
# Navigate to blank page
self.driver.get("about:blank")

# Wait for navigation to complete
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

WebDriverWait(self.driver, 2).until(EC.url_to_be("about:blank"))

# Clear storage
self.clear_storage()

# Reset timestamp for log filtering
self._last_ts = 0
except Exception:
pass

def __enter__(self):
self._reset_browser_state()
return self

def __exit__(self, exc_type, exc_val, traceback):
pass


@pytest.fixture(scope="session")
def _dcc_browser_session(request, tmp_path_factory):
"""Session-scoped browser instance for DCC tests."""
download_path = tmp_path_factory.mktemp("download")
browser = Browser(
browser=request.config.getoption("webdriver"),
remote=request.config.getoption("remote"),
remote_url=request.config.getoption("remote_url"),
headless=request.config.getoption("headless"),
options=request.config.hook.pytest_setup_options(),
download_path=str(download_path),
percy_assets_root=request.config.getoption("percy_assets"),
percy_finalize=request.config.getoption("nopercyfinalize"),
pause=request.config.getoption("pause"),
)
yield browser
browser.__exit__(None, None, None)


@pytest.fixture
def dash_dcc(request, dash_thread_server, _dcc_browser_session):
with _ReusableDashCoreComponentsComposite(
dash_thread_server,
browser_instance=_dcc_browser_session,
) as dc:
yield dc


@pytest.fixture
def dash_dcc(request, dash_thread_server, tmpdir):
def dash_dcc_fresh_browser(request, dash_thread_server, tmpdir):
"""DCC test fixture with a fresh browser instance (for tests that need isolation)."""
with DashCoreComponentsComposite(
dash_thread_server,
browser=request.config.getoption("webdriver"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ def test_a11y003_keyboard_navigation_arrows(dash_dcc):
assert dash_dcc.get_logs() == []


def test_a11y004_keyboard_navigation_home_end(dash_dcc):
def test_a11y004_keyboard_navigation_home_end(dash_dcc_fresh_browser):
dash_dcc = dash_dcc_fresh_browser
app = create_date_picker_app(
{
"date": "2021-01-15", # Friday, Jan 15, 2021
Expand Down Expand Up @@ -178,7 +179,8 @@ def test_a11y004_keyboard_navigation_home_end(dash_dcc):
assert dash_dcc.get_logs() == []


def test_a11y005_keyboard_navigation_home_end_monday_start(dash_dcc):
def test_a11y005_keyboard_navigation_home_end_monday_start(dash_dcc_fresh_browser):
dash_dcc = dash_dcc_fresh_browser
app = create_date_picker_app(
{
"date": "2021-01-15", # Friday, Jan 15, 2021
Expand All @@ -205,7 +207,8 @@ def test_a11y005_keyboard_navigation_home_end_monday_start(dash_dcc):
assert dash_dcc.get_logs() == []


def test_a11y006_keyboard_navigation_rtl(dash_dcc):
def test_a11y006_keyboard_navigation_rtl(dash_dcc_fresh_browser):
dash_dcc = dash_dcc_fresh_browser
app = create_date_picker_app(
{
"date": "2021-01-15",
Expand Down Expand Up @@ -367,7 +370,8 @@ def test_a11y008_all_keyboard_keys_respect_disabled_days(dash_dcc):
assert dash_dcc.get_logs() == []


def test_a11y009_keyboard_space_selects_date(dash_dcc):
def test_a11y009_keyboard_space_selects_date(dash_dcc_fresh_browser):
dash_dcc = dash_dcc_fresh_browser
app = create_date_picker_app(
{
"date": "2021-01-15",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@


@pytest.mark.DCC594
def test_cdpr001_date_clearable_true_works(dash_dcc):

def test_cdpr001_date_clearable_true_works(dash_dcc_fresh_browser):
dash_dcc = dash_dcc_fresh_browser
app = Dash(__name__)
app.layout = html.Div(
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,13 @@ def test_dppt000_datepicker_single_default(dash_dcc):
assert dash_dcc.get_logs() == []


def test_dppt001_datepicker_single_with_portal(dash_dcc):
def test_dppt001_datepicker_single_with_portal(dash_dcc_fresh_browser):
"""Test DatePickerSingle with with_portal=True.

Verifies that the calendar opens in a portal (document.body) and all
elements are clickable.
elements are clickable. Uses fresh browser to avoid state bleeding.
"""
dash_dcc = dash_dcc_fresh_browser
app = Dash(__name__)

app.layout = html.Div(
Expand Down Expand Up @@ -113,12 +114,13 @@ def test_dppt001_datepicker_single_with_portal(dash_dcc):
assert dash_dcc.get_logs() == []


def test_dppt006_fullscreen_portal_close_button_keyboard(dash_dcc):
def test_dppt006_fullscreen_portal_close_button_keyboard(dash_dcc_fresh_browser):
"""Test fullscreen portal dismiss behavior and keyboard accessibility.

Verifies clicking background doesn't close the portal and close button
is keyboard-accessible.
is keyboard-accessible. Uses fresh browser to avoid state bleeding.
"""
dash_dcc = dash_dcc_fresh_browser
app = Dash(__name__)
app.layout = html.Div(
[
Expand Down Expand Up @@ -159,7 +161,8 @@ def test_dppt006_fullscreen_portal_close_button_keyboard(dash_dcc):
assert dash_dcc.get_logs() == []


def test_dppt007_portal_close_by_clicking_outside(dash_dcc):
def test_dppt007_portal_close_by_clicking_outside(dash_dcc_fresh_browser):
dash_dcc = dash_dcc_fresh_browser
"""Test regular portal closes when clicking outside the calendar."""
app = Dash(__name__)
app.layout = html.Div(
Expand Down Expand Up @@ -188,11 +191,12 @@ def test_dppt007_portal_close_by_clicking_outside(dash_dcc):
assert dash_dcc.get_logs() == []


def test_dppt001a_datepicker_range_default(dash_dcc):
def test_dppt001a_datepicker_range_default(dash_dcc_fresh_browser):
"""Test DatePickerRange with default (no portal) configuration.

Verifies that the calendar opens without portal and all elements are clickable.
"""
dash_dcc = dash_dcc_fresh_browser
app = Dash(__name__)

app.layout = html.Div(
Expand Down Expand Up @@ -359,12 +363,14 @@ def test_dppt004_datepicker_range_with_fullscreen_portal(dash_dcc):
click_everything_in_datepicker("#dpr-fullscreen", dash_dcc)


def test_dppt005_portal_has_correct_classes(dash_dcc):
def test_dppt005_portal_has_correct_classes(dash_dcc_fresh_browser):
"""Test that portal datepickers have the correct CSS classes.

Verifies that default datepickers don't have portal classes, while
with_portal=True datepickers have the portal class but not fullscreen class.
Uses fresh browser to avoid state bleeding.
"""
dash_dcc = dash_dcc_fresh_browser
app = Dash(__name__)

app.layout = html.Div(
Expand Down
Loading
Loading