mirror of
https://github.com/daijro/camoufox.git
synced 2026-09-09 08:01:15 +00:00
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
This commit is contained in:
committed by
Jake Writer
parent
22c6ffbdda
commit
a5afa46cfa
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user