Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
SeleniumHQ
GitHub Repository: SeleniumHQ/Selenium
Path: blob/trunk/py/selenium/webdriver/common/bidi/browsing_context.py
1864 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
import threading
19
from dataclasses import dataclass
20
from typing import Any, Callable, Optional, Union
21
22
from selenium.webdriver.common.bidi.common import command_builder
23
24
from .session import Session
25
26
27
class ReadinessState:
28
"""Represents the stage of document loading at which a navigation command will return."""
29
30
NONE = "none"
31
INTERACTIVE = "interactive"
32
COMPLETE = "complete"
33
34
35
class UserPromptType:
36
"""Represents the possible user prompt types."""
37
38
ALERT = "alert"
39
BEFORE_UNLOAD = "beforeunload"
40
CONFIRM = "confirm"
41
PROMPT = "prompt"
42
43
44
class NavigationInfo:
45
"""Provides details of an ongoing navigation."""
46
47
def __init__(
48
self,
49
context: str,
50
navigation: Optional[str],
51
timestamp: int,
52
url: str,
53
):
54
self.context = context
55
self.navigation = navigation
56
self.timestamp = timestamp
57
self.url = url
58
59
@classmethod
60
def from_json(cls, json: dict) -> "NavigationInfo":
61
"""Creates a NavigationInfo instance from a dictionary.
62
63
Parameters:
64
-----------
65
json: A dictionary containing the navigation information.
66
67
Returns:
68
-------
69
NavigationInfo: A new instance of NavigationInfo.
70
"""
71
context = json.get("context")
72
if context is None or not isinstance(context, str):
73
raise ValueError("context is required and must be a string")
74
75
navigation = json.get("navigation")
76
if navigation is not None and not isinstance(navigation, str):
77
raise ValueError("navigation must be a string")
78
79
timestamp = json.get("timestamp")
80
if timestamp is None or not isinstance(timestamp, int) or timestamp < 0:
81
raise ValueError("timestamp is required and must be a non-negative integer")
82
83
url = json.get("url")
84
if url is None or not isinstance(url, str):
85
raise ValueError("url is required and must be a string")
86
87
return cls(context, navigation, timestamp, url)
88
89
90
class BrowsingContextInfo:
91
"""Represents the properties of a navigable."""
92
93
def __init__(
94
self,
95
context: str,
96
url: str,
97
children: Optional[list["BrowsingContextInfo"]],
98
client_window: str,
99
user_context: str,
100
parent: Optional[str] = None,
101
original_opener: Optional[str] = None,
102
):
103
self.context = context
104
self.url = url
105
self.children = children
106
self.parent = parent
107
self.user_context = user_context
108
self.original_opener = original_opener
109
self.client_window = client_window
110
111
@classmethod
112
def from_json(cls, json: dict) -> "BrowsingContextInfo":
113
"""Creates a BrowsingContextInfo instance from a dictionary.
114
115
Parameters:
116
-----------
117
json: A dictionary containing the browsing context information.
118
119
Returns:
120
-------
121
BrowsingContextInfo: A new instance of BrowsingContextInfo.
122
"""
123
children = None
124
raw_children = json.get("children")
125
if raw_children is not None:
126
if not isinstance(raw_children, list):
127
raise ValueError("children must be a list if provided")
128
129
children = []
130
for child in raw_children:
131
if not isinstance(child, dict):
132
raise ValueError(f"Each child must be a dictionary, got {type(child)}")
133
children.append(BrowsingContextInfo.from_json(child))
134
135
context = json.get("context")
136
if context is None or not isinstance(context, str):
137
raise ValueError("context is required and must be a string")
138
139
url = json.get("url")
140
if url is None or not isinstance(url, str):
141
raise ValueError("url is required and must be a string")
142
143
parent = json.get("parent")
144
if parent is not None and not isinstance(parent, str):
145
raise ValueError("parent must be a string if provided")
146
147
user_context = json.get("userContext")
148
if user_context is None or not isinstance(user_context, str):
149
raise ValueError("userContext is required and must be a string")
150
151
original_opener = json.get("originalOpener")
152
if original_opener is not None and not isinstance(original_opener, str):
153
raise ValueError("originalOpener must be a string if provided")
154
155
client_window = json.get("clientWindow")
156
if client_window is None or not isinstance(client_window, str):
157
raise ValueError("clientWindow is required and must be a string")
158
159
return cls(
160
context=context,
161
url=url,
162
children=children,
163
client_window=client_window,
164
user_context=user_context,
165
parent=parent,
166
original_opener=original_opener,
167
)
168
169
170
class DownloadWillBeginParams(NavigationInfo):
171
"""Parameters for the downloadWillBegin event."""
172
173
def __init__(
174
self,
175
context: str,
176
navigation: Optional[str],
177
timestamp: int,
178
url: str,
179
suggested_filename: str,
180
):
181
super().__init__(context, navigation, timestamp, url)
182
self.suggested_filename = suggested_filename
183
184
@classmethod
185
def from_json(cls, json: dict) -> "DownloadWillBeginParams":
186
nav_info = NavigationInfo.from_json(json)
187
188
suggested_filename = json.get("suggestedFilename")
189
if suggested_filename is None or not isinstance(suggested_filename, str):
190
raise ValueError("suggestedFilename is required and must be a string")
191
192
return cls(
193
context=nav_info.context,
194
navigation=nav_info.navigation,
195
timestamp=nav_info.timestamp,
196
url=nav_info.url,
197
suggested_filename=suggested_filename,
198
)
199
200
201
class UserPromptOpenedParams:
202
"""Parameters for the userPromptOpened event."""
203
204
def __init__(
205
self,
206
context: str,
207
handler: str,
208
message: str,
209
type: str,
210
default_value: Optional[str] = None,
211
):
212
self.context = context
213
self.handler = handler
214
self.message = message
215
self.type = type
216
self.default_value = default_value
217
218
@classmethod
219
def from_json(cls, json: dict) -> "UserPromptOpenedParams":
220
"""Creates a UserPromptOpenedParams instance from a dictionary.
221
222
Parameters:
223
-----------
224
json: A dictionary containing the user prompt parameters.
225
226
Returns:
227
-------
228
UserPromptOpenedParams: A new instance of UserPromptOpenedParams.
229
"""
230
context = json.get("context")
231
if context is None or not isinstance(context, str):
232
raise ValueError("context is required and must be a string")
233
234
handler = json.get("handler")
235
if handler is None or not isinstance(handler, str):
236
raise ValueError("handler is required and must be a string")
237
238
message = json.get("message")
239
if message is None or not isinstance(message, str):
240
raise ValueError("message is required and must be a string")
241
242
type_value = json.get("type")
243
if type_value is None or not isinstance(type_value, str):
244
raise ValueError("type is required and must be a string")
245
246
default_value = json.get("defaultValue")
247
if default_value is not None and not isinstance(default_value, str):
248
raise ValueError("defaultValue must be a string if provided")
249
250
return cls(
251
context=context,
252
handler=handler,
253
message=message,
254
type=type_value,
255
default_value=default_value,
256
)
257
258
259
class UserPromptClosedParams:
260
"""Parameters for the userPromptClosed event."""
261
262
def __init__(
263
self,
264
context: str,
265
accepted: bool,
266
type: str,
267
user_text: Optional[str] = None,
268
):
269
self.context = context
270
self.accepted = accepted
271
self.type = type
272
self.user_text = user_text
273
274
@classmethod
275
def from_json(cls, json: dict) -> "UserPromptClosedParams":
276
"""Creates a UserPromptClosedParams instance from a dictionary.
277
278
Parameters:
279
-----------
280
json: A dictionary containing the user prompt closed parameters.
281
282
Returns:
283
-------
284
UserPromptClosedParams: A new instance of UserPromptClosedParams.
285
"""
286
context = json.get("context")
287
if context is None or not isinstance(context, str):
288
raise ValueError("context is required and must be a string")
289
290
accepted = json.get("accepted")
291
if accepted is None or not isinstance(accepted, bool):
292
raise ValueError("accepted is required and must be a boolean")
293
294
type_value = json.get("type")
295
if type_value is None or not isinstance(type_value, str):
296
raise ValueError("type is required and must be a string")
297
298
user_text = json.get("userText")
299
if user_text is not None and not isinstance(user_text, str):
300
raise ValueError("userText must be a string if provided")
301
302
return cls(
303
context=context,
304
accepted=accepted,
305
type=type_value,
306
user_text=user_text,
307
)
308
309
310
class HistoryUpdatedParams:
311
"""Parameters for the historyUpdated event."""
312
313
def __init__(
314
self,
315
context: str,
316
timestamp: int,
317
url: str,
318
):
319
self.context = context
320
self.timestamp = timestamp
321
self.url = url
322
323
@classmethod
324
def from_json(cls, json: dict) -> "HistoryUpdatedParams":
325
"""Creates a HistoryUpdatedParams instance from a dictionary.
326
327
Parameters:
328
-----------
329
json: A dictionary containing the history updated parameters.
330
331
Returns:
332
-------
333
HistoryUpdatedParams: A new instance of HistoryUpdatedParams.
334
"""
335
context = json.get("context")
336
if context is None or not isinstance(context, str):
337
raise ValueError("context is required and must be a string")
338
339
timestamp = json.get("timestamp")
340
if timestamp is None or not isinstance(timestamp, int) or timestamp < 0:
341
raise ValueError("timestamp is required and must be a non-negative integer")
342
343
url = json.get("url")
344
if url is None or not isinstance(url, str):
345
raise ValueError("url is required and must be a string")
346
347
return cls(
348
context=context,
349
timestamp=timestamp,
350
url=url,
351
)
352
353
354
class DownloadCanceledParams(NavigationInfo):
355
def __init__(
356
self,
357
context: str,
358
navigation: Optional[str],
359
timestamp: int,
360
url: str,
361
status: str = "canceled",
362
):
363
super().__init__(context, navigation, timestamp, url)
364
self.status = status
365
366
@classmethod
367
def from_json(cls, json: dict) -> "DownloadCanceledParams":
368
nav_info = NavigationInfo.from_json(json)
369
370
status = json.get("status")
371
if status is None or status != "canceled":
372
raise ValueError("status is required and must be 'canceled'")
373
374
return cls(
375
context=nav_info.context,
376
navigation=nav_info.navigation,
377
timestamp=nav_info.timestamp,
378
url=nav_info.url,
379
status=status,
380
)
381
382
383
class DownloadCompleteParams(NavigationInfo):
384
def __init__(
385
self,
386
context: str,
387
navigation: Optional[str],
388
timestamp: int,
389
url: str,
390
status: str = "complete",
391
filepath: Optional[str] = None,
392
):
393
super().__init__(context, navigation, timestamp, url)
394
self.status = status
395
self.filepath = filepath
396
397
@classmethod
398
def from_json(cls, json: dict) -> "DownloadCompleteParams":
399
nav_info = NavigationInfo.from_json(json)
400
401
status = json.get("status")
402
if status is None or status != "complete":
403
raise ValueError("status is required and must be 'complete'")
404
405
filepath = json.get("filepath")
406
if filepath is not None and not isinstance(filepath, str):
407
raise ValueError("filepath must be a string if provided")
408
409
return cls(
410
context=nav_info.context,
411
navigation=nav_info.navigation,
412
timestamp=nav_info.timestamp,
413
url=nav_info.url,
414
status=status,
415
filepath=filepath,
416
)
417
418
419
class DownloadEndParams:
420
"""Parameters for the downloadEnd event."""
421
422
def __init__(
423
self,
424
download_params: Union[DownloadCanceledParams, DownloadCompleteParams],
425
):
426
self.download_params = download_params
427
428
@classmethod
429
def from_json(cls, json: dict) -> "DownloadEndParams":
430
status = json.get("status")
431
if status == "canceled":
432
return cls(DownloadCanceledParams.from_json(json))
433
elif status == "complete":
434
return cls(DownloadCompleteParams.from_json(json))
435
else:
436
raise ValueError("status must be either 'canceled' or 'complete'")
437
438
439
class ContextCreated:
440
"""Event class for browsingContext.contextCreated event."""
441
442
event_class = "browsingContext.contextCreated"
443
444
@classmethod
445
def from_json(cls, json: dict):
446
if isinstance(json, BrowsingContextInfo):
447
return json
448
return BrowsingContextInfo.from_json(json)
449
450
451
class ContextDestroyed:
452
"""Event class for browsingContext.contextDestroyed event."""
453
454
event_class = "browsingContext.contextDestroyed"
455
456
@classmethod
457
def from_json(cls, json: dict):
458
if isinstance(json, BrowsingContextInfo):
459
return json
460
return BrowsingContextInfo.from_json(json)
461
462
463
class NavigationStarted:
464
"""Event class for browsingContext.navigationStarted event."""
465
466
event_class = "browsingContext.navigationStarted"
467
468
@classmethod
469
def from_json(cls, json: dict):
470
if isinstance(json, NavigationInfo):
471
return json
472
return NavigationInfo.from_json(json)
473
474
475
class NavigationCommitted:
476
"""Event class for browsingContext.navigationCommitted event."""
477
478
event_class = "browsingContext.navigationCommitted"
479
480
@classmethod
481
def from_json(cls, json: dict):
482
if isinstance(json, NavigationInfo):
483
return json
484
return NavigationInfo.from_json(json)
485
486
487
class NavigationFailed:
488
"""Event class for browsingContext.navigationFailed event."""
489
490
event_class = "browsingContext.navigationFailed"
491
492
@classmethod
493
def from_json(cls, json: dict):
494
if isinstance(json, NavigationInfo):
495
return json
496
return NavigationInfo.from_json(json)
497
498
499
class NavigationAborted:
500
"""Event class for browsingContext.navigationAborted event."""
501
502
event_class = "browsingContext.navigationAborted"
503
504
@classmethod
505
def from_json(cls, json: dict):
506
if isinstance(json, NavigationInfo):
507
return json
508
return NavigationInfo.from_json(json)
509
510
511
class DomContentLoaded:
512
"""Event class for browsingContext.domContentLoaded event."""
513
514
event_class = "browsingContext.domContentLoaded"
515
516
@classmethod
517
def from_json(cls, json: dict):
518
if isinstance(json, NavigationInfo):
519
return json
520
return NavigationInfo.from_json(json)
521
522
523
class Load:
524
"""Event class for browsingContext.load event."""
525
526
event_class = "browsingContext.load"
527
528
@classmethod
529
def from_json(cls, json: dict):
530
if isinstance(json, NavigationInfo):
531
return json
532
return NavigationInfo.from_json(json)
533
534
535
class FragmentNavigated:
536
"""Event class for browsingContext.fragmentNavigated event."""
537
538
event_class = "browsingContext.fragmentNavigated"
539
540
@classmethod
541
def from_json(cls, json: dict):
542
if isinstance(json, NavigationInfo):
543
return json
544
return NavigationInfo.from_json(json)
545
546
547
class DownloadWillBegin:
548
"""Event class for browsingContext.downloadWillBegin event."""
549
550
event_class = "browsingContext.downloadWillBegin"
551
552
@classmethod
553
def from_json(cls, json: dict):
554
return DownloadWillBeginParams.from_json(json)
555
556
557
class UserPromptOpened:
558
"""Event class for browsingContext.userPromptOpened event."""
559
560
event_class = "browsingContext.userPromptOpened"
561
562
@classmethod
563
def from_json(cls, json: dict):
564
return UserPromptOpenedParams.from_json(json)
565
566
567
class UserPromptClosed:
568
"""Event class for browsingContext.userPromptClosed event."""
569
570
event_class = "browsingContext.userPromptClosed"
571
572
@classmethod
573
def from_json(cls, json: dict):
574
return UserPromptClosedParams.from_json(json)
575
576
577
class HistoryUpdated:
578
"""Event class for browsingContext.historyUpdated event."""
579
580
event_class = "browsingContext.historyUpdated"
581
582
@classmethod
583
def from_json(cls, json: dict):
584
return HistoryUpdatedParams.from_json(json)
585
586
587
class DownloadEnd:
588
"""Event class for browsingContext.downloadEnd event."""
589
590
event_class = "browsingContext.downloadEnd"
591
592
@classmethod
593
def from_json(cls, json: dict):
594
return DownloadEndParams.from_json(json)
595
596
597
@dataclass
598
class EventConfig:
599
event_key: str
600
bidi_event: str
601
event_class: type
602
603
604
class _EventManager:
605
"""Class to manage event subscriptions and callbacks for BrowsingContext."""
606
607
def __init__(self, conn, event_configs: dict[str, EventConfig]):
608
self.conn = conn
609
self.event_configs = event_configs
610
self.subscriptions: dict = {}
611
self._bidi_to_class = {config.bidi_event: config.event_class for config in event_configs.values()}
612
self._available_events = ", ".join(sorted(event_configs.keys()))
613
# Thread safety lock for subscription operations
614
self._subscription_lock = threading.Lock()
615
616
def validate_event(self, event: str) -> EventConfig:
617
event_config = self.event_configs.get(event)
618
if not event_config:
619
raise ValueError(f"Event '{event}' not found. Available events: {self._available_events}")
620
return event_config
621
622
def subscribe_to_event(self, bidi_event: str, contexts: Optional[list[str]] = None) -> None:
623
"""Subscribe to a BiDi event if not already subscribed.
624
625
Parameters:
626
----------
627
bidi_event: The BiDi event name.
628
contexts: Optional browsing context IDs to subscribe to.
629
"""
630
with self._subscription_lock:
631
if bidi_event not in self.subscriptions:
632
session = Session(self.conn)
633
self.conn.execute(session.subscribe(bidi_event, browsing_contexts=contexts))
634
self.subscriptions[bidi_event] = []
635
636
def unsubscribe_from_event(self, bidi_event: str) -> None:
637
"""Unsubscribe from a BiDi event if no more callbacks exist.
638
639
Parameters:
640
----------
641
bidi_event: The BiDi event name.
642
"""
643
with self._subscription_lock:
644
callback_list = self.subscriptions.get(bidi_event)
645
if callback_list is not None and not callback_list:
646
session = Session(self.conn)
647
self.conn.execute(session.unsubscribe(bidi_event))
648
del self.subscriptions[bidi_event]
649
650
def add_callback_to_tracking(self, bidi_event: str, callback_id: int) -> None:
651
with self._subscription_lock:
652
self.subscriptions[bidi_event].append(callback_id)
653
654
def remove_callback_from_tracking(self, bidi_event: str, callback_id: int) -> None:
655
with self._subscription_lock:
656
callback_list = self.subscriptions.get(bidi_event)
657
if callback_list and callback_id in callback_list:
658
callback_list.remove(callback_id)
659
660
def add_event_handler(self, event: str, callback: Callable, contexts: Optional[list[str]] = None) -> int:
661
event_config = self.validate_event(event)
662
663
callback_id = self.conn.add_callback(event_config.event_class, callback)
664
665
# Subscribe to the event if needed
666
self.subscribe_to_event(event_config.bidi_event, contexts)
667
668
# Track the callback
669
self.add_callback_to_tracking(event_config.bidi_event, callback_id)
670
671
return callback_id
672
673
def remove_event_handler(self, event: str, callback_id: int) -> None:
674
event_config = self.validate_event(event)
675
676
# Remove the callback from the connection
677
self.conn.remove_callback(event_config.event_class, callback_id)
678
679
# Remove from tracking collections
680
self.remove_callback_from_tracking(event_config.bidi_event, callback_id)
681
682
# Unsubscribe if no more callbacks exist
683
self.unsubscribe_from_event(event_config.bidi_event)
684
685
def clear_event_handlers(self) -> None:
686
"""Clear all event handlers from the browsing context."""
687
with self._subscription_lock:
688
if not self.subscriptions:
689
return
690
691
session = Session(self.conn)
692
693
for bidi_event, callback_ids in list(self.subscriptions.items()):
694
event_class = self._bidi_to_class.get(bidi_event)
695
if event_class:
696
# Remove all callbacks for this event
697
for callback_id in callback_ids:
698
self.conn.remove_callback(event_class, callback_id)
699
700
self.conn.execute(session.unsubscribe(bidi_event))
701
702
self.subscriptions.clear()
703
704
705
class BrowsingContext:
706
"""BiDi implementation of the browsingContext module."""
707
708
EVENT_CONFIGS = {
709
"context_created": EventConfig("context_created", "browsingContext.contextCreated", ContextCreated),
710
"context_destroyed": EventConfig("context_destroyed", "browsingContext.contextDestroyed", ContextDestroyed),
711
"dom_content_loaded": EventConfig("dom_content_loaded", "browsingContext.domContentLoaded", DomContentLoaded),
712
"download_end": EventConfig("download_end", "browsingContext.downloadEnd", DownloadEnd),
713
"download_will_begin": EventConfig(
714
"download_will_begin", "browsingContext.downloadWillBegin", DownloadWillBegin
715
),
716
"fragment_navigated": EventConfig("fragment_navigated", "browsingContext.fragmentNavigated", FragmentNavigated),
717
"history_updated": EventConfig("history_updated", "browsingContext.historyUpdated", HistoryUpdated),
718
"load": EventConfig("load", "browsingContext.load", Load),
719
"navigation_aborted": EventConfig("navigation_aborted", "browsingContext.navigationAborted", NavigationAborted),
720
"navigation_committed": EventConfig(
721
"navigation_committed", "browsingContext.navigationCommitted", NavigationCommitted
722
),
723
"navigation_failed": EventConfig("navigation_failed", "browsingContext.navigationFailed", NavigationFailed),
724
"navigation_started": EventConfig("navigation_started", "browsingContext.navigationStarted", NavigationStarted),
725
"user_prompt_closed": EventConfig("user_prompt_closed", "browsingContext.userPromptClosed", UserPromptClosed),
726
"user_prompt_opened": EventConfig("user_prompt_opened", "browsingContext.userPromptOpened", UserPromptOpened),
727
}
728
729
def __init__(self, conn):
730
self.conn = conn
731
self._event_manager = _EventManager(conn, self.EVENT_CONFIGS)
732
733
@classmethod
734
def get_event_names(cls) -> list[str]:
735
"""Get a list of all available event names.
736
737
Returns:
738
-------
739
List[str]: A list of event names that can be used with event handlers.
740
"""
741
return list(cls.EVENT_CONFIGS.keys())
742
743
def activate(self, context: str) -> None:
744
"""Activates and focuses the given top-level traversable.
745
746
Parameters:
747
-----------
748
context: The browsing context ID to activate.
749
750
Raises:
751
------
752
Exception: If the browsing context is not a top-level traversable.
753
"""
754
params = {"context": context}
755
self.conn.execute(command_builder("browsingContext.activate", params))
756
757
def capture_screenshot(
758
self,
759
context: str,
760
origin: str = "viewport",
761
format: Optional[dict] = None,
762
clip: Optional[dict] = None,
763
) -> str:
764
"""Captures an image of the given navigable, and returns it as a Base64-encoded string.
765
766
Parameters:
767
-----------
768
context: The browsing context ID to capture.
769
origin: The origin of the screenshot, either "viewport" or "document".
770
format: The format of the screenshot.
771
clip: The clip rectangle of the screenshot.
772
773
Returns:
774
-------
775
str: The Base64-encoded screenshot.
776
"""
777
params: dict[str, Any] = {"context": context, "origin": origin}
778
if format is not None:
779
params["format"] = format
780
if clip is not None:
781
params["clip"] = clip
782
783
result = self.conn.execute(command_builder("browsingContext.captureScreenshot", params))
784
return result["data"]
785
786
def close(self, context: str, prompt_unload: bool = False) -> None:
787
"""Closes a top-level traversable.
788
789
Parameters:
790
-----------
791
context: The browsing context ID to close.
792
prompt_unload: Whether to prompt to unload.
793
794
Raises:
795
------
796
Exception: If the browsing context is not a top-level traversable.
797
"""
798
params = {"context": context, "promptUnload": prompt_unload}
799
self.conn.execute(command_builder("browsingContext.close", params))
800
801
def create(
802
self,
803
type: str,
804
reference_context: Optional[str] = None,
805
background: bool = False,
806
user_context: Optional[str] = None,
807
) -> str:
808
"""Creates a new navigable, either in a new tab or in a new window, and returns its navigable id.
809
810
Parameters:
811
-----------
812
type: The type of the new navigable, either "tab" or "window".
813
reference_context: The reference browsing context ID.
814
background: Whether to create the new navigable in the background.
815
user_context: The user context ID.
816
817
Returns:
818
-------
819
str: The browsing context ID of the created navigable.
820
"""
821
params: dict[str, Any] = {"type": type}
822
if reference_context is not None:
823
params["referenceContext"] = reference_context
824
if background is not None:
825
params["background"] = background
826
if user_context is not None:
827
params["userContext"] = user_context
828
829
result = self.conn.execute(command_builder("browsingContext.create", params))
830
return result["context"]
831
832
def get_tree(
833
self,
834
max_depth: Optional[int] = None,
835
root: Optional[str] = None,
836
) -> list[BrowsingContextInfo]:
837
"""Returns a tree of all descendent navigables including the given parent itself, or all top-level contexts
838
when no parent is provided.
839
840
Parameters:
841
-----------
842
max_depth: The maximum depth of the tree.
843
root: The root browsing context ID.
844
845
Returns:
846
-------
847
List[BrowsingContextInfo]: A list of browsing context information.
848
"""
849
params: dict[str, Any] = {}
850
if max_depth is not None:
851
params["maxDepth"] = max_depth
852
if root is not None:
853
params["root"] = root
854
855
result = self.conn.execute(command_builder("browsingContext.getTree", params))
856
return [BrowsingContextInfo.from_json(context) for context in result["contexts"]]
857
858
def handle_user_prompt(
859
self,
860
context: str,
861
accept: Optional[bool] = None,
862
user_text: Optional[str] = None,
863
) -> None:
864
"""Allows closing an open prompt.
865
866
Parameters:
867
-----------
868
context: The browsing context ID.
869
accept: Whether to accept the prompt.
870
user_text: The text to enter in the prompt.
871
"""
872
params: dict[str, Any] = {"context": context}
873
if accept is not None:
874
params["accept"] = accept
875
if user_text is not None:
876
params["userText"] = user_text
877
878
self.conn.execute(command_builder("browsingContext.handleUserPrompt", params))
879
880
def locate_nodes(
881
self,
882
context: str,
883
locator: dict,
884
max_node_count: Optional[int] = None,
885
serialization_options: Optional[dict] = None,
886
start_nodes: Optional[list[dict]] = None,
887
) -> list[dict]:
888
"""Returns a list of all nodes matching the specified locator.
889
890
Parameters:
891
-----------
892
context: The browsing context ID.
893
locator: The locator to use.
894
max_node_count: The maximum number of nodes to return.
895
serialization_options: The serialization options.
896
start_nodes: The start nodes.
897
898
Returns:
899
-------
900
List[Dict]: A list of nodes.
901
"""
902
params: dict[str, Any] = {"context": context, "locator": locator}
903
if max_node_count is not None:
904
params["maxNodeCount"] = max_node_count
905
if serialization_options is not None:
906
params["serializationOptions"] = serialization_options
907
if start_nodes is not None:
908
params["startNodes"] = start_nodes
909
910
result = self.conn.execute(command_builder("browsingContext.locateNodes", params))
911
return result["nodes"]
912
913
def navigate(
914
self,
915
context: str,
916
url: str,
917
wait: Optional[str] = None,
918
) -> dict:
919
"""Navigates a navigable to the given URL.
920
921
Parameters:
922
-----------
923
context: The browsing context ID.
924
url: The URL to navigate to.
925
wait: The readiness state to wait for.
926
927
Returns:
928
-------
929
Dict: A dictionary containing the navigation result.
930
"""
931
params = {"context": context, "url": url}
932
if wait is not None:
933
params["wait"] = wait
934
935
result = self.conn.execute(command_builder("browsingContext.navigate", params))
936
return result
937
938
def print(
939
self,
940
context: str,
941
background: bool = False,
942
margin: Optional[dict] = None,
943
orientation: str = "portrait",
944
page: Optional[dict] = None,
945
page_ranges: Optional[list[Union[int, str]]] = None,
946
scale: float = 1.0,
947
shrink_to_fit: bool = True,
948
) -> str:
949
"""Creates a paginated representation of a document, and returns it as a PDF document represented as a
950
Base64-encoded string.
951
952
Parameters:
953
-----------
954
context: The browsing context ID.
955
background: Whether to include the background.
956
margin: The margin parameters.
957
orientation: The orientation, either "portrait" or "landscape".
958
page: The page parameters.
959
page_ranges: The page ranges.
960
scale: The scale.
961
shrink_to_fit: Whether to shrink to fit.
962
963
Returns:
964
-------
965
str: The Base64-encoded PDF document.
966
"""
967
params = {
968
"context": context,
969
"background": background,
970
"orientation": orientation,
971
"scale": scale,
972
"shrinkToFit": shrink_to_fit,
973
}
974
if margin is not None:
975
params["margin"] = margin
976
if page is not None:
977
params["page"] = page
978
if page_ranges is not None:
979
params["pageRanges"] = page_ranges
980
981
result = self.conn.execute(command_builder("browsingContext.print", params))
982
return result["data"]
983
984
def reload(
985
self,
986
context: str,
987
ignore_cache: Optional[bool] = None,
988
wait: Optional[str] = None,
989
) -> dict:
990
"""Reloads a navigable.
991
992
Parameters:
993
-----------
994
context: The browsing context ID.
995
ignore_cache: Whether to ignore the cache.
996
wait: The readiness state to wait for.
997
998
Returns:
999
-------
1000
Dict: A dictionary containing the navigation result.
1001
"""
1002
params: dict[str, Any] = {"context": context}
1003
if ignore_cache is not None:
1004
params["ignoreCache"] = ignore_cache
1005
if wait is not None:
1006
params["wait"] = wait
1007
1008
result = self.conn.execute(command_builder("browsingContext.reload", params))
1009
return result
1010
1011
def set_viewport(
1012
self,
1013
context: Optional[str] = None,
1014
viewport: Optional[dict] = None,
1015
device_pixel_ratio: Optional[float] = None,
1016
user_contexts: Optional[list[str]] = None,
1017
) -> None:
1018
"""Modifies specific viewport characteristics on the given top-level traversable.
1019
1020
Parameters:
1021
-----------
1022
context: The browsing context ID.
1023
viewport: The viewport parameters.
1024
device_pixel_ratio: The device pixel ratio.
1025
user_contexts: The user context IDs.
1026
1027
Raises:
1028
------
1029
Exception: If the browsing context is not a top-level traversable.
1030
"""
1031
params: dict[str, Any] = {}
1032
if context is not None:
1033
params["context"] = context
1034
if viewport is not None:
1035
params["viewport"] = viewport
1036
if device_pixel_ratio is not None:
1037
params["devicePixelRatio"] = device_pixel_ratio
1038
if user_contexts is not None:
1039
params["userContexts"] = user_contexts
1040
1041
self.conn.execute(command_builder("browsingContext.setViewport", params))
1042
1043
def traverse_history(self, context: str, delta: int) -> dict:
1044
"""Traverses the history of a given navigable by a delta.
1045
1046
Parameters:
1047
-----------
1048
context: The browsing context ID.
1049
delta: The delta to traverse by.
1050
1051
Returns:
1052
-------
1053
Dict: A dictionary containing the traverse history result.
1054
"""
1055
params = {"context": context, "delta": delta}
1056
result = self.conn.execute(command_builder("browsingContext.traverseHistory", params))
1057
return result
1058
1059
def add_event_handler(self, event: str, callback: Callable, contexts: Optional[list[str]] = None) -> int:
1060
"""Add an event handler to the browsing context.
1061
1062
Parameters:
1063
----------
1064
event: The event to subscribe to.
1065
callback: The callback function to execute on event.
1066
contexts: The browsing context IDs to subscribe to.
1067
1068
Returns:
1069
-------
1070
int: callback id
1071
"""
1072
return self._event_manager.add_event_handler(event, callback, contexts)
1073
1074
def remove_event_handler(self, event: str, callback_id: int) -> None:
1075
"""Remove an event handler from the browsing context.
1076
1077
Parameters:
1078
----------
1079
event: The event to unsubscribe from.
1080
callback_id: The callback id to remove.
1081
"""
1082
self._event_manager.remove_event_handler(event, callback_id)
1083
1084
def clear_event_handlers(self) -> None:
1085
"""Clear all event handlers from the browsing context."""
1086
self._event_manager.clear_event_handlers()
1087
1088