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

Open
wants to merge 27 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4eeef6b
implement bidi emulation module
navin772 May 29, 2025
caf7136
add tests
navin772 May 29, 2025
297ef0b
edge supports emulation from version 137
navin772 May 30, 2025
032f4c6
Merge branch 'trunk' into py-bidi-emulation
navin772 May 30, 2025
ae1383e
Merge branch 'trunk' into py-bidi-emulation
navin772 May 30, 2025
18eeca9
Merge branch 'trunk' of https://github.com/SeleniumHQ/selenium into p…
navin772 May 30, 2025
386917e
Merge branch 'trunk' into py-bidi-emulation
navin772 Jun 4, 2025
d9ab207
set geolocation permission
navin772 Jun 4, 2025
e75e23e
resolve mypy type errors
navin772 Jun 4, 2025
9a2dcfb
Merge branch 'trunk' into py-bidi-emulation
navin772 Jun 4, 2025
0aa9ba6
Merge branch 'trunk' into py-bidi-emulation
navin772 Jun 6, 2025
79ae9c4
Merge branch 'trunk' into py-bidi-emulation
navin772 Jun 8, 2025
96ce50a
Merge branch 'trunk' into py-bidi-emulation
navin772 Jun 10, 2025
4d87cbb
Merge branch 'trunk' into py-bidi-emulation
navin772 Jun 10, 2025
5ea382a
Merge branch 'trunk' into py-bidi-emulation
navin772 Jun 11, 2025
8ec5be6
Merge branch 'trunk' into py-bidi-emulation
navin772 Jun 20, 2025
d83bc9f
Merge branch 'trunk' of https://github.com/SeleniumHQ/selenium into p…
navin772 Jun 26, 2025
a6c1bd2
add test for `error` param
navin772 Jun 26, 2025
626a016
Merge remote-tracking branch 'origin/py-bidi-emulation' into py-bidi-…
navin772 Jun 26, 2025
363f1d4
Merge branch 'trunk' into py-bidi-emulation
cgoldberg Jun 29, 2025
a1c3460
Merge branch 'trunk' into py-bidi-emulation
Delta456 Jun 30, 2025
ee88b21
Merge branch 'trunk' into py-bidi-emulation
navin772 Jul 1, 2025
73a9b6c
use pinned browsers in CI
navin772 Jul 1, 2025
446f8ee
Merge branch 'trunk' into py-bidi-emulation
navin772 Jul 2, 2025
dbf9454
enable test for edge
navin772 Jul 2, 2025
59e4394
use native typing
navin772 Jul 2, 2025
0c46ee6
Merge branch 'trunk' into py-bidi-emulation
navin772 Jul 2, 2025
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
162 changes: 162 additions & 0 deletions py/selenium/webdriver/common/bidi/emulation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# 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 Any, Optional, Union

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[str, Union[float, None]]:
result: dict[str, Union[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


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[str, str]:
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: 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))
24 changes: 24 additions & 0 deletions py/selenium/webdriver/remote/webdriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,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.permissions import Permissions
from selenium.webdriver.common.bidi.script import Script
Expand Down Expand Up @@ -270,6 +271,7 @@ def __init__(
self._storage = None
self._webextension = None
self._permissions = None
self._emulation = None
self._devtools = None

def __repr__(self):
Expand Down Expand Up @@ -1390,6 +1392,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