Skip to content
Closed
3 changes: 2 additions & 1 deletion .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
104 changes: 63 additions & 41 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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(
Expand All @@ -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()
7 changes: 7 additions & 0 deletions tests/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

import requestium.requestium

from .conftest import validate_session


@pytest.mark.parametrize(
"headless",
Expand All @@ -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")

Expand All @@ -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")

Expand All @@ -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")

Expand All @@ -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")

Expand All @@ -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")

Expand Down
Loading