# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations
from enum import Enum
from typing import TYPE_CHECKING, Any, TypeVar
from selenium.webdriver.common.bidi.common import command_builder
if TYPE_CHECKING:
from selenium.webdriver.remote.websocket_connection import WebSocketConnection
[docs]
class ScreenOrientationNatural(Enum):
"""Natural screen orientation."""
PORTRAIT = "portrait"
LANDSCAPE = "landscape"
[docs]
class ScreenOrientationType(Enum):
"""Screen orientation type."""
PORTRAIT_PRIMARY = "portrait-primary"
PORTRAIT_SECONDARY = "portrait-secondary"
LANDSCAPE_PRIMARY = "landscape-primary"
LANDSCAPE_SECONDARY = "landscape-secondary"
E = TypeVar("E", ScreenOrientationNatural, ScreenOrientationType)
def _convert_to_enum(value: E | str, enum_class: type[E]) -> E:
if isinstance(value, enum_class):
return value
assert isinstance(value, str)
try:
return enum_class(value.lower())
except ValueError:
raise ValueError(f"Invalid orientation: {value}")
[docs]
class ScreenOrientation:
"""Represents screen orientation configuration."""
def __init__(
self,
natural: ScreenOrientationNatural | str,
type: ScreenOrientationType | str,
):
"""Initialize ScreenOrientation.
Args:
natural: Natural screen orientation ("portrait" or "landscape").
type: Screen orientation type ("portrait-primary", "portrait-secondary",
"landscape-primary", or "landscape-secondary").
Raises:
ValueError: If natural or type values are invalid.
"""
# handle string values
self.natural = _convert_to_enum(natural, ScreenOrientationNatural)
self.type = _convert_to_enum(type, ScreenOrientationType)
[docs]
def to_dict(self) -> dict[str, str]:
return {
"natural": self.natural.value,
"type": self.type.value,
}
[docs]
class GeolocationCoordinates:
"""Represents geolocation coordinates."""
def __init__(
self,
latitude: float,
longitude: float,
accuracy: float = 1.0,
altitude: float | None = None,
altitude_accuracy: float | None = None,
heading: float | None = None,
speed: float | None = None,
):
"""Initialize GeolocationCoordinates.
Args:
latitude: Latitude coordinate (-90.0 to 90.0).
longitude: Longitude coordinate (-180.0 to 180.0).
accuracy: Accuracy in meters (>= 0.0), defaults to 1.0.
altitude: Altitude in meters or None, defaults to None.
altitude_accuracy: Altitude accuracy in meters (>= 0.0) or None, defaults to None.
heading: Heading in degrees (0.0 to 360.0) or None, defaults to None.
speed: Speed in meters per second (>= 0.0) or None, defaults to None.
Raises:
ValueError: If coordinates are out of valid range or if altitude_accuracy is provided without altitude.
"""
self.latitude = latitude
self.longitude = longitude
self.accuracy = accuracy
self.altitude = altitude
self.altitude_accuracy = altitude_accuracy
self.heading = heading
self.speed = speed
@property
def latitude(self) -> float:
return self._latitude
@latitude.setter
def latitude(self, value: float) -> None:
if not (-90.0 <= value <= 90.0):
raise ValueError("latitude must be between -90.0 and 90.0")
self._latitude = value
@property
def longitude(self) -> float:
return self._longitude
@longitude.setter
def longitude(self, value: float) -> None:
if not (-180.0 <= value <= 180.0):
raise ValueError("longitude must be between -180.0 and 180.0")
self._longitude = value
@property
def accuracy(self) -> float:
return self._accuracy
@accuracy.setter
def accuracy(self, value: float) -> None:
if value < 0.0:
raise ValueError("accuracy must be >= 0.0")
self._accuracy = value
@property
def altitude(self) -> float | None:
return self._altitude
@altitude.setter
def altitude(self, value: float | None) -> None:
self._altitude = value
@property
def altitude_accuracy(self) -> float | None:
return self._altitude_accuracy
@altitude_accuracy.setter
def altitude_accuracy(self, value: float | None) -> None:
if value is not None and self.altitude is None:
raise ValueError("altitude_accuracy cannot be set without altitude")
if value is not None and value < 0.0:
raise ValueError("altitude_accuracy must be >= 0.0")
self._altitude_accuracy = value
@property
def heading(self) -> float | None:
return self._heading
@heading.setter
def heading(self, value: float | None) -> None:
if value is not None and not (0.0 <= value < 360.0):
raise ValueError("heading must be between 0.0 and 360.0")
self._heading = value
@property
def speed(self) -> float | None:
return self._speed
@speed.setter
def speed(self, value: float | None) -> None:
if value is not None and value < 0.0:
raise ValueError("speed must be >= 0.0")
self._speed = value
[docs]
def to_dict(self) -> dict[str, float | None]:
result: dict[str, float | None] = {
"latitude": self.latitude,
"longitude": self.longitude,
"accuracy": self.accuracy,
}
if self.altitude is not None:
result["altitude"] = self.altitude
if self.altitude_accuracy is not None:
result["altitudeAccuracy"] = self.altitude_accuracy
if self.heading is not None:
result["heading"] = self.heading
if self.speed is not None:
result["speed"] = self.speed
return result
[docs]
class GeolocationPositionError:
"""Represents a geolocation position error."""
TYPE_POSITION_UNAVAILABLE = "positionUnavailable"
def __init__(self, type: str = TYPE_POSITION_UNAVAILABLE):
if type != self.TYPE_POSITION_UNAVAILABLE:
raise ValueError(f'type must be "{self.TYPE_POSITION_UNAVAILABLE}"')
self.type = type
[docs]
def to_dict(self) -> dict[str, str]:
return {"type": self.type}
[docs]
class Emulation:
"""BiDi implementation of the emulation module."""
def __init__(self, conn: WebSocketConnection) -> None:
self.conn = conn
[docs]
def set_geolocation_override(
self,
coordinates: GeolocationCoordinates | None = None,
error: GeolocationPositionError | None = None,
contexts: list[str] | None = None,
user_contexts: list[str] | None = None,
) -> None:
"""Set geolocation override for the given contexts or user contexts.
Args:
coordinates: Geolocation coordinates to emulate, or None.
error: Geolocation error to emulate, or None.
contexts: List of browsing context IDs to apply the override to.
user_contexts: List of user context IDs to apply the override to.
Raises:
ValueError: If both coordinates and error are provided, or if both contexts
and user_contexts are provided, or if neither contexts nor
user_contexts are provided.
"""
if coordinates is not None and error is not None:
raise ValueError("Cannot specify both coordinates and error")
if contexts is not None and user_contexts is not None:
raise ValueError("Cannot specify both contexts and userContexts")
if contexts is None and user_contexts is None:
raise ValueError("Must specify either contexts or userContexts")
params: dict[str, Any] = {}
if coordinates is not None:
params["coordinates"] = coordinates.to_dict()
elif error is not None:
params["error"] = error.to_dict()
if contexts is not None:
params["contexts"] = contexts
elif user_contexts is not None:
params["userContexts"] = user_contexts
self.conn.execute(command_builder("emulation.setGeolocationOverride", params))
[docs]
def set_timezone_override(
self,
timezone: str | None = None,
contexts: list[str] | None = None,
user_contexts: list[str] | None = None,
) -> None:
"""Set timezone override for the given contexts or user contexts.
Args:
timezone: Timezone identifier (IANA timezone name or offset string like '+01:00'),
or None to clear the override.
contexts: List of browsing context IDs to apply the override to.
user_contexts: List of user context IDs to apply the override to.
Raises:
ValueError: If both contexts and user_contexts are provided, or if neither
contexts nor user_contexts are provided.
"""
if contexts is not None and user_contexts is not None:
raise ValueError("Cannot specify both contexts and user_contexts")
if contexts is None and user_contexts is None:
raise ValueError("Must specify either contexts or user_contexts")
params: dict[str, Any] = {"timezone": timezone}
if contexts is not None:
params["contexts"] = contexts
elif user_contexts is not None:
params["userContexts"] = user_contexts
self.conn.execute(command_builder("emulation.setTimezoneOverride", params))
[docs]
def set_locale_override(
self,
locale: str | None = None,
contexts: list[str] | None = None,
user_contexts: list[str] | None = None,
) -> None:
"""Set locale override for the given contexts or user contexts.
Args:
locale: Locale string as per BCP 47, or None to clear override.
contexts: List of browsing context IDs to apply the override to.
user_contexts: List of user context IDs to apply the override to.
Raises:
ValueError: If both contexts and user_contexts are provided, or if neither
contexts nor user_contexts are provided, or if locale is invalid.
"""
if contexts is not None and user_contexts is not None:
raise ValueError("Cannot specify both contexts and userContexts")
if contexts is None and user_contexts is None:
raise ValueError("Must specify either contexts or userContexts")
params: dict[str, Any] = {"locale": locale}
if contexts is not None:
params["contexts"] = contexts
elif user_contexts is not None:
params["userContexts"] = user_contexts
self.conn.execute(command_builder("emulation.setLocaleOverride", params))
[docs]
def set_scripting_enabled(
self,
enabled: bool | None = False,
contexts: list[str] | None = None,
user_contexts: list[str] | None = None,
) -> None:
"""Set scripting enabled override for the given contexts or user contexts.
Args:
enabled: False to disable scripting, None to clear the override.
Note: Only emulation of disabled JavaScript is supported.
contexts: List of browsing context IDs to apply the override to.
user_contexts: List of user context IDs to apply the override to.
Raises:
ValueError: If both contexts and user_contexts are provided, or if neither
contexts nor user_contexts are provided, or if enabled is True.
"""
if enabled:
raise ValueError("Only emulation of disabled JavaScript is supported (enabled must be False or None)")
if contexts is not None and user_contexts is not None:
raise ValueError("Cannot specify both contexts and userContexts")
if contexts is None and user_contexts is None:
raise ValueError("Must specify either contexts or userContexts")
params: dict[str, Any] = {"enabled": enabled}
if contexts is not None:
params["contexts"] = contexts
elif user_contexts is not None:
params["userContexts"] = user_contexts
self.conn.execute(command_builder("emulation.setScriptingEnabled", params))
[docs]
def set_screen_orientation_override(
self,
screen_orientation: ScreenOrientation | None = None,
contexts: list[str] | None = None,
user_contexts: list[str] | None = None,
) -> None:
"""Set screen orientation override for the given contexts or user contexts.
Args:
screen_orientation: ScreenOrientation object to emulate, or None to clear the override.
contexts: List of browsing context IDs to apply the override to.
user_contexts: List of user context IDs to apply the override to.
Raises:
ValueError: If both contexts and user_contexts are provided, or if neither
contexts nor user_contexts are provided.
"""
if contexts is not None and user_contexts is not None:
raise ValueError("Cannot specify both contexts and userContexts")
if contexts is None and user_contexts is None:
raise ValueError("Must specify either contexts or userContexts")
params: dict[str, Any] = {
"screenOrientation": screen_orientation.to_dict() if screen_orientation is not None else None
}
if contexts is not None:
params["contexts"] = contexts
elif user_contexts is not None:
params["userContexts"] = user_contexts
self.conn.execute(command_builder("emulation.setScreenOrientationOverride", params))
[docs]
def set_user_agent_override(
self,
user_agent: str | None = None,
contexts: list[str] | None = None,
user_contexts: list[str] | None = None,
) -> None:
"""Set user agent override for the given contexts or user contexts.
Args:
user_agent: User agent string to emulate, or None to clear the override.
contexts: List of browsing context IDs to apply the override to.
user_contexts: List of user context IDs to apply the override to.
Raises:
ValueError: If both contexts and user_contexts are provided, or if neither
contexts nor user_contexts are provided.
"""
if contexts is not None and user_contexts is not None:
raise ValueError("Cannot specify both contexts and user_contexts")
if contexts is None and user_contexts is None:
raise ValueError("Must specify either contexts or user_contexts")
params: dict[str, Any] = {"userAgent": user_agent}
if contexts is not None:
params["contexts"] = contexts
elif user_contexts is not None:
params["userContexts"] = user_contexts
self.conn.execute(command_builder("emulation.setUserAgentOverride", params))
[docs]
def set_network_conditions(
self,
offline: bool = False,
contexts: list[str] | None = None,
user_contexts: list[str] | None = None,
) -> None:
"""Set network conditions for the given contexts or user contexts.
Args:
offline: True to emulate offline network conditions, False to clear the override.
contexts: List of browsing context IDs to apply the conditions to.
user_contexts: List of user context IDs to apply the conditions to.
Raises:
ValueError: If both contexts and user_contexts are provided, or if neither
contexts nor user_contexts are provided.
"""
if contexts is not None and user_contexts is not None:
raise ValueError("Cannot specify both contexts and user_contexts")
if contexts is None and user_contexts is None:
raise ValueError("Must specify either contexts or user_contexts")
params: dict[str, Any] = {}
if offline:
params["networkConditions"] = {"type": "offline"}
else:
# if offline is False or None, then clear the override
params["networkConditions"] = None
if contexts is not None:
params["contexts"] = contexts
elif user_contexts is not None:
params["userContexts"] = user_contexts
self.conn.execute(command_builder("emulation.setNetworkConditions", params))