From a5afa46cfa72c6ac5edcaf9cc1d3c0d12cab42ed Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Tue, 21 Jul 2026 21:06:55 +0530 Subject: [PATCH] Apply screen constraints on Windows and macOS get_screen_cons() was gated on DISPLAY being set, which only ever happens on Linux, so headful runs on Windows and macOS generated fingerprints with no monitor bound at all. Fixes #425 --- pythonlib/camoufox/display.py | 15 ++++++++- pythonlib/camoufox/utils.py | 4 +-- pythonlib/tests/test_display.py | 21 ++++++++++++ pythonlib/tests/test_launch_geometry.py | 43 +++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 3 deletions(-) diff --git a/pythonlib/camoufox/display.py b/pythonlib/camoufox/display.py index a4243af..0de6120 100644 --- a/pythonlib/camoufox/display.py +++ b/pythonlib/camoufox/display.py @@ -12,7 +12,7 @@ macOS (`NSScreen.frame`) and X11 (xrandr) already report CSS pixels, so scaling only ever applies on Windows. """ -from typing import Any, NamedTuple, Optional +from typing import Any, Mapping, NamedTuple, Optional from screeninfo import get_monitors @@ -33,6 +33,19 @@ class DisplaySize(NamedTuple): height: int +def has_display(env: Mapping[str, Any]) -> bool: + """ + Whether the host has a desktop session for Camoufox's window to open on. + + DISPLAY / WAYLAND_DISPLAY only ever exist on Linux, so they cannot be the + sole probe: keying off DISPLAY alone skipped the screen constraints entirely + on Windows and macOS, where a session is always present. + """ + if OS_NAME != 'lin': + return True + return bool(env.get('DISPLAY') or env.get('WAYLAND_DISPLAY')) + + def largest_display() -> Optional[DisplaySize]: """ Size of the roomiest attached monitor in CSS pixels, or None when the diff --git a/pythonlib/camoufox/utils.py b/pythonlib/camoufox/utils.py index 2f5f9ea..d87c73d 100644 --- a/pythonlib/camoufox/utils.py +++ b/pythonlib/camoufox/utils.py @@ -15,7 +15,7 @@ from typing_extensions import TypeAlias from ua_parser import user_agent_parser from .addons import DefaultAddons, add_default_addons, confirm_paths -from .display import largest_display +from .display import has_display, largest_display from .exceptions import ( InvalidOS, InvalidPropertyType, @@ -678,7 +678,7 @@ def launch_options( # Bound the geometry to the real display. BrowserForge only honours this when # its pool has a match, so it is re-applied after generation as well. - screen_cons = screen or get_screen_cons(headless or 'DISPLAY' in env) + screen_cons = screen or get_screen_cons(headless or has_display(env)) if not _used_preset and fingerprint is None: # Default: BrowserForge synthetic generation (infinite unique fingerprints) diff --git a/pythonlib/tests/test_display.py b/pythonlib/tests/test_display.py index 0729b87..febc701 100644 --- a/pythonlib/tests/test_display.py +++ b/pythonlib/tests/test_display.py @@ -90,3 +90,24 @@ class TestLargestDisplay: monkeypatch.setattr(display, "get_monitors", boom) assert display.largest_display() is None + + +class TestHasDisplay: + @pytest.mark.parametrize("os_name", ["win", "mac"]) + def test_always_present_off_linux(self, monkeypatch, os_name): + """Regression: keying off DISPLAY alone skipped Windows/macOS entirely.""" + monkeypatch.setattr(display, "OS_NAME", os_name) + assert display.has_display({}) is True + + @pytest.mark.parametrize( + ("env", "expected"), + [ + ({}, False), + ({"DISPLAY": ":0"}, True), + ({"WAYLAND_DISPLAY": "wayland-0"}, True), + ({"DISPLAY": ""}, False), + ], + ) + def test_linux_requires_a_session(self, monkeypatch, env, expected): + monkeypatch.setattr(display, "OS_NAME", "lin") + assert display.has_display(env) is expected diff --git a/pythonlib/tests/test_launch_geometry.py b/pythonlib/tests/test_launch_geometry.py index 25bec27..78ca2ea 100644 --- a/pythonlib/tests/test_launch_geometry.py +++ b/pythonlib/tests/test_launch_geometry.py @@ -13,6 +13,7 @@ from unittest import mock sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) import orjson # noqa: E402 +import pytest # noqa: E402 from browserforge.fingerprints import Screen # noqa: E402 from camoufox import utils # noqa: E402 @@ -68,3 +69,45 @@ class TestVirtualDisplayIsNotAScreen: for key, value in config.items(): if key.startswith("screen.") or key.startswith("window.outer"): assert value >= 0, f"{key} is negative: {value}" + + +# A 1920x1080 panel at 150% Windows scaling, in CSS pixels +SCALED_DISPLAY = Screen(max_width=1280, max_height=720) + +# Config key -> the display bound it must respect +BOUNDED_BY = { + "screen.width": "max_width", + "screen.height": "max_height", + "window.outerWidth": "max_width", + "window.outerHeight": "max_height", +} + + +class TestHeadfulFitsOnDisplay: + """BrowserForge drops a screen constraint it cannot satisfy, so assert the + outcome rather than trusting the constraint to have been honoured.""" + + @pytest.mark.parametrize("attempt", range(15)) + def test_generated_geometry_never_exceeds_the_display(self, attempt): + with host(SCALED_DISPLAY): + config = launch(headless=False) + + for key, bound in BOUNDED_BY.items(): + limit = getattr(SCALED_DISPLAY, bound) + assert config[key] <= limit, f"{key}={config[key]} exceeds {limit}" + + def test_geometry_stays_internally_consistent(self): + with host(SCALED_DISPLAY): + config = launch(headless=False) + + assert config["screen.availWidth"] <= config["screen.width"] + assert config["screen.availHeight"] < config["screen.height"] # taskbar + assert config["window.outerWidth"] <= config["screen.availWidth"] + assert config["window.outerHeight"] <= config["screen.availHeight"] + + def test_unprobeable_display_is_not_clamped(self): + """A host we cannot measure must not silently shrink the fingerprint.""" + with host(None), mock.patch.object(utils, "clamp_screen_to_display") as clamp: + launch(headless=False) + + clamp.assert_not_called()