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

Sign up to get free protection for your applications and to get access to all the features.
@@ -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