Skip to content

Commit 6c35645

Browse files
authored
[py][bidi]: implement bidi module - emulation (#15819)
1 parent db00a84 commit 6c35645

File tree

3 files changed

+402
-0
lines changed

3 files changed

+402
-0
lines changed
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# Licensed to the Software Freedom Conservancy (SFC) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The SFC licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
from typing import Any, Optional, Union
19+
20+
from selenium.webdriver.common.bidi.common import command_builder
21+
22+
23+
class GeolocationCoordinates:
24+
"""Represents geolocation coordinates."""
25+
26+
def __init__(
27+
self,
28+
latitude: float,
29+
longitude: float,
30+
accuracy: float = 1.0,
31+
altitude: Optional[float] = None,
32+
altitude_accuracy: Optional[float] = None,
33+
heading: Optional[float] = None,
34+
speed: Optional[float] = None,
35+
):
36+
"""Initialize GeolocationCoordinates.
37+
38+
Parameters:
39+
-----------
40+
latitude: Latitude coordinate (-90.0 to 90.0).
41+
longitude: Longitude coordinate (-180.0 to 180.0).
42+
accuracy: Accuracy in meters (>= 0.0), defaults to 1.0.
43+
altitude: Altitude in meters or None, defaults to None.
44+
altitude_accuracy: Altitude accuracy in meters (>= 0.0) or None, defaults to None.
45+
heading: Heading in degrees (0.0 to 360.0) or None, defaults to None.
46+
speed: Speed in meters per second (>= 0.0) or None, defaults to None.
47+
48+
Raises:
49+
------
50+
ValueError: If coordinates are out of valid range or if altitude_accuracy is provided without altitude.
51+
"""
52+
if not (-90.0 <= latitude <= 90.0):
53+
raise ValueError("Latitude must be between -90.0 and 90.0")
54+
if not (-180.0 <= longitude <= 180.0):
55+
raise ValueError("Longitude must be between -180.0 and 180.0")
56+
if accuracy < 0.0:
57+
raise ValueError("Accuracy must be >= 0.0")
58+
if altitude_accuracy is not None and altitude is None:
59+
raise ValueError("altitude_accuracy cannot be set without altitude")
60+
if altitude_accuracy is not None and altitude_accuracy < 0.0:
61+
raise ValueError("Altitude accuracy must be >= 0.0")
62+
if heading is not None and not (0.0 <= heading < 360.0):
63+
raise ValueError("Heading must be between 0.0 and 360.0")
64+
if speed is not None and speed < 0.0:
65+
raise ValueError("Speed must be >= 0.0")
66+
67+
self.latitude = latitude
68+
self.longitude = longitude
69+
self.accuracy = accuracy
70+
self.altitude = altitude
71+
self.altitude_accuracy = altitude_accuracy
72+
self.heading = heading
73+
self.speed = speed
74+
75+
def to_dict(self) -> dict[str, Union[float, None]]:
76+
result: dict[str, Union[float, None]] = {
77+
"latitude": self.latitude,
78+
"longitude": self.longitude,
79+
"accuracy": self.accuracy,
80+
}
81+
82+
if self.altitude is not None:
83+
result["altitude"] = self.altitude
84+
85+
if self.altitude_accuracy is not None:
86+
result["altitudeAccuracy"] = self.altitude_accuracy
87+
88+
if self.heading is not None:
89+
result["heading"] = self.heading
90+
91+
if self.speed is not None:
92+
result["speed"] = self.speed
93+
94+
return result
95+
96+
97+
class GeolocationPositionError:
98+
"""Represents a geolocation position error."""
99+
100+
TYPE_POSITION_UNAVAILABLE = "positionUnavailable"
101+
102+
def __init__(self, type: str = TYPE_POSITION_UNAVAILABLE):
103+
if type != self.TYPE_POSITION_UNAVAILABLE:
104+
raise ValueError(f'type must be "{self.TYPE_POSITION_UNAVAILABLE}"')
105+
self.type = type
106+
107+
def to_dict(self) -> dict[str, str]:
108+
return {"type": self.type}
109+
110+
111+
class Emulation:
112+
"""
113+
BiDi implementation of the emulation module.
114+
"""
115+
116+
def __init__(self, conn):
117+
self.conn = conn
118+
119+
def set_geolocation_override(
120+
self,
121+
coordinates: Optional[GeolocationCoordinates] = None,
122+
error: Optional[GeolocationPositionError] = None,
123+
contexts: Optional[list[str]] = None,
124+
user_contexts: Optional[list[str]] = None,
125+
) -> None:
126+
"""Set geolocation override for the given contexts or user contexts.
127+
128+
Parameters:
129+
-----------
130+
coordinates: Geolocation coordinates to emulate, or None.
131+
error: Geolocation error to emulate, or None.
132+
contexts: List of browsing context IDs to apply the override to.
133+
user_contexts: List of user context IDs to apply the override to.
134+
135+
Raises:
136+
------
137+
ValueError: If both coordinates and error are provided, or if both contexts
138+
and user_contexts are provided, or if neither contexts nor
139+
user_contexts are provided.
140+
"""
141+
if coordinates is not None and error is not None:
142+
raise ValueError("Cannot specify both coordinates and error")
143+
144+
if contexts is not None and user_contexts is not None:
145+
raise ValueError("Cannot specify both contexts and userContexts")
146+
147+
if contexts is None and user_contexts is None:
148+
raise ValueError("Must specify either contexts or userContexts")
149+
150+
params: dict[str, Any] = {}
151+
152+
if coordinates is not None:
153+
params["coordinates"] = coordinates.to_dict()
154+
elif error is not None:
155+
params["error"] = error.to_dict()
156+
157+
if contexts is not None:
158+
params["contexts"] = contexts
159+
elif user_contexts is not None:
160+
params["userContexts"] = user_contexts
161+
162+
self.conn.execute(command_builder("emulation.setGeolocationOverride", params))

py/selenium/webdriver/remote/webdriver.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
)
4141
from selenium.webdriver.common.bidi.browser import Browser
4242
from selenium.webdriver.common.bidi.browsing_context import BrowsingContext
43+
from selenium.webdriver.common.bidi.emulation import Emulation
4344
from selenium.webdriver.common.bidi.network import Network
4445
from selenium.webdriver.common.bidi.permissions import Permissions
4546
from selenium.webdriver.common.bidi.script import Script
@@ -270,6 +271,7 @@ def __init__(
270271
self._storage = None
271272
self._webextension = None
272273
self._permissions = None
274+
self._emulation = None
273275
self._devtools = None
274276

275277
def __repr__(self):
@@ -1390,6 +1392,28 @@ def webextension(self):
13901392

13911393
return self._webextension
13921394

1395+
@property
1396+
def emulation(self):
1397+
"""Returns an emulation module object for BiDi emulation commands.
1398+
1399+
Returns:
1400+
--------
1401+
Emulation: an object containing access to BiDi emulation commands.
1402+
1403+
Examples:
1404+
---------
1405+
>>> from selenium.webdriver.common.bidi.emulation import GeolocationCoordinates
1406+
>>> coordinates = GeolocationCoordinates(37.7749, -122.4194)
1407+
>>> driver.emulation.set_geolocation_override(coordinates=coordinates, contexts=[context_id])
1408+
"""
1409+
if not self._websocket_connection:
1410+
self._start_bidi()
1411+
1412+
if self._emulation is None:
1413+
self._emulation = Emulation(self._websocket_connection)
1414+
1415+
return self._emulation
1416+
13931417
def _get_cdp_details(self):
13941418
import json
13951419

0 commit comments

Comments
 (0)