Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
SeleniumHQ
GitHub Repository: SeleniumHQ/Selenium
Path: blob/trunk/py/selenium/webdriver/remote/webdriver.py
4004 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
18
"""The WebDriver implementation."""
19
20
import base64
21
import contextlib
22
import copy
23
import os
24
import pkgutil
25
import tempfile
26
import types
27
import warnings
28
import zipfile
29
from abc import ABCMeta
30
from base64 import b64decode, urlsafe_b64encode
31
from contextlib import asynccontextmanager, contextmanager
32
from importlib import import_module
33
from typing import Any, cast
34
35
from selenium.common.exceptions import (
36
InvalidArgumentException,
37
JavascriptException,
38
NoSuchCookieException,
39
NoSuchElementException,
40
WebDriverException,
41
)
42
from selenium.webdriver.common.bidi.browser import Browser
43
from selenium.webdriver.common.bidi.browsing_context import BrowsingContext
44
from selenium.webdriver.common.bidi.emulation import Emulation
45
from selenium.webdriver.common.bidi.input import Input
46
from selenium.webdriver.common.bidi.network import Network
47
from selenium.webdriver.common.bidi.permissions import Permissions
48
from selenium.webdriver.common.bidi.script import Script
49
from selenium.webdriver.common.bidi.session import Session
50
from selenium.webdriver.common.bidi.storage import Storage
51
from selenium.webdriver.common.bidi.webextension import WebExtension
52
from selenium.webdriver.common.by import By
53
from selenium.webdriver.common.fedcm.dialog import Dialog
54
from selenium.webdriver.common.options import ArgOptions, BaseOptions
55
from selenium.webdriver.common.print_page_options import PrintOptions
56
from selenium.webdriver.common.timeouts import Timeouts
57
from selenium.webdriver.common.virtual_authenticator import (
58
Credential,
59
VirtualAuthenticatorOptions,
60
required_virtual_authenticator,
61
)
62
from selenium.webdriver.remote.bidi_connection import BidiConnection
63
from selenium.webdriver.remote.client_config import ClientConfig
64
from selenium.webdriver.remote.command import Command
65
from selenium.webdriver.remote.errorhandler import ErrorHandler
66
from selenium.webdriver.remote.fedcm import FedCM
67
from selenium.webdriver.remote.file_detector import FileDetector, LocalFileDetector
68
from selenium.webdriver.remote.locator_converter import LocatorConverter
69
from selenium.webdriver.remote.mobile import Mobile
70
from selenium.webdriver.remote.remote_connection import RemoteConnection
71
from selenium.webdriver.remote.script_key import ScriptKey
72
from selenium.webdriver.remote.shadowroot import ShadowRoot
73
from selenium.webdriver.remote.switch_to import SwitchTo
74
from selenium.webdriver.remote.webelement import WebElement
75
from selenium.webdriver.remote.websocket_connection import WebSocketConnection
76
from selenium.webdriver.support.relative_locator import RelativeBy
77
78
cdp = None
79
80
81
def import_cdp() -> None:
82
global cdp
83
if not cdp:
84
cdp = import_module("selenium.webdriver.common.bidi.cdp")
85
86
87
def _create_caps(caps) -> dict:
88
"""Makes a W3C alwaysMatch capabilities object.
89
90
Filters out capability names that are not in the W3C spec. Spec-compliant
91
drivers will reject requests containing unknown capability names.
92
93
Moves the Firefox profile, if present, from the old location to the new Firefox
94
options object.
95
96
Args:
97
caps: A dictionary of capabilities requested by the caller.
98
"""
99
caps = copy.deepcopy(caps)
100
always_match = {}
101
for k, v in caps.items():
102
always_match[k] = v
103
return {"capabilities": {"firstMatch": [{}], "alwaysMatch": always_match}}
104
105
106
def get_remote_connection(
107
capabilities: dict,
108
command_executor: str | RemoteConnection,
109
keep_alive: bool,
110
ignore_local_proxy: bool,
111
client_config: ClientConfig | None = None,
112
) -> RemoteConnection:
113
if isinstance(command_executor, str):
114
client_config = client_config or ClientConfig(remote_server_addr=command_executor)
115
client_config.remote_server_addr = command_executor
116
command_executor = RemoteConnection(client_config=client_config)
117
118
browser_name = capabilities.get("browserName")
119
handler: type[RemoteConnection]
120
if browser_name == "chrome":
121
from selenium.webdriver.chrome.remote_connection import ChromeRemoteConnection
122
123
handler = ChromeRemoteConnection
124
elif browser_name == "MicrosoftEdge":
125
from selenium.webdriver.edge.remote_connection import EdgeRemoteConnection
126
127
handler = EdgeRemoteConnection
128
elif browser_name == "firefox":
129
from selenium.webdriver.firefox.remote_connection import FirefoxRemoteConnection
130
131
handler = FirefoxRemoteConnection
132
elif browser_name == "Safari":
133
from selenium.webdriver.safari.remote_connection import SafariRemoteConnection
134
135
handler = SafariRemoteConnection
136
else:
137
handler = RemoteConnection
138
139
if hasattr(command_executor, "client_config") and command_executor.client_config:
140
remote_server_addr = command_executor.client_config.remote_server_addr
141
else:
142
remote_server_addr = command_executor
143
144
return handler(
145
remote_server_addr=remote_server_addr,
146
keep_alive=keep_alive,
147
ignore_proxy=ignore_local_proxy,
148
client_config=client_config,
149
)
150
151
152
def create_matches(options: list[BaseOptions]) -> dict:
153
capabilities: dict[str, Any] = {"capabilities": {}}
154
opts = []
155
for opt in options:
156
opts.append(opt.to_capabilities())
157
opts_size = len(opts)
158
samesies = {}
159
160
# Can not use bitwise operations on the dicts or lists due to
161
# https://bugs.python.org/issue38210
162
for i in range(opts_size):
163
min_index = i
164
if i + 1 < opts_size:
165
first_keys = opts[min_index].keys()
166
167
for kys in first_keys:
168
if kys in opts[i + 1].keys():
169
if opts[min_index][kys] == opts[i + 1][kys]:
170
samesies.update({kys: opts[min_index][kys]})
171
172
always = {}
173
for k, v in samesies.items():
174
always[k] = v
175
176
for opt_dict in opts:
177
for k in always:
178
del opt_dict[k]
179
180
capabilities["capabilities"]["alwaysMatch"] = always
181
capabilities["capabilities"]["firstMatch"] = opts
182
183
return capabilities
184
185
186
class BaseWebDriver(metaclass=ABCMeta):
187
"""Abstract Base Class for all Webdriver subtypes.
188
189
ABC's allow custom implementations of Webdriver to be registered so
190
that isinstance type checks will succeed.
191
"""
192
193
194
class WebDriver(BaseWebDriver):
195
"""Control a browser by sending commands to a remote WebDriver server.
196
197
This class expects the remote server to be running the WebDriver wire protocol
198
as defined at https://www.selenium.dev/documentation/legacy/json_wire_protocol/.
199
200
Attributes:
201
-----------
202
session_id - String ID of the browser session started and controlled by this WebDriver.
203
capabilities - Dictionary of effective capabilities of this browser session as returned
204
by the remote server. See https://www.selenium.dev/documentation/legacy/desired_capabilities/
205
command_executor : str or remote_connection.RemoteConnection object used to execute commands.
206
error_handler - errorhandler.ErrorHandler object used to handle errors.
207
"""
208
209
_web_element_cls = WebElement
210
_shadowroot_cls = ShadowRoot
211
212
def __init__(
213
self,
214
command_executor: str | RemoteConnection = "http://127.0.0.1:4444",
215
keep_alive: bool = True,
216
file_detector: FileDetector | None = None,
217
options: BaseOptions | list[BaseOptions] | None = None,
218
locator_converter: LocatorConverter | None = None,
219
web_element_cls: type[WebElement] | None = None,
220
client_config: ClientConfig | None = None,
221
) -> None:
222
"""Create a new driver instance that issues commands using the WebDriver protocol.
223
224
Args:
225
command_executor: Either a string representing the URL of the remote
226
server or a custom remote_connection.RemoteConnection object.
227
Defaults to 'http://127.0.0.1:4444/wd/hub'.
228
keep_alive: (Deprecated) Whether to configure
229
remote_connection.RemoteConnection to use HTTP keep-alive.
230
Defaults to True.
231
file_detector: Pass a custom file detector object during
232
instantiation. If None, the default LocalFileDetector() will be
233
used.
234
options: Instance of a driver options.Options class.
235
locator_converter: Custom locator converter to use. Defaults to None.
236
web_element_cls: Custom class to use for web elements. Defaults to
237
WebElement.
238
client_config: Custom client configuration to use. Defaults to None.
239
"""
240
if options is None:
241
raise TypeError(
242
"missing 1 required keyword-only argument: 'options' (instance of driver `options.Options` class)"
243
)
244
elif isinstance(options, list):
245
capabilities = create_matches(options)
246
_ignore_local_proxy = False
247
else:
248
capabilities = options.to_capabilities()
249
_ignore_local_proxy = options._ignore_local_proxy
250
self.command_executor = command_executor
251
if isinstance(self.command_executor, (str, bytes)):
252
self.command_executor = get_remote_connection(
253
capabilities,
254
command_executor=command_executor,
255
keep_alive=keep_alive,
256
ignore_local_proxy=_ignore_local_proxy,
257
client_config=client_config,
258
)
259
self._is_remote = True
260
self.session_id: str | None = None
261
self.caps: dict[str, Any] = {}
262
self.pinned_scripts: dict[str, Any] = {}
263
self.error_handler = ErrorHandler()
264
self._switch_to = SwitchTo(self)
265
self._mobile = Mobile(self)
266
self.file_detector = file_detector or LocalFileDetector()
267
self.locator_converter = locator_converter or LocatorConverter()
268
self._web_element_cls = web_element_cls or self._web_element_cls
269
self._authenticator_id = None
270
self.start_client()
271
self.start_session(capabilities)
272
self._fedcm = FedCM(self)
273
274
self._websocket_connection: WebSocketConnection | None = None
275
self._script: Script | None = None
276
self._network: Network | None = None
277
self._browser: Browser | None = None
278
self._bidi_session: Session | None = None
279
self._browsing_context: BrowsingContext | None = None
280
self._storage: Storage | None = None
281
self._webextension: WebExtension | None = None
282
self._permissions: Permissions | None = None
283
self._emulation: Emulation | None = None
284
self._input: Input | None = None
285
self._devtools: Any | None = None
286
287
def __repr__(self) -> str:
288
return f'<{type(self).__module__}.{type(self).__name__} (session="{self.session_id}")>'
289
290
def __enter__(self) -> "WebDriver":
291
return self
292
293
def __exit__(
294
self,
295
exc_type: type[BaseException] | None,
296
exc: BaseException | None,
297
traceback: types.TracebackType | None,
298
):
299
self.quit()
300
301
@contextmanager
302
def file_detector_context(self, file_detector_class, *args, **kwargs):
303
"""Override the current file detector temporarily within a limited context.
304
305
Ensures the original file detector is set after exiting the context.
306
307
Args:
308
file_detector_class: Class of the desired file detector. If the
309
class is different from the current file_detector, then the
310
class is instantiated with args and kwargs and used as a file
311
detector during the duration of the context manager.
312
*args: Optional arguments that get passed to the file detector class
313
during instantiation.
314
**kwargs: Keyword arguments, passed the same way as args.
315
316
Example:
317
```
318
with webdriver.file_detector_context(UselessFileDetector):
319
someinput.send_keys("/etc/hosts")
320
````
321
"""
322
last_detector = None
323
if not isinstance(self.file_detector, file_detector_class):
324
last_detector = self.file_detector
325
self.file_detector = file_detector_class(*args, **kwargs)
326
try:
327
yield
328
finally:
329
if last_detector:
330
self.file_detector = last_detector
331
332
@property
333
def mobile(self) -> Mobile:
334
return self._mobile
335
336
@property
337
def name(self) -> str:
338
"""Returns the name of the underlying browser for this instance."""
339
if "browserName" in self.caps:
340
return self.caps["browserName"]
341
raise KeyError("browserName not specified in session capabilities")
342
343
def start_client(self) -> None:
344
"""Called before starting a new session.
345
346
This method may be overridden to define custom startup behavior.
347
"""
348
pass
349
350
def stop_client(self) -> None:
351
"""Called after executing a quit command.
352
353
This method may be overridden to define custom shutdown
354
behavior.
355
"""
356
pass
357
358
def start_session(self, capabilities: dict) -> None:
359
"""Creates a new session with the desired capabilities.
360
361
Args:
362
capabilities: A capabilities dict to start the session with.
363
"""
364
caps = _create_caps(capabilities)
365
try:
366
response = self.execute(Command.NEW_SESSION, caps)["value"]
367
self.session_id = response.get("sessionId")
368
self.caps = response.get("capabilities")
369
except Exception:
370
if hasattr(self, "service") and self.service is not None:
371
self.service.stop()
372
raise
373
374
def _wrap_value(self, value):
375
if isinstance(value, dict):
376
converted = {}
377
for key, val in value.items():
378
converted[key] = self._wrap_value(val)
379
return converted
380
if isinstance(value, self._web_element_cls):
381
return {"element-6066-11e4-a52e-4f735466cecf": value.id}
382
if isinstance(value, self._shadowroot_cls):
383
return {"shadow-6066-11e4-a52e-4f735466cecf": value.id}
384
if isinstance(value, list):
385
return list(self._wrap_value(item) for item in value)
386
return value
387
388
def create_web_element(self, element_id: str) -> WebElement:
389
"""Creates a web element with the specified `element_id`."""
390
return self._web_element_cls(self, element_id)
391
392
def _unwrap_value(self, value):
393
if isinstance(value, dict):
394
if "element-6066-11e4-a52e-4f735466cecf" in value:
395
return self.create_web_element(value["element-6066-11e4-a52e-4f735466cecf"])
396
if "shadow-6066-11e4-a52e-4f735466cecf" in value:
397
return self._shadowroot_cls(self, value["shadow-6066-11e4-a52e-4f735466cecf"])
398
for key, val in value.items():
399
value[key] = self._unwrap_value(val)
400
return value
401
if isinstance(value, list):
402
return list(self._unwrap_value(item) for item in value)
403
return value
404
405
def execute_cdp_cmd(self, cmd: str, cmd_args: dict):
406
"""Execute Chrome Devtools Protocol command and get returned result.
407
408
The command and command args should follow chrome devtools protocol domains/commands:
409
- https://chromedevtools.github.io/devtools-protocol/
410
411
Args:
412
cmd: Command name.
413
cmd_args: Command args. Empty dict {} if there is no command args.
414
415
Returns:
416
A dict, empty dict {} if there is no result to return. To
417
getResponseBody: {'base64Encoded': False, 'body': 'response body
418
string'}
419
420
Example:
421
`driver.execute_cdp_cmd("Network.getResponseBody", {"requestId": requestId})`
422
"""
423
return self.execute("executeCdpCommand", {"cmd": cmd, "params": cmd_args})["value"]
424
425
def execute(self, driver_command: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
426
"""Sends a command to be executed by a command.CommandExecutor.
427
428
Args:
429
driver_command: The name of the command to execute as a string.
430
params: A dictionary of named parameters to send with the command.
431
432
Returns:
433
The command's JSON response loaded into a dictionary object.
434
"""
435
params = self._wrap_value(params)
436
437
if self.session_id:
438
if not params:
439
params = {"sessionId": self.session_id}
440
elif "sessionId" not in params:
441
params["sessionId"] = self.session_id
442
443
response = cast(RemoteConnection, self.command_executor).execute(driver_command, params)
444
445
if response:
446
self.error_handler.check_response(response)
447
response["value"] = self._unwrap_value(response.get("value", None))
448
return response
449
# If the server doesn't send a response, assume the command was
450
# a success
451
return {"success": 0, "value": None, "sessionId": self.session_id}
452
453
def get(self, url: str) -> None:
454
"""Navigate the browser to the specified URL.
455
456
The method does not return until the page is fully loaded (i.e. the
457
onload event has fired) in the current window or tab.
458
459
Args:
460
url: The URL to be opened by the browser. Must include the protocol
461
(e.g., http://, https://).
462
463
Example:
464
`driver.get("https://example.com")`
465
"""
466
self.execute(Command.GET, {"url": url})
467
468
@property
469
def title(self) -> str:
470
"""Returns the title of the current page.
471
472
Example:
473
```
474
element = driver.find_element(By.ID, "foo")
475
print(element.title())
476
```
477
"""
478
return self.execute(Command.GET_TITLE).get("value", "")
479
480
def pin_script(self, script: str, script_key=None) -> ScriptKey:
481
"""Store a JavaScript script by a unique hashable ID for later execution.
482
483
Example:
484
`script = "return document.getElementById('foo').value"`
485
"""
486
script_key_instance = ScriptKey(script_key)
487
self.pinned_scripts[script_key_instance.id] = script
488
return script_key_instance
489
490
def unpin(self, script_key: ScriptKey) -> None:
491
"""Remove a pinned script from storage.
492
493
Example:
494
`driver.unpin(script_key)`
495
"""
496
try:
497
self.pinned_scripts.pop(script_key.id)
498
except KeyError:
499
raise KeyError(f"No script with key: {script_key} existed in {self.pinned_scripts}") from None
500
501
def get_pinned_scripts(self) -> list[str]:
502
"""Return a list of all pinned scripts.
503
504
Example:
505
`pinned_scripts = driver.get_pinned_scripts()`
506
"""
507
return list(self.pinned_scripts)
508
509
def execute_script(self, script: str, *args):
510
"""Synchronously Executes JavaScript in the current window/frame.
511
512
Args:
513
script: The javascript to execute.
514
*args: Any applicable arguments for your JavaScript.
515
516
Example:
517
```
518
id = "username"
519
value = "test_user"
520
driver.execute_script("document.getElementById(arguments[0]).value = arguments[1];", id, value)
521
```
522
"""
523
if isinstance(script, ScriptKey):
524
try:
525
script = self.pinned_scripts[script.id]
526
except KeyError:
527
raise JavascriptException("Pinned script could not be found")
528
529
converted_args = list(args)
530
command = Command.W3C_EXECUTE_SCRIPT
531
532
return self.execute(command, {"script": script, "args": converted_args})["value"]
533
534
def execute_async_script(self, script: str, *args) -> dict:
535
"""Asynchronously Executes JavaScript in the current window/frame.
536
537
Args:
538
script: The javascript to execute.
539
*args: Any applicable arguments for your JavaScript.
540
541
Example:
542
```
543
script = "var callback = arguments[arguments.length - 1]; "
544
"window.setTimeout(function(){ callback('timeout') }, 3000);"
545
driver.execute_async_script(script)
546
```
547
"""
548
converted_args = list(args)
549
command = Command.W3C_EXECUTE_SCRIPT_ASYNC
550
551
return self.execute(command, {"script": script, "args": converted_args})["value"]
552
553
@property
554
def current_url(self) -> str:
555
"""Gets the URL of the current page."""
556
return self.execute(Command.GET_CURRENT_URL)["value"]
557
558
@property
559
def page_source(self) -> str:
560
"""Gets the source of the current page."""
561
return self.execute(Command.GET_PAGE_SOURCE)["value"]
562
563
def close(self) -> None:
564
"""Closes the current window."""
565
self.execute(Command.CLOSE)
566
567
def quit(self) -> None:
568
"""Quits the driver and closes every associated window."""
569
try:
570
self.execute(Command.QUIT)
571
finally:
572
self.stop_client()
573
executor = cast(RemoteConnection, self.command_executor)
574
executor.close()
575
576
@property
577
def current_window_handle(self) -> str:
578
"""Returns the handle of the current window."""
579
return self.execute(Command.W3C_GET_CURRENT_WINDOW_HANDLE)["value"]
580
581
@property
582
def window_handles(self) -> list[str]:
583
"""Returns the handles of all windows within the current session."""
584
return self.execute(Command.W3C_GET_WINDOW_HANDLES)["value"]
585
586
def maximize_window(self) -> None:
587
"""Maximizes the current window that webdriver is using."""
588
command = Command.W3C_MAXIMIZE_WINDOW
589
self.execute(command, None)
590
591
def fullscreen_window(self) -> None:
592
"""Invokes the window manager-specific 'full screen' operation."""
593
self.execute(Command.FULLSCREEN_WINDOW)
594
595
def minimize_window(self) -> None:
596
"""Invokes the window manager-specific 'minimize' operation."""
597
self.execute(Command.MINIMIZE_WINDOW)
598
599
def print_page(self, print_options: PrintOptions | None = None) -> str:
600
"""Takes PDF of the current page.
601
602
The driver makes a best effort to return a PDF based on the
603
provided parameters.
604
"""
605
options: dict[str, Any] | Any = {}
606
if print_options:
607
options = print_options.to_dict()
608
609
return self.execute(Command.PRINT_PAGE, options)["value"]
610
611
@property
612
def switch_to(self) -> SwitchTo:
613
"""Return an object containing all options to switch focus into.
614
615
Returns:
616
An object containing all options to switch focus into.
617
618
Examples:
619
`element = driver.switch_to.active_element`
620
`alert = driver.switch_to.alert`
621
`driver.switch_to.default_content()`
622
`driver.switch_to.frame("frame_name")`
623
`driver.switch_to.frame(1)`
624
`driver.switch_to.frame(driver.find_elements(By.TAG_NAME, "iframe")[0])`
625
`driver.switch_to.parent_frame()`
626
`driver.switch_to.window("main")`
627
"""
628
return self._switch_to
629
630
# Navigation
631
def back(self) -> None:
632
"""Goes one step backward in the browser history."""
633
self.execute(Command.GO_BACK)
634
635
def forward(self) -> None:
636
"""Goes one step forward in the browser history."""
637
self.execute(Command.GO_FORWARD)
638
639
def refresh(self) -> None:
640
"""Refreshes the current page."""
641
self.execute(Command.REFRESH)
642
643
def get_cookies(self) -> list[dict]:
644
"""Get all cookies visible to the current WebDriver instance.
645
646
Returns:
647
A list of dictionaries, corresponding to cookies visible in the
648
current session.
649
"""
650
return self.execute(Command.GET_ALL_COOKIES)["value"]
651
652
def get_cookie(self, name) -> dict | None:
653
"""Get a single cookie by name (case-sensitive,).
654
655
Returns:
656
A cookie dictionary or None if not found.
657
658
Raises:
659
ValueError if the name is empty or whitespace.
660
661
Example:
662
`cookie = driver.get_cookie("my_cookie")`
663
"""
664
if not name or name.isspace():
665
raise ValueError("Cookie name cannot be empty")
666
667
with contextlib.suppress(NoSuchCookieException):
668
return self.execute(Command.GET_COOKIE, {"name": name})["value"]
669
670
return None
671
672
def delete_cookie(self, name) -> None:
673
"""Delete a single cookie with the given name (case-sensitive).
674
675
Raises:
676
ValueError if the name is empty or whitespace.
677
678
Example:
679
`driver.delete_cookie("my_cookie")`
680
"""
681
# Firefox deletes all cookies when "" is passed as name
682
if not name or name.isspace():
683
raise ValueError("Cookie name cannot be empty")
684
685
self.execute(Command.DELETE_COOKIE, {"name": name})
686
687
def delete_all_cookies(self) -> None:
688
"""Delete all cookies in the scope of the session."""
689
self.execute(Command.DELETE_ALL_COOKIES)
690
691
def add_cookie(self, cookie_dict) -> None:
692
"""Adds a cookie to your current session.
693
694
Args:
695
cookie_dict: A dictionary object, with required keys - "name" and
696
"value"; Optional keys - "path", "domain", "secure", "httpOnly",
697
"expiry", "sameSite".
698
699
Examples:
700
`driver.add_cookie({"name": "foo", "value": "bar"})`
701
`driver.add_cookie({"name": "foo", "value": "bar", "path": "/"})`
702
`driver.add_cookie({"name": "foo", "value": "bar", "path": "/", "secure": True})`
703
`driver.add_cookie({"name": "foo", "value": "bar", "sameSite": "Strict"})`
704
"""
705
if "sameSite" in cookie_dict:
706
assert cookie_dict["sameSite"] in ["Strict", "Lax", "None"]
707
self.execute(Command.ADD_COOKIE, {"cookie": cookie_dict})
708
else:
709
self.execute(Command.ADD_COOKIE, {"cookie": cookie_dict})
710
711
# Timeouts
712
def implicitly_wait(self, time_to_wait: float) -> None:
713
"""Set a sticky implicit timeout for element location and command completion.
714
715
This method sets a timeout that applies to all element location strategies
716
for the duration of the session. It only needs to be called once per session.
717
To set the timeout for asynchronous script execution, see set_script_timeout.
718
719
Args:
720
time_to_wait: Amount of time to wait (in seconds).
721
722
Example:
723
`driver.implicitly_wait(30)`
724
"""
725
self.execute(Command.SET_TIMEOUTS, {"implicit": int(float(time_to_wait) * 1000)})
726
727
def set_script_timeout(self, time_to_wait: float) -> None:
728
"""Set the timeout for asynchronous script execution.
729
730
This timeout specifies how long a script can run during an
731
execute_async_script call before throwing an error.
732
733
Args:
734
time_to_wait: The amount of time to wait (in seconds).
735
736
Example:
737
`driver.set_script_timeout(30)`
738
"""
739
self.execute(Command.SET_TIMEOUTS, {"script": int(float(time_to_wait) * 1000)})
740
741
def set_page_load_timeout(self, time_to_wait: float) -> None:
742
"""Set the timeout for page load completion.
743
744
This specifies how long to wait for a page load to complete before
745
throwing an error.
746
747
Args:
748
time_to_wait: The amount of time to wait (in seconds).
749
750
Example:
751
`driver.set_page_load_timeout(30)`
752
"""
753
try:
754
self.execute(Command.SET_TIMEOUTS, {"pageLoad": int(float(time_to_wait) * 1000)})
755
except WebDriverException:
756
self.execute(Command.SET_TIMEOUTS, {"ms": float(time_to_wait) * 1000, "type": "page load"})
757
758
@property
759
def timeouts(self) -> Timeouts:
760
"""Get all the timeouts that have been set on the current session.
761
762
Returns:
763
A named tuple with the following fields:
764
- implicit_wait: The time to wait for elements to be found.
765
- page_load: The time to wait for a page to load.
766
- script: The time to wait for scripts to execute.
767
768
Example:
769
`driver.timeouts`
770
"""
771
timeouts = self.execute(Command.GET_TIMEOUTS)["value"]
772
timeouts["implicit_wait"] = timeouts.pop("implicit") / 1000
773
timeouts["page_load"] = timeouts.pop("pageLoad") / 1000
774
timeouts["script"] = timeouts.pop("script") / 1000
775
return Timeouts(**timeouts)
776
777
@timeouts.setter
778
def timeouts(self, timeouts) -> None:
779
"""Set all timeouts for the session.
780
781
This will override any previously set timeouts.
782
783
Example:
784
```
785
my_timeouts = Timeouts()
786
my_timeouts.implicit_wait = 10
787
driver.timeouts = my_timeouts
788
```
789
"""
790
_ = self.execute(Command.SET_TIMEOUTS, timeouts._to_json())["value"]
791
792
def find_element(self, by: str | RelativeBy = By.ID, value: str | None = None) -> WebElement:
793
"""Find an element given a By strategy and locator.
794
795
Args:
796
by: The locating strategy to use. Default is `By.ID`. Supported
797
values include: By.ID, By.NAME, By.XPATH, By.CSS_SELECTOR,
798
By.CLASS_NAME, By.TAG_NAME, By.LINK_TEXT, By.PARTIAL_LINK_TEXT,
799
or RelativeBy.
800
value: The locator value to use with the specified `by` strategy.
801
802
Returns:
803
The first matching WebElement found on the page.
804
805
Example:
806
`element = driver.find_element(By.ID, 'foo')`
807
"""
808
by, value = self.locator_converter.convert(by, value)
809
810
if isinstance(by, RelativeBy):
811
elements = self.find_elements(by=by, value=value)
812
if not elements:
813
raise NoSuchElementException(f"Cannot locate relative element with: {by.root}")
814
return elements[0]
815
816
return self.execute(Command.FIND_ELEMENT, {"using": by, "value": value})["value"]
817
818
def find_elements(self, by: str | RelativeBy = By.ID, value: str | None = None) -> list[WebElement]:
819
"""Find elements given a By strategy and locator.
820
821
Args:
822
by: The locating strategy to use. Default is `By.ID`. Supported
823
values include: By.ID, By.NAME, By.XPATH, By.CSS_SELECTOR,
824
By.CLASS_NAME, By.TAG_NAME, By.LINK_TEXT, By.PARTIAL_LINK_TEXT,
825
or RelativeBy.
826
value: The locator value to use with the specified `by` strategy.
827
828
Returns:
829
List of WebElements matching locator strategy found on the page.
830
831
Example:
832
`element = driver.find_elements(By.ID, 'foo')`
833
"""
834
by, value = self.locator_converter.convert(by, value)
835
836
if isinstance(by, RelativeBy):
837
_pkg = ".".join(__name__.split(".")[:-1])
838
raw_data = pkgutil.get_data(_pkg, "findElements.js")
839
if raw_data is None:
840
raise FileNotFoundError(f"Could not find findElements.js in package {_pkg}")
841
raw_function = raw_data.decode("utf8")
842
find_element_js = f"/* findElements */return ({raw_function}).apply(null, arguments);"
843
return self.execute_script(find_element_js, by.to_dict())
844
845
# Return empty list if driver returns null
846
# See https://github.com/SeleniumHQ/selenium/issues/4555
847
return self.execute(Command.FIND_ELEMENTS, {"using": by, "value": value})["value"] or []
848
849
@property
850
def capabilities(self) -> dict:
851
"""Returns the drivers current capabilities being used."""
852
return self.caps
853
854
def get_screenshot_as_file(self, filename) -> bool:
855
"""Save a screenshot of the current window to a PNG image file.
856
857
Returns:
858
False if there is any IOError, else returns True. Use full paths in your filename.
859
860
Args:
861
filename: The full path you wish to save your screenshot to. This
862
should end with a `.png` extension.
863
864
Example:
865
`driver.get_screenshot_as_file("./screenshots/foo.png")`
866
"""
867
if not str(filename).lower().endswith(".png"):
868
warnings.warn(
869
"name used for saved screenshot does not match file type. It should end with a `.png` extension",
870
UserWarning,
871
stacklevel=2,
872
)
873
png = self.get_screenshot_as_png()
874
try:
875
with open(filename, "wb") as f:
876
f.write(png)
877
except OSError:
878
return False
879
finally:
880
del png
881
return True
882
883
def save_screenshot(self, filename) -> bool:
884
"""Save a screenshot of the current window to a PNG image file.
885
886
Returns:
887
False if there is any IOError, else returns True. Use full paths in your filename.
888
889
Args:
890
filename: The full path you wish to save your screenshot to. This
891
should end with a `.png` extension.
892
893
Example:
894
`driver.save_screenshot("./screenshots/foo.png")`
895
"""
896
return self.get_screenshot_as_file(filename)
897
898
def get_screenshot_as_png(self) -> bytes:
899
"""Gets the screenshot of the current window as a binary data.
900
901
Example:
902
`driver.get_screenshot_as_png()`
903
"""
904
return b64decode(self.get_screenshot_as_base64().encode("ascii"))
905
906
def get_screenshot_as_base64(self) -> str:
907
"""Get a base64-encoded screenshot of the current window.
908
909
This encoding is useful for embedding screenshots in HTML.
910
911
Example:
912
`driver.get_screenshot_as_base64()`
913
"""
914
return self.execute(Command.SCREENSHOT)["value"]
915
916
def set_window_size(self, width, height, windowHandle: str = "current") -> None:
917
"""Sets the width and height of the current window.
918
919
Args:
920
width: The width in pixels to set the window to.
921
height: The height in pixels to set the window to.
922
windowHandle: The handle of the window to resize. Default is "current".
923
924
Example:
925
`driver.set_window_size(800, 600)`
926
"""
927
self._check_if_window_handle_is_current(windowHandle)
928
self.set_window_rect(width=int(width), height=int(height))
929
930
def get_window_size(self, windowHandle: str = "current") -> dict:
931
"""Gets the width and height of the current window.
932
933
Example:
934
`driver.get_window_size()`
935
"""
936
self._check_if_window_handle_is_current(windowHandle)
937
size = self.get_window_rect()
938
939
if size.get("value", None):
940
size = size["value"]
941
942
return {k: size[k] for k in ("width", "height")}
943
944
def set_window_position(self, x: float, y: float, windowHandle: str = "current") -> dict:
945
"""Sets the x,y position of the current window.
946
947
Args:
948
x: The x-coordinate in pixels to set the window position.
949
y: The y-coordinate in pixels to set the window position.
950
windowHandle: The handle of the window to reposition. Default is "current".
951
952
Example:
953
`driver.set_window_position(0, 0)`
954
"""
955
self._check_if_window_handle_is_current(windowHandle)
956
return self.set_window_rect(x=int(x), y=int(y))
957
958
def get_window_position(self, windowHandle="current") -> dict:
959
"""Gets the x,y position of the current window.
960
961
Example:
962
`driver.get_window_position()`
963
"""
964
self._check_if_window_handle_is_current(windowHandle)
965
position = self.get_window_rect()
966
967
return {k: position[k] for k in ("x", "y")}
968
969
def _check_if_window_handle_is_current(self, windowHandle: str) -> None:
970
"""Warns if the window handle is not equal to `current`."""
971
if windowHandle != "current":
972
warnings.warn("Only 'current' window is supported for W3C compatible browsers.", stacklevel=2)
973
974
def get_window_rect(self) -> dict:
975
"""Get the window's position and size.
976
977
Returns:
978
x, y coordinates and height and width of the current window.
979
980
Example:
981
`driver.get_window_rect()`
982
"""
983
return self.execute(Command.GET_WINDOW_RECT)["value"]
984
985
def set_window_rect(self, x=None, y=None, width=None, height=None) -> dict:
986
"""Set the window's position and size.
987
988
Sets the x, y coordinates and height and width of the current window.
989
This method is only supported for W3C compatible browsers; other browsers
990
should use `set_window_position` and `set_window_size`.
991
992
Example:
993
`driver.set_window_rect(x=10, y=10)`
994
`driver.set_window_rect(width=100, height=200)`
995
`driver.set_window_rect(x=10, y=10, width=100, height=200)`
996
"""
997
if (x is None and y is None) and (not height and not width):
998
raise InvalidArgumentException("x and y or height and width need values")
999
1000
return self.execute(Command.SET_WINDOW_RECT, {"x": x, "y": y, "width": width, "height": height})["value"]
1001
1002
@property
1003
def file_detector(self) -> FileDetector:
1004
return self._file_detector
1005
1006
@file_detector.setter
1007
def file_detector(self, detector) -> None:
1008
"""Set the file detector for keyboard input.
1009
1010
By default, this is set to a file detector that does nothing.
1011
See FileDetector, LocalFileDetector, and UselessFileDetector.
1012
1013
Args:
1014
detector: The detector to use. Must not be None.
1015
"""
1016
if not detector:
1017
raise WebDriverException("You may not set a file detector that is null")
1018
if not isinstance(detector, FileDetector):
1019
raise WebDriverException("Detector has to be instance of FileDetector")
1020
self._file_detector = detector
1021
1022
@property
1023
def orientation(self) -> dict:
1024
"""Gets the current orientation of the device.
1025
1026
Example:
1027
`orientation = driver.orientation`
1028
"""
1029
return self.execute(Command.GET_SCREEN_ORIENTATION)["value"]
1030
1031
@orientation.setter
1032
def orientation(self, value) -> None:
1033
"""Sets the current orientation of the device.
1034
1035
Args:
1036
value: Orientation to set it to.
1037
1038
Example:
1039
`driver.orientation = "landscape"`
1040
"""
1041
allowed_values = ["LANDSCAPE", "PORTRAIT"]
1042
if value.upper() in allowed_values:
1043
self.execute(Command.SET_SCREEN_ORIENTATION, {"orientation": value})
1044
else:
1045
raise WebDriverException("You can only set the orientation to 'LANDSCAPE' and 'PORTRAIT'")
1046
1047
def start_devtools(self) -> tuple[Any, WebSocketConnection]:
1048
global cdp
1049
import_cdp()
1050
if self.caps.get("se:cdp"):
1051
ws_url = self.caps.get("se:cdp")
1052
cdp_version = self.caps.get("se:cdpVersion")
1053
if cdp_version is None:
1054
raise WebDriverException("CDP version not found in capabilities")
1055
version = cdp_version.split(".")[0]
1056
else:
1057
version, ws_url = self._get_cdp_details()
1058
1059
if not ws_url:
1060
raise WebDriverException("Unable to find url to connect to from capabilities")
1061
1062
if cdp is None:
1063
raise WebDriverException("CDP module not loaded")
1064
1065
self._devtools = cdp.import_devtools(version)
1066
if self._websocket_connection:
1067
return self._devtools, self._websocket_connection
1068
if self.caps["browserName"].lower() == "firefox":
1069
raise RuntimeError("CDP support for Firefox has been removed. Please switch to WebDriver BiDi.")
1070
if not isinstance(self.command_executor, RemoteConnection):
1071
raise WebDriverException("command_executor must be a RemoteConnection instance for CDP support")
1072
self._websocket_connection = WebSocketConnection(
1073
ws_url,
1074
self.command_executor.client_config.websocket_timeout,
1075
self.command_executor.client_config.websocket_interval,
1076
)
1077
targets = self._websocket_connection.execute(self._devtools.target.get_targets())
1078
for target in targets:
1079
if target.target_id == self.current_window_handle:
1080
target_id = target.target_id
1081
break
1082
session = self._websocket_connection.execute(self._devtools.target.attach_to_target(target_id, True))
1083
self._websocket_connection.session_id = session
1084
return self._devtools, self._websocket_connection
1085
1086
@asynccontextmanager
1087
async def bidi_connection(self):
1088
global cdp
1089
import_cdp()
1090
if self.caps.get("se:cdp"):
1091
ws_url = self.caps.get("se:cdp")
1092
version = self.caps.get("se:cdpVersion").split(".")[0]
1093
else:
1094
version, ws_url = self._get_cdp_details()
1095
1096
if not ws_url:
1097
raise WebDriverException("Unable to find url to connect to from capabilities")
1098
1099
devtools = cdp.import_devtools(version)
1100
async with cdp.open_cdp(ws_url) as conn:
1101
targets = await conn.execute(devtools.target.get_targets())
1102
for target in targets:
1103
if target.target_id == self.current_window_handle:
1104
target_id = target.target_id
1105
break
1106
async with conn.open_session(target_id) as session:
1107
yield BidiConnection(session, cdp, devtools)
1108
1109
@property
1110
def script(self) -> Script:
1111
if not self._websocket_connection:
1112
self._start_bidi()
1113
1114
if not self._script:
1115
self._script = Script(self._websocket_connection, self)
1116
1117
return self._script
1118
1119
def _start_bidi(self) -> None:
1120
if self.caps.get("webSocketUrl"):
1121
ws_url = self.caps.get("webSocketUrl")
1122
else:
1123
raise WebDriverException("Unable to find url to connect to from capabilities")
1124
1125
if not isinstance(self.command_executor, RemoteConnection):
1126
raise WebDriverException("command_executor must be a RemoteConnection instance for BiDi support")
1127
1128
self._websocket_connection = WebSocketConnection(
1129
ws_url,
1130
self.command_executor.client_config.websocket_timeout,
1131
self.command_executor.client_config.websocket_interval,
1132
)
1133
1134
@property
1135
def network(self) -> Network:
1136
if not self._websocket_connection:
1137
self._start_bidi()
1138
1139
assert self._websocket_connection is not None
1140
if not hasattr(self, "_network") or self._network is None:
1141
assert self._websocket_connection is not None
1142
self._network = Network(self._websocket_connection)
1143
1144
return self._network
1145
1146
@property
1147
def browser(self) -> Browser:
1148
"""Returns a browser module object for BiDi browser commands.
1149
1150
Returns:
1151
An object containing access to BiDi browser commands.
1152
1153
Examples:
1154
`user_context = driver.browser.create_user_context()`
1155
`user_contexts = driver.browser.get_user_contexts()`
1156
`client_windows = driver.browser.get_client_windows()`
1157
`driver.browser.remove_user_context(user_context)`
1158
"""
1159
if not self._websocket_connection:
1160
self._start_bidi()
1161
1162
if self._browser is None:
1163
self._browser = Browser(self._websocket_connection)
1164
1165
return self._browser
1166
1167
@property
1168
def _session(self) -> Session:
1169
"""Returns the BiDi session object for the current WebDriver session."""
1170
if not self._websocket_connection:
1171
self._start_bidi()
1172
1173
if self._bidi_session is None:
1174
self._bidi_session = Session(self._websocket_connection)
1175
1176
return self._bidi_session
1177
1178
@property
1179
def browsing_context(self) -> BrowsingContext:
1180
"""Returns a browsing context module object for BiDi browsing context commands.
1181
1182
Returns:
1183
An object containing access to BiDi browsing context commands.
1184
1185
Examples:
1186
`context_id = driver.browsing_context.create(type="tab")`
1187
`driver.browsing_context.navigate(context=context_id, url="https://www.selenium.dev")`
1188
`driver.browsing_context.capture_screenshot(context=context_id)`
1189
`driver.browsing_context.close(context_id)`
1190
"""
1191
if not self._websocket_connection:
1192
self._start_bidi()
1193
1194
if self._browsing_context is None:
1195
self._browsing_context = BrowsingContext(self._websocket_connection)
1196
1197
return self._browsing_context
1198
1199
@property
1200
def storage(self) -> Storage:
1201
"""Returns a storage module object for BiDi storage commands.
1202
1203
Returns:
1204
An object containing access to BiDi storage commands.
1205
1206
Examples:
1207
```
1208
cookie_filter = CookieFilter(name="example")
1209
result = driver.storage.get_cookies(filter=cookie_filter)
1210
cookie=PartialCookie("name", BytesValue(BytesValue.TYPE_STRING, "value")
1211
driver.storage.set_cookie(cookie=cookie, "domain"))
1212
cookie_filter=CookieFilter(name="example")
1213
driver.storage.delete_cookies(filter=cookie_filter)
1214
```
1215
"""
1216
if not self._websocket_connection:
1217
self._start_bidi()
1218
1219
assert self._websocket_connection is not None
1220
if self._storage is None:
1221
self._storage = Storage(self._websocket_connection)
1222
1223
return self._storage
1224
1225
@property
1226
def permissions(self) -> Permissions:
1227
"""Get a permissions module object for BiDi permissions commands.
1228
1229
Returns:
1230
An object containing access to BiDi permissions commands.
1231
1232
Examples:
1233
```
1234
from selenium.webdriver.common.bidi.permissions import PermissionDescriptor, PermissionState
1235
1236
descriptor = PermissionDescriptor("geolocation")
1237
driver.permissions.set_permission(descriptor, PermissionState.GRANTED, "https://example.com")
1238
```
1239
"""
1240
if not self._websocket_connection:
1241
self._start_bidi()
1242
1243
if self._permissions is None:
1244
self._permissions = Permissions(self._websocket_connection)
1245
1246
return self._permissions
1247
1248
@property
1249
def webextension(self) -> WebExtension:
1250
"""Get a webextension module object for BiDi webextension commands.
1251
1252
Returns:
1253
An object containing access to BiDi webextension commands.
1254
1255
Examples:
1256
`extension_path = "/path/to/extension"`
1257
`extension_result = driver.webextension.install(path=extension_path)`
1258
`driver.webextension.uninstall(extension_result)`
1259
"""
1260
if not self._websocket_connection:
1261
self._start_bidi()
1262
1263
if self._webextension is None:
1264
self._webextension = WebExtension(self._websocket_connection)
1265
1266
return self._webextension
1267
1268
@property
1269
def emulation(self) -> Emulation:
1270
"""Get an emulation module object for BiDi emulation commands.
1271
1272
Returns:
1273
An object containing access to BiDi emulation commands.
1274
1275
Examples:
1276
```
1277
from selenium.webdriver.common.bidi.emulation import GeolocationCoordinates
1278
1279
coordinates = GeolocationCoordinates(37.7749, -122.4194)
1280
driver.emulation.set_geolocation_override(coordinates=coordinates, contexts=[context_id])
1281
```
1282
"""
1283
if not self._websocket_connection:
1284
self._start_bidi()
1285
1286
assert self._websocket_connection is not None
1287
if self._emulation is None:
1288
self._emulation = Emulation(self._websocket_connection)
1289
1290
return self._emulation
1291
1292
@property
1293
def input(self) -> Input:
1294
"""Get an input module object for BiDi input commands.
1295
1296
Returns:
1297
An object containing access to BiDi input commands.
1298
1299
Examples:
1300
```
1301
from selenium.webdriver.common.bidi.input import KeySourceActions, KeyDownAction, KeyUpAction
1302
1303
actions = KeySourceActions(id="keyboard", actions=[KeyDownAction(value="a"), KeyUpAction(value="a")])
1304
driver.input.perform_actions(driver.current_window_handle, [actions])
1305
driver.input.release_actions(driver.current_window_handle)
1306
```
1307
"""
1308
if not self._websocket_connection:
1309
self._start_bidi()
1310
1311
if self._input is None:
1312
self._input = Input(self._websocket_connection)
1313
1314
return self._input
1315
1316
def _get_cdp_details(self):
1317
import json
1318
1319
import urllib3
1320
1321
http = urllib3.PoolManager()
1322
try:
1323
if self.caps.get("browserName") == "chrome":
1324
debugger_address = self.caps.get("goog:chromeOptions").get("debuggerAddress")
1325
elif self.caps.get("browserName") in ("MicrosoftEdge", "webview2"):
1326
debugger_address = self.caps.get("ms:edgeOptions").get("debuggerAddress")
1327
except AttributeError:
1328
raise WebDriverException("Can't get debugger address.")
1329
1330
res = http.request("GET", f"http://{debugger_address}/json/version")
1331
data = json.loads(res.data)
1332
1333
browser_version = data.get("Browser")
1334
websocket_url = data.get("webSocketDebuggerUrl")
1335
1336
import re
1337
1338
version = re.search(r".*/(\d+)\.", browser_version).group(1)
1339
1340
return version, websocket_url
1341
1342
# Virtual Authenticator Methods
1343
def add_virtual_authenticator(self, options: VirtualAuthenticatorOptions) -> None:
1344
"""Adds a virtual authenticator with the given options.
1345
1346
Example:
1347
```
1348
from selenium.webdriver.common.virtual_authenticator import VirtualAuthenticatorOptions
1349
1350
options = VirtualAuthenticatorOptions(protocol="u2f", transport="usb", device_id="myDevice123")
1351
driver.add_virtual_authenticator(options)
1352
```
1353
"""
1354
self._authenticator_id = self.execute(Command.ADD_VIRTUAL_AUTHENTICATOR, options.to_dict())["value"]
1355
1356
@property
1357
def virtual_authenticator_id(self) -> str | None:
1358
"""Returns the id of the virtual authenticator."""
1359
return self._authenticator_id
1360
1361
@required_virtual_authenticator
1362
def remove_virtual_authenticator(self) -> None:
1363
"""Removes a previously added virtual authenticator.
1364
1365
The authenticator is no longer valid after removal, so no
1366
methods may be called.
1367
"""
1368
self.execute(Command.REMOVE_VIRTUAL_AUTHENTICATOR, {"authenticatorId": self._authenticator_id})
1369
self._authenticator_id = None
1370
1371
@required_virtual_authenticator
1372
def add_credential(self, credential: Credential) -> None:
1373
"""Injects a credential into the authenticator.
1374
1375
Example:
1376
```
1377
from selenium.webdriver.common.credential import Credential
1378
1379
credential = Credential(id="[email protected]", password="aPassword")
1380
driver.add_credential(credential)
1381
```
1382
"""
1383
self.execute(Command.ADD_CREDENTIAL, {**credential.to_dict(), "authenticatorId": self._authenticator_id})
1384
1385
@required_virtual_authenticator
1386
def get_credentials(self) -> list[Credential]:
1387
"""Returns the list of credentials owned by the authenticator."""
1388
credential_data = self.execute(Command.GET_CREDENTIALS, {"authenticatorId": self._authenticator_id})
1389
return [Credential.from_dict(credential) for credential in credential_data["value"]]
1390
1391
@required_virtual_authenticator
1392
def remove_credential(self, credential_id: str | bytearray) -> None:
1393
"""Removes a credential from the authenticator.
1394
1395
Example:
1396
`credential_id = "[email protected]"`
1397
`driver.remove_credential(credential_id)`
1398
"""
1399
# Check if the credential is bytearray converted to b64 string
1400
if isinstance(credential_id, bytearray):
1401
credential_id = urlsafe_b64encode(credential_id).decode()
1402
1403
self.execute(
1404
Command.REMOVE_CREDENTIAL, {"credentialId": credential_id, "authenticatorId": self._authenticator_id}
1405
)
1406
1407
@required_virtual_authenticator
1408
def remove_all_credentials(self) -> None:
1409
"""Removes all credentials from the authenticator."""
1410
self.execute(Command.REMOVE_ALL_CREDENTIALS, {"authenticatorId": self._authenticator_id})
1411
1412
@required_virtual_authenticator
1413
def set_user_verified(self, verified: bool) -> None:
1414
"""Set whether the authenticator will simulate success or failure on user verification.
1415
1416
Args:
1417
verified: True if the authenticator will pass user verification,
1418
False otherwise.
1419
1420
Example:
1421
`driver.set_user_verified(True)`
1422
"""
1423
self.execute(Command.SET_USER_VERIFIED, {"authenticatorId": self._authenticator_id, "isUserVerified": verified})
1424
1425
def get_downloadable_files(self) -> list:
1426
"""Retrieves the downloadable files as a list of file names."""
1427
if "se:downloadsEnabled" not in self.capabilities:
1428
raise WebDriverException("You must enable downloads in order to work with downloadable files.")
1429
1430
return self.execute(Command.GET_DOWNLOADABLE_FILES)["value"]["names"]
1431
1432
def download_file(self, file_name: str, target_directory: str) -> None:
1433
"""Download a file with the specified file name to the target directory.
1434
1435
Args:
1436
file_name: The name of the file to download.
1437
target_directory: The path to the directory to save the downloaded file.
1438
1439
Example:
1440
`driver.download_file("example.zip", "/path/to/directory")`
1441
"""
1442
if "se:downloadsEnabled" not in self.capabilities:
1443
raise WebDriverException("You must enable downloads in order to work with downloadable files.")
1444
1445
if not os.path.exists(target_directory):
1446
os.makedirs(target_directory)
1447
1448
contents = self.execute(Command.DOWNLOAD_FILE, {"name": file_name})["value"]["contents"]
1449
1450
with tempfile.TemporaryDirectory() as tmp_dir:
1451
zip_file = os.path.join(tmp_dir, file_name + ".zip")
1452
with open(zip_file, "wb") as file:
1453
file.write(base64.b64decode(contents))
1454
1455
with zipfile.ZipFile(zip_file, "r") as zip_ref:
1456
zip_ref.extractall(target_directory)
1457
1458
def delete_downloadable_files(self) -> None:
1459
"""Deletes all downloadable files."""
1460
if "se:downloadsEnabled" not in self.capabilities:
1461
raise WebDriverException("You must enable downloads in order to work with downloadable files.")
1462
1463
self.execute(Command.DELETE_DOWNLOADABLE_FILES)
1464
1465
def fire_session_event(self, event_type: str, payload: dict | None = None) -> dict:
1466
"""Fire a custom session event to the remote server event bus.
1467
1468
This allows test code to trigger server-side utilities that subscribe to
1469
the event bus.
1470
1471
Args:
1472
event_type: The type of event (e.g., "test:failed", "log:collect", "marker:add").
1473
payload: Optional data to include with the event.
1474
1475
Returns:
1476
A dictionary containing the response data including success status,
1477
event type, and timestamp.
1478
1479
Raises:
1480
WebDriverException: If the event cannot be fired.
1481
1482
Examples:
1483
Simple event::
1484
1485
driver.fire_session_event("test:started")
1486
1487
Event with payload::
1488
1489
driver.fire_session_event("test:failed", {"testName": "LoginTest", "error": "Element not found"})
1490
"""
1491
params: dict[str, str | dict] = {"eventType": event_type}
1492
if payload:
1493
params["payload"] = payload
1494
return self.execute(Command.FIRE_SESSION_EVENT, params)["value"]
1495
1496
@property
1497
def fedcm(self) -> FedCM:
1498
"""Get the Federated Credential Management (FedCM) dialog commands.
1499
1500
Returns:
1501
An object providing access to all Federated Credential Management
1502
(FedCM) dialog commands.
1503
1504
Examples:
1505
`driver.fedcm.title`
1506
`driver.fedcm.subtitle`
1507
`driver.fedcm.dialog_type`
1508
`driver.fedcm.account_list`
1509
`driver.fedcm.select_account(0)`
1510
`driver.fedcm.accept()`
1511
`driver.fedcm.dismiss()`
1512
`driver.fedcm.enable_delay()`
1513
`driver.fedcm.disable_delay()`
1514
`driver.fedcm.reset_cooldown()`
1515
"""
1516
return self._fedcm
1517
1518
@property
1519
def supports_fedcm(self) -> bool:
1520
"""Returns whether the browser supports FedCM capabilities."""
1521
return self.capabilities.get(ArgOptions.FEDCM_CAPABILITY, False)
1522
1523
def _require_fedcm_support(self) -> None:
1524
"""Raises an exception if FedCM is not supported."""
1525
if not self.supports_fedcm:
1526
raise WebDriverException(
1527
"This browser does not support Federated Credential Management. "
1528
"Please ensure you're using a supported browser."
1529
)
1530
1531
@property
1532
def dialog(self) -> Dialog:
1533
"""Returns the FedCM dialog object for interaction."""
1534
self._require_fedcm_support()
1535
return Dialog(self)
1536
1537
def fedcm_dialog(self, timeout=5, poll_frequency=0.5, ignored_exceptions=None):
1538
"""Waits for and returns the FedCM dialog.
1539
1540
Args:
1541
timeout: How long to wait for the dialog.
1542
poll_frequency: How frequently to poll.
1543
ignored_exceptions: Exceptions to ignore while waiting.
1544
1545
Returns:
1546
The FedCM dialog object if found.
1547
1548
Raises:
1549
TimeoutException: If dialog doesn't appear.
1550
WebDriverException: If FedCM not supported.
1551
"""
1552
from selenium.common.exceptions import NoAlertPresentException
1553
from selenium.webdriver.support.wait import WebDriverWait
1554
1555
self._require_fedcm_support()
1556
1557
if ignored_exceptions is None:
1558
ignored_exceptions = (NoAlertPresentException,)
1559
1560
def _check_fedcm() -> Dialog | None:
1561
try:
1562
dialog = Dialog(self)
1563
return dialog if dialog.type else None
1564
except NoAlertPresentException:
1565
return None
1566
1567
wait = WebDriverWait(self, timeout, poll_frequency=poll_frequency, ignored_exceptions=ignored_exceptions)
1568
return wait.until(lambda _: _check_fedcm())
1569
1570