Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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.

767 changes: 656 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 Down Expand Up @@ -367,7 +369,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 @@ -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 @@ -359,12 +362,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
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
],
],
)
def test_cnfd001_dialog(dash_dcc, confirm_callback, confirms, components):
def test_cnfd001_dialog(dash_dcc_fresh_browser, confirm_callback, confirms, components):
dash_dcc = dash_dcc_fresh_browser
app = Dash(__name__)
app.layout = html.Div(components + [html.Div(id="confirmed")])

Expand Down Expand Up @@ -88,7 +89,8 @@ def on_confirmed(
assert dash_dcc.get_logs() == []


def test_cnfd002_injected_confirm(dash_dcc):
def test_cnfd002_injected_confirm(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 @@ -335,7 +335,9 @@ def update_output(value):
assert dash_duo.get_logs() == []


def test_a11y008_home_end_pageup_pagedown_navigation(dash_duo):
def test_a11y008_home_end_pageup_pagedown_navigation(dash_duo_fresh_browser):
dash_duo = dash_duo_fresh_browser

def send_keys(key):
actions = ActionChains(dash_duo.driver)
actions.send_keys(key)
Expand Down
Loading
Loading