test(cdp): expand multi-page isolation coverage

This commit is contained in:
ldm0
2026-09-01 00:12:24 +08:00
committed by Donough Liu
parent 2a10f8efca
commit 4e9dfd46c8
3 changed files with 901 additions and 0 deletions
+9
View File
@@ -1393,6 +1393,15 @@ class FixtureServer:
"userAgent": self.headers.get("User-Agent"),
"acceptLanguage": self.headers.get("Accept-Language"),
"extraHeader": self.headers.get("x-moli-profile-smoke"),
"sessionHeaderFirst": self.headers.get(
"x-moli-session-first"
),
"sessionHeaderSecond": self.headers.get(
"x-moli-session-second"
),
"sessionHeaderPeer": self.headers.get(
"x-moli-session-peer"
),
"referer": self.headers.get("Referer"),
}
self._send_html("<!doctype html><main>profile headers</main>")
@@ -12,6 +12,7 @@ from playwright.async_api import TimeoutError as PlaywrightTimeoutError
from ..assertions import SmokeError, assert_equal, record, wait_until
from ..helpers import attach_cdp_event_collector, run_worker_command
from .multi_page_contracts import run_multi_page_contracts
from .multi_page_isolation_contracts import run_multi_page_isolation_contracts
async def run_multi_page_group(
@@ -32,6 +33,7 @@ async def run_multi_page_group(
await _background_tasks_workers_and_screenshots(browser, fixture, results)
await _context_teardown_with_pending_commands(browser, fixture, results)
await run_multi_page_contracts(browser, fixture, results)
await run_multi_page_isolation_contracts(browser, fixture, results)
async def _page_churn_and_session_reattach(
@@ -0,0 +1,890 @@
from __future__ import annotations
import asyncio
import json
import urllib.request
from contextlib import suppress
from typing import Any, Awaitable
from ..assertions import SmokeError, assert_equal, record_contract, wait_until
from ..helpers import attach_cdp_event_collector
from ..progress import await_with_progress
async def run_multi_page_isolation_contracts(
browser: Any,
fixture: str,
results: list[dict[str, Any]],
) -> None:
"""Exercise shared-renderer boundaries that can silently cross Page owners."""
contracts = (
_busy_runtime_termination_is_target_local,
_stop_loading_is_target_local,
_same_target_session_policies_follow_chromium,
_activation_churn_preserves_target_ownership,
)
for contract in contracts:
await await_with_progress(
f"multi-page/{contract.__name__.removeprefix('_')}",
contract(browser, fixture, results),
timeout_seconds=20,
)
async def _busy_runtime_termination_is_target_local(
browser: Any,
fixture: str,
results: list[dict[str, Any]],
) -> None:
context = await browser.new_context()
sessions: list[Any] = []
busy: asyncio.Task[Any] | None = None
peer: asyncio.Task[Any] | None = None
try:
page_a, page_b = await asyncio.gather(context.new_page(), context.new_page())
await asyncio.gather(
page_a.goto(f"{fixture}/plain?multi-page-runtime=a", wait_until="load"),
page_b.goto(f"{fixture}/plain?multi-page-runtime=b", wait_until="load"),
)
session_a = await context.new_cdp_session(page_a)
session_b = await context.new_cdp_session(page_b)
sessions = [session_a, session_b]
await asyncio.to_thread(
_read_fixture_json,
f"{fixture}/inspector-routing-witness/reset",
)
busy = asyncio.create_task(
session_a.send(
"Runtime.evaluate",
{
"expression": """const witness = new XMLHttpRequest();
witness.open('GET', '/inspector-routing-witness/entered', false);
witness.send();
globalThis.__multiPageBusyEntered =
(globalThis.__multiPageBusyEntered || 0) + 1;
for (;;) {}""",
"returnByValue": True,
},
)
)
async def busy_loop_entered() -> bool:
try:
status = await asyncio.to_thread(
_read_fixture_json,
f"{fixture}/inspector-routing-witness/status",
)
except Exception:
return False
return status.get("enteredCount") == 1
await wait_until(
busy_loop_entered,
"target A non-yielding Runtime evaluation",
timeout_ms=5_000,
)
peer = asyncio.create_task(
session_b.send(
"Runtime.evaluate",
{
"expression": (
"globalThis.__multiPagePeerRan = "
"(globalThis.__multiPagePeerRan || 0) + 1"
),
"returnByValue": True,
},
)
)
await asyncio.sleep(0)
terminate = await asyncio.wait_for(
session_a.send("Runtime.terminateExecution"),
timeout=5,
)
assert_equal(terminate, {}, "target A Runtime.terminateExecution response")
busy_result, peer_result = await asyncio.wait_for(
asyncio.gather(busy, peer, return_exceptions=True),
timeout=5,
)
if not isinstance(busy_result, BaseException):
raise SmokeError(
"target A non-yielding evaluation was not terminated: "
f"{busy_result!r}"
)
if "terminated" not in str(busy_result).lower():
raise SmokeError(
"target A termination returned an unexpected error: "
f"{busy_result!r}"
)
if isinstance(peer_result, BaseException):
raise SmokeError(
"target B Runtime command was terminated with target A: "
f"{peer_result!r}"
)
assert_equal(
_runtime_value(peer_result),
1,
"target B queued Runtime command after target A termination",
)
recovery_a, recovery_b = await asyncio.gather(
session_a.send(
"Runtime.evaluate",
{
"expression": "globalThis.__multiPageBusyEntered",
"returnByValue": True,
},
),
session_b.send(
"Runtime.evaluate",
{
"expression": "globalThis.__multiPagePeerRan",
"returnByValue": True,
},
),
)
assert_equal(
[_runtime_value(recovery_a), _runtime_value(recovery_b)],
[1, 1],
"target-local Runtime recovery after termination",
)
record_contract(
results,
"multi_page_busy_runtime_termination_is_target_local",
contract=(
"Runtime.terminateExecution interrupts non-yielding JavaScript on its "
"Page target without terminating or misrouting a queued command on a peer; "
"both target sessions recover."
),
source="Chromium DevToolsSession executable oracle",
commands=["Runtime.evaluate x4", "Runtime.terminateExecution"],
observed={
"busyTargetTerminated": True,
"peerValue": _runtime_value(peer_result),
"recoveryValues": [
_runtime_value(recovery_a),
_runtime_value(recovery_b),
],
},
)
finally:
if busy is not None and not busy.done() and sessions:
with suppress(Exception):
await asyncio.wait_for(
sessions[0].send("Runtime.terminateExecution"),
timeout=2,
)
for task in (busy, peer):
if task is not None and not task.done():
task.cancel()
await asyncio.gather(
*(task for task in (busy, peer) if task is not None),
return_exceptions=True,
)
await asyncio.gather(
*(session.detach() for session in sessions),
return_exceptions=True,
)
await _close_context(context)
async def _stop_loading_is_target_local(
browser: Any,
fixture: str,
results: list[dict[str, Any]],
) -> None:
context = await browser.new_context()
sessions: list[Any] = []
paused_request_id: str | None = None
try:
page_a, page_b = await asyncio.gather(context.new_page(), context.new_page())
session_a = await context.new_cdp_session(page_a)
session_b = await context.new_cdp_session(page_b)
sessions = [session_a, session_b]
events_a = attach_cdp_event_collector(
session_a,
["Fetch.requestPaused", "Network.loadingFailed"],
)
await asyncio.gather(
session_a.send("Network.enable"),
session_b.send("Network.enable"),
session_a.send(
"Fetch.enable",
{
"patterns": [
{
"urlPattern": "*delayed-image.png*",
"requestStage": "Request",
}
]
},
),
)
response_a = await page_a.goto(
f"{fixture}/lifecycle-load-state?multi-page-stop=a",
wait_until="domcontentloaded",
timeout=5_000,
)
assert_equal(
response_a.status if response_a is not None else None,
200,
"target A DOMContentLoaded navigation status",
)
await wait_until(
lambda: any(
event.get("method") == "Fetch.requestPaused"
and "delayed-image.png"
in event.get("params", {}).get("request", {}).get("url", "")
for event in events_a
),
"target A paused image request",
timeout_ms=5_000,
)
paused = next(
event
for event in events_a
if event.get("method") == "Fetch.requestPaused"
and "delayed-image.png"
in event.get("params", {}).get("request", {}).get("url", "")
)
paused_request_id = paused.get("params", {}).get("requestId")
network_id = paused.get("params", {}).get("networkId")
if not isinstance(paused_request_id, str) or not isinstance(network_id, str):
raise SmokeError(f"paused image request had no CDP identifiers: {paused!r}")
before_stop = await page_a.evaluate(
"""() => ({
dcl: document.body.dataset.dcl,
load: document.body.dataset.load,
readyState: document.readyState,
})"""
)
assert_equal(
before_stop,
{"dcl": "1", "load": "0", "readyState": "interactive"},
"target A lifecycle before Page.stopLoading",
)
response_b = await page_b.goto(
f"{fixture}/lifecycle-load-state?multi-page-stop=b",
wait_until="load",
timeout=5_000,
)
assert_equal(
response_b.status if response_b is not None else None,
200,
"target B load while target A image is paused",
)
peer_state = await page_b.evaluate(
"""() => ({
load: document.body.dataset.load,
readyState: document.readyState,
})"""
)
assert_equal(
peer_state,
{"load": "1", "readyState": "complete"},
"target B lifecycle while target A is paused",
)
stop = await asyncio.wait_for(session_a.send("Page.stopLoading"), timeout=5)
assert_equal(stop, {}, "target A Page.stopLoading response")
await wait_until(
lambda: any(
event.get("method") == "Network.loadingFailed"
and event.get("params", {}).get("requestId") == network_id
for event in events_a
),
"target A canceled image request",
timeout_ms=5_000,
)
loading_failed = next(
event
for event in events_a
if event.get("method") == "Network.loadingFailed"
and event.get("params", {}).get("requestId") == network_id
)
assert_equal(
loading_failed.get("params", {}).get("canceled"),
True,
"target A stopped image request cancellation flag",
)
after_stop = await page_a.evaluate(
"""() => ({
dcl: document.body.dataset.dcl,
load: document.body.dataset.load,
readyState: document.readyState,
})"""
)
assert_equal(
after_stop,
{"dcl": "1", "load": "0", "readyState": "complete"},
"target A lifecycle after Page.stopLoading",
)
stale_interception_error = await _expect_protocol_error(
session_a.send(
"Fetch.continueRequest",
{"requestId": paused_request_id},
),
"stale Fetch interception after Page.stopLoading",
)
paused_request_id = None
await session_a.send("Fetch.disable")
await asyncio.gather(
page_a.goto(
f"{fixture}/plain?multi-page-stop=recovered-a",
wait_until="load",
),
page_b.goto(
f"{fixture}/plain?multi-page-stop=recovered-b",
wait_until="load",
),
)
assert_equal(
await asyncio.gather(page_a.text_content("main"), page_b.text_content("main")),
["plain ok", "plain ok"],
"both targets after target-local Page.stopLoading",
)
record_contract(
results,
"multi_page_stop_loading_is_target_local",
contract=(
"Page.stopLoading cancels only its target's paused subresource, retires the "
"Fetch interception id, preserves the committed Document, and does not "
"delay a peer target's load."
),
source="Chromium Page and Fetch executable oracle",
commands=[
"Fetch.enable",
"Fetch.requestPaused",
"Page.stopLoading",
"Network.loadingFailed",
"Fetch.continueRequest",
],
observed={
"beforeStop": before_stop,
"afterStop": after_stop,
"peerState": peer_state,
"staleInterceptionRejected": True,
"staleInterceptionError": stale_interception_error,
},
)
finally:
if sessions:
if paused_request_id is not None:
with suppress(Exception):
await sessions[0].send(
"Fetch.continueRequest",
{"requestId": paused_request_id},
)
with suppress(Exception):
await sessions[0].send("Fetch.disable")
await asyncio.gather(
*(session.detach() for session in sessions),
return_exceptions=True,
)
await _close_context(context)
async def _same_target_session_policies_follow_chromium(
browser: Any,
fixture: str,
results: list[dict[str, Any]],
) -> None:
context = await browser.new_context()
sessions: list[Any] = []
try:
owner, peer = await asyncio.gather(context.new_page(), context.new_page())
await asyncio.gather(
owner.goto(f"{fixture}/plain?multi-page-policy=owner", wait_until="load"),
peer.goto(f"{fixture}/plain?multi-page-policy=peer", wait_until="load"),
)
first = await context.new_cdp_session(owner)
second = await context.new_cdp_session(owner)
peer_session = await context.new_cdp_session(peer)
sessions = [first, second, peer_session]
await asyncio.gather(
first.send("Network.enable"),
second.send("Network.enable"),
peer_session.send("Network.enable"),
)
await second.send(
"Emulation.setUserAgentOverride",
{
"userAgent": "MultiPageAttachmentSecond/1.0",
"acceptLanguage": "ja-JP",
},
)
await first.send(
"Emulation.setUserAgentOverride",
{
"userAgent": "MultiPageFirstSetLast/1.0",
"acceptLanguage": "fr-FR",
},
)
await asyncio.gather(
first.send(
"Network.setExtraHTTPHeaders",
{"headers": {"x-moli-session-first": "first"}},
),
second.send(
"Network.setExtraHTTPHeaders",
{"headers": {"x-moli-session-second": "second"}},
),
peer_session.send(
"Network.setExtraHTTPHeaders",
{"headers": {"x-moli-session-peer": "peer"}},
),
)
await first.send("Emulation.setLocaleOverride", {"locale": "fr-FR"})
await first.send(
"Emulation.setTimezoneOverride",
{"timezoneId": "Europe/Paris"},
)
locale_error = await _expect_protocol_error(
second.send("Emulation.setLocaleOverride", {"locale": "ja-JP"}),
"second same-target locale claim",
)
timezone_error = await _expect_protocol_error(
second.send(
"Emulation.setTimezoneOverride",
{"timezoneId": "Asia/Tokyo"},
),
"second same-target timezone claim",
)
if "Another locale override is already in effect" not in locale_error:
raise SmokeError(f"unexpected locale claim error: {locale_error}")
if "Timezone override is already in effect" not in timezone_error:
raise SmokeError(f"unexpected timezone claim error: {timezone_error}")
owner_before_token = "multi-page-session-policy-before"
peer_before_token = "multi-page-session-policy-peer-before"
await asyncio.gather(
owner.goto(
f"{fixture}/profile-headers?token={owner_before_token}",
wait_until="load",
),
peer.goto(
f"{fixture}/profile-headers?token={peer_before_token}",
wait_until="load",
),
)
owner_wire_before, peer_wire_before = await asyncio.gather(
_read_fixture_profile(fixture, owner_before_token),
_read_fixture_profile(fixture, peer_before_token),
)
owner_runtime_before = await _runtime_identity(owner)
assert_equal(
{
"userAgent": owner_wire_before.get("userAgent"),
"acceptLanguage": owner_wire_before.get("acceptLanguage"),
"first": owner_wire_before.get("sessionHeaderFirst"),
"second": owner_wire_before.get("sessionHeaderSecond"),
"peer": owner_wire_before.get("sessionHeaderPeer"),
},
{
"userAgent": "MultiPageAttachmentSecond/1.0",
"acceptLanguage": "ja-JP",
"first": "first",
"second": "second",
"peer": None,
},
"same-target network policy aggregation",
)
assert_equal(
owner_runtime_before,
{
"userAgent": "MultiPageFirstSetLast/1.0",
"language": "fr-FR",
"timezone": "Europe/Paris",
},
"same-target live renderer policy before detach",
)
assert_equal(
{
"first": peer_wire_before.get("sessionHeaderFirst"),
"second": peer_wire_before.get("sessionHeaderSecond"),
"peer": peer_wire_before.get("sessionHeaderPeer"),
},
{"first": None, "second": None, "peer": "peer"},
"peer target network policy isolation",
)
await first.detach()
sessions.remove(first)
await second.send("Emulation.setLocaleOverride", {"locale": "ja-JP"})
await second.send(
"Emulation.setTimezoneOverride",
{"timezoneId": "Asia/Tokyo"},
)
owner_after_first_token = "multi-page-session-policy-after-first"
await owner.goto(
f"{fixture}/profile-headers?token={owner_after_first_token}",
wait_until="load",
)
owner_wire_after_first = await _read_fixture_profile(
fixture,
owner_after_first_token,
)
owner_runtime_after_first = await _runtime_identity(owner)
assert_equal(
{
"userAgent": owner_wire_after_first.get("userAgent"),
"first": owner_wire_after_first.get("sessionHeaderFirst"),
"second": owner_wire_after_first.get("sessionHeaderSecond"),
},
{
"userAgent": "MultiPageAttachmentSecond/1.0",
"first": None,
"second": "second",
},
"same-target policy after first session detach",
)
assert_equal(
owner_runtime_after_first,
{
"userAgent": "MultiPageAttachmentSecond/1.0",
"language": "ja-JP",
"timezone": "Asia/Tokyo",
},
"released locale/timezone claims acquired by surviving session",
)
await second.detach()
sessions.remove(second)
owner_after_all_token = "multi-page-session-policy-after-all"
peer_after_token = "multi-page-session-policy-peer-after"
await asyncio.gather(
owner.goto(
f"{fixture}/profile-headers?token={owner_after_all_token}",
wait_until="load",
),
peer.goto(
f"{fixture}/profile-headers?token={peer_after_token}",
wait_until="load",
),
)
owner_wire_after_all, peer_wire_after = await asyncio.gather(
_read_fixture_profile(fixture, owner_after_all_token),
_read_fixture_profile(fixture, peer_after_token),
)
owner_runtime_after_all = await _runtime_identity(owner)
assert_equal(
[
owner_wire_after_all.get("sessionHeaderFirst"),
owner_wire_after_all.get("sessionHeaderSecond"),
owner_wire_after_all.get("sessionHeaderPeer"),
],
[None, None, None],
"all owner session headers after detach",
)
if owner_runtime_after_all["userAgent"] in {
"MultiPageFirstSetLast/1.0",
"MultiPageAttachmentSecond/1.0",
}:
raise SmokeError(
"detached owner retained a session user agent: "
f"{owner_runtime_after_all!r}"
)
assert_equal(
peer_wire_after.get("sessionHeaderPeer"),
"peer",
"peer target policy after all owner sessions detach",
)
record_contract(
results,
"multi_page_same_target_session_policy_aggregation",
contract=(
"Enabled Network sessions merge non-conflicting headers and select wire "
"identity by attachment order; live renderer identity follows renderer-agent "
"activation order, locale/timezone use exclusive claims, and detach "
"recomputes only the owning target."
),
source="Chromium NetworkHandler and InspectorEmulationAgent executable oracle",
commands=[
"Network.enable x3",
"Network.setExtraHTTPHeaders x3",
"Emulation.setUserAgentOverride x2",
"Emulation.setLocaleOverride",
"Emulation.setTimezoneOverride",
"Target.detachFromTarget x2",
],
observed={
"wireBeforeDetach": owner_wire_before,
"runtimeBeforeDetach": owner_runtime_before,
"runtimeAfterFirstDetach": owner_runtime_after_first,
"runtimeAfterAllDetach": owner_runtime_after_all,
"exclusiveClaimsRejected": True,
"peerPolicySurvived": True,
},
)
finally:
await asyncio.gather(
*(session.detach() for session in sessions),
return_exceptions=True,
)
await _close_context(context)
async def _activation_churn_preserves_target_ownership(
browser: Any,
fixture: str,
results: list[dict[str, Any]],
) -> None:
context = await browser.new_context()
browser_session = await browser.new_browser_cdp_session()
sessions: list[Any] = []
try:
pages = [await context.new_page() for _ in range(4)]
initial_urls = [
f"{fixture}/plain?multi-page-activation={index}" for index in range(4)
]
await asyncio.gather(
*(
page.goto(url, wait_until="load")
for page, url in zip(pages, initial_urls, strict=True)
)
)
sessions = [await context.new_cdp_session(page) for page in pages]
for index, session in enumerate(sessions):
await session.send("Network.enable")
await session.send(
"Network.setExtraHTTPHeaders",
{"headers": {"x-moli-profile-smoke": f"activation-{index}"}},
)
await session.send(
"Page.addScriptToEvaluateOnNewDocument",
{
"source": (
"globalThis.__multiPageActivationOwner = "
f"'activation-{index}';"
)
},
)
await session.send(
"Runtime.evaluate",
{
"expression": (
"globalThis.__multiPageActivationOwner = "
f"'activation-{index}'"
),
"returnByValue": True,
},
)
target_infos = (await browser_session.send("Target.getTargets")).get(
"targetInfos",
[],
)
target_ids_by_url = {
info.get("url"): info.get("targetId")
for info in target_infos
if info.get("type") == "page"
}
target_ids = [target_ids_by_url.get(url) for url in initial_urls]
if not all(isinstance(target_id, str) for target_id in target_ids):
raise SmokeError(
"could not resolve every activation target: "
f"urls={initial_urls!r}, targets={target_infos!r}"
)
activation_order = [2, 0, 3, 1, 0, 2]
for active_index in activation_order:
await browser_session.send(
"Target.activateTarget",
{"targetId": target_ids[active_index]},
)
owner_values = await asyncio.gather(
*(
session.send(
"Runtime.evaluate",
{
"expression": "globalThis.__multiPageActivationOwner",
"returnByValue": True,
},
)
for session in sessions
)
)
assert_equal(
[_runtime_value(value) for value in owner_values],
[f"activation-{index}" for index in range(4)],
f"session ownership after activating target {active_index}",
)
frame_trees = await asyncio.gather(
*(session.send("Page.getFrameTree") for session in sessions)
)
assert_equal(
[
tree.get("frameTree", {}).get("frame", {}).get("url")
for tree in frame_trees
],
initial_urls,
f"frame ownership after activating target {active_index}",
)
close_result = await browser_session.send(
"Target.closeTarget",
{"targetId": target_ids[2]},
)
assert_equal(close_result.get("success"), True, "close active churn target")
await wait_until(
pages[2].is_closed,
"active churn target close",
timeout_ms=5_000,
)
survivor_indexes = [0, 1, 3]
replacement = await context.new_page()
survivor_tokens = [f"multi-page-activation-survivor-{index}" for index in survivor_indexes]
replacement_token = "multi-page-activation-replacement"
await asyncio.gather(
*(
pages[index].goto(
f"{fixture}/profile-headers?token={token}",
wait_until="load",
)
for index, token in zip(
survivor_indexes,
survivor_tokens,
strict=True,
)
),
replacement.goto(
f"{fixture}/profile-headers?token={replacement_token}",
wait_until="load",
),
)
survivor_owners = await asyncio.gather(
*(
pages[index].evaluate("globalThis.__multiPageActivationOwner")
for index in survivor_indexes
)
)
assert_equal(
survivor_owners,
[f"activation-{index}" for index in survivor_indexes],
"preload ownership after activation churn and active-target close",
)
assert_equal(
await replacement.evaluate("globalThis.__multiPageActivationOwner"),
None,
"replacement target does not inherit closed target preload state",
)
profiles = await asyncio.gather(
*(_read_fixture_profile(fixture, token) for token in survivor_tokens),
_read_fixture_profile(fixture, replacement_token),
)
assert_equal(
[profile.get("extraHeader") for profile in profiles],
[
*(f"activation-{index}" for index in survivor_indexes),
None,
],
"network policy after activation churn and active-target close",
)
survivor_frame_trees = await asyncio.gather(
*(sessions[index].send("Page.getFrameTree") for index in survivor_indexes)
)
assert_equal(
[
tree.get("frameTree", {}).get("frame", {}).get("url")
for tree in survivor_frame_trees
],
[
f"{fixture}/profile-headers?token={token}"
for token in survivor_tokens
],
"surviving sessions after active-target close",
)
record_contract(
results,
"multi_page_activation_churn_preserves_target_ownership",
contract=(
"Repeated Target.activateTarget calls change foreground selection without "
"moving session, frame, preload, or Network state; closing the active Page "
"retires only that target and a replacement starts clean."
),
source="Chromium stable WebContents/DevToolsSession ownership model",
commands=[
"Target.activateTarget x6",
"Runtime.evaluate",
"Page.getFrameTree",
"Page.addScriptToEvaluateOnNewDocument",
"Network.setExtraHTTPHeaders",
"Target.closeTarget",
],
observed={
"activationOrder": activation_order,
"survivorOwners": survivor_owners,
"survivorHeaders": [
profile.get("extraHeader") for profile in profiles[:-1]
],
"replacementStartedClean": True,
},
)
finally:
await asyncio.gather(
*(session.detach() for session in sessions),
return_exceptions=True,
)
with suppress(Exception):
await browser_session.detach()
await _close_context(context)
async def _expect_protocol_error(
awaitable: Awaitable[Any],
label: str,
) -> str:
try:
result = await asyncio.wait_for(awaitable, timeout=5)
except Exception as error:
return str(error)
raise SmokeError(f"{label} unexpectedly succeeded: {result!r}")
async def _runtime_identity(page: Any) -> dict[str, Any]:
value = await page.evaluate(
"""() => ({
userAgent: navigator.userAgent,
language: navigator.language,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
})"""
)
if not isinstance(value, dict):
raise SmokeError(f"Runtime identity was not an object: {value!r}")
return value
async def _read_fixture_profile(fixture: str, token: str) -> dict[str, Any]:
value = await asyncio.to_thread(
_read_fixture_json,
f"{fixture}/profile-result?token={token}",
)
if not isinstance(value, dict):
raise SmokeError(f"fixture recorded no profile for {token}: {value!r}")
return value
def _runtime_value(response: dict[str, Any]) -> Any:
return response.get("result", {}).get("value")
async def _close_context(context: Any) -> None:
try:
await asyncio.wait_for(context.close(), timeout=5)
except Exception as error:
raise SmokeError(
f"BrowserContext.close failed: {type(error).__name__}: {error}"
) from error
def _read_fixture_json(url: str) -> Any:
with urllib.request.urlopen(url, timeout=2) as response:
return json.loads(response.read().decode("utf-8"))