fix: improve Windows browser autodetection paths and diagnose fallback (#816)

## ℹ️ Description
This pull request fixes Windows browser auto-detection failures reported
by users where `diagnose`/startup could not find an installed browser
even when Chrome or Edge were present in standard locations. It also
makes diagnostics resilient when auto-detection fails by avoiding an
assertion-driven abort and continuing with a clear failure log.

- Link to the related issue(s): Issue #815
- Describe the motivation and context for this change.
- Users reported `Installed browser could not be detected` on Windows
despite having a browser installed.
- The previous Windows candidate list used a mix of incomplete paths and
direct `os.environ[...]` lookups that could raise when variables were
missing.
- The updated path candidates and ordering were aligned with common
Windows install locations used by Playwright’s channel/executable
resolution logic (Chrome/Edge under `LOCALAPPDATA`, `PROGRAMFILES`, and
`PROGRAMFILES(X86)`).

## 📋 Changes Summary
- Expanded Windows browser path candidates in `get_compatible_browser()`
to include common Google Chrome and Microsoft Edge install paths, while
keeping Chromium and PATH fallbacks.
- Replaced unsafe direct env-var indexing with safe retrieval
(`os.environ.get(...)`) and added a fallback derivation for
`LOCALAPPDATA` via `USERPROFILE\\AppData\\Local` when needed.
- Kept legacy Chrome path candidates
(`...\\Chrome\\Application\\chrome.exe`) as compatibility fallback.
- Updated diagnostics flow to catch browser auto-detection assertion
failures and continue with `(fail) No compatible browser found` instead
of crashing.
- Added/updated unit tests to verify:
  - Windows detection for LocalAppData Chrome/Edge/Chromium paths.
- Missing Windows env vars no longer cause key lookup failures and still
surface the intended final detection assertion.
- `diagnose_browser_issues()` handles auto-detection assertion failures
without raising and logs the expected failure message.


### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [x] 🐞 Bug fix (non-breaking change which fixes an issue)


##  Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Hardened Windows browser auto-detection: checks additional common
installation locations for Chrome/Chromium/Edge and treats detection
failures as non-fatal, allowing diagnostics to continue with fallback
behavior and debug logging when no browser is found.

* **Tests**
* Expanded Windows detection tests to cover more path scenarios and
added cases verifying failure-mode diagnostics and logging.

* **Style**
  * Minor formatting tweak in default configuration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Jens
2026-02-09 19:55:05 +01:00
committed by GitHub
parent 7ae5f3122a
commit c212113638
3 changed files with 62 additions and 15 deletions

View File

@@ -582,7 +582,8 @@ class KleinanzeigenBot(WebScrapingMixin): # noqa: PLR0904
self.config_file_path,
default_config,
header = ("# yaml-language-server: $schema=https://raw.githubusercontent.com/Second-Hand-Friends/kleinanzeigen-bot/main/schemas/config.schema.json"),
exclude = {"ad_defaults": {"description"}},
exclude = {
"ad_defaults": {"description"}},
)
def load_config(self) -> None:

View File

@@ -4,7 +4,7 @@
import asyncio, enum, inspect, json, os, platform, secrets, shutil, subprocess, urllib.request # isort: skip # noqa: S404
from collections.abc import Awaitable, Callable, Coroutine, Iterable
from gettext import gettext as _
from pathlib import Path
from pathlib import Path, PureWindowsPath
from typing import Any, Final, Optional, cast
try:
@@ -441,7 +441,11 @@ class WebScrapingMixin:
else:
LOG.error("(fail) Browser binary not found: %s", self.browser_config.binary_location)
else:
browser_path = self.get_compatible_browser()
try:
browser_path = self.get_compatible_browser()
except AssertionError as exc:
LOG.debug("Browser auto-detection failed: %s", exc)
browser_path = None
if browser_path:
LOG.info("(ok) Auto-detected browser: %s", browser_path)
# Set the binary location for Chrome version detection
@@ -579,15 +583,31 @@ class WebScrapingMixin:
]
case "Windows":
def win_path(*parts:str) -> str:
return str(PureWindowsPath(*parts))
program_files = os.environ.get("PROGRAMFILES", "C:\\Program Files")
program_files_x86 = os.environ.get("PROGRAMFILES(X86)", "C:\\Program Files (x86)")
local_app_data = os.environ.get("LOCALAPPDATA")
if not local_app_data:
user_profile = os.environ.get("USERPROFILE")
if user_profile:
local_app_data = win_path(user_profile, "AppData", "Local")
browser_paths = [
os.environ.get("PROGRAMFILES", "C:\\Program Files") + r"\Microsoft\Edge\Application\msedge.exe",
os.environ.get("PROGRAMFILES(X86)", "C:\\Program Files (x86)") + r"\Microsoft\Edge\Application\msedge.exe",
os.environ["PROGRAMFILES"] + r"\Chromium\Application\chrome.exe",
os.environ["PROGRAMFILES(X86)"] + r"\Chromium\Application\chrome.exe",
os.environ["LOCALAPPDATA"] + r"\Chromium\Application\chrome.exe",
os.environ["PROGRAMFILES"] + r"\Chrome\Application\chrome.exe",
os.environ["PROGRAMFILES(X86)"] + r"\Chrome\Application\chrome.exe",
os.environ["LOCALAPPDATA"] + r"\Chrome\Application\chrome.exe",
win_path(local_app_data, "Google", "Chrome", "Application", "chrome.exe") if local_app_data else None,
win_path(program_files, "Google", "Chrome", "Application", "chrome.exe"),
win_path(program_files_x86, "Google", "Chrome", "Application", "chrome.exe"),
win_path(local_app_data, "Microsoft", "Edge", "Application", "msedge.exe") if local_app_data else None,
win_path(program_files, "Microsoft", "Edge", "Application", "msedge.exe"),
win_path(program_files_x86, "Microsoft", "Edge", "Application", "msedge.exe"),
win_path(local_app_data, "Chromium", "Application", "chrome.exe") if local_app_data else None,
win_path(program_files, "Chromium", "Application", "chrome.exe"),
win_path(program_files_x86, "Chromium", "Application", "chrome.exe"),
# Intentional fallback for portable/custom distributions installed under a bare "Chrome" directory.
win_path(program_files, "Chrome", "Application", "chrome.exe"),
win_path(program_files_x86, "Chrome", "Application", "chrome.exe"),
win_path(local_app_data, "Chrome", "Application", "chrome.exe") if local_app_data else None,
shutil.which("msedge.exe"),
shutil.which("chromium.exe"),
shutil.which("chrome.exe"),