diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 8e75963..403cd7d 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -62,9 +62,10 @@ jobs: - uses: GabrielBB/xvfb-action@5bcda06da84ba084708898801da79736b88e00a9 env: + SE_AUTO_UPDATE: true # opt-in to selenium 5 behavior COVERAGE_FILE: .coverage.${{ matrix.os }}.${{ matrix.python-version }}.${{ matrix.resolution }} with: - run: pytest + run: pytest -r A - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f with: diff --git a/tests/conftest.py b/tests/conftest.py index cfc66f9..dcd9b8b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,19 +3,18 @@ from typing import TYPE_CHECKING, cast import pytest +import urllib3.exceptions from _pytest.fixtures import FixtureRequest from selenium import webdriver from selenium.common import WebDriverException +from selenium.webdriver.support import expected_conditions as EC # noqa: N812 from selenium.webdriver.support.wait import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC import requestium if TYPE_CHECKING: from requestium.requestium_mixin import DriverMixin -# ruff: noqa FBT003 - @pytest.fixture(scope="module") def example_html() -> str: @@ -35,28 +34,58 @@ def example_html() -> str: """ -def _create_chrome_driver(headless: bool) -> webdriver.Chrome: +def _create_chrome_driver(*, headless: bool) -> webdriver.Chrome: options = webdriver.ChromeOptions() options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") if headless: options.add_argument("--headless=new") - driver = webdriver.Chrome(options=options) - WebDriverWait(driver, 5).until(EC.number_of_windows_to_be(1)) - return driver + + try: + driver = webdriver.Chrome(options=options) + WebDriverWait(driver, 5).until(EC.number_of_windows_to_be(1)) + return driver + except (urllib3.exceptions.ReadTimeoutError, TimeoutError, WebDriverException) as e: + error_msg = f"Chrome driver initialization failed: {e}" + raise RuntimeError(error_msg) from e -def _create_firefox_driver(headless: bool) -> webdriver.Firefox: +def _create_firefox_driver(*, headless: bool) -> webdriver.Firefox: options = webdriver.FirefoxOptions() - options.set_preference("browser.cache.disk.enable", False) - options.set_preference("browser.cache.memory.enable", False) - options.set_preference("browser.cache.offline.enable", False) - options.set_preference("network.http.use-cache", False) + options.set_preference("browser.cache.disk.enable", value=False) + options.set_preference("browser.cache.memory.enable", value=False) + options.set_preference("browser.cache.offline.enable", value=False) + options.set_preference("network.http.use-cache", value=False) if headless: options.add_argument("--headless") - driver = webdriver.Firefox(options=options) - WebDriverWait(driver, 5).until(EC.number_of_windows_to_be(1)) - return driver + + try: + driver = webdriver.Firefox(options=options) + WebDriverWait(driver, 5).until(EC.number_of_windows_to_be(1)) + return driver + except (urllib3.exceptions.ReadTimeoutError, TimeoutError, WebDriverException) as e: + error_msg = f"Firefox driver initialization failed: {e}" + raise RuntimeError(error_msg) from e + + +def validate_session(session: requestium.Session) -> None: + """ + Check basic validity of requestium Session object. + + If browser context is missing, try recovering. + If attempted recovery raises WebDriverException, skip test. + """ + try: + _ = session.driver.current_url + _ = session.driver.window_handles + except WebDriverException as e: + if "Browsing context has been discarded" not in str(e): + raise + + try: + session.driver.switch_to.new_window("tab") + except WebDriverException as e: + pytest.skip(f"Browser context discarded and cannot be recovered: {e!s}") @pytest.fixture( @@ -68,37 +97,30 @@ def session(request: FixtureRequest) -> Generator[requestium.Session, None, None browser, _, mode = driver_type.partition("-") headless = mode == "headless" - driver: webdriver.Chrome | webdriver.Firefox - if browser == "chrome": - driver = _create_chrome_driver(headless) - elif browser == "firefox": - driver = _create_firefox_driver(headless) - else: - msg = f"Unknown driver type: {browser}" - raise ValueError(msg) + driver: webdriver.Chrome | webdriver.Firefox | None = None try: + if browser == "chrome": + driver = _create_chrome_driver(headless=headless) + elif browser == "firefox": + driver = _create_firefox_driver(headless=headless) + else: + msg = f"Unknown driver type: {browser}" + raise ValueError(msg) + assert driver.name in browser session = requestium.Session(driver=cast("DriverMixin", driver)) assert session.driver.name in browser - - yield session - finally: - with contextlib.suppress(WebDriverException, OSError): - driver.quit() + validate_session(session) -@pytest.fixture(autouse=True) -def ensure_valid_session(session: requestium.Session) -> None: - """Skip test if browser context is discarded.""" - try: - _ = session.driver.current_url - _ = session.driver.window_handles - except WebDriverException as e: - if "Browsing context has been discarded" not in str(e): - raise + yield session - try: - session.driver.switch_to.new_window("tab") - except WebDriverException: - pytest.skip("Browser context discarded and cannot be recovered") + except RuntimeError as e: + # Driver creation failed - skip all tests using this session + pytest.skip(str(e)) + + finally: + if driver: + with contextlib.suppress(WebDriverException, OSError, Exception): + driver.quit() diff --git a/tests/test_session.py b/tests/test_session.py index d8985cf..a059c82 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -7,6 +7,8 @@ import requestium.requestium +from .conftest import validate_session + @pytest.mark.parametrize( "headless", @@ -19,6 +21,7 @@ ) def test_initialize_session_without_explicit_driver(example_html: str, headless: bool) -> None: # noqa: FBT001 session = requestium.Session(headless=headless) + validate_session(session) session.driver.get(f"data:text/html,{example_html}") session.driver.ensure_element(By.TAG_NAME, "h1") @@ -30,6 +33,7 @@ def test_initialize_session_without_explicit_driver(example_html: str, headless: def test_initialize_session_with_webdriver_options(example_html: str) -> None: session = requestium.Session(webdriver_options={"arguments": ["headless=new"]}) + validate_session(session) session.driver.get(f"data:text/html,{example_html}") session.driver.ensure_element(By.TAG_NAME, "h1") @@ -41,6 +45,7 @@ def test_initialize_session_with_webdriver_options(example_html: str) -> None: def test_initialize_session_with_experimental_options(example_html: str) -> None: session = requestium.Session(webdriver_options={"experimental_options": {"useAutomationExtension": False}}) + validate_session(session) session.driver.get(f"data:text/html,{example_html}") session.driver.ensure_element(By.TAG_NAME, "h1") @@ -52,6 +57,7 @@ def test_initialize_session_with_experimental_options(example_html: str) -> None def test_initialize_session_with_webdriver_prefs(example_html: str) -> None: session = requestium.Session(webdriver_options={"prefs": {"plugins.always_open_pdf_externally": True}}) + validate_session(session) session.driver.get(f"data:text/html,{example_html}") session.driver.ensure_element(By.TAG_NAME, "h1") @@ -64,6 +70,7 @@ def test_initialize_session_with_webdriver_prefs(example_html: str) -> None: def test_initialize_session_with_extension(example_html: str) -> None: test_extension_path = Path(__file__).parent / "resources/test_extension.crx" session = requestium.Session(webdriver_options={"extensions": [str(test_extension_path)]}) + validate_session(session) session.driver.get(f"data:text/html,{example_html}") session.driver.ensure_element(By.TAG_NAME, "h1")