Skip to content

Commit 4eeef6b

Browse files
committed
implement bidi emulation module
1 parent 8fdc63a commit 4eeef6b

File tree

2 files changed

+194
-0
lines changed

2 files changed

+194
-0
lines changed
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
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 Dict, List, Optional
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:
76+
result = {
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+
else:
85+
result["altitude"] = None
86+
87+
if self.altitude_accuracy is not None:
88+
result["altitudeAccuracy"] = self.altitude_accuracy
89+
else:
90+
result["altitudeAccuracy"] = None
91+
92+
if self.heading is not None:
93+
result["heading"] = self.heading
94+
else:
95+
result["heading"] = None
96+
97+
if self.speed is not None:
98+
result["speed"] = self.speed
99+
else:
100+
result["speed"] = None
101+
102+
return result
103+
104+
105+
class GeolocationPositionError:
106+
"""Represents a geolocation position error."""
107+
108+
TYPE_POSITION_UNAVAILABLE = "positionUnavailable"
109+
110+
def __init__(self, type: str = TYPE_POSITION_UNAVAILABLE):
111+
if type != self.TYPE_POSITION_UNAVAILABLE:
112+
raise ValueError(f'type must be "{self.TYPE_POSITION_UNAVAILABLE}"')
113+
self.type = type
114+
115+
def to_dict(self) -> Dict:
116+
return {"type": self.type}
117+
118+
119+
class Emulation:
120+
"""
121+
BiDi implementation of the emulation module.
122+
"""
123+
124+
def __init__(self, conn):
125+
self.conn = conn
126+
127+
def set_geolocation_override(
128+
self,
129+
coordinates: Optional[GeolocationCoordinates] = None,
130+
error: Optional[GeolocationPositionError] = None,
131+
contexts: Optional[List[str]] = None,
132+
user_contexts: Optional[List[str]] = None,
133+
) -> None:
134+
"""Set geolocation override for the given contexts or user contexts.
135+
136+
Parameters:
137+
-----------
138+
coordinates: Geolocation coordinates to emulate, or None.
139+
error: Geolocation error to emulate, or None.
140+
contexts: List of browsing context IDs to apply the override to.
141+
user_contexts: List of user context IDs to apply the override to.
142+
143+
Raises:
144+
------
145+
ValueError: If both coordinates and error are provided, or if both contexts
146+
and user_contexts are provided, or if neither contexts nor
147+
user_contexts are provided.
148+
"""
149+
if coordinates is not None and error is not None:
150+
raise ValueError("Cannot specify both coordinates and error")
151+
152+
if contexts is not None and user_contexts is not None:
153+
raise ValueError("Cannot specify both contexts and userContexts")
154+
155+
if contexts is None and user_contexts is None:
156+
raise ValueError("Must specify either contexts or userContexts")
157+
158+
params = {}
159+
160+
if coordinates is not None:
161+
params["coordinates"] = coordinates.to_dict()
162+
elif error is not None:
163+
params["error"] = error.to_dict()
164+
165+
if contexts is not None:
166+
params["contexts"] = contexts
167+
elif user_contexts is not None:
168+
params["userContexts"] = user_contexts
169+
170+
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
@@ -41,6 +41,7 @@
4141
)
4242
from selenium.webdriver.common.bidi.browser import Browser
4343
from selenium.webdriver.common.bidi.browsing_context import BrowsingContext
44+
from selenium.webdriver.common.bidi.emulation import Emulation
4445
from selenium.webdriver.common.bidi.network import Network
4546
from selenium.webdriver.common.bidi.script import Script
4647
from selenium.webdriver.common.bidi.session import Session
@@ -265,6 +266,7 @@ def __init__(
265266
self._browsing_context = None
266267
self._storage = None
267268
self._webextension = None
269+
self._emulation = None
268270

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

13621364
return self._webextension
13631365

1366+
@property
1367+
def emulation(self):
1368+
"""Returns an emulation module object for BiDi emulation commands.
1369+
1370+
Returns:
1371+
--------
1372+
Emulation: an object containing access to BiDi emulation commands.
1373+
1374+
Examples:
1375+
---------
1376+
>>> from selenium.webdriver.common.bidi.emulation import GeolocationCoordinates
1377+
>>> coordinates = GeolocationCoordinates(37.7749, -122.4194)
1378+
>>> driver.emulation.set_geolocation_override(coordinates=coordinates, contexts=[context_id])
1379+
"""
1380+
if not self._websocket_connection:
1381+
self._start_bidi()
1382+
1383+
if self._emulation is None:
1384+
self._emulation = Emulation(self._websocket_connection)
1385+
1386+
return self._emulation
1387+
13641388
def _get_cdp_details(self):
13651389
import json
13661390

0 commit comments

Comments
 (0)