seleniumbase 4.32.1__py3-none-any.whl → 4.32.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1150 @@
1
+ from __future__ import annotations
2
+ import asyncio
3
+ import json
4
+ import logging
5
+ import pathlib
6
+ import secrets
7
+ import typing
8
+ from . import cdp_util as util
9
+ from ._contradict import ContraDict
10
+ from .config import PathLike
11
+ import mycdp as cdp
12
+ import mycdp.input_
13
+ import mycdp.dom
14
+ import mycdp.overlay
15
+ import mycdp.page
16
+ import mycdp.runtime
17
+
18
+ logger = logging.getLogger(__name__)
19
+ if typing.TYPE_CHECKING:
20
+ from .tab import Tab
21
+
22
+
23
+ def create(
24
+ node: cdp.dom.Node,
25
+ tab: Tab, tree:
26
+ typing.Optional[cdp.dom.Node] = None
27
+ ):
28
+ """
29
+ Factory for Elements.
30
+ This is used with Tab.query_selector(_all).
31
+ Since we already have the tree,
32
+ we don't need to fetch it for every single element.
33
+ :param node: cdp dom node representation
34
+ :type node: cdp.dom.Node
35
+ :param tab: the target object to which this element belongs
36
+ :type tab: Tab
37
+ :param tree: [Optional] the full node tree to which <node> belongs,
38
+ enhances performance. When not provided, you need to
39
+ call `await elem.update()` before using .children / .parent
40
+ :type tree:
41
+ """
42
+ elem = Element(node, tab, tree)
43
+ return elem
44
+
45
+
46
+ class Element:
47
+ def __init__(
48
+ self, node: cdp.dom.Node, tab: Tab, tree: cdp.dom.Node = None
49
+ ):
50
+ """
51
+ Represents an (HTML) DOM Element
52
+ :param node: cdp dom node representation
53
+ :type node: cdp.dom.Node
54
+ :param tab: the target object to which this element belongs
55
+ :type tab: Tab
56
+ """
57
+ if not node:
58
+ raise Exception("Node cannot be None!")
59
+ self._tab = tab
60
+ # if node.node_name == 'IFRAME':
61
+ # self._node = node.content_document
62
+ self._node = node
63
+ self._tree = tree
64
+ self._parent = None
65
+ self._remote_object = None
66
+ self._attrs = ContraDict(silent=True)
67
+ self._make_attrs()
68
+
69
+ @property
70
+ def tag(self):
71
+ if self.node_name:
72
+ return self.node_name.lower()
73
+
74
+ @property
75
+ def tag_name(self):
76
+ return self.tag
77
+
78
+ @property
79
+ def node_id(self):
80
+ return self.node.node_id
81
+
82
+ @property
83
+ def backend_node_id(self):
84
+ return self.node.backend_node_id
85
+
86
+ @property
87
+ def node_type(self):
88
+ return self.node.node_type
89
+
90
+ @property
91
+ def node_name(self):
92
+ return self.node.node_name
93
+
94
+ @property
95
+ def local_name(self):
96
+ return self.node.local_name
97
+
98
+ @property
99
+ def node_value(self):
100
+ return self.node.node_value
101
+
102
+ @property
103
+ def parent_id(self):
104
+ return self.node.parent_id
105
+
106
+ @property
107
+ def child_node_count(self):
108
+ return self.node.child_node_count
109
+
110
+ @property
111
+ def attributes(self):
112
+ return self.node.attributes
113
+
114
+ @property
115
+ def document_url(self):
116
+ return self.node.document_url
117
+
118
+ @property
119
+ def base_url(self):
120
+ return self.node.base_url
121
+
122
+ @property
123
+ def public_id(self):
124
+ return self.node.public_id
125
+
126
+ @property
127
+ def system_id(self):
128
+ return self.node.system_id
129
+
130
+ @property
131
+ def internal_subset(self):
132
+ return self.node.internal_subset
133
+
134
+ @property
135
+ def xml_version(self):
136
+ return self.node.xml_version
137
+
138
+ @property
139
+ def value(self):
140
+ return self.node.value
141
+
142
+ @property
143
+ def pseudo_type(self):
144
+ return self.node.pseudo_type
145
+
146
+ @property
147
+ def pseudo_identifier(self):
148
+ return self.node.pseudo_identifier
149
+
150
+ @property
151
+ def shadow_root_type(self):
152
+ return self.node.shadow_root_type
153
+
154
+ @property
155
+ def frame_id(self):
156
+ return self.node.frame_id
157
+
158
+ @property
159
+ def content_document(self):
160
+ return self.node.content_document
161
+
162
+ @property
163
+ def shadow_roots(self):
164
+ return self.node.shadow_roots
165
+
166
+ @property
167
+ def template_content(self):
168
+ return self.node.template_content
169
+
170
+ @property
171
+ def pseudo_elements(self):
172
+ return self.node.pseudo_elements
173
+
174
+ @property
175
+ def imported_document(self):
176
+ return self.node.imported_document
177
+
178
+ @property
179
+ def distributed_nodes(self):
180
+ return self.node.distributed_nodes
181
+
182
+ @property
183
+ def is_svg(self):
184
+ return self.node.is_svg
185
+
186
+ @property
187
+ def compatibility_mode(self):
188
+ return self.node.compatibility_mode
189
+
190
+ @property
191
+ def assigned_slot(self):
192
+ return self.node.assigned_slot
193
+
194
+ @property
195
+ def tab(self):
196
+ return self._tab
197
+
198
+ def __getattr__(self, item):
199
+ # If attribute is not found on the element object,
200
+ # check if it is present in the element attributes
201
+ # (Eg. href=, src=, alt=).
202
+ # Returns None when attribute is not found,
203
+ # instead of raising AttributeError.
204
+ x = getattr(self.attrs, item, None)
205
+ if x:
206
+ return x
207
+
208
+ def __setattr__(self, key, value):
209
+ if key[0] != "_":
210
+ if key[1:] not in vars(self).keys():
211
+ self.attrs.__setattr__(key, value)
212
+ return
213
+ super().__setattr__(key, value)
214
+
215
+ def __setitem__(self, key, value):
216
+ if key[0] != "_":
217
+ if key[1:] not in vars(self).keys():
218
+ self.attrs[key] = value
219
+
220
+ def __getitem__(self, item):
221
+ return self.attrs.get(item, None)
222
+
223
+ async def save_to_dom_async(self):
224
+ """Saves element to DOM."""
225
+ self._remote_object = await self._tab.send(
226
+ cdp.dom.resolve_node(backend_node_id=self.backend_node_id)
227
+ )
228
+ await self._tab.send(
229
+ cdp.dom.set_outer_html(self.node_id, outer_html=str(self))
230
+ )
231
+ await self.update()
232
+
233
+ async def remove_from_dom_async(self):
234
+ """Removes element from DOM."""
235
+ await self.update() # Ensure we have latest node_id
236
+ node = util.filter_recurse(
237
+ self._tree,
238
+ lambda node: node.backend_node_id == self.backend_node_id
239
+ )
240
+ if node:
241
+ await self.tab.send(cdp.dom.remove_node(node.node_id))
242
+ # self._tree = util.remove_from_tree(self.tree, self.node)
243
+
244
+ async def update(self, _node=None):
245
+ """
246
+ Updates element to retrieve more properties.
247
+ For example this enables:
248
+ :py:obj:`~children` and :py:obj:`~parent` attributes.
249
+ Also resolves js object,
250
+ which is a stored object in :py:obj:`~remote_object`.
251
+ Usually you will get element nodes by the usage of:
252
+ :py:meth:`Tab.query_selector_all()`
253
+ :py:meth:`Tab.find_elements_by_text()`
254
+ Those elements are already updated and you can browse
255
+ through children directly.
256
+ """
257
+ if _node:
258
+ doc = _node
259
+ # self._node = _node
260
+ # self._children.clear()
261
+ self._parent = None
262
+ else:
263
+ doc = await self._tab.send(cdp.dom.get_document(-1, True))
264
+ self._parent = None
265
+ # if self.node_name != "IFRAME":
266
+ updated_node = util.filter_recurse(
267
+ doc, lambda n: n.backend_node_id == self._node.backend_node_id
268
+ )
269
+ if updated_node:
270
+ logger.debug("Node changed, and has now been updated.")
271
+ self._node = updated_node
272
+ self._tree = doc
273
+ self._remote_object = await self._tab.send(
274
+ cdp.dom.resolve_node(backend_node_id=self._node.backend_node_id)
275
+ )
276
+ # self.attrs.clear()
277
+ self._make_attrs()
278
+ if self.node_name != "IFRAME":
279
+ parent_node = util.filter_recurse(
280
+ doc, lambda n: n.node_id == self.node.parent_id
281
+ )
282
+ if not parent_node:
283
+ # Could happen if node is for example <html>
284
+ return self
285
+ self._parent = create(parent_node, tab=self._tab, tree=self._tree)
286
+ return self
287
+
288
+ @property
289
+ def node(self):
290
+ return self._node
291
+
292
+ @property
293
+ def tree(self) -> cdp.dom.Node:
294
+ return self._tree
295
+
296
+ @tree.setter
297
+ def tree(self, tree: cdp.dom.Node):
298
+ self._tree = tree
299
+
300
+ @property
301
+ def attrs(self):
302
+ """
303
+ Attributes are stored here.
304
+ You can also set them directly on the element object.
305
+ """
306
+ return self._attrs
307
+
308
+ @property
309
+ def parent(self) -> typing.Union[Element, None]:
310
+ """Get the parent element (node) of current element (node)."""
311
+ if not self.tree:
312
+ raise RuntimeError(
313
+ "Could not get parent since the element has no tree set."
314
+ )
315
+ parent_node = util.filter_recurse(
316
+ self.tree, lambda n: n.node_id == self.parent_id
317
+ )
318
+ if not parent_node:
319
+ return None
320
+ parent_element = create(parent_node, tab=self._tab, tree=self.tree)
321
+ return parent_element
322
+
323
+ @property
324
+ def children(self) -> typing.Union[typing.List[Element], str]:
325
+ """
326
+ Returns the element's children.
327
+ Those children also have a children property
328
+ so that you can browse through the entire tree as well.
329
+ """
330
+ _children = []
331
+ if self._node.node_name == "IFRAME":
332
+ # iframes are not the same as other nodes.
333
+ # The children of iframes are found under
334
+ # the .content_document property,
335
+ # which is more useful than the node itself.
336
+ frame = self._node.content_document
337
+ if not frame.child_node_count:
338
+ return []
339
+ for child in frame.children:
340
+ child_elem = create(child, self._tab, frame)
341
+ if child_elem:
342
+ _children.append(child_elem)
343
+ # self._node = frame
344
+ return _children
345
+ elif not self.node.child_node_count:
346
+ return []
347
+ if self.node.children:
348
+ for child in self.node.children:
349
+ child_elem = create(child, self._tab, self.tree)
350
+ if child_elem:
351
+ _children.append(child_elem)
352
+ return _children
353
+
354
+ @property
355
+ def remote_object(self) -> cdp.runtime.RemoteObject:
356
+ return self._remote_object
357
+
358
+ @property
359
+ def object_id(self) -> cdp.runtime.RemoteObjectId:
360
+ try:
361
+ return self.remote_object.object_id
362
+ except AttributeError:
363
+ pass
364
+
365
+ async def click_async(self):
366
+ """Click the element."""
367
+ self._remote_object = await self._tab.send(
368
+ cdp.dom.resolve_node(
369
+ backend_node_id=self.backend_node_id
370
+ )
371
+ )
372
+ arguments = [cdp.runtime.CallArgument(
373
+ object_id=self._remote_object.object_id
374
+ )]
375
+ await self.flash_async(0.25)
376
+ await self._tab.send(
377
+ cdp.runtime.call_function_on(
378
+ "(el) => el.click()",
379
+ object_id=self._remote_object.object_id,
380
+ arguments=arguments,
381
+ await_promise=True,
382
+ user_gesture=True,
383
+ return_by_value=True,
384
+ )
385
+ )
386
+
387
+ async def get_js_attributes_async(self):
388
+ return ContraDict(
389
+ json.loads(
390
+ await self.apply(
391
+ """
392
+ function (e) {
393
+ let o = {}
394
+ for(let k in e){
395
+ o[k] = e[k]
396
+ }
397
+ return JSON.stringify(o)
398
+ }
399
+ """
400
+ )
401
+ )
402
+ )
403
+
404
+ def __await__(self):
405
+ return self.update().__await__()
406
+
407
+ def __call__(self, js_method):
408
+ return self.apply(f"(e) => e['{js_method}']()")
409
+
410
+ async def apply(self, js_function, return_by_value=True):
411
+ """
412
+ Apply javascript to this element.
413
+ The given js_function string should accept the js element as parameter,
414
+ and can be a arrow function, or function declaration.
415
+ Eg:
416
+ - '(elem) => {
417
+ elem.value = "blabla"; consolelog(elem);
418
+ alert(JSON.stringify(elem);
419
+ } '
420
+ - 'elem => elem.play()'
421
+ - function myFunction(elem) { alert(elem) }
422
+ :param js_function: JS function definition which received this element.
423
+ :param return_by_value:
424
+ """
425
+ self._remote_object = await self._tab.send(
426
+ cdp.dom.resolve_node(backend_node_id=self.backend_node_id)
427
+ )
428
+ result: typing.Tuple[cdp.runtime.RemoteObject, typing.Any] = (
429
+ await self._tab.send(
430
+ cdp.runtime.call_function_on(
431
+ js_function,
432
+ object_id=self._remote_object.object_id,
433
+ arguments=[
434
+ cdp.runtime.CallArgument(
435
+ object_id=self._remote_object.object_id
436
+ )
437
+ ],
438
+ return_by_value=True,
439
+ user_gesture=True,
440
+ )
441
+ )
442
+ )
443
+ if result and result[0]:
444
+ if return_by_value:
445
+ return result[0].value
446
+ return result[0]
447
+ elif result[1]:
448
+ return result[1]
449
+
450
+ async def get_position_async(self, abs=False) -> Position:
451
+ if not self.parent or not self.object_id:
452
+ self._remote_object = await self._tab.send(
453
+ cdp.dom.resolve_node(backend_node_id=self.backend_node_id)
454
+ )
455
+ # await self.update()
456
+ try:
457
+ quads = await self.tab.send(
458
+ cdp.dom.get_content_quads(
459
+ object_id=self.remote_object.object_id
460
+ )
461
+ )
462
+ if not quads:
463
+ raise Exception("Could not find position for %s " % self)
464
+ pos = Position(quads[0])
465
+ if abs:
466
+ scroll_y = (await self.tab.evaluate("window.scrollY")).value
467
+ scroll_x = (await self.tab.evaluate("window.scrollX")).value
468
+ abs_x = pos.left + scroll_x + (pos.width / 2)
469
+ abs_y = pos.top + scroll_y + (pos.height / 2)
470
+ pos.abs_x = abs_x
471
+ pos.abs_y = abs_y
472
+ return pos
473
+ except IndexError:
474
+ logger.debug(
475
+ "No content quads for %s. "
476
+ "Mostly caused by element which is not 'in plain sight'."
477
+ % self
478
+ )
479
+
480
+ async def mouse_click_async(
481
+ self,
482
+ button: str = "left",
483
+ buttons: typing.Optional[int] = 1,
484
+ modifiers: typing.Optional[int] = 0,
485
+ hold: bool = False,
486
+ _until_event: typing.Optional[type] = None,
487
+ ):
488
+ """
489
+ Native click (on element).
490
+ Note: This likely does not work at the moment. Use click() instead.
491
+ :param button: str (default = "left")
492
+ :param buttons: which button (default 1 = left)
493
+ :param modifiers: *(Optional)*
494
+ Bit field representing pressed modifier keys.
495
+ Alt=1, Ctrl=2, Meta/Command=4, Shift=8 (default: 0).
496
+ :param _until_event: Internal. Event to wait for before returning.
497
+ """
498
+ try:
499
+ center = (await self.get_position_async()).center
500
+ except AttributeError:
501
+ return
502
+ if not center:
503
+ logger.warning("Could not calculate box model for %s", self)
504
+ return
505
+ logger.debug("Clicking on location: %.2f, %.2f" % center)
506
+ await asyncio.gather(
507
+ self._tab.send(
508
+ cdp.input_.dispatch_mouse_event(
509
+ "mousePressed",
510
+ x=center[0],
511
+ y=center[1],
512
+ modifiers=modifiers,
513
+ button=cdp.input_.MouseButton(button),
514
+ buttons=buttons,
515
+ click_count=1,
516
+ )
517
+ ),
518
+ self._tab.send(
519
+ cdp.input_.dispatch_mouse_event(
520
+ "mouseReleased",
521
+ x=center[0],
522
+ y=center[1],
523
+ modifiers=modifiers,
524
+ button=cdp.input_.MouseButton(button),
525
+ buttons=buttons,
526
+ click_count=1,
527
+ )
528
+ ),
529
+ )
530
+ try:
531
+ await self.flash_async()
532
+ except BaseException:
533
+ pass
534
+
535
+ async def mouse_move_async(self):
536
+ """
537
+ Moves the mouse to the element position.
538
+ When an element has an hover/mouseover effect, this triggers it.
539
+ """
540
+ try:
541
+ center = (await self.get_position_async()).center
542
+ except AttributeError:
543
+ logger.debug("Did not find location for %s", self)
544
+ return
545
+ logger.debug(
546
+ "Mouse move to location %.2f, %.2f where %s is located",
547
+ *center,
548
+ self,
549
+ )
550
+ await self._tab.send(
551
+ cdp.input_.dispatch_mouse_event(
552
+ "mouseMoved", x=center[0], y=center[1]
553
+ )
554
+ )
555
+ await self._tab.sleep(0.05)
556
+ await self._tab.send(
557
+ cdp.input_.dispatch_mouse_event(
558
+ "mouseReleased", x=center[0], y=center[1]
559
+ )
560
+ )
561
+
562
+ async def mouse_drag_async(
563
+ self,
564
+ destination: typing.Union[Element, typing.Tuple[int, int]],
565
+ relative: bool = False,
566
+ steps: int = 1,
567
+ ):
568
+ """
569
+ Drags an element to another element or target coordinates.
570
+ Dragging of elements should be supported by the site.
571
+ :param destination: Another element where to drag to,
572
+ or a tuple (x,y) of ints representing coordinates.
573
+ :type destination: Element or coordinate as x,y tuple
574
+ :param relative: when True, treats coordinate as relative.
575
+ For example (-100, 200) will move left 100px and down 200px.
576
+ :type relative:
577
+ :param steps: Move in <steps> points.
578
+ This could make it look more "natural" (default 1),
579
+ but also a lot slower. (For very smooth actions, use 50-100)
580
+ :type steps: int
581
+ """
582
+ try:
583
+ start_point = (await self.get_position_async()).center
584
+ except AttributeError:
585
+ return
586
+ if not start_point:
587
+ logger.warning("Could not calculate box model for %s", self)
588
+ return
589
+ end_point = None
590
+ if isinstance(destination, Element):
591
+ try:
592
+ end_point = (await destination.get_position_async()).center
593
+ except AttributeError:
594
+ return
595
+ if not end_point:
596
+ logger.warning(
597
+ "Could not calculate box model for %s", destination
598
+ )
599
+ return
600
+ elif isinstance(destination, (tuple, list)):
601
+ if relative:
602
+ end_point = (
603
+ start_point[0] + destination[0],
604
+ start_point[1] + destination[1],
605
+ )
606
+ else:
607
+ end_point = destination
608
+ await self._tab.send(
609
+ cdp.input_.dispatch_mouse_event(
610
+ "mousePressed",
611
+ x=start_point[0],
612
+ y=start_point[1],
613
+ button=cdp.input_.MouseButton("left"),
614
+ )
615
+ )
616
+ steps = 1 if (not steps or steps < 1) else steps
617
+ if steps == 1:
618
+ await self._tab.send(
619
+ cdp.input_.dispatch_mouse_event(
620
+ "mouseMoved",
621
+ x=end_point[0],
622
+ y=end_point[1],
623
+ )
624
+ )
625
+ elif steps > 1:
626
+ step_size_x = (end_point[0] - start_point[0]) / steps
627
+ step_size_y = (end_point[1] - start_point[1]) / steps
628
+ pathway = [
629
+ (
630
+ start_point[0] + step_size_x * i,
631
+ start_point[1] + step_size_y * i,
632
+ )
633
+ for i in range(steps + 1)
634
+ ]
635
+ for point in pathway:
636
+ await self._tab.send(
637
+ cdp.input_.dispatch_mouse_event(
638
+ "mouseMoved",
639
+ x=point[0],
640
+ y=point[1],
641
+ )
642
+ )
643
+ await asyncio.sleep(0)
644
+ await self._tab.send(
645
+ cdp.input_.dispatch_mouse_event(
646
+ type_="mouseReleased",
647
+ x=end_point[0],
648
+ y=end_point[1],
649
+ button=cdp.input_.MouseButton("left"),
650
+ )
651
+ )
652
+
653
+ async def scroll_into_view_async(self):
654
+ """Scrolls element into view."""
655
+ try:
656
+ await self.tab.send(
657
+ cdp.dom.scroll_into_view_if_needed(
658
+ backend_node_id=self.backend_node_id
659
+ )
660
+ )
661
+ except Exception as e:
662
+ logger.debug("Could not scroll into view: %s", e)
663
+ return
664
+ # await self.apply("""(el) => el.scrollIntoView(false)""")
665
+
666
+ async def clear_input_async(self, _until_event: type = None):
667
+ """Clears an input field."""
668
+ try:
669
+ await self.apply('function (element) { element.value = "" } ')
670
+ except Exception as e:
671
+ logger.debug("Could not clear element field: %s", e)
672
+ return
673
+
674
+ async def send_keys_async(self, text: str):
675
+ """
676
+ Send text to an input field, or any other html element.
677
+ Hint: If you ever get stuck where using py:meth:`~click`
678
+ does not work, sending the keystroke \\n or \\r\\n
679
+ or a spacebar works wonders!
680
+ :param text: text to send
681
+ :return: None
682
+ """
683
+ await self.apply("(elem) => elem.focus()")
684
+ [
685
+ await self._tab.send(
686
+ cdp.input_.dispatch_key_event("char", text=char)
687
+ )
688
+ for char in list(text)
689
+ ]
690
+
691
+ async def send_file_async(self, *file_paths: PathLike):
692
+ """
693
+ Some form input require a file (upload).
694
+ A full path needs to be provided.
695
+ This method sends 1 or more file(s) to the input field.
696
+ Make sure the field accepts multiple files in order to send more files.
697
+ (Otherwise the browser might crash.)
698
+ Example:
699
+ `await fileinputElement.send_file('c:/tmp/img.png', 'c:/dir/lol.gif')`
700
+ """
701
+ file_paths = [str(p) for p in file_paths]
702
+ await self._tab.send(
703
+ cdp.dom.set_file_input_files(
704
+ files=[*file_paths],
705
+ backend_node_id=self.backend_node_id,
706
+ object_id=self.object_id,
707
+ )
708
+ )
709
+
710
+ async def focus_async(self):
711
+ """Focus the current element. Often useful in form (select) fields."""
712
+ return await self.apply("(element) => element.focus()")
713
+
714
+ async def select_option_async(self):
715
+ """
716
+ For form (select) fields. When you have queried the options
717
+ you can call this method on the option object.
718
+ Calling :func:`option.select_option()` uses option as selected value.
719
+ (Does not work in all cases.)
720
+ """
721
+ if self.node_name == "OPTION":
722
+ await self.apply(
723
+ """
724
+ (o) => {
725
+ o.selected = true;
726
+ o.dispatchEvent(new Event(
727
+ 'change', {view: window,bubbles: true})
728
+ )
729
+ }
730
+ """
731
+ )
732
+
733
+ async def set_value_async(self, value):
734
+ await self._tab.send(
735
+ cdp.dom.set_node_value(node_id=self.node_id, value=value)
736
+ )
737
+
738
+ async def set_text_async(self, value):
739
+ if not self.node_type == 3:
740
+ if self.child_node_count == 1:
741
+ child_node = self.children[0]
742
+ await child_node.set_text_async(value)
743
+ await self.update()
744
+ return
745
+ else:
746
+ raise RuntimeError("Could only set value of text nodes.")
747
+ await self.update()
748
+ await self._tab.send(
749
+ cdp.dom.set_node_value(node_id=self.node_id, value=value)
750
+ )
751
+
752
+ async def get_html_async(self):
753
+ return await self._tab.send(
754
+ cdp.dom.get_outer_html(backend_node_id=self.backend_node_id)
755
+ )
756
+
757
+ @property
758
+ def text_fragment(self) -> str:
759
+ """Gets the text content of this specific element node."""
760
+ text_node = util.filter_recurse(self.node, lambda n: n.node_type == 3)
761
+ if text_node:
762
+ return text_node.node_value.strip()
763
+ return ""
764
+
765
+ @property
766
+ def text(self):
767
+ """
768
+ Gets the text contents of this element and child nodes, concatenated.
769
+ Note: This includes text in the form of script content, (text nodes).
770
+ """
771
+ text_nodes = util.filter_recurse_all(
772
+ self.node, lambda n: n.node_type == 3
773
+ )
774
+ return " ".join([n.node_value for n in text_nodes]).strip()
775
+
776
+ @property
777
+ def text_all(self):
778
+ """Same as text(). Kept for backwards compatibility."""
779
+ text_nodes = util.filter_recurse_all(
780
+ self.node, lambda n: n.node_type == 3
781
+ )
782
+ return " ".join([n.node_value for n in text_nodes]).strip()
783
+
784
+ async def query_selector_all_async(self, selector: str):
785
+ """Like JS querySelectorAll()"""
786
+ await self.update()
787
+ return await self.tab.query_selector_all(selector, _node=self)
788
+
789
+ async def query_selector_async(self, selector: str):
790
+ """Like JS querySelector()"""
791
+ await self.update()
792
+ return await self.tab.query_selector(selector, self)
793
+
794
+ async def save_screenshot_async(
795
+ self,
796
+ filename: typing.Optional[PathLike] = "auto",
797
+ format: typing.Optional[str] = "png",
798
+ scale: typing.Optional[typing.Union[int, float]] = 1,
799
+ ):
800
+ """
801
+ Saves a screenshot of this element (only).
802
+ This is not the same as :py:obj:`Tab.save_screenshot`,
803
+ which saves a "regular" screenshot.
804
+ When the element is hidden, or has no size,
805
+ or is otherwise not capturable, a RuntimeError is raised.
806
+ :param filename: uses this as the save path
807
+ :type filename: PathLike
808
+ :param format: jpeg or png (defaults to png)
809
+ :type format: str
810
+ :param scale: the scale of the screenshot,
811
+ eg: 1 = size as is, 2 = double, 0.5 is half.
812
+ :return: the path/filename of saved screenshot
813
+ :rtype: str
814
+ """
815
+ import urllib.parse
816
+ import datetime
817
+ import base64
818
+
819
+ pos = await self.get_position_async()
820
+ if not pos:
821
+ raise RuntimeError(
822
+ "Could not determine position of element. "
823
+ "Probably because it's not in view, or hidden."
824
+ )
825
+ viewport = pos.to_viewport(scale)
826
+ path = None
827
+ await self.tab.sleep()
828
+ if not filename or filename == "auto":
829
+ parsed = urllib.parse.urlparse(self.tab.target.url)
830
+ parts = parsed.path.split("/")
831
+ last_part = parts[-1]
832
+ last_part = last_part.rsplit("?", 1)[0]
833
+ dt_str = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
834
+ candidate = f"{parsed.hostname}__{last_part}_{dt_str}"
835
+ ext = ""
836
+ if format.lower() in ["jpg", "jpeg"]:
837
+ ext = ".jpg"
838
+ format = "jpeg"
839
+ elif format.lower() in ["png"]:
840
+ ext = ".png"
841
+ format = "png"
842
+ path = pathlib.Path(candidate + ext)
843
+ else:
844
+ if filename.lower().endswith(".png"):
845
+ format = "png"
846
+ elif (
847
+ filename.lower().endswith(".jpg")
848
+ or filename.lower().endswith(".jpeg")
849
+ ):
850
+ format = "jpeg"
851
+ path = pathlib.Path(filename)
852
+ path.parent.mkdir(parents=True, exist_ok=True)
853
+ data = await self._tab.send(
854
+ cdp.page.capture_screenshot(
855
+ format, clip=viewport, capture_beyond_viewport=True
856
+ )
857
+ )
858
+ if not data:
859
+ from .connection import ProtocolException
860
+
861
+ raise ProtocolException(
862
+ "Could not take screenshot. "
863
+ "Most possible cause is the page has not finished loading yet."
864
+ )
865
+ data_bytes = base64.b64decode(data)
866
+ if not path:
867
+ raise RuntimeError("Invalid filename or path: '%s'" % filename)
868
+ path.write_bytes(data_bytes)
869
+ return str(path)
870
+
871
+ async def flash_async(self, duration: typing.Union[float, int] = 0.5):
872
+ """
873
+ Displays for a short time a red dot on the element.
874
+ (Only if the element itself is visible)
875
+ :param coords: x,y
876
+ :param duration: seconds (default 0.5)
877
+ """
878
+ from .connection import ProtocolException
879
+
880
+ if not self.remote_object:
881
+ try:
882
+ self._remote_object = await self.tab.send(
883
+ cdp.dom.resolve_node(backend_node_id=self.backend_node_id)
884
+ )
885
+ except ProtocolException:
886
+ return
887
+ try:
888
+ pos = await self.get_position_async()
889
+ except (Exception,):
890
+ logger.debug("flash() : Could not determine position.")
891
+ return
892
+ style = (
893
+ "position:absolute;z-index:99999999;padding:0;margin:0;"
894
+ "left:{:.1f}px; top: {:.1f}px; opacity:0.7;"
895
+ "width:8px;height:8px;border-radius:50%;background:#EE4488;"
896
+ "animation:show-pointer-ani {:.2f}s ease 1;"
897
+ ).format(
898
+ pos.center[0] - 4, # -4 to account for drawn circle itself (w,h)
899
+ pos.center[1] - 4,
900
+ duration,
901
+ )
902
+ script = (
903
+ """
904
+ (targetElement) => {{
905
+ var css = document.styleSheets[0];
906
+ for( let css of [...document.styleSheets]) {{
907
+ try {{
908
+ css.insertRule(`
909
+ @keyframes show-pointer-ani {{
910
+ 0% {{ opacity: 1; transform: scale(2, 2);}}
911
+ 25% {{ transform: scale(5,5) }}
912
+ 50% {{ transform: scale(3, 3);}}
913
+ 75%: {{ transform: scale(2,2) }}
914
+ 100% {{ transform: scale(1, 1); opacity: 0;}}
915
+ }}`,css.cssRules.length);
916
+ break;
917
+ }} catch (e) {{
918
+ console.log(e)
919
+ }}
920
+ }};
921
+ var _d = document.createElement('div');
922
+ _d.style = `{0:s}`;
923
+ _d.id = `{1:s}`;
924
+ document.body.insertAdjacentElement('afterBegin', _d);
925
+ setTimeout(
926
+ () => document.getElementById('{1:s}').remove(), {2:d}
927
+ );
928
+ }}
929
+ """.format(
930
+ style,
931
+ secrets.token_hex(8),
932
+ int(duration * 1000),
933
+ )
934
+ .replace(" ", "")
935
+ .replace("\n", "")
936
+ )
937
+ arguments = [cdp.runtime.CallArgument(
938
+ object_id=self._remote_object.object_id
939
+ )]
940
+ await self._tab.send(
941
+ cdp.runtime.call_function_on(
942
+ script,
943
+ object_id=self._remote_object.object_id,
944
+ arguments=arguments,
945
+ await_promise=True,
946
+ user_gesture=True,
947
+ )
948
+ )
949
+
950
+ async def highlight_overlay_async(self):
951
+ """
952
+ Highlights the element devtools-style.
953
+ To remove the highlight, call the method again.
954
+ """
955
+ if getattr(self, "_is_highlighted", False):
956
+ del self._is_highlighted
957
+ await self.tab.send(cdp.overlay.hide_highlight())
958
+ await self.tab.send(cdp.dom.disable())
959
+ await self.tab.send(cdp.overlay.disable())
960
+ return
961
+ await self.tab.send(cdp.dom.enable())
962
+ await self.tab.send(cdp.overlay.enable())
963
+ conf = cdp.overlay.HighlightConfig(
964
+ show_info=True, show_extension_lines=True, show_styles=True
965
+ )
966
+ await self.tab.send(
967
+ cdp.overlay.highlight_node(
968
+ highlight_config=conf, backend_node_id=self.backend_node_id
969
+ )
970
+ )
971
+ setattr(self, "_is_highlighted", 1)
972
+
973
+ async def record_video_async(
974
+ self,
975
+ filename: typing.Optional[str] = None,
976
+ folder: typing.Optional[str] = None,
977
+ duration: typing.Optional[typing.Union[int, float]] = None,
978
+ ):
979
+ """
980
+ Experimental option.
981
+ :param filename: the desired filename
982
+ :param folder: the download folder path
983
+ :param duration: record for this many seconds and then download
984
+ On html5 video nodes,
985
+ you can call this method to start recording of the video.
986
+ When any of the follow happens, the video recorded will be downloaded:
987
+ - video ends
988
+ - calling videoelement('pause')
989
+ - video stops
990
+ """
991
+ if self.node_name != "VIDEO":
992
+ raise RuntimeError(
993
+ "record_video() can only be called on html5 video elements"
994
+ )
995
+ if not folder:
996
+ directory_path = pathlib.Path.cwd() / "downloads"
997
+ else:
998
+ directory_path = pathlib.Path(folder)
999
+ directory_path.mkdir(exist_ok=True)
1000
+ await self._tab.send(
1001
+ cdp.browser.set_download_behavior(
1002
+ "allow", download_path=str(directory_path)
1003
+ )
1004
+ )
1005
+ await self("pause")
1006
+ dtm = 'document.title + ".mp4"'
1007
+ await self.apply(
1008
+ """
1009
+ function extractVid(vid) {{
1010
+ var duration = {duration:.1f};
1011
+ var stream = vid.captureStream();
1012
+ var mr = new MediaRecorder(
1013
+ stream, {{audio:true, video:true}}
1014
+ )
1015
+ mr.ondataavailable = function(e) {{
1016
+ vid['_recording'] = false
1017
+ var blob = e.data;
1018
+ f = new File(
1019
+ [blob], {{name: {filename}, type:'octet/stream'}}
1020
+ );
1021
+ var objectUrl = URL.createObjectURL(f);
1022
+ var link = document.createElement('a');
1023
+ link.setAttribute('href', objectUrl)
1024
+ link.setAttribute('download', {filename})
1025
+ link.style.display = 'none'
1026
+ document.body.appendChild(link)
1027
+ link.click()
1028
+ document.body.removeChild(link)
1029
+ }}
1030
+ mr.start()
1031
+ vid.addEventListener('ended' , (e) => mr.stop())
1032
+ vid.addEventListener('pause' , (e) => mr.stop())
1033
+ vid.addEventListener('abort', (e) => mr.stop())
1034
+ if ( duration ) {{
1035
+ setTimeout(
1036
+ () => {{ vid.pause(); vid.play() }}, duration
1037
+ );
1038
+ }}
1039
+ vid['_recording'] = true
1040
+ ;}}
1041
+ """.format(
1042
+ filename=f'"{filename}"' if filename else dtm,
1043
+ duration=int(duration * 1000) if duration else 0,
1044
+ )
1045
+ )
1046
+ await self("play")
1047
+ await self._tab
1048
+
1049
+ async def is_recording_async(self):
1050
+ return await self.apply('(vid) => vid["_recording"]')
1051
+
1052
+ def _make_attrs(self):
1053
+ sav = None
1054
+ if self.node.attributes:
1055
+ for i, a in enumerate(self.node.attributes):
1056
+ if i == 0 or i % 2 == 0:
1057
+ if a == "class":
1058
+ a = "class_"
1059
+ sav = a
1060
+ else:
1061
+ if sav:
1062
+ self.attrs[sav] = a
1063
+
1064
+ def __eq__(self, other: Element) -> bool:
1065
+ # if other.__dict__.values() == self.__dict__.values():
1066
+ # return True
1067
+ if other.backend_node_id and self.backend_node_id:
1068
+ return other.backend_node_id == self.backend_node_id
1069
+ return False
1070
+
1071
+ def __repr__(self):
1072
+ tag_name = self.node.node_name.lower()
1073
+ content = ""
1074
+ # Collect all text from this leaf.
1075
+ if self.child_node_count:
1076
+ if self.child_node_count == 1:
1077
+ if self.children:
1078
+ content += str(self.children[0])
1079
+ elif self.child_node_count > 1:
1080
+ if self.children:
1081
+ for child in self.children:
1082
+ content += str(child)
1083
+ if self.node.node_type == 3: # Could be a text node
1084
+ content += self.node_value
1085
+ # Return text only. (No tag names)
1086
+ # This makes it look most natural.
1087
+ return content
1088
+ attrs = " ".join(
1089
+ [
1090
+ f'{k if k != "class_" else "class"}="{v}"'
1091
+ for k, v in self.attrs.items()
1092
+ ]
1093
+ )
1094
+ s = f"<{tag_name} {attrs}>{content}</{tag_name}>"
1095
+ return s
1096
+
1097
+
1098
+ class Position(cdp.dom.Quad):
1099
+ """Helper class for element-positioning."""
1100
+
1101
+ def __init__(self, points):
1102
+ super().__init__(points)
1103
+ (
1104
+ self.left,
1105
+ self.top,
1106
+ self.right,
1107
+ self.top,
1108
+ self.right,
1109
+ self.bottom,
1110
+ self.left,
1111
+ self.bottom,
1112
+ ) = points
1113
+ self.abs_x: float = 0
1114
+ self.abs_y: float = 0
1115
+ self.x = self.left
1116
+ self.y = self.top
1117
+ self.height, self.width = (
1118
+ self.bottom - self.top, self.right - self.left
1119
+ )
1120
+ self.center = (
1121
+ self.left + (self.width / 2),
1122
+ self.top + (self.height / 2),
1123
+ )
1124
+
1125
+ def to_viewport(self, scale=1):
1126
+ return cdp.page.Viewport(
1127
+ x=self.x,
1128
+ y=self.y,
1129
+ width=self.width,
1130
+ height=self.height,
1131
+ scale=scale,
1132
+ )
1133
+
1134
+ def __repr__(self):
1135
+ return (
1136
+ f"""<Position(x={self.left}, y={self.top},
1137
+ width={self.width}, height={self.height})>
1138
+ """
1139
+ )
1140
+
1141
+
1142
+ async def resolve_node(tab: Tab, node_id: cdp.dom.NodeId):
1143
+ remote_obj: cdp.runtime.RemoteObject = await tab.send(
1144
+ cdp.dom.resolve_node(node_id=node_id)
1145
+ )
1146
+ node_id: cdp.dom.NodeId = await tab.send(cdp.dom.request_node(
1147
+ remote_obj.object_id
1148
+ ))
1149
+ node: cdp.dom.Node = await tab.send(cdp.dom.describe_node(node_id))
1150
+ return node