seleniumbase 4.24.10__py3-none-any.whl → 4.33.15__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. sbase/__init__.py +1 -0
  2. sbase/steps.py +7 -0
  3. seleniumbase/__init__.py +16 -7
  4. seleniumbase/__version__.py +1 -1
  5. seleniumbase/behave/behave_sb.py +97 -32
  6. seleniumbase/common/decorators.py +16 -7
  7. seleniumbase/config/proxy_list.py +3 -3
  8. seleniumbase/config/settings.py +4 -0
  9. seleniumbase/console_scripts/logo_helper.py +47 -8
  10. seleniumbase/console_scripts/run.py +345 -335
  11. seleniumbase/console_scripts/sb_behave_gui.py +5 -12
  12. seleniumbase/console_scripts/sb_caseplans.py +6 -13
  13. seleniumbase/console_scripts/sb_commander.py +5 -12
  14. seleniumbase/console_scripts/sb_install.py +62 -54
  15. seleniumbase/console_scripts/sb_mkchart.py +13 -20
  16. seleniumbase/console_scripts/sb_mkdir.py +11 -17
  17. seleniumbase/console_scripts/sb_mkfile.py +69 -43
  18. seleniumbase/console_scripts/sb_mkpres.py +13 -20
  19. seleniumbase/console_scripts/sb_mkrec.py +88 -21
  20. seleniumbase/console_scripts/sb_objectify.py +30 -30
  21. seleniumbase/console_scripts/sb_print.py +5 -12
  22. seleniumbase/console_scripts/sb_recorder.py +16 -11
  23. seleniumbase/core/browser_launcher.py +1658 -221
  24. seleniumbase/core/detect_b_ver.py +7 -8
  25. seleniumbase/core/log_helper.py +42 -27
  26. seleniumbase/core/mysql.py +1 -4
  27. seleniumbase/core/proxy_helper.py +35 -30
  28. seleniumbase/core/recorder_helper.py +24 -5
  29. seleniumbase/core/sb_cdp.py +1951 -0
  30. seleniumbase/core/sb_driver.py +162 -8
  31. seleniumbase/core/settings_parser.py +6 -0
  32. seleniumbase/core/style_sheet.py +10 -0
  33. seleniumbase/extensions/recorder.zip +0 -0
  34. seleniumbase/fixtures/base_case.py +1234 -632
  35. seleniumbase/fixtures/constants.py +10 -1
  36. seleniumbase/fixtures/js_utils.py +171 -144
  37. seleniumbase/fixtures/page_actions.py +177 -13
  38. seleniumbase/fixtures/page_utils.py +25 -53
  39. seleniumbase/fixtures/shared_utils.py +97 -11
  40. seleniumbase/js_code/active_css_js.py +1 -1
  41. seleniumbase/js_code/recorder_js.py +1 -1
  42. seleniumbase/plugins/base_plugin.py +2 -3
  43. seleniumbase/plugins/driver_manager.py +340 -65
  44. seleniumbase/plugins/pytest_plugin.py +276 -47
  45. seleniumbase/plugins/sb_manager.py +412 -99
  46. seleniumbase/plugins/selenium_plugin.py +122 -17
  47. seleniumbase/translate/translator.py +0 -7
  48. seleniumbase/undetected/__init__.py +59 -52
  49. seleniumbase/undetected/cdp.py +0 -1
  50. seleniumbase/undetected/cdp_driver/__init__.py +1 -0
  51. seleniumbase/undetected/cdp_driver/_contradict.py +110 -0
  52. seleniumbase/undetected/cdp_driver/browser.py +829 -0
  53. seleniumbase/undetected/cdp_driver/cdp_util.py +458 -0
  54. seleniumbase/undetected/cdp_driver/config.py +334 -0
  55. seleniumbase/undetected/cdp_driver/connection.py +639 -0
  56. seleniumbase/undetected/cdp_driver/element.py +1168 -0
  57. seleniumbase/undetected/cdp_driver/tab.py +1323 -0
  58. seleniumbase/undetected/dprocess.py +4 -7
  59. seleniumbase/undetected/options.py +6 -8
  60. seleniumbase/undetected/patcher.py +11 -13
  61. seleniumbase/undetected/reactor.py +0 -1
  62. seleniumbase/undetected/webelement.py +16 -3
  63. {seleniumbase-4.24.10.dist-info → seleniumbase-4.33.15.dist-info}/LICENSE +1 -1
  64. {seleniumbase-4.24.10.dist-info → seleniumbase-4.33.15.dist-info}/METADATA +299 -252
  65. {seleniumbase-4.24.10.dist-info → seleniumbase-4.33.15.dist-info}/RECORD +68 -70
  66. {seleniumbase-4.24.10.dist-info → seleniumbase-4.33.15.dist-info}/WHEEL +1 -1
  67. sbase/ReadMe.txt +0 -2
  68. seleniumbase/ReadMe.md +0 -25
  69. seleniumbase/common/ReadMe.md +0 -71
  70. seleniumbase/console_scripts/ReadMe.md +0 -731
  71. seleniumbase/drivers/ReadMe.md +0 -27
  72. seleniumbase/extensions/ReadMe.md +0 -12
  73. seleniumbase/masterqa/ReadMe.md +0 -61
  74. seleniumbase/resources/ReadMe.md +0 -31
  75. seleniumbase/resources/favicon.ico +0 -0
  76. seleniumbase/utilities/selenium_grid/ReadMe.md +0 -84
  77. seleniumbase/utilities/selenium_ide/ReadMe.md +0 -111
  78. {seleniumbase-4.24.10.dist-info → seleniumbase-4.33.15.dist-info}/entry_points.txt +0 -0
  79. {seleniumbase-4.24.10.dist-info → seleniumbase-4.33.15.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1168 @@
1
+ from __future__ import annotations
2
+ import asyncio
3
+ import logging
4
+ import pathlib
5
+ import secrets
6
+ import typing
7
+ from contextlib import suppress
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
+ await self.apply(
390
+ """
391
+ function (e) {
392
+ let o = {}
393
+ for(let k in e){
394
+ o[k] = e[k]
395
+ }
396
+ return o
397
+ }
398
+ """
399
+ )
400
+ )
401
+
402
+ def __await__(self):
403
+ return self.update().__await__()
404
+
405
+ def __call__(self, js_method):
406
+ return self.apply(f"(e) => e['{js_method}']()")
407
+
408
+ async def apply(self, js_function, return_by_value=True):
409
+ """
410
+ Apply javascript to this element.
411
+ The given js_function string should accept the js element as parameter,
412
+ and can be a arrow function, or function declaration.
413
+ Eg:
414
+ - '(elem) => {
415
+ elem.value = "blabla"; consolelog(elem);
416
+ alert(JSON.stringify(elem);
417
+ } '
418
+ - 'elem => elem.play()'
419
+ - function myFunction(elem) { alert(elem) }
420
+ :param js_function: JS function definition which received this element.
421
+ :param return_by_value:
422
+ """
423
+ self._remote_object = await self._tab.send(
424
+ cdp.dom.resolve_node(backend_node_id=self.backend_node_id)
425
+ )
426
+ result: typing.Tuple[cdp.runtime.RemoteObject, typing.Any] = (
427
+ await self._tab.send(
428
+ cdp.runtime.call_function_on(
429
+ js_function,
430
+ object_id=self._remote_object.object_id,
431
+ arguments=[
432
+ cdp.runtime.CallArgument(
433
+ object_id=self._remote_object.object_id
434
+ )
435
+ ],
436
+ return_by_value=True,
437
+ user_gesture=True,
438
+ )
439
+ )
440
+ )
441
+ try:
442
+ if result and result[0]:
443
+ if return_by_value:
444
+ return result[0].value
445
+ return result[0]
446
+ elif result[1]:
447
+ return result[1]
448
+ except Exception:
449
+ return self
450
+
451
+ async def get_position_async(self, abs=False) -> Position:
452
+ if not self.parent or not self.object_id:
453
+ self._remote_object = await self._tab.send(
454
+ cdp.dom.resolve_node(backend_node_id=self.backend_node_id)
455
+ )
456
+ # await self.update()
457
+ try:
458
+ quads = await self.tab.send(
459
+ cdp.dom.get_content_quads(
460
+ object_id=self.remote_object.object_id
461
+ )
462
+ )
463
+ if not quads:
464
+ raise Exception("Could not find position for %s " % self)
465
+ pos = Position(quads[0])
466
+ if abs:
467
+ scroll_y = (await self.tab.evaluate("window.scrollY")).value
468
+ scroll_x = (await self.tab.evaluate("window.scrollX")).value
469
+ abs_x = pos.left + scroll_x + (pos.width / 2)
470
+ abs_y = pos.top + scroll_y + (pos.height / 2)
471
+ pos.abs_x = abs_x
472
+ pos.abs_y = abs_y
473
+ return pos
474
+ except IndexError:
475
+ logger.debug(
476
+ "No content quads for %s. "
477
+ "Mostly caused by element which is not 'in plain sight'."
478
+ % self
479
+ )
480
+
481
+ async def mouse_click_async(
482
+ self,
483
+ button: str = "left",
484
+ buttons: typing.Optional[int] = 1,
485
+ modifiers: typing.Optional[int] = 0,
486
+ hold: bool = False,
487
+ _until_event: typing.Optional[type] = None,
488
+ ):
489
+ """
490
+ Native click (on element).
491
+ Note: This likely does not work at the moment. Use click() instead.
492
+ :param button: str (default = "left")
493
+ :param buttons: which button (default 1 = left)
494
+ :param modifiers: *(Optional)*
495
+ Bit field representing pressed modifier keys.
496
+ Alt=1, Ctrl=2, Meta/Command=4, Shift=8 (default: 0).
497
+ :param _until_event: Internal. Event to wait for before returning.
498
+ """
499
+ try:
500
+ center = (await self.get_position_async()).center
501
+ except AttributeError:
502
+ return
503
+ if not center:
504
+ logger.warning("Could not calculate box model for %s", self)
505
+ return
506
+ logger.debug("Clicking on location: %.2f, %.2f" % center)
507
+ await asyncio.gather(
508
+ self._tab.send(
509
+ cdp.input_.dispatch_mouse_event(
510
+ "mousePressed",
511
+ x=center[0],
512
+ y=center[1],
513
+ modifiers=modifiers,
514
+ button=cdp.input_.MouseButton(button),
515
+ buttons=buttons,
516
+ click_count=1,
517
+ )
518
+ ),
519
+ self._tab.send(
520
+ cdp.input_.dispatch_mouse_event(
521
+ "mouseReleased",
522
+ x=center[0],
523
+ y=center[1],
524
+ modifiers=modifiers,
525
+ button=cdp.input_.MouseButton(button),
526
+ buttons=buttons,
527
+ click_count=1,
528
+ )
529
+ ),
530
+ )
531
+ try:
532
+ await self.flash_async()
533
+ except BaseException:
534
+ pass
535
+
536
+ async def mouse_move_async(self):
537
+ """
538
+ Moves the mouse to the element position.
539
+ When an element has an hover/mouseover effect, this triggers it.
540
+ """
541
+ try:
542
+ center = (await self.get_position_async()).center
543
+ except AttributeError:
544
+ logger.debug("Did not find location for %s", self)
545
+ return
546
+ logger.debug(
547
+ "Mouse move to location %.2f, %.2f where %s is located",
548
+ *center,
549
+ self,
550
+ )
551
+ await self._tab.send(
552
+ cdp.input_.dispatch_mouse_event(
553
+ "mouseMoved", x=center[0], y=center[1]
554
+ )
555
+ )
556
+ await self._tab.sleep(0.05)
557
+ await self._tab.send(
558
+ cdp.input_.dispatch_mouse_event(
559
+ "mouseReleased", x=center[0], y=center[1]
560
+ )
561
+ )
562
+
563
+ async def mouse_drag_async(
564
+ self,
565
+ destination: typing.Union[Element, typing.Tuple[int, int]],
566
+ relative: bool = False,
567
+ steps: int = 1,
568
+ ):
569
+ """
570
+ Drags an element to another element or target coordinates.
571
+ Dragging of elements should be supported by the site.
572
+ :param destination: Another element where to drag to,
573
+ or a tuple (x,y) of ints representing coordinates.
574
+ :type destination: Element or coordinate as x,y tuple
575
+ :param relative: when True, treats coordinate as relative.
576
+ For example (-100, 200) will move left 100px and down 200px.
577
+ :type relative:
578
+ :param steps: Move in <steps> points.
579
+ This could make it look more "natural" (default 1),
580
+ but also a lot slower. (For very smooth actions, use 50-100)
581
+ :type steps: int
582
+ """
583
+ try:
584
+ start_point = (await self.get_position_async()).center
585
+ except AttributeError:
586
+ return
587
+ if not start_point:
588
+ logger.warning("Could not calculate box model for %s", self)
589
+ return
590
+ end_point = None
591
+ if isinstance(destination, Element):
592
+ try:
593
+ end_point = (await destination.get_position_async()).center
594
+ except AttributeError:
595
+ return
596
+ if not end_point:
597
+ logger.warning(
598
+ "Could not calculate box model for %s", destination
599
+ )
600
+ return
601
+ elif isinstance(destination, (tuple, list)):
602
+ if relative:
603
+ end_point = (
604
+ start_point[0] + destination[0],
605
+ start_point[1] + destination[1],
606
+ )
607
+ else:
608
+ end_point = destination
609
+ await self._tab.send(
610
+ cdp.input_.dispatch_mouse_event(
611
+ "mousePressed",
612
+ x=start_point[0],
613
+ y=start_point[1],
614
+ button=cdp.input_.MouseButton("left"),
615
+ )
616
+ )
617
+ steps = 1 if (not steps or steps < 1) else steps
618
+ if steps == 1:
619
+ await self._tab.send(
620
+ cdp.input_.dispatch_mouse_event(
621
+ "mouseMoved",
622
+ x=end_point[0],
623
+ y=end_point[1],
624
+ )
625
+ )
626
+ elif steps > 1:
627
+ step_size_x = (end_point[0] - start_point[0]) / steps
628
+ step_size_y = (end_point[1] - start_point[1]) / steps
629
+ pathway = [
630
+ (
631
+ start_point[0] + step_size_x * i,
632
+ start_point[1] + step_size_y * i,
633
+ )
634
+ for i in range(steps + 1)
635
+ ]
636
+ for point in pathway:
637
+ await self._tab.send(
638
+ cdp.input_.dispatch_mouse_event(
639
+ "mouseMoved",
640
+ x=point[0],
641
+ y=point[1],
642
+ )
643
+ )
644
+ await asyncio.sleep(0)
645
+ await self._tab.send(
646
+ cdp.input_.dispatch_mouse_event(
647
+ type_="mouseReleased",
648
+ x=end_point[0],
649
+ y=end_point[1],
650
+ button=cdp.input_.MouseButton("left"),
651
+ )
652
+ )
653
+
654
+ async def scroll_into_view_async(self):
655
+ """Scrolls element into view."""
656
+ try:
657
+ await self.tab.send(
658
+ cdp.dom.scroll_into_view_if_needed(
659
+ backend_node_id=self.backend_node_id
660
+ )
661
+ )
662
+ except Exception as e:
663
+ logger.debug("Could not scroll into view: %s", e)
664
+ return
665
+ # await self.apply("""(el) => el.scrollIntoView(false)""")
666
+
667
+ async def clear_input_async(self, _until_event: type = None):
668
+ """Clears an input field."""
669
+ try:
670
+ await self.apply('function (element) { element.value = "" } ')
671
+ except Exception as e:
672
+ logger.debug("Could not clear element field: %s", e)
673
+ return
674
+
675
+ async def send_keys_async(self, text: str):
676
+ """
677
+ Send text to an input field, or any other html element.
678
+ Hint: If you ever get stuck where using py:meth:`~click`
679
+ does not work, sending the keystroke \\n or \\r\\n
680
+ or a spacebar works wonders!
681
+ :param text: text to send
682
+ :return: None
683
+ """
684
+ await self.apply("(elem) => elem.focus()")
685
+ [
686
+ await self._tab.send(
687
+ cdp.input_.dispatch_key_event("char", text=char)
688
+ )
689
+ for char in list(text)
690
+ ]
691
+
692
+ async def send_file_async(self, *file_paths: PathLike):
693
+ """
694
+ Some form input require a file (upload).
695
+ A full path needs to be provided.
696
+ This method sends 1 or more file(s) to the input field.
697
+ Make sure the field accepts multiple files in order to send more files.
698
+ (Otherwise the browser might crash.)
699
+ Example:
700
+ `await fileinputElement.send_file('c:/tmp/img.png', 'c:/dir/lol.gif')`
701
+ """
702
+ file_paths = [str(p) for p in file_paths]
703
+ await self._tab.send(
704
+ cdp.dom.set_file_input_files(
705
+ files=[*file_paths],
706
+ backend_node_id=self.backend_node_id,
707
+ object_id=self.object_id,
708
+ )
709
+ )
710
+
711
+ async def focus_async(self):
712
+ """Focus the current element. Often useful in form (select) fields."""
713
+ return await self.apply("(element) => element.focus()")
714
+
715
+ async def select_option_async(self):
716
+ """
717
+ For form (select) fields. When you have queried the options
718
+ you can call this method on the option object.
719
+ Calling :func:`option.select_option()` uses option as selected value.
720
+ (Does not work in all cases.)
721
+ """
722
+ if self.node_name == "OPTION":
723
+ await self.apply(
724
+ """
725
+ (o) => {
726
+ o.selected = true;
727
+ o.dispatchEvent(new Event(
728
+ 'change', {view: window,bubbles: true})
729
+ )
730
+ }
731
+ """
732
+ )
733
+
734
+ async def set_value_async(self, value):
735
+ await self._tab.send(
736
+ cdp.dom.set_node_value(node_id=self.node_id, value=value)
737
+ )
738
+
739
+ async def set_text_async(self, value):
740
+ if not self.node_type == 3:
741
+ if self.child_node_count == 1:
742
+ child_node = self.children[0]
743
+ await child_node.set_text_async(value)
744
+ await self.update()
745
+ return
746
+ else:
747
+ raise RuntimeError("Could only set value of text nodes.")
748
+ await self.update()
749
+ await self._tab.send(
750
+ cdp.dom.set_node_value(node_id=self.node_id, value=value)
751
+ )
752
+
753
+ async def get_html_async(self):
754
+ return await self._tab.send(
755
+ cdp.dom.get_outer_html(backend_node_id=self.backend_node_id)
756
+ )
757
+
758
+ @property
759
+ def text_fragment(self) -> str:
760
+ """Gets the text content of this specific element node."""
761
+ text_node = util.filter_recurse(self.node, lambda n: n.node_type == 3)
762
+ if text_node:
763
+ return text_node.node_value.strip()
764
+ return ""
765
+
766
+ @property
767
+ def text(self):
768
+ """
769
+ Gets the text contents of this element and child nodes, concatenated.
770
+ Note: This includes text in the form of script content, (text nodes).
771
+ """
772
+ with suppress(Exception):
773
+ if self.node.node_name.lower() in ["input", "textarea"]:
774
+ input_node = self.node.shadow_roots[0].children[0].children[0]
775
+ if input_node:
776
+ return input_node.node_value
777
+ text_nodes = util.filter_recurse_all(
778
+ self.node, lambda n: n.node_type == 3
779
+ )
780
+ return " ".join([n.node_value for n in text_nodes]).strip()
781
+
782
+ @property
783
+ def text_all(self):
784
+ """Same as text(). Kept for backwards compatibility."""
785
+ with suppress(Exception):
786
+ if self.node.node_name.lower() in ["input", "textarea"]:
787
+ input_node = self.node.shadow_roots[0].children[0].children[0]
788
+ if input_node:
789
+ return input_node.node_value
790
+ text_nodes = util.filter_recurse_all(
791
+ self.node, lambda n: n.node_type == 3
792
+ )
793
+ return " ".join([n.node_value for n in text_nodes]).strip()
794
+
795
+ async def query_selector_all_async(self, selector: str):
796
+ """Like JS querySelectorAll()"""
797
+ await self.update()
798
+ return await self.tab.query_selector_all(selector, _node=self)
799
+
800
+ async def query_selector_async(self, selector: str):
801
+ """Like JS querySelector()"""
802
+ await self.update()
803
+ return await self.tab.query_selector(selector, self)
804
+
805
+ async def save_screenshot_async(
806
+ self,
807
+ filename: typing.Optional[PathLike] = "auto",
808
+ format: typing.Optional[str] = "png",
809
+ scale: typing.Optional[typing.Union[int, float]] = 1,
810
+ ):
811
+ """
812
+ Saves a screenshot of this element (only).
813
+ This is not the same as :py:obj:`Tab.save_screenshot`,
814
+ which saves a "regular" screenshot.
815
+ When the element is hidden, or has no size,
816
+ or is otherwise not capturable, a RuntimeError is raised.
817
+ :param filename: uses this as the save path
818
+ :type filename: PathLike
819
+ :param format: jpeg or png (defaults to png)
820
+ :type format: str
821
+ :param scale: the scale of the screenshot,
822
+ eg: 1 = size as is, 2 = double, 0.5 is half.
823
+ :return: the path/filename of saved screenshot
824
+ :rtype: str
825
+ """
826
+ import urllib.parse
827
+ import datetime
828
+ import base64
829
+
830
+ pos = await self.get_position_async()
831
+ if not pos:
832
+ raise RuntimeError(
833
+ "Could not determine position of element. "
834
+ "Probably because it's not in view, or hidden."
835
+ )
836
+ viewport = pos.to_viewport(scale)
837
+ path = None
838
+ await self.tab.sleep()
839
+ if not filename or filename == "auto":
840
+ parsed = urllib.parse.urlparse(self.tab.target.url)
841
+ parts = parsed.path.split("/")
842
+ last_part = parts[-1]
843
+ last_part = last_part.rsplit("?", 1)[0]
844
+ dt_str = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
845
+ candidate = f"{parsed.hostname}__{last_part}_{dt_str}"
846
+ ext = ""
847
+ if format.lower() in ["jpg", "jpeg"]:
848
+ ext = ".jpg"
849
+ format = "jpeg"
850
+ elif format.lower() in ["png"]:
851
+ ext = ".png"
852
+ format = "png"
853
+ path = pathlib.Path(candidate + ext)
854
+ else:
855
+ if filename.lower().endswith(".png"):
856
+ format = "png"
857
+ elif (
858
+ filename.lower().endswith(".jpg")
859
+ or filename.lower().endswith(".jpeg")
860
+ ):
861
+ format = "jpeg"
862
+ path = pathlib.Path(filename)
863
+ path.parent.mkdir(parents=True, exist_ok=True)
864
+ data = await self._tab.send(
865
+ cdp.page.capture_screenshot(
866
+ format, clip=viewport, capture_beyond_viewport=True
867
+ )
868
+ )
869
+ if not data:
870
+ from .connection import ProtocolException
871
+
872
+ raise ProtocolException(
873
+ "Could not take screenshot. "
874
+ "Most possible cause is the page has not finished loading yet."
875
+ )
876
+ data_bytes = base64.b64decode(data)
877
+ if not path:
878
+ raise RuntimeError("Invalid filename or path: '%s'" % filename)
879
+ path.write_bytes(data_bytes)
880
+ return str(path)
881
+
882
+ async def flash_async(
883
+ self,
884
+ duration: typing.Union[float, int] = 0.5,
885
+ color: typing.Optional[str] = "EE4488",
886
+ x_offset: typing.Union[float, int] = 0,
887
+ y_offset: typing.Union[float, int] = 0,
888
+ ):
889
+ """
890
+ Displays for a short time a red dot on the element.
891
+ (Only if the element itself is visible)
892
+ :param coords: x,y
893
+ :param duration: seconds (default 0.5)
894
+ """
895
+ from .connection import ProtocolException
896
+
897
+ if not self.remote_object:
898
+ try:
899
+ self._remote_object = await self.tab.send(
900
+ cdp.dom.resolve_node(backend_node_id=self.backend_node_id)
901
+ )
902
+ except ProtocolException:
903
+ return
904
+ try:
905
+ pos = await self.get_position_async()
906
+ except (Exception,):
907
+ logger.debug("flash() : Could not determine position.")
908
+ return
909
+ style = (
910
+ "position:absolute;z-index:99999999;padding:0;margin:0;"
911
+ "left:{:.1f}px; top: {:.1f}px; opacity:0.7;"
912
+ "width:8px;height:8px;border-radius:50%;background:#{};"
913
+ "animation:show-pointer-ani {:.2f}s ease 1;"
914
+ ).format(
915
+ pos.center[0] + x_offset - 4, # -4 to account for the circle
916
+ pos.center[1] + y_offset - 4, # -4 to account for the circle
917
+ color,
918
+ duration,
919
+ )
920
+ script = (
921
+ """
922
+ (targetElement) => {{
923
+ var css = document.styleSheets[0];
924
+ for( let css of [...document.styleSheets]) {{
925
+ try {{
926
+ css.insertRule(`
927
+ @keyframes show-pointer-ani {{
928
+ 0% {{ opacity: 1; transform: scale(2, 2);}}
929
+ 25% {{ transform: scale(5,5) }}
930
+ 50% {{ transform: scale(3, 3);}}
931
+ 75%: {{ transform: scale(2,2) }}
932
+ 100% {{ transform: scale(1, 1); opacity: 0;}}
933
+ }}`,css.cssRules.length);
934
+ break;
935
+ }} catch (e) {{
936
+ console.log(e)
937
+ }}
938
+ }};
939
+ var _d = document.createElement('div');
940
+ _d.style = `{0:s}`;
941
+ _d.id = `{1:s}`;
942
+ document.body.insertAdjacentElement('afterBegin', _d);
943
+ setTimeout(
944
+ () => document.getElementById('{1:s}').remove(), {2:d}
945
+ );
946
+ }}
947
+ """.format(
948
+ style,
949
+ secrets.token_hex(8),
950
+ int(duration * 1000),
951
+ )
952
+ .replace(" ", "")
953
+ .replace("\n", "")
954
+ )
955
+ arguments = [cdp.runtime.CallArgument(
956
+ object_id=self._remote_object.object_id
957
+ )]
958
+ await self._tab.send(
959
+ cdp.runtime.call_function_on(
960
+ script,
961
+ object_id=self._remote_object.object_id,
962
+ arguments=arguments,
963
+ await_promise=True,
964
+ user_gesture=True,
965
+ )
966
+ )
967
+
968
+ async def highlight_overlay_async(self):
969
+ """
970
+ Highlights the element devtools-style.
971
+ To remove the highlight, call the method again.
972
+ """
973
+ if getattr(self, "_is_highlighted", False):
974
+ del self._is_highlighted
975
+ await self.tab.send(cdp.overlay.hide_highlight())
976
+ await self.tab.send(cdp.dom.disable())
977
+ await self.tab.send(cdp.overlay.disable())
978
+ return
979
+ await self.tab.send(cdp.dom.enable())
980
+ await self.tab.send(cdp.overlay.enable())
981
+ conf = cdp.overlay.HighlightConfig(
982
+ show_info=True, show_extension_lines=True, show_styles=True
983
+ )
984
+ await self.tab.send(
985
+ cdp.overlay.highlight_node(
986
+ highlight_config=conf, backend_node_id=self.backend_node_id
987
+ )
988
+ )
989
+ setattr(self, "_is_highlighted", 1)
990
+
991
+ async def record_video_async(
992
+ self,
993
+ filename: typing.Optional[str] = None,
994
+ folder: typing.Optional[str] = None,
995
+ duration: typing.Optional[typing.Union[int, float]] = None,
996
+ ):
997
+ """
998
+ Experimental option.
999
+ :param filename: the desired filename
1000
+ :param folder: the download folder path
1001
+ :param duration: record for this many seconds and then download
1002
+ On html5 video nodes,
1003
+ you can call this method to start recording of the video.
1004
+ When any of the follow happens, the video recorded will be downloaded:
1005
+ - video ends
1006
+ - calling videoelement('pause')
1007
+ - video stops
1008
+ """
1009
+ if self.node_name != "VIDEO":
1010
+ raise RuntimeError(
1011
+ "record_video() can only be called on html5 video elements"
1012
+ )
1013
+ if not folder:
1014
+ directory_path = pathlib.Path.cwd() / "downloads"
1015
+ else:
1016
+ directory_path = pathlib.Path(folder)
1017
+ directory_path.mkdir(exist_ok=True)
1018
+ await self._tab.send(
1019
+ cdp.browser.set_download_behavior(
1020
+ "allow", download_path=str(directory_path)
1021
+ )
1022
+ )
1023
+ await self("pause")
1024
+ dtm = 'document.title + ".mp4"'
1025
+ await self.apply(
1026
+ """
1027
+ function extractVid(vid) {{
1028
+ var duration = {duration:.1f};
1029
+ var stream = vid.captureStream();
1030
+ var mr = new MediaRecorder(
1031
+ stream, {{audio:true, video:true}}
1032
+ )
1033
+ mr.ondataavailable = function(e) {{
1034
+ vid['_recording'] = false
1035
+ var blob = e.data;
1036
+ f = new File(
1037
+ [blob], {{name: {filename}, type:'octet/stream'}}
1038
+ );
1039
+ var objectUrl = URL.createObjectURL(f);
1040
+ var link = document.createElement('a');
1041
+ link.setAttribute('href', objectUrl)
1042
+ link.setAttribute('download', {filename})
1043
+ link.style.display = 'none'
1044
+ document.body.appendChild(link)
1045
+ link.click()
1046
+ document.body.removeChild(link)
1047
+ }}
1048
+ mr.start()
1049
+ vid.addEventListener('ended' , (e) => mr.stop())
1050
+ vid.addEventListener('pause' , (e) => mr.stop())
1051
+ vid.addEventListener('abort', (e) => mr.stop())
1052
+ if ( duration ) {{
1053
+ setTimeout(
1054
+ () => {{ vid.pause(); vid.play() }}, duration
1055
+ );
1056
+ }}
1057
+ vid['_recording'] = true
1058
+ ;}}
1059
+ """.format(
1060
+ filename=f'"{filename}"' if filename else dtm,
1061
+ duration=int(duration * 1000) if duration else 0,
1062
+ )
1063
+ )
1064
+ await self("play")
1065
+ await self._tab
1066
+
1067
+ async def is_recording_async(self):
1068
+ return await self.apply('(vid) => vid["_recording"]')
1069
+
1070
+ def _make_attrs(self):
1071
+ sav = None
1072
+ if self.node.attributes:
1073
+ for i, a in enumerate(self.node.attributes):
1074
+ if i == 0 or i % 2 == 0:
1075
+ if a == "class":
1076
+ a = "class_"
1077
+ sav = a
1078
+ else:
1079
+ if sav:
1080
+ self.attrs[sav] = a
1081
+
1082
+ def __eq__(self, other: Element) -> bool:
1083
+ # if other.__dict__.values() == self.__dict__.values():
1084
+ # return True
1085
+ if other.backend_node_id and self.backend_node_id:
1086
+ return other.backend_node_id == self.backend_node_id
1087
+ return False
1088
+
1089
+ def __repr__(self):
1090
+ tag_name = self.node.node_name.lower()
1091
+ content = ""
1092
+ # Collect all text from this leaf.
1093
+ if self.child_node_count:
1094
+ if self.child_node_count == 1:
1095
+ if self.children:
1096
+ content += str(self.children[0])
1097
+ elif self.child_node_count > 1:
1098
+ if self.children:
1099
+ for child in self.children:
1100
+ content += str(child)
1101
+ if self.node.node_type == 3: # Could be a text node
1102
+ content += self.node_value
1103
+ # Return text only. (No tag names)
1104
+ # This makes it look most natural.
1105
+ return content
1106
+ attrs = " ".join(
1107
+ [
1108
+ f'{k if k != "class_" else "class"}="{v}"'
1109
+ for k, v in self.attrs.items()
1110
+ ]
1111
+ )
1112
+ s = f"<{tag_name} {attrs}>{content}</{tag_name}>"
1113
+ return s
1114
+
1115
+
1116
+ class Position(cdp.dom.Quad):
1117
+ """Helper class for element-positioning."""
1118
+
1119
+ def __init__(self, points):
1120
+ super().__init__(points)
1121
+ (
1122
+ self.left,
1123
+ self.top,
1124
+ self.right,
1125
+ self.top,
1126
+ self.right,
1127
+ self.bottom,
1128
+ self.left,
1129
+ self.bottom,
1130
+ ) = points
1131
+ self.abs_x: float = 0
1132
+ self.abs_y: float = 0
1133
+ self.x = self.left
1134
+ self.y = self.top
1135
+ self.height, self.width = (
1136
+ self.bottom - self.top, self.right - self.left
1137
+ )
1138
+ self.center = (
1139
+ self.left + (self.width / 2),
1140
+ self.top + (self.height / 2),
1141
+ )
1142
+
1143
+ def to_viewport(self, scale=1):
1144
+ return cdp.page.Viewport(
1145
+ x=self.x,
1146
+ y=self.y,
1147
+ width=self.width,
1148
+ height=self.height,
1149
+ scale=scale,
1150
+ )
1151
+
1152
+ def __repr__(self):
1153
+ return (
1154
+ f"""<Position(x={self.left}, y={self.top},
1155
+ width={self.width}, height={self.height})>
1156
+ """
1157
+ )
1158
+
1159
+
1160
+ async def resolve_node(tab: Tab, node_id: cdp.dom.NodeId):
1161
+ remote_obj: cdp.runtime.RemoteObject = await tab.send(
1162
+ cdp.dom.resolve_node(node_id=node_id)
1163
+ )
1164
+ node_id: cdp.dom.NodeId = await tab.send(cdp.dom.request_node(
1165
+ remote_obj.object_id
1166
+ ))
1167
+ node: cdp.dom.Node = await tab.send(cdp.dom.describe_node(node_id))
1168
+ return node