From 133119d77280c5cfe956f8fdf3e98f8f209d3379 Mon Sep 17 00:00:00 2001 From: rukouai <255437651+rukouai@users.noreply.github.com> Date: Tue, 14 Jul 2026 21:47:42 -0700 Subject: [PATCH] fix: preserve native mobile viewport insets --- aimlib/__init__.py | 61 ++++++++++++++++++++-------------------------- pyproject.toml | 2 +- tests/test_sdk.py | 41 ++++++++++--------------------- 3 files changed, 41 insertions(+), 63 deletions(-) diff --git a/aimlib/__init__.py b/aimlib/__init__.py index 6a77e3b..07b7147 100644 --- a/aimlib/__init__.py +++ b/aimlib/__init__.py @@ -297,34 +297,29 @@ def _validated_footprint_identity(value: object, applied_footprint: str) -> Opti } -def _device_metrics_override(identity: Optional[dict]) -> Optional[dict]: - """Convert physical display pixels into the target's portrait CSS viewport.""" +def _expected_device_metrics(identity: Optional[dict]) -> Optional[dict]: + """Convert the OS-applied physical display into expected portrait CSS metrics.""" if identity is None: return None ratio = identity["device_pixel_ratio"] css_width = math.ceil(identity["screen_width"] / ratio) css_height = math.ceil(identity["screen_height"] / ratio) return { - "width": css_width, - "height": css_height, - "deviceScaleFactor": ratio, - "mobile": True, "screenWidth": css_width, "screenHeight": css_height, - "positionX": 0, - "positionY": 0, - "screenOrientation": {"type": "portraitPrimary", "angle": 0}, + "devicePixelRatio": ratio, } -def _identity_verification(identity: Optional[dict], metrics: Optional[dict]) -> Optional[dict]: +def _identity_verification(identity: Optional[dict]) -> Optional[dict]: + metrics = _expected_device_metrics(identity) if identity is None or metrics is None: return None return { "model": identity["model"], "screenWidth": metrics["screenWidth"], "screenHeight": metrics["screenHeight"], - "devicePixelRatio": metrics["deviceScaleFactor"], + "devicePixelRatio": metrics["devicePixelRatio"], } @@ -373,10 +368,14 @@ def _identity_verification(identity: Optional[dict], metrics: Optional[dict]) -> const branded = data.brands.some(row => row.brand === 'Google Chrome') && high.fullVersionList.some(row => row.brand === 'Google Chrome'); if (!branded || !expected) return branded; + const headfulInsets = screen.availHeight < screen.height && + innerHeight < screen.availHeight && + visualViewport && visualViewport.height > 0 && visualViewport.height <= innerHeight; return high.model === expected.model && screen.width === expected.screenWidth && screen.height === expected.screenHeight && - Math.abs(devicePixelRatio - expected.devicePixelRatio) < 0.001; + Math.abs(devicePixelRatio - expected.devicePixelRatio) < 0.001 && + headfulInsets; }""" @@ -746,24 +745,23 @@ async def _apply_page_identity(self, page): identity, footprint_identity["model"] if footprint_identity else None, ) - metrics = _device_metrics_override(footprint_identity) - if override is not None or metrics is not None: + expected = _identity_verification(footprint_identity) + if override is not None: cdp_session = await page.context.new_cdp_session(page) - if override is not None: - await cdp_session.send("Emulation.setUserAgentOverride", override) - if metrics is not None: - await cdp_session.send("Emulation.setDeviceMetricsOverride", metrics) + await cdp_session.send("Emulation.setUserAgentOverride", override) + if override is not None or expected is not None: verified = await page.evaluate( _VERIFY_MANAGED_BROWSER_IDENTITY, - _identity_verification(footprint_identity, metrics), + expected, ) # Internal/about:blank startup pages cannot expose UA-CH. The override has already # been validated on the secure bootstrap tab below; a definitive false result on a # page that does expose UA-CH still fails closed. if verified is False: raise BrowserPolicyError("managed browser identity did not apply") - self._identity_cdp_sessions.append(cdp_session) - cdp_session = None + if cdp_session is not None: + self._identity_cdp_sessions.append(cdp_session) + cdp_session = None self._identity_pages.add(page_key) except BrowserPolicyError: raise @@ -796,24 +794,19 @@ async def _native_identity_for_page(self, page, footprint_identity: Optional[dic identity, footprint_identity["model"] if footprint_identity else None, ) - metrics = _device_metrics_override(footprint_identity) - if override is not None or metrics is not None: + expected = _identity_verification(footprint_identity) + if override is not None: bootstrap_cdp_session = await page.context.new_cdp_session( bootstrap_page ) - if override is not None: - await bootstrap_cdp_session.send( - "Emulation.setUserAgentOverride", - override, - ) - if metrics is not None: - await bootstrap_cdp_session.send( - "Emulation.setDeviceMetricsOverride", - metrics, - ) + await bootstrap_cdp_session.send( + "Emulation.setUserAgentOverride", + override, + ) + if override is not None or expected is not None: verified = await bootstrap_page.evaluate( _VERIFY_MANAGED_BROWSER_IDENTITY, - _identity_verification(footprint_identity, metrics), + expected, ) if verified is not True: raise BrowserPolicyError( diff --git a/pyproject.toml b/pyproject.toml index 541b149..40cc608 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "aimlib" -version = "0.4.3" +version = "0.4.4" description = "Python SDK for aimlib mobile proxies and remote-browser sessions" readme = "README.md" requires-python = ">=3.10" diff --git a/tests/test_sdk.py b/tests/test_sdk.py index 006e8c6..391e1c6 100644 --- a/tests/test_sdk.py +++ b/tests/test_sdk.py @@ -18,11 +18,12 @@ _Operations, _apply_browser_driver_policy, _async_playwright, - _device_metrics_override, + _expected_device_metrics, _google_chrome_user_agent_override, _raise_for_typed, _ttl_seconds, _validated_footprint_identity, + _VERIFY_MANAGED_BROWSER_IDENTITY, ) @@ -222,7 +223,7 @@ def test_rewrites_model_even_when_google_chrome_is_already_branded(self): self.assertEqual(override["userAgentMetadata"]["model"], "Pixel 6 Pro") self.assertEqual(override["userAgent"], identity["userAgent"]) - def test_validates_public_identity_and_builds_pixel_6_pro_metrics(self): + def test_validates_public_identity_and_builds_pixel_6_pro_expectations(self): profile = _validated_footprint_identity( { "name": "pixel-6-pro", @@ -235,20 +236,19 @@ def test_validates_public_identity_and_builds_pixel_6_pro_metrics(self): ) self.assertEqual( - _device_metrics_override(profile), + _expected_device_metrics(profile), { - "width": 412, - "height": 892, - "deviceScaleFactor": 3.5, - "mobile": True, "screenWidth": 412, "screenHeight": 892, - "positionX": 0, - "positionY": 0, - "screenOrientation": {"type": "portraitPrimary", "angle": 0}, + "devicePixelRatio": 3.5, }, ) + def test_footprint_verification_requires_headful_browser_insets(self): + self.assertIn("screen.availHeight < screen.height", _VERIFY_MANAGED_BROWSER_IDENTITY) + self.assertIn("innerHeight < screen.availHeight", _VERIFY_MANAGED_BROWSER_IDENTITY) + self.assertIn("visualViewport.height <= innerHeight", _VERIFY_MANAGED_BROWSER_IDENTITY) + def test_rejects_missing_or_mismatched_selected_identity(self): for profile in ({}, {"name": "pixel-6a"}): with self.subTest(profile=profile), self.assertRaises(BrowserPolicyError): @@ -373,7 +373,7 @@ async def test_does_not_open_a_cdp_session_for_official_chrome(self): page.context.new_cdp_session.assert_not_awaited() - async def test_applies_selected_model_and_display_metrics_to_the_target(self): + async def test_applies_selected_model_and_validates_os_display_metrics(self): session = session_for(MagicMock()) session.applied_footprint = "pixel-6-pro" session.fingerprint = { @@ -394,28 +394,13 @@ async def test_applies_selected_model_and_display_metrics_to_the_target(self): await session._apply_page_identity(page) - self.assertEqual(cdp_session.send.await_count, 2) - ua_call, metrics_call = cdp_session.send.await_args_list + self.assertEqual(cdp_session.send.await_count, 1) + ua_call = cdp_session.send.await_args self.assertEqual(ua_call.args[0], "Emulation.setUserAgentOverride") self.assertEqual( ua_call.args[1]["userAgentMetadata"]["model"], "Pixel 6 Pro", ) - self.assertEqual(metrics_call.args[0], "Emulation.setDeviceMetricsOverride") - self.assertEqual( - metrics_call.args[1], - { - "width": 412, - "height": 892, - "deviceScaleFactor": 3.5, - "mobile": True, - "screenWidth": 412, - "screenHeight": 892, - "positionX": 0, - "positionY": 0, - "screenOrientation": {"type": "portraitPrimary", "angle": 0}, - }, - ) _, expected = page.evaluate.await_args.args self.assertEqual( expected,