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