diff --git a/pythonlib/camoufox/utils.py b/pythonlib/camoufox/utils.py index 795a90d..88137bf 100644 --- a/pythonlib/camoufox/utils.py +++ b/pythonlib/camoufox/utils.py @@ -705,11 +705,11 @@ def launch_options( if not _user_set_navigator: fix_navigator_arch(config, target_os) if not _user_set_screen_window: - # Headful only: this bound exists so the real window fits the monitor. - # headless has no window to overflow, and headless='virtual' runs a 1x1 - # Xvfb (see virtdisplay.py) that would otherwise shrink the whole - # fingerprint to 1x1. - if headless is False and screen_cons: + # Headful on a real monitor only: this bound exists so the window fits + # the screen it is drawn on. headless has no window to overflow, and + # headless='virtual' reaches here as headless=False (see async_api) with + # a 1x1 Xvfb (virtdisplay.py) that is not a real screen. + if headless is False and not virtual_display and screen_cons: clamp_screen_to_display(config, screen_cons.max_width, screen_cons.max_height) fix_screen_no_taskbar(config, target_os) clamp_window_dimensions(config) diff --git a/pythonlib/tests/test_launch_geometry.py b/pythonlib/tests/test_launch_geometry.py new file mode 100644 index 0000000..25bec27 --- /dev/null +++ b/pythonlib/tests/test_launch_geometry.py @@ -0,0 +1,70 @@ +""" +Launch-level guards for the display clamp in launch_options(). + +Run with: + cd pythonlib && python -m pytest tests/test_launch_geometry.py -v +""" + +import os +import sys +from contextlib import contextmanager +from unittest import mock + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +import orjson # noqa: E402 +from browserforge.fingerprints import Screen # noqa: E402 + +from camoufox import utils # noqa: E402 + +# What get_screen_cons() reports under the Xvfb that headless='virtual' starts: +# virtdisplay.py sizes it "1x1x24", and launch_options mutates os.environ's +# DISPLAY, so the parent process enumerates that stub as its only monitor. +XVFB_STUB = Screen(max_width=1, max_height=1) + + +@contextmanager +def host(screen_cons): + """Run launch_options() against a stubbed host, without touching the disk.""" + with mock.patch.object(utils, "get_screen_cons", lambda headless: screen_cons), ( + mock.patch.object(utils, "installed_verstr", lambda: "150.0.2") + ), mock.patch.object(utils, "launch_path", lambda **kwargs: "/nonexistent/camoufox"): + yield + + +def config_of(options): + """Reassemble the chunked CAMOU_CONFIG_ env vars into a dict.""" + env = options["env"] + chunks = sorted( + (int(k.rsplit("_", 1)[1]), v) for k, v in env.items() if k.startswith("CAMOU_CONFIG_") + ) + return orjson.loads("".join(chunk for _, chunk in chunks)) + + +def launch(**kwargs): + kwargs.setdefault("os", "windows") + kwargs.setdefault("i_know_what_im_doing", True) + return config_of(utils.launch_options(**kwargs)) + + +class TestVirtualDisplayIsNotAScreen: + """headless='virtual' arrives here as headless=False (async_api rewrites it), + so the headful gate alone would clamp the fingerprint to the 1x1 Xvfb.""" + + def test_fingerprint_is_not_shrunk_to_the_xvfb(self): + with host(XVFB_STUB): + config = launch(headless=False, virtual_display=":99") + + assert config["screen.width"] > 1 + assert config["screen.height"] > 1 + assert config["window.outerWidth"] > 1 + + def test_screen_dimensions_stay_valid(self): + """Clamping to 1x1 drives availHeight negative once fix_screen_no_taskbar + subtracts the taskbar, which validate_config rejects as a uint.""" + with host(XVFB_STUB): + config = launch(headless=False, virtual_display=":99") + + for key, value in config.items(): + if key.startswith("screen.") or key.startswith("window.outer"): + assert value >= 0, f"{key} is negative: {value}"