Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
SeleniumHQ
GitHub Repository: SeleniumHQ/Selenium
Path: blob/trunk/py/selenium/webdriver/common/bidi/emulation.py
4057 views
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
from __future__ import annotations
18
19
from enum import Enum
20
from typing import TYPE_CHECKING, Any, TypeVar
21
22
from selenium.webdriver.common.bidi.common import command_builder
23
24
if TYPE_CHECKING:
25
from selenium.webdriver.remote.websocket_connection import WebSocketConnection
26
27
28
class ScreenOrientationNatural(Enum):
29
"""Natural screen orientation."""
30
31
PORTRAIT = "portrait"
32
LANDSCAPE = "landscape"
33
34
35
class ScreenOrientationType(Enum):
36
"""Screen orientation type."""
37
38
PORTRAIT_PRIMARY = "portrait-primary"
39
PORTRAIT_SECONDARY = "portrait-secondary"
40
LANDSCAPE_PRIMARY = "landscape-primary"
41
LANDSCAPE_SECONDARY = "landscape-secondary"
42
43
44
E = TypeVar("E", ScreenOrientationNatural, ScreenOrientationType)
45
46
47
def _convert_to_enum(value: E | str, enum_class: type[E]) -> E:
48
if isinstance(value, enum_class):
49
return value
50
assert isinstance(value, str)
51
try:
52
return enum_class(value.lower())
53
except ValueError:
54
raise ValueError(f"Invalid orientation: {value}")
55
56
57
class ScreenOrientation:
58
"""Represents screen orientation configuration."""
59
60
def __init__(
61
self,
62
natural: ScreenOrientationNatural | str,
63
type: ScreenOrientationType | str,
64
):
65
"""Initialize ScreenOrientation.
66
67
Args:
68
natural: Natural screen orientation ("portrait" or "landscape").
69
type: Screen orientation type ("portrait-primary", "portrait-secondary",
70
"landscape-primary", or "landscape-secondary").
71
72
Raises:
73
ValueError: If natural or type values are invalid.
74
"""
75
# handle string values
76
self.natural = _convert_to_enum(natural, ScreenOrientationNatural)
77
self.type = _convert_to_enum(type, ScreenOrientationType)
78
79
def to_dict(self) -> dict[str, str]:
80
return {
81
"natural": self.natural.value,
82
"type": self.type.value,
83
}
84
85
86
class GeolocationCoordinates:
87
"""Represents geolocation coordinates."""
88
89
def __init__(
90
self,
91
latitude: float,
92
longitude: float,
93
accuracy: float = 1.0,
94
altitude: float | None = None,
95
altitude_accuracy: float | None = None,
96
heading: float | None = None,
97
speed: float | None = None,
98
):
99
"""Initialize GeolocationCoordinates.
100
101
Args:
102
latitude: Latitude coordinate (-90.0 to 90.0).
103
longitude: Longitude coordinate (-180.0 to 180.0).
104
accuracy: Accuracy in meters (>= 0.0), defaults to 1.0.
105
altitude: Altitude in meters or None, defaults to None.
106
altitude_accuracy: Altitude accuracy in meters (>= 0.0) or None, defaults to None.
107
heading: Heading in degrees (0.0 to 360.0) or None, defaults to None.
108
speed: Speed in meters per second (>= 0.0) or None, defaults to None.
109
110
Raises:
111
ValueError: If coordinates are out of valid range or if altitude_accuracy is provided without altitude.
112
"""
113
self.latitude = latitude
114
self.longitude = longitude
115
self.accuracy = accuracy
116
self.altitude = altitude
117
self.altitude_accuracy = altitude_accuracy
118
self.heading = heading
119
self.speed = speed
120
121
@property
122
def latitude(self) -> float:
123
return self._latitude
124
125
@latitude.setter
126
def latitude(self, value: float) -> None:
127
if not (-90.0 <= value <= 90.0):
128
raise ValueError("latitude must be between -90.0 and 90.0")
129
self._latitude = value
130
131
@property
132
def longitude(self) -> float:
133
return self._longitude
134
135
@longitude.setter
136
def longitude(self, value: float) -> None:
137
if not (-180.0 <= value <= 180.0):
138
raise ValueError("longitude must be between -180.0 and 180.0")
139
self._longitude = value
140
141
@property
142
def accuracy(self) -> float:
143
return self._accuracy
144
145
@accuracy.setter
146
def accuracy(self, value: float) -> None:
147
if value < 0.0:
148
raise ValueError("accuracy must be >= 0.0")
149
self._accuracy = value
150
151
@property
152
def altitude(self) -> float | None:
153
return self._altitude
154
155
@altitude.setter
156
def altitude(self, value: float | None) -> None:
157
self._altitude = value
158
159
@property
160
def altitude_accuracy(self) -> float | None:
161
return self._altitude_accuracy
162
163
@altitude_accuracy.setter
164
def altitude_accuracy(self, value: float | None) -> None:
165
if value is not None and self.altitude is None:
166
raise ValueError("altitude_accuracy cannot be set without altitude")
167
if value is not None and value < 0.0:
168
raise ValueError("altitude_accuracy must be >= 0.0")
169
self._altitude_accuracy = value
170
171
@property
172
def heading(self) -> float | None:
173
return self._heading
174
175
@heading.setter
176
def heading(self, value: float | None) -> None:
177
if value is not None and not (0.0 <= value < 360.0):
178
raise ValueError("heading must be between 0.0 and 360.0")
179
self._heading = value
180
181
@property
182
def speed(self) -> float | None:
183
return self._speed
184
185
@speed.setter
186
def speed(self, value: float | None) -> None:
187
if value is not None and value < 0.0:
188
raise ValueError("speed must be >= 0.0")
189
self._speed = value
190
191
def to_dict(self) -> dict[str, float | None]:
192
result: dict[str, float | None] = {
193
"latitude": self.latitude,
194
"longitude": self.longitude,
195
"accuracy": self.accuracy,
196
}
197
198
if self.altitude is not None:
199
result["altitude"] = self.altitude
200
201
if self.altitude_accuracy is not None:
202
result["altitudeAccuracy"] = self.altitude_accuracy
203
204
if self.heading is not None:
205
result["heading"] = self.heading
206
207
if self.speed is not None:
208
result["speed"] = self.speed
209
210
return result
211
212
213
class GeolocationPositionError:
214
"""Represents a geolocation position error."""
215
216
TYPE_POSITION_UNAVAILABLE = "positionUnavailable"
217
218
def __init__(self, type: str = TYPE_POSITION_UNAVAILABLE):
219
if type != self.TYPE_POSITION_UNAVAILABLE:
220
raise ValueError(f'type must be "{self.TYPE_POSITION_UNAVAILABLE}"')
221
self.type = type
222
223
def to_dict(self) -> dict[str, str]:
224
return {"type": self.type}
225
226
227
class Emulation:
228
"""BiDi implementation of the emulation module."""
229
230
def __init__(self, conn: WebSocketConnection) -> None:
231
self.conn = conn
232
233
def set_geolocation_override(
234
self,
235
coordinates: GeolocationCoordinates | None = None,
236
error: GeolocationPositionError | None = None,
237
contexts: list[str] | None = None,
238
user_contexts: list[str] | None = None,
239
) -> None:
240
"""Set geolocation override for the given contexts or user contexts.
241
242
Args:
243
coordinates: Geolocation coordinates to emulate, or None.
244
error: Geolocation error to emulate, or None.
245
contexts: List of browsing context IDs to apply the override to.
246
user_contexts: List of user context IDs to apply the override to.
247
248
Raises:
249
ValueError: If both coordinates and error are provided, or if both contexts
250
and user_contexts are provided, or if neither contexts nor
251
user_contexts are provided.
252
"""
253
if coordinates is not None and error is not None:
254
raise ValueError("Cannot specify both coordinates and error")
255
256
if contexts is not None and user_contexts is not None:
257
raise ValueError("Cannot specify both contexts and userContexts")
258
259
if contexts is None and user_contexts is None:
260
raise ValueError("Must specify either contexts or userContexts")
261
262
params: dict[str, Any] = {}
263
264
if coordinates is not None:
265
params["coordinates"] = coordinates.to_dict()
266
elif error is not None:
267
params["error"] = error.to_dict()
268
269
if contexts is not None:
270
params["contexts"] = contexts
271
elif user_contexts is not None:
272
params["userContexts"] = user_contexts
273
274
self.conn.execute(command_builder("emulation.setGeolocationOverride", params))
275
276
def set_timezone_override(
277
self,
278
timezone: str | None = None,
279
contexts: list[str] | None = None,
280
user_contexts: list[str] | None = None,
281
) -> None:
282
"""Set timezone override for the given contexts or user contexts.
283
284
Args:
285
timezone: Timezone identifier (IANA timezone name or offset string like '+01:00'),
286
or None to clear the override.
287
contexts: List of browsing context IDs to apply the override to.
288
user_contexts: List of user context IDs to apply the override to.
289
290
Raises:
291
ValueError: If both contexts and user_contexts are provided, or if neither
292
contexts nor user_contexts are provided.
293
"""
294
if contexts is not None and user_contexts is not None:
295
raise ValueError("Cannot specify both contexts and user_contexts")
296
297
if contexts is None and user_contexts is None:
298
raise ValueError("Must specify either contexts or user_contexts")
299
300
params: dict[str, Any] = {"timezone": timezone}
301
302
if contexts is not None:
303
params["contexts"] = contexts
304
elif user_contexts is not None:
305
params["userContexts"] = user_contexts
306
307
self.conn.execute(command_builder("emulation.setTimezoneOverride", params))
308
309
def set_locale_override(
310
self,
311
locale: str | None = None,
312
contexts: list[str] | None = None,
313
user_contexts: list[str] | None = None,
314
) -> None:
315
"""Set locale override for the given contexts or user contexts.
316
317
Args:
318
locale: Locale string as per BCP 47, or None to clear override.
319
contexts: List of browsing context IDs to apply the override to.
320
user_contexts: List of user context IDs to apply the override to.
321
322
Raises:
323
ValueError: If both contexts and user_contexts are provided, or if neither
324
contexts nor user_contexts are provided, or if locale is invalid.
325
"""
326
if contexts is not None and user_contexts is not None:
327
raise ValueError("Cannot specify both contexts and userContexts")
328
329
if contexts is None and user_contexts is None:
330
raise ValueError("Must specify either contexts or userContexts")
331
332
params: dict[str, Any] = {"locale": locale}
333
334
if contexts is not None:
335
params["contexts"] = contexts
336
elif user_contexts is not None:
337
params["userContexts"] = user_contexts
338
339
self.conn.execute(command_builder("emulation.setLocaleOverride", params))
340
341
def set_scripting_enabled(
342
self,
343
enabled: bool | None = False,
344
contexts: list[str] | None = None,
345
user_contexts: list[str] | None = None,
346
) -> None:
347
"""Set scripting enabled override for the given contexts or user contexts.
348
349
Args:
350
enabled: False to disable scripting, None to clear the override.
351
Note: Only emulation of disabled JavaScript is supported.
352
contexts: List of browsing context IDs to apply the override to.
353
user_contexts: List of user context IDs to apply the override to.
354
355
Raises:
356
ValueError: If both contexts and user_contexts are provided, or if neither
357
contexts nor user_contexts are provided, or if enabled is True.
358
"""
359
if enabled:
360
raise ValueError("Only emulation of disabled JavaScript is supported (enabled must be False or None)")
361
362
if contexts is not None and user_contexts is not None:
363
raise ValueError("Cannot specify both contexts and userContexts")
364
365
if contexts is None and user_contexts is None:
366
raise ValueError("Must specify either contexts or userContexts")
367
368
params: dict[str, Any] = {"enabled": enabled}
369
370
if contexts is not None:
371
params["contexts"] = contexts
372
elif user_contexts is not None:
373
params["userContexts"] = user_contexts
374
375
self.conn.execute(command_builder("emulation.setScriptingEnabled", params))
376
377
def set_screen_orientation_override(
378
self,
379
screen_orientation: ScreenOrientation | None = None,
380
contexts: list[str] | None = None,
381
user_contexts: list[str] | None = None,
382
) -> None:
383
"""Set screen orientation override for the given contexts or user contexts.
384
385
Args:
386
screen_orientation: ScreenOrientation object to emulate, or None to clear the override.
387
contexts: List of browsing context IDs to apply the override to.
388
user_contexts: List of user context IDs to apply the override to.
389
390
Raises:
391
ValueError: If both contexts and user_contexts are provided, or if neither
392
contexts nor user_contexts are provided.
393
"""
394
if contexts is not None and user_contexts is not None:
395
raise ValueError("Cannot specify both contexts and userContexts")
396
397
if contexts is None and user_contexts is None:
398
raise ValueError("Must specify either contexts or userContexts")
399
400
params: dict[str, Any] = {
401
"screenOrientation": screen_orientation.to_dict() if screen_orientation is not None else None
402
}
403
404
if contexts is not None:
405
params["contexts"] = contexts
406
elif user_contexts is not None:
407
params["userContexts"] = user_contexts
408
409
self.conn.execute(command_builder("emulation.setScreenOrientationOverride", params))
410
411
def set_user_agent_override(
412
self,
413
user_agent: str | None = None,
414
contexts: list[str] | None = None,
415
user_contexts: list[str] | None = None,
416
) -> None:
417
"""Set user agent override for the given contexts or user contexts.
418
419
Args:
420
user_agent: User agent string to emulate, or None to clear the override.
421
contexts: List of browsing context IDs to apply the override to.
422
user_contexts: List of user context IDs to apply the override to.
423
424
Raises:
425
ValueError: If both contexts and user_contexts are provided, or if neither
426
contexts nor user_contexts are provided.
427
"""
428
if contexts is not None and user_contexts is not None:
429
raise ValueError("Cannot specify both contexts and user_contexts")
430
431
if contexts is None and user_contexts is None:
432
raise ValueError("Must specify either contexts or user_contexts")
433
434
params: dict[str, Any] = {"userAgent": user_agent}
435
436
if contexts is not None:
437
params["contexts"] = contexts
438
elif user_contexts is not None:
439
params["userContexts"] = user_contexts
440
441
self.conn.execute(command_builder("emulation.setUserAgentOverride", params))
442
443
def set_network_conditions(
444
self,
445
offline: bool = False,
446
contexts: list[str] | None = None,
447
user_contexts: list[str] | None = None,
448
) -> None:
449
"""Set network conditions for the given contexts or user contexts.
450
451
Args:
452
offline: True to emulate offline network conditions, False to clear the override.
453
contexts: List of browsing context IDs to apply the conditions to.
454
user_contexts: List of user context IDs to apply the conditions to.
455
456
Raises:
457
ValueError: If both contexts and user_contexts are provided, or if neither
458
contexts nor user_contexts are provided.
459
"""
460
if contexts is not None and user_contexts is not None:
461
raise ValueError("Cannot specify both contexts and user_contexts")
462
463
if contexts is None and user_contexts is None:
464
raise ValueError("Must specify either contexts or user_contexts")
465
466
params: dict[str, Any] = {}
467
468
if offline:
469
params["networkConditions"] = {"type": "offline"}
470
else:
471
# if offline is False or None, then clear the override
472
params["networkConditions"] = None
473
474
if contexts is not None:
475
params["contexts"] = contexts
476
elif user_contexts is not None:
477
params["userContexts"] = user_contexts
478
479
self.conn.execute(command_builder("emulation.setNetworkConditions", params))
480
481
def set_screen_settings_override(
482
self,
483
width: int | None = None,
484
height: int | None = None,
485
contexts: list[str] | None = None,
486
user_contexts: list[str] | None = None,
487
) -> None:
488
"""Set screen settings override for the given contexts or user contexts.
489
490
Args:
491
width: Screen width in pixels (>= 0). None to clear the override.
492
height: Screen height in pixels (>= 0). None to clear the override.
493
contexts: List of browsing context IDs to apply the override to.
494
user_contexts: List of user context IDs to apply the override to.
495
496
Raises:
497
ValueError: If only one of width/height is provided, or if both contexts
498
and user_contexts are provided, or if neither is provided.
499
"""
500
if (width is None) != (height is None):
501
raise ValueError("Must provide both width and height, or neither to clear the override")
502
503
if contexts is not None and user_contexts is not None:
504
raise ValueError("Cannot specify both contexts and user_contexts")
505
506
if contexts is None and user_contexts is None:
507
raise ValueError("Must specify either contexts or user_contexts")
508
509
screen_area = None
510
if width is not None and height is not None:
511
if not isinstance(width, int) or not isinstance(height, int):
512
raise ValueError("width and height must be integers")
513
if width < 0 or height < 0:
514
raise ValueError("width and height must be >= 0")
515
screen_area = {"width": width, "height": height}
516
517
params: dict[str, Any] = {"screenArea": screen_area}
518
519
if contexts is not None:
520
params["contexts"] = contexts
521
elif user_contexts is not None:
522
params["userContexts"] = user_contexts
523
524
self.conn.execute(command_builder("emulation.setScreenSettingsOverride", params))
525
526