Skip to content
Merged
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
24 changes: 22 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ jobs:
with:
python-version: ${{ matrix.python-version }}

- name: Install Chromium on Linux
if: runner.os == 'Linux'
id: setup-chromium
uses: browser-actions/setup-chrome@v2
with:
# "latest" is resolved from Chromium Snapshots, not stable Google Chrome.
chrome-version: latest
install-dependencies: true

- name: Install browsers on Linux
if: runner.os == 'Linux'
run: |
Expand All @@ -92,7 +101,8 @@ jobs:
sudo apt-get update
sudo apt-get -y --no-install-recommends install opera-stable

sudo apt-get install chromium-browser
sudo ln -sf "${{ steps.setup-chromium.outputs.chrome-path }}" /usr/local/bin/chromium
chromium --version

sudo curl -fsSLo /usr/share/keyrings/brave-browser-archive-keyring.gpg https://brave-browser-apt-release.s3.brave.com/brave-browser-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/brave-browser-archive-keyring.gpg] https://brave-browser-apt-release.s3.brave.com/ stable main"|sudo tee /etc/apt/sources.list.d/brave-browser-release.list
Expand All @@ -112,7 +122,17 @@ jobs:
if: runner.os == 'Windows'
shell: powershell
run: |
choco install chromium opera brave googlechrome --no-progress -y --force
choco install chromium opera brave --no-progress -y --force

$chromePaths = @(
"$env:PROGRAMFILES\Google\Chrome\Application\chrome.exe",
"${env:PROGRAMFILES(X86)}\Google\Chrome\Application\chrome.exe",
"$env:LOCALAPPDATA\Google\Chrome\Application\chrome.exe"
)
$chromePath = $chromePaths | Where-Object { Test-Path $_ } | Select-Object -First 1
if (-not $chromePath) {
choco install googlechrome --no-progress -y --force
}

- name: Install browsers on MacOS
if: startsWith(runner.os, 'macOS')
Expand Down
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

---

## 4.1.2

### Fixes

- ChromeDriver: restored legacy ChromeDriver storage URL handling for Chrome/ChromeDriver 114 and older, fixing invalid Chrome for Testing download URLs for older versions such as ChromeDriver `102.0.5005.61` on Windows. (#736)
- ChromeDriver on 64-bit Windows: use the legacy `win32` archive name for ChromeDriver 114 and older while preserving `win64` Chrome for Testing downloads for ChromeDriver 115 and newer. (#736)
- Firefox/GeckoDriver: report a readable error when a GeckoDriver release does not contain an asset matching the requested OS type instead of failing with an ambiguous missing-list entry.

### Tests

- Added regression coverage for ChromeDriver `102.0.5005.61` URL construction, legacy latest-release lookup, and the `ChromeDriverManager.install()` download-manager path without live network calls. (#736)
- Pinned GeckoDriver cache coverage to a known release and added coverage for missing GeckoDriver release assets.

### CI

- Windows: install Google Chrome only when it is missing, while continuing to install Chromium, Opera, and Brave through Chocolatey.
- Linux: install Chromium through `browser-actions/setup-chrome` and expose it as `chromium`, avoiding the unavailable/unstable `apt` Chromium package path on GitHub Actions.

---

## 4.1.1

### Packaging
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "webdriver-manager"
version = "4.1.1"
version = "4.1.2"
description = "Library provides the way to automatically manage drivers for different browsers"
readme = "README.md"
requires-python = ">=3.7"
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 4.1.1
current_version = 4.1.2
commit = True
tag = True

Expand Down
23 changes: 16 additions & 7 deletions tests/helper.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import json

from webdriver_manager.core.os_manager import ChromeType
from webdriver_manager.drivers.chrome import ChromeDriver
from webdriver_manager.drivers.chrome import (
CHROME_FOR_TESTING_DOWNLOAD_URL,
CHROME_FOR_TESTING_LATEST_RELEASE_URL,
ChromeDriver,
)


class ResponseMock:
Expand Down Expand Up @@ -33,15 +37,20 @@ def get_browser_version_from_os(self, browser_type=None):
return self.browser_version


def chrome_driver_for(browser_version, responses, chrome_type=ChromeType.CHROMIUM):
def chrome_driver_for(
browser_version,
responses,
chrome_type=ChromeType.CHROMIUM,
driver_version=None,
url=CHROME_FOR_TESTING_DOWNLOAD_URL,
latest_release_url=CHROME_FOR_TESTING_LATEST_RELEASE_URL,
):
http_client = HttpClientMock(responses)
driver = ChromeDriver(
name="chromedriver",
driver_version=None,
url="https://storage.googleapis.com/chrome-for-testing-public/",
latest_release_url=(
"https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_STABLE"
),
driver_version=driver_version,
url=url,
latest_release_url=latest_release_url,
http_client=http_client,
os_system_manager=OperationSystemManagerMock(browser_version),
chrome_type=chrome_type,
Expand Down
131 changes: 129 additions & 2 deletions tests/test_chrome_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@

from webdriver_manager.core.driver_cache import DriverCacheManager
from webdriver_manager.core.os_manager import OperationSystemManager, ChromeType
from webdriver_manager.drivers.chrome import CHROME_FOR_TESTING_LATEST_PATCH_VERSIONS_PER_BUILD_URL, \
CHROME_FOR_TESTING_KNOWN_GOOD_VERSIONS_URL
from webdriver_manager.drivers.chrome import (
CHROMEDRIVER_STORAGE_LATEST_RELEASE_URL,
CHROME_FOR_TESTING_LATEST_PATCH_VERSIONS_PER_BUILD_URL,
CHROME_FOR_TESTING_KNOWN_GOOD_VERSIONS_URL,
)

os.environ.setdefault("WDM_LOCAL", "false")

Expand Down Expand Up @@ -156,6 +159,130 @@ def test_chrome_118_resolves_cft_driver_version_and_download_url():
assert CHROME_FOR_TESTING_KNOWN_GOOD_VERSIONS_URL in http_client.requested_urls


def test_chrome_102_uses_legacy_storage_url_and_win32_archive_for_win64():
driver, http_client = chrome_driver_for(
browser_version="102.0.5005.63",
driver_version="102.0.5005.61",
chrome_type=ChromeType.GOOGLE,
responses={},
)

assert driver.get_driver_download_url("win64") == (
"https://chromedriver.storage.googleapis.com/"
"102.0.5005.61/chromedriver_win32.zip"
)
assert http_client.requested_urls == []


def test_chrome_102_detected_version_uses_legacy_latest_release_url():
expected_url = f"{CHROMEDRIVER_STORAGE_LATEST_RELEASE_URL}_102.0.5005"
driver, http_client = chrome_driver_for(
browser_version="102.0.5005.63",
chrome_type=ChromeType.GOOGLE,
responses={
expected_url: "102.0.5005.61",
},
)

assert driver.get_latest_release_version() == "102.0.5005.61"
assert http_client.requested_urls == [expected_url]


def test_chrome_download_url_boundary_switches_from_legacy_to_cft():
legacy_driver, legacy_http_client = chrome_driver_for(
browser_version="114.0.5735.199",
driver_version="114.0.5735.90",
chrome_type=ChromeType.GOOGLE,
responses={},
)
cft_url = (
"https://storage.googleapis.com/chrome-for-testing-public/"
"115.0.5790.170/win64/chromedriver-win64.zip"
)
cft_driver, cft_http_client = chrome_driver_for(
browser_version="115.0.5790.99",
driver_version="115.0.5790.170",
chrome_type=ChromeType.GOOGLE,
responses={
CHROME_FOR_TESTING_KNOWN_GOOD_VERSIONS_URL: {
"versions": [
{
"version": "115.0.5790.170",
"downloads": {
"chromedriver": [
{"platform": "win64", "url": cft_url},
],
},
},
],
},
},
)

assert legacy_driver.get_driver_download_url("win64") == (
"https://chromedriver.storage.googleapis.com/"
"114.0.5735.90/chromedriver_win32.zip"
)
assert legacy_http_client.requested_urls == []
assert cft_driver.get_driver_download_url("win64") == cft_url
assert cft_http_client.requested_urls == [
CHROME_FOR_TESTING_KNOWN_GOOD_VERSIONS_URL,
]


def test_chrome_manager_downloads_legacy_chrome_102_url_for_win64(tmp_path):
class CacheManagerMock:
def find_driver(self, _driver):
return None

def get_driver_lock_path(self, _driver_name, _os_type):
return str(tmp_path / ".wdm-lock")

def save_file_to_cache(self, _driver, _file):
driver_path = tmp_path / "chromedriver.exe"
driver_path.write_text("")
return str(driver_path)

class DownloadManagerMock:
http_client = None

def __init__(self):
self.requested_urls = []

def download_file(self, url):
self.requested_urls.append(url)
return object()

class Windows64OSManagerMock:
def get_os_type(self):
return "win64"

def get_os_architecture(self):
return 64

def is_mac_os(self, _os_type):
return False

def get_browser_version_from_os(self, _browser_type=None):
return "102.0.5005.63"

download_manager = DownloadManagerMock()
manager = ChromeDriverManager(
driver_version="102.0.5005.61",
download_manager=download_manager,
cache_manager=CacheManagerMock(),
os_system_manager=Windows64OSManagerMock(),
)

driver_path = manager.install()

assert os.path.exists(driver_path)
assert download_manager.requested_urls == [
"https://chromedriver.storage.googleapis.com/"
"102.0.5005.61/chromedriver_win32.zip"
]


def test_chrome_115_plus_prefers_win64_download_when_available():
expected_url = (
"https://storage.googleapis.com/chrome-for-testing-public/"
Expand Down
11 changes: 9 additions & 2 deletions tests/test_firefox_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ def _cache_os_types_for_current_platform():
@pytest.mark.parametrize('os_type', _cache_os_types_for_current_platform())
@requires_gh_token
def test_can_get_driver_from_cache(os_type):
GeckoDriverManager(os_system_manager=OperationSystemManager(os_type)).install()
driver_path = GeckoDriverManager(os_system_manager=OperationSystemManager(os_type)).install()
driver_version = "v0.36.0"
GeckoDriverManager(
version=driver_version,
os_system_manager=OperationSystemManager(os_type),
).install()
driver_path = GeckoDriverManager(
version=driver_version,
os_system_manager=OperationSystemManager(os_type),
).install()
assert os.path.exists(driver_path)
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion webdriver_manager/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "4.1.1"
__version__ = "4.1.2"
10 changes: 7 additions & 3 deletions webdriver_manager/chrome.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@
from webdriver_manager.core.driver_cache import DriverCacheManager
from webdriver_manager.core.manager import DriverManager
from webdriver_manager.core.os_manager import OperationSystemManager, ChromeType
from webdriver_manager.drivers.chrome import ChromeDriver
from webdriver_manager.drivers.chrome import (
CHROME_FOR_TESTING_DOWNLOAD_URL,
CHROME_FOR_TESTING_LATEST_RELEASE_URL,
ChromeDriver,
)


class ChromeDriverManager(DriverManager):
def __init__(
self,
driver_version: Optional[str] = None,
name: str = "chromedriver",
url: str = "https://storage.googleapis.com/chrome-for-testing-public/",
latest_release_url: str = "https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_STABLE",
url: str = CHROME_FOR_TESTING_DOWNLOAD_URL,
latest_release_url: str = CHROME_FOR_TESTING_LATEST_RELEASE_URL,
chrome_type: str = ChromeType.GOOGLE,
download_manager: Optional[DownloadManager] = None,
cache_manager: Optional[DriverCacheManager] = None,
Expand Down
30 changes: 28 additions & 2 deletions webdriver_manager/drivers/chrome.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@
CHROME_FOR_TESTING_KNOWN_GOOD_VERSIONS_URL = (
"https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json"
)
CHROME_FOR_TESTING_DOWNLOAD_URL = "https://storage.googleapis.com/chrome-for-testing-public/"
CHROME_FOR_TESTING_LATEST_RELEASE_URL = (
"https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_STABLE"
)
CHROMEDRIVER_STORAGE_URL = "https://chromedriver.storage.googleapis.com"
CHROMEDRIVER_STORAGE_LATEST_RELEASE_URL = (
f"{CHROMEDRIVER_STORAGE_URL}/LATEST_RELEASE"
)


class ChromeDriver(Driver):
Expand Down Expand Up @@ -59,7 +67,13 @@ def get_driver_download_url(self, os_type):
log(f"Modern chrome version {modern_version_url}")
return modern_version_url

return f"{self._url}/{driver_version_to_download}/{self.get_name()}_{os_type}.zip"
if os_type == "win64":
os_type = "win32"

return (
f"{self._legacy_url()}/{driver_version_to_download}/"
f"{self.get_name()}_{os_type}.zip"
)

def get_browser_type(self):
return self._browser_type
Expand All @@ -74,13 +88,25 @@ def get_latest_release_version(self):
elif determined_browser_version is not None:
# Remove the build version (the last segment) from determined_browser_version for version < 115
determined_browser_version = ".".join(determined_browser_version.split(".")[:3])
latest_release_url = f"{self._latest_release_url}_{determined_browser_version}"
latest_release_url = (
f"{self._legacy_latest_release_url()}_{determined_browser_version}"
)
else:
latest_release_url = self._latest_release_url

resp = self._http_client.get(url=latest_release_url)
return resp.text.rstrip()

def _legacy_url(self):
if self._url.rstrip("/") == CHROME_FOR_TESTING_DOWNLOAD_URL.rstrip("/"):
return CHROMEDRIVER_STORAGE_URL
return self._url.rstrip("/")

def _legacy_latest_release_url(self):
if self._latest_release_url == CHROME_FOR_TESTING_LATEST_RELEASE_URL:
return CHROMEDRIVER_STORAGE_LATEST_RELEASE_URL
return self._latest_release_url

def _latest_cft_version_for_browser_version(self, browser_version):
browser_build_version = ".".join(browser_version.split(".")[:3])
browser_milestone = browser_version.split(".")[0]
Expand Down
5 changes: 5 additions & 0 deletions webdriver_manager/drivers/firefox.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ def get_driver_download_url(self, os_type):
name = f"{self.get_name()}-{driver_version_to_download}-{os_type}."
output_dict = [
asset for asset in assets if asset["name"].startswith(name)]
if not output_dict:
available_assets = ", ".join(asset.get("name", "") for asset in assets)
raise ValueError(
f"Could not find GeckoDriver asset for '{name}'. Available assets: {available_assets}"
)
return output_dict[0]["browser_download_url"]

@property
Expand Down
Loading