Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .github/workflows/acceptance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:
TEST_GROUP_STRATEGY: roundrobin
PYTHONHASHSEED: '0'
XDIST_PER_WORKER_SNUBA: '1'
XDIST_WORKERS: '2'
XDIST_WORKERS: '3'

steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
Expand Down
63 changes: 53 additions & 10 deletions src/sentry/testutils/pytest/selenium.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import pytest
from selenium import webdriver
from selenium.common.exceptions import (
NoAlertPresentException,
NoSuchElementException,
SessionNotCreatedException,
WebDriverException,
Expand All @@ -31,6 +32,11 @@
logger = logging.getLogger("sentry.testutils")


# Selenium's default of 0.5s adds up to half a second to every wait that
# isn't already satisfied, and acceptance tests wait many times each.
WAIT_POLL_SECONDS = 0.1


class Browser:
def __init__(self, driver, live_server):
self.driver = driver
Expand Down Expand Up @@ -214,7 +220,7 @@ def wait_until_clickable(self, selector=None, xpath=None, timeout=10):
Waits until ``selector`` is visible and enabled to be clicked, or until ``timeout``
is hit, whichever happens first.
"""
wait = WebDriverWait(self.driver, timeout)
wait = WebDriverWait(self.driver, timeout, poll_frequency=WAIT_POLL_SECONDS)
if selector:
wait.until(expected_conditions.element_to_be_clickable((By.CSS_SELECTOR, selector)))
elif xpath:
Expand All @@ -229,7 +235,7 @@ def wait_until(self, selector=None, xpath=None, title=None, timeout=10):
Waits until ``selector`` is found in the browser, or until ``timeout``
is hit, whichever happens first.
"""
wait = WebDriverWait(self.driver, timeout)
wait = WebDriverWait(self.driver, timeout, poll_frequency=WAIT_POLL_SECONDS)
if selector:
wait.until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, selector)))
elif xpath:
Expand All @@ -249,7 +255,7 @@ def wait_until_not(self, selector=None, xpath=None, title=None, timeout=10):
Waits until ``selector`` is NOT found in the browser, or until
``timeout`` is hit, whichever happens first.
"""
wait = WebDriverWait(self.driver, timeout)
wait = WebDriverWait(self.driver, timeout, poll_frequency=WAIT_POLL_SECONDS)
if selector:
wait.until_not(
expected_conditions.presence_of_element_located((By.CSS_SELECTOR, selector))
Expand All @@ -268,7 +274,7 @@ def wait_until_script_execution(self, script, timeout=10):
Waits until ``script`` executes and evaluates truthy,
or until ``timeout`` is hit, whichever happens first.
"""
wait = WebDriverWait(self.driver, timeout)
wait = WebDriverWait(self.driver, timeout, poll_frequency=WAIT_POLL_SECONDS)
wait.until(lambda driver: driver.execute_script(script))

return self
Expand Down Expand Up @@ -499,12 +505,43 @@ def _take_prelaunched_chrome() -> webdriver.Chrome | None:
def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None:
# A worker whose tests never use the browser still has to close the
# prelaunched Chrome.
driver = _take_prelaunched_chrome()
if driver is not None:
for driver in (_take_prelaunched_chrome(), _reusable_chrome.pop("driver", None)):
if driver is not None:
try:
driver.quit()
except Exception:
pass


# Starting Chrome for every test costs the launch, and each test then loads the
# app bundle into a cold cache. One Chrome per worker, reset between tests,
# keeps the cache warm.
_reusable_chrome: dict[str, webdriver.Chrome] = {}


def _reset_chrome(driver: webdriver.Chrome, origin: str) -> bool:
"""Clears what a test can leave behind. Returns False if Chrome is unusable."""
try:
try:
driver.quit()
except Exception:
driver.switch_to.alert.dismiss()
except NoAlertPresentException:
pass
handles = driver.window_handles
for handle in handles[1:]:
driver.switch_to.window(handle)
driver.close()
driver.switch_to.window(handles[0])
driver.get("about:blank")
driver.execute_cdp_cmd("Network.clearBrowserCookies", {})
driver.execute_cdp_cmd(
"Storage.clearDataForOrigin", {"origin": origin, "storageTypes": "all"}
)
driver.execute_cdp_cmd("Emulation.setTouchEmulationEnabled", {"enabled": False})
driver.execute_cdp_cmd("Emulation.clearDeviceMetricsOverride", {})
except Exception:
logger.warning("selenium.reset-chrome.failed", exc_info=True)
return False
return True


@pytest.fixture(scope="function")
Expand All @@ -515,7 +552,8 @@ def browser(request, live_server):
driver_type = request.config.getoption("selenium_driver")
driver: WebDriver
if driver_type == "chrome":
driver = _take_prelaunched_chrome() or _launch_chrome(request.config)
reused = _reusable_chrome.pop("driver", None)
driver = reused or _take_prelaunched_chrome() or _launch_chrome(request.config)
elif driver_type == "firefox":
driver = webdriver.Firefox()
else:
Expand Down Expand Up @@ -550,6 +588,9 @@ def browser(request, live_server):
sys.stderr.write("[browser console] ")
sys.stderr.write(repr(entry))
sys.stderr.write("\n")
if isinstance(driver, webdriver.Chrome) and _reset_chrome(driver, live_server.url):
_reusable_chrome["driver"] = driver
return
# Teardown Selenium.
try:
driver.quit()
Expand All @@ -564,7 +605,9 @@ def pytest_runtest_makereport(item, call):
summary: MutableSequence[str] = []
extra = getattr(report, "extra", [])
driver = getattr(item, "_driver", None)
if driver is not None:
# Each phase of every test used to take a screenshot and dump the page
# source. Only a failure needs them.
if driver is not None and report.failed:
_gather_url(item, report, driver, summary, extra)
_gather_screenshot(item, report, driver, summary, extra)
_gather_html(item, report, driver, summary, extra)
Expand Down
Loading