Path: blob/trunk/py/selenium/webdriver/common/bidi/emulation.py
4057 views
# Licensed to the Software Freedom Conservancy (SFC) under one1# or more contributor license agreements. See the NOTICE file2# distributed with this work for additional information3# regarding copyright ownership. The SFC licenses this file4# to you under the Apache License, Version 2.0 (the5# "License"); you may not use this file except in compliance6# with the License. You may obtain a copy of the License at7#8# http://www.apache.org/licenses/LICENSE-2.09#10# Unless required by applicable law or agreed to in writing,11# software distributed under the License is distributed on an12# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13# KIND, either express or implied. See the License for the14# specific language governing permissions and limitations15# under the License.16from __future__ import annotations1718from enum import Enum19from typing import TYPE_CHECKING, Any, TypeVar2021from selenium.webdriver.common.bidi.common import command_builder2223if TYPE_CHECKING:24from selenium.webdriver.remote.websocket_connection import WebSocketConnection252627class ScreenOrientationNatural(Enum):28"""Natural screen orientation."""2930PORTRAIT = "portrait"31LANDSCAPE = "landscape"323334class ScreenOrientationType(Enum):35"""Screen orientation type."""3637PORTRAIT_PRIMARY = "portrait-primary"38PORTRAIT_SECONDARY = "portrait-secondary"39LANDSCAPE_PRIMARY = "landscape-primary"40LANDSCAPE_SECONDARY = "landscape-secondary"414243E = TypeVar("E", ScreenOrientationNatural, ScreenOrientationType)444546def _convert_to_enum(value: E | str, enum_class: type[E]) -> E:47if isinstance(value, enum_class):48return value49assert isinstance(value, str)50try:51return enum_class(value.lower())52except ValueError:53raise ValueError(f"Invalid orientation: {value}")545556class ScreenOrientation:57"""Represents screen orientation configuration."""5859def __init__(60self,61natural: ScreenOrientationNatural | str,62type: ScreenOrientationType | str,63):64"""Initialize ScreenOrientation.6566Args:67natural: Natural screen orientation ("portrait" or "landscape").68type: Screen orientation type ("portrait-primary", "portrait-secondary",69"landscape-primary", or "landscape-secondary").7071Raises:72ValueError: If natural or type values are invalid.73"""74# handle string values75self.natural = _convert_to_enum(natural, ScreenOrientationNatural)76self.type = _convert_to_enum(type, ScreenOrientationType)7778def to_dict(self) -> dict[str, str]:79return {80"natural": self.natural.value,81"type": self.type.value,82}838485class GeolocationCoordinates:86"""Represents geolocation coordinates."""8788def __init__(89self,90latitude: float,91longitude: float,92accuracy: float = 1.0,93altitude: float | None = None,94altitude_accuracy: float | None = None,95heading: float | None = None,96speed: float | None = None,97):98"""Initialize GeolocationCoordinates.99100Args:101latitude: Latitude coordinate (-90.0 to 90.0).102longitude: Longitude coordinate (-180.0 to 180.0).103accuracy: Accuracy in meters (>= 0.0), defaults to 1.0.104altitude: Altitude in meters or None, defaults to None.105altitude_accuracy: Altitude accuracy in meters (>= 0.0) or None, defaults to None.106heading: Heading in degrees (0.0 to 360.0) or None, defaults to None.107speed: Speed in meters per second (>= 0.0) or None, defaults to None.108109Raises:110ValueError: If coordinates are out of valid range or if altitude_accuracy is provided without altitude.111"""112self.latitude = latitude113self.longitude = longitude114self.accuracy = accuracy115self.altitude = altitude116self.altitude_accuracy = altitude_accuracy117self.heading = heading118self.speed = speed119120@property121def latitude(self) -> float:122return self._latitude123124@latitude.setter125def latitude(self, value: float) -> None:126if not (-90.0 <= value <= 90.0):127raise ValueError("latitude must be between -90.0 and 90.0")128self._latitude = value129130@property131def longitude(self) -> float:132return self._longitude133134@longitude.setter135def longitude(self, value: float) -> None:136if not (-180.0 <= value <= 180.0):137raise ValueError("longitude must be between -180.0 and 180.0")138self._longitude = value139140@property141def accuracy(self) -> float:142return self._accuracy143144@accuracy.setter145def accuracy(self, value: float) -> None:146if value < 0.0:147raise ValueError("accuracy must be >= 0.0")148self._accuracy = value149150@property151def altitude(self) -> float | None:152return self._altitude153154@altitude.setter155def altitude(self, value: float | None) -> None:156self._altitude = value157158@property159def altitude_accuracy(self) -> float | None:160return self._altitude_accuracy161162@altitude_accuracy.setter163def altitude_accuracy(self, value: float | None) -> None:164if value is not None and self.altitude is None:165raise ValueError("altitude_accuracy cannot be set without altitude")166if value is not None and value < 0.0:167raise ValueError("altitude_accuracy must be >= 0.0")168self._altitude_accuracy = value169170@property171def heading(self) -> float | None:172return self._heading173174@heading.setter175def heading(self, value: float | None) -> None:176if value is not None and not (0.0 <= value < 360.0):177raise ValueError("heading must be between 0.0 and 360.0")178self._heading = value179180@property181def speed(self) -> float | None:182return self._speed183184@speed.setter185def speed(self, value: float | None) -> None:186if value is not None and value < 0.0:187raise ValueError("speed must be >= 0.0")188self._speed = value189190def to_dict(self) -> dict[str, float | None]:191result: dict[str, float | None] = {192"latitude": self.latitude,193"longitude": self.longitude,194"accuracy": self.accuracy,195}196197if self.altitude is not None:198result["altitude"] = self.altitude199200if self.altitude_accuracy is not None:201result["altitudeAccuracy"] = self.altitude_accuracy202203if self.heading is not None:204result["heading"] = self.heading205206if self.speed is not None:207result["speed"] = self.speed208209return result210211212class GeolocationPositionError:213"""Represents a geolocation position error."""214215TYPE_POSITION_UNAVAILABLE = "positionUnavailable"216217def __init__(self, type: str = TYPE_POSITION_UNAVAILABLE):218if type != self.TYPE_POSITION_UNAVAILABLE:219raise ValueError(f'type must be "{self.TYPE_POSITION_UNAVAILABLE}"')220self.type = type221222def to_dict(self) -> dict[str, str]:223return {"type": self.type}224225226class Emulation:227"""BiDi implementation of the emulation module."""228229def __init__(self, conn: WebSocketConnection) -> None:230self.conn = conn231232def set_geolocation_override(233self,234coordinates: GeolocationCoordinates | None = None,235error: GeolocationPositionError | None = None,236contexts: list[str] | None = None,237user_contexts: list[str] | None = None,238) -> None:239"""Set geolocation override for the given contexts or user contexts.240241Args:242coordinates: Geolocation coordinates to emulate, or None.243error: Geolocation error to emulate, or None.244contexts: List of browsing context IDs to apply the override to.245user_contexts: List of user context IDs to apply the override to.246247Raises:248ValueError: If both coordinates and error are provided, or if both contexts249and user_contexts are provided, or if neither contexts nor250user_contexts are provided.251"""252if coordinates is not None and error is not None:253raise ValueError("Cannot specify both coordinates and error")254255if contexts is not None and user_contexts is not None:256raise ValueError("Cannot specify both contexts and userContexts")257258if contexts is None and user_contexts is None:259raise ValueError("Must specify either contexts or userContexts")260261params: dict[str, Any] = {}262263if coordinates is not None:264params["coordinates"] = coordinates.to_dict()265elif error is not None:266params["error"] = error.to_dict()267268if contexts is not None:269params["contexts"] = contexts270elif user_contexts is not None:271params["userContexts"] = user_contexts272273self.conn.execute(command_builder("emulation.setGeolocationOverride", params))274275def set_timezone_override(276self,277timezone: str | None = None,278contexts: list[str] | None = None,279user_contexts: list[str] | None = None,280) -> None:281"""Set timezone override for the given contexts or user contexts.282283Args:284timezone: Timezone identifier (IANA timezone name or offset string like '+01:00'),285or None to clear the override.286contexts: List of browsing context IDs to apply the override to.287user_contexts: List of user context IDs to apply the override to.288289Raises:290ValueError: If both contexts and user_contexts are provided, or if neither291contexts nor user_contexts are provided.292"""293if contexts is not None and user_contexts is not None:294raise ValueError("Cannot specify both contexts and user_contexts")295296if contexts is None and user_contexts is None:297raise ValueError("Must specify either contexts or user_contexts")298299params: dict[str, Any] = {"timezone": timezone}300301if contexts is not None:302params["contexts"] = contexts303elif user_contexts is not None:304params["userContexts"] = user_contexts305306self.conn.execute(command_builder("emulation.setTimezoneOverride", params))307308def set_locale_override(309self,310locale: str | None = None,311contexts: list[str] | None = None,312user_contexts: list[str] | None = None,313) -> None:314"""Set locale override for the given contexts or user contexts.315316Args:317locale: Locale string as per BCP 47, or None to clear override.318contexts: List of browsing context IDs to apply the override to.319user_contexts: List of user context IDs to apply the override to.320321Raises:322ValueError: If both contexts and user_contexts are provided, or if neither323contexts nor user_contexts are provided, or if locale is invalid.324"""325if contexts is not None and user_contexts is not None:326raise ValueError("Cannot specify both contexts and userContexts")327328if contexts is None and user_contexts is None:329raise ValueError("Must specify either contexts or userContexts")330331params: dict[str, Any] = {"locale": locale}332333if contexts is not None:334params["contexts"] = contexts335elif user_contexts is not None:336params["userContexts"] = user_contexts337338self.conn.execute(command_builder("emulation.setLocaleOverride", params))339340def set_scripting_enabled(341self,342enabled: bool | None = False,343contexts: list[str] | None = None,344user_contexts: list[str] | None = None,345) -> None:346"""Set scripting enabled override for the given contexts or user contexts.347348Args:349enabled: False to disable scripting, None to clear the override.350Note: Only emulation of disabled JavaScript is supported.351contexts: List of browsing context IDs to apply the override to.352user_contexts: List of user context IDs to apply the override to.353354Raises:355ValueError: If both contexts and user_contexts are provided, or if neither356contexts nor user_contexts are provided, or if enabled is True.357"""358if enabled:359raise ValueError("Only emulation of disabled JavaScript is supported (enabled must be False or None)")360361if contexts is not None and user_contexts is not None:362raise ValueError("Cannot specify both contexts and userContexts")363364if contexts is None and user_contexts is None:365raise ValueError("Must specify either contexts or userContexts")366367params: dict[str, Any] = {"enabled": enabled}368369if contexts is not None:370params["contexts"] = contexts371elif user_contexts is not None:372params["userContexts"] = user_contexts373374self.conn.execute(command_builder("emulation.setScriptingEnabled", params))375376def set_screen_orientation_override(377self,378screen_orientation: ScreenOrientation | None = None,379contexts: list[str] | None = None,380user_contexts: list[str] | None = None,381) -> None:382"""Set screen orientation override for the given contexts or user contexts.383384Args:385screen_orientation: ScreenOrientation object to emulate, or None to clear the override.386contexts: List of browsing context IDs to apply the override to.387user_contexts: List of user context IDs to apply the override to.388389Raises:390ValueError: If both contexts and user_contexts are provided, or if neither391contexts nor user_contexts are provided.392"""393if contexts is not None and user_contexts is not None:394raise ValueError("Cannot specify both contexts and userContexts")395396if contexts is None and user_contexts is None:397raise ValueError("Must specify either contexts or userContexts")398399params: dict[str, Any] = {400"screenOrientation": screen_orientation.to_dict() if screen_orientation is not None else None401}402403if contexts is not None:404params["contexts"] = contexts405elif user_contexts is not None:406params["userContexts"] = user_contexts407408self.conn.execute(command_builder("emulation.setScreenOrientationOverride", params))409410def set_user_agent_override(411self,412user_agent: str | None = None,413contexts: list[str] | None = None,414user_contexts: list[str] | None = None,415) -> None:416"""Set user agent override for the given contexts or user contexts.417418Args:419user_agent: User agent string to emulate, or None to clear the override.420contexts: List of browsing context IDs to apply the override to.421user_contexts: List of user context IDs to apply the override to.422423Raises:424ValueError: If both contexts and user_contexts are provided, or if neither425contexts nor user_contexts are provided.426"""427if contexts is not None and user_contexts is not None:428raise ValueError("Cannot specify both contexts and user_contexts")429430if contexts is None and user_contexts is None:431raise ValueError("Must specify either contexts or user_contexts")432433params: dict[str, Any] = {"userAgent": user_agent}434435if contexts is not None:436params["contexts"] = contexts437elif user_contexts is not None:438params["userContexts"] = user_contexts439440self.conn.execute(command_builder("emulation.setUserAgentOverride", params))441442def set_network_conditions(443self,444offline: bool = False,445contexts: list[str] | None = None,446user_contexts: list[str] | None = None,447) -> None:448"""Set network conditions for the given contexts or user contexts.449450Args:451offline: True to emulate offline network conditions, False to clear the override.452contexts: List of browsing context IDs to apply the conditions to.453user_contexts: List of user context IDs to apply the conditions to.454455Raises:456ValueError: If both contexts and user_contexts are provided, or if neither457contexts nor user_contexts are provided.458"""459if contexts is not None and user_contexts is not None:460raise ValueError("Cannot specify both contexts and user_contexts")461462if contexts is None and user_contexts is None:463raise ValueError("Must specify either contexts or user_contexts")464465params: dict[str, Any] = {}466467if offline:468params["networkConditions"] = {"type": "offline"}469else:470# if offline is False or None, then clear the override471params["networkConditions"] = None472473if contexts is not None:474params["contexts"] = contexts475elif user_contexts is not None:476params["userContexts"] = user_contexts477478self.conn.execute(command_builder("emulation.setNetworkConditions", params))479480def set_screen_settings_override(481self,482width: int | None = None,483height: int | None = None,484contexts: list[str] | None = None,485user_contexts: list[str] | None = None,486) -> None:487"""Set screen settings override for the given contexts or user contexts.488489Args:490width: Screen width in pixels (>= 0). None to clear the override.491height: Screen height in pixels (>= 0). None to clear the override.492contexts: List of browsing context IDs to apply the override to.493user_contexts: List of user context IDs to apply the override to.494495Raises:496ValueError: If only one of width/height is provided, or if both contexts497and user_contexts are provided, or if neither is provided.498"""499if (width is None) != (height is None):500raise ValueError("Must provide both width and height, or neither to clear the override")501502if contexts is not None and user_contexts is not None:503raise ValueError("Cannot specify both contexts and user_contexts")504505if contexts is None and user_contexts is None:506raise ValueError("Must specify either contexts or user_contexts")507508screen_area = None509if width is not None and height is not None:510if not isinstance(width, int) or not isinstance(height, int):511raise ValueError("width and height must be integers")512if width < 0 or height < 0:513raise ValueError("width and height must be >= 0")514screen_area = {"width": width, "height": height}515516params: dict[str, Any] = {"screenArea": screen_area}517518if contexts is not None:519params["contexts"] = contexts520elif user_contexts is not None:521params["userContexts"] = user_contexts522523self.conn.execute(command_builder("emulation.setScreenSettingsOverride", params))524525526