Skip to content

[py][bidi]: implement bidi module - emulation #15819

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: trunk
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions py/selenium/webdriver/common/bidi/emulation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# 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 typing import Dict, List, Optional

from selenium.webdriver.common.bidi.common import command_builder


class GeolocationCoordinates:
"""Represents geolocation coordinates."""

def __init__(
self,
latitude: float,
longitude: float,
accuracy: float = 1.0,
altitude: Optional[float] = None,
altitude_accuracy: Optional[float] = None,
heading: Optional[float] = None,
speed: Optional[float] = None,
):
"""Initialize GeolocationCoordinates.

Parameters:
-----------
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.
"""
if not (-90.0 <= latitude <= 90.0):
raise ValueError("Latitude must be between -90.0 and 90.0")
if not (-180.0 <= longitude <= 180.0):
raise ValueError("Longitude must be between -180.0 and 180.0")
if accuracy < 0.0:
raise ValueError("Accuracy must be >= 0.0")
if altitude_accuracy is not None and altitude is None:
raise ValueError("altitude_accuracy cannot be set without altitude")
if altitude_accuracy is not None and altitude_accuracy < 0.0:
raise ValueError("Altitude accuracy must be >= 0.0")
if heading is not None and not (0.0 <= heading < 360.0):
raise ValueError("Heading must be between 0.0 and 360.0")
if speed is not None and speed < 0.0:
raise ValueError("Speed must be >= 0.0")

self.latitude = latitude
self.longitude = longitude
self.accuracy = accuracy
self.altitude = altitude
self.altitude_accuracy = altitude_accuracy
self.heading = heading
self.speed = speed

def to_dict(self) -> Dict:
result = {
"latitude": self.latitude,
"longitude": self.longitude,
"accuracy": self.accuracy,
}

if self.altitude is not None:
result["altitude"] = self.altitude
else:
result["altitude"] = None

if self.altitude_accuracy is not None:
result["altitudeAccuracy"] = self.altitude_accuracy
else:
result["altitudeAccuracy"] = None

if self.heading is not None:
result["heading"] = self.heading
else:
result["heading"] = None

if self.speed is not None:
result["speed"] = self.speed
else:
result["speed"] = None

return result


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

def to_dict(self) -> Dict:
return {"type": self.type}


class Emulation:
"""
BiDi implementation of the emulation module.
"""

def __init__(self, conn):
self.conn = conn

def set_geolocation_override(
self,
coordinates: Optional[GeolocationCoordinates] = None,
error: Optional[GeolocationPositionError] = None,
contexts: Optional[List[str]] = None,
user_contexts: Optional[List[str]] = None,
) -> None:
"""Set geolocation override for the given contexts or user contexts.

Parameters:
-----------
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 = {}

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))
24 changes: 24 additions & 0 deletions py/selenium/webdriver/remote/webdriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
)
from selenium.webdriver.common.bidi.browser import Browser
from selenium.webdriver.common.bidi.browsing_context import BrowsingContext
from selenium.webdriver.common.bidi.emulation import Emulation
from selenium.webdriver.common.bidi.network import Network
from selenium.webdriver.common.bidi.script import Script
from selenium.webdriver.common.bidi.session import Session
Expand Down Expand Up @@ -265,6 +266,7 @@ def __init__(
self._browsing_context = None
self._storage = None
self._webextension = None
self._emulation = None

def __repr__(self):
return f'<{type(self).__module__}.{type(self).__name__} (session="{self.session_id}")>'
Expand Down Expand Up @@ -1361,6 +1363,28 @@ def webextension(self):

return self._webextension

@property
def emulation(self):
"""Returns an emulation module object for BiDi emulation commands.

Returns:
--------
Emulation: an object containing access to BiDi emulation commands.

Examples:
---------
>>> from selenium.webdriver.common.bidi.emulation import GeolocationCoordinates
>>> coordinates = GeolocationCoordinates(37.7749, -122.4194)
>>> driver.emulation.set_geolocation_override(coordinates=coordinates, contexts=[context_id])
"""
if not self._websocket_connection:
self._start_bidi()

if self._emulation is None:
self._emulation = Emulation(self._websocket_connection)

return self._emulation

def _get_cdp_details(self):
import json

Expand Down
Loading
Loading