seleniumbase 4.24.11__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 (78) 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/log_helper.py +42 -27
  25. seleniumbase/core/mysql.py +1 -4
  26. seleniumbase/core/proxy_helper.py +35 -30
  27. seleniumbase/core/recorder_helper.py +24 -5
  28. seleniumbase/core/sb_cdp.py +1951 -0
  29. seleniumbase/core/sb_driver.py +162 -8
  30. seleniumbase/core/settings_parser.py +6 -0
  31. seleniumbase/core/style_sheet.py +10 -0
  32. seleniumbase/extensions/recorder.zip +0 -0
  33. seleniumbase/fixtures/base_case.py +1225 -614
  34. seleniumbase/fixtures/constants.py +10 -1
  35. seleniumbase/fixtures/js_utils.py +171 -144
  36. seleniumbase/fixtures/page_actions.py +177 -13
  37. seleniumbase/fixtures/page_utils.py +25 -53
  38. seleniumbase/fixtures/shared_utils.py +97 -11
  39. seleniumbase/js_code/active_css_js.py +1 -1
  40. seleniumbase/js_code/recorder_js.py +1 -1
  41. seleniumbase/plugins/base_plugin.py +2 -3
  42. seleniumbase/plugins/driver_manager.py +340 -65
  43. seleniumbase/plugins/pytest_plugin.py +276 -47
  44. seleniumbase/plugins/sb_manager.py +412 -99
  45. seleniumbase/plugins/selenium_plugin.py +122 -17
  46. seleniumbase/translate/translator.py +0 -7
  47. seleniumbase/undetected/__init__.py +59 -52
  48. seleniumbase/undetected/cdp.py +0 -1
  49. seleniumbase/undetected/cdp_driver/__init__.py +1 -0
  50. seleniumbase/undetected/cdp_driver/_contradict.py +110 -0
  51. seleniumbase/undetected/cdp_driver/browser.py +829 -0
  52. seleniumbase/undetected/cdp_driver/cdp_util.py +458 -0
  53. seleniumbase/undetected/cdp_driver/config.py +334 -0
  54. seleniumbase/undetected/cdp_driver/connection.py +639 -0
  55. seleniumbase/undetected/cdp_driver/element.py +1168 -0
  56. seleniumbase/undetected/cdp_driver/tab.py +1323 -0
  57. seleniumbase/undetected/dprocess.py +4 -7
  58. seleniumbase/undetected/options.py +6 -8
  59. seleniumbase/undetected/patcher.py +11 -13
  60. seleniumbase/undetected/reactor.py +0 -1
  61. seleniumbase/undetected/webelement.py +16 -3
  62. {seleniumbase-4.24.11.dist-info → seleniumbase-4.33.15.dist-info}/LICENSE +1 -1
  63. {seleniumbase-4.24.11.dist-info → seleniumbase-4.33.15.dist-info}/METADATA +299 -252
  64. {seleniumbase-4.24.11.dist-info → seleniumbase-4.33.15.dist-info}/RECORD +67 -69
  65. {seleniumbase-4.24.11.dist-info → seleniumbase-4.33.15.dist-info}/WHEEL +1 -1
  66. sbase/ReadMe.txt +0 -2
  67. seleniumbase/ReadMe.md +0 -25
  68. seleniumbase/common/ReadMe.md +0 -71
  69. seleniumbase/console_scripts/ReadMe.md +0 -731
  70. seleniumbase/drivers/ReadMe.md +0 -27
  71. seleniumbase/extensions/ReadMe.md +0 -12
  72. seleniumbase/masterqa/ReadMe.md +0 -61
  73. seleniumbase/resources/ReadMe.md +0 -31
  74. seleniumbase/resources/favicon.ico +0 -0
  75. seleniumbase/utilities/selenium_grid/ReadMe.md +0 -84
  76. seleniumbase/utilities/selenium_ide/ReadMe.md +0 -111
  77. {seleniumbase-4.24.11.dist-info → seleniumbase-4.33.15.dist-info}/entry_points.txt +0 -0
  78. {seleniumbase-4.24.11.dist-info → seleniumbase-4.33.15.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1323 @@
1
+ from __future__ import annotations
2
+ import asyncio
3
+ import logging
4
+ import pathlib
5
+ import warnings
6
+ from typing import Dict, List, Union, Optional, Tuple
7
+ from . import browser as cdp_browser
8
+ from . import element
9
+ from . import cdp_util as util
10
+ from .config import PathLike
11
+ from .connection import Connection, ProtocolException
12
+ import mycdp as cdp
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class Tab(Connection):
18
+ """
19
+ :ref:`tab` is the controlling mechanism/connection to a 'target',
20
+ for most of us 'target' can be read as 'tab'. However it could also
21
+ be an iframe, serviceworker or background script for example,
22
+ although there isn't much to control for those.
23
+ If you open a new window by using
24
+ :py:meth:`browser.get(..., new_window=True)`
25
+ Your url will open a new window. This window is a 'tab'.
26
+ When you browse to another page, the tab will be the same (browser view).
27
+ It's important to keep some reference to tab objects, in case you're
28
+ done interacting with elements and want to operate on the page level again.
29
+
30
+ Custom CDP commands
31
+ ---------------------------
32
+ Tab object provide many useful and often-used methods. It is also possible
33
+ to utilize the included cdp classes to to something totally custom.
34
+
35
+ The cdp package is a set of so-called "domains" with each having methods,
36
+ events and types.
37
+ To send a cdp method, for example :py:obj:`cdp.page.navigate`,
38
+ you'll have to check whether the method accepts any parameters
39
+ and whether they are required or not.
40
+
41
+ You can use:
42
+
43
+ ```python
44
+ await tab.send(cdp.page.navigate(url='https://Your-URL-Here'))
45
+ ```
46
+
47
+ So tab.send() accepts a generator object,
48
+ which is created by calling a cdp method.
49
+ This way you can build very detailed and customized commands.
50
+ (Note: Finding correct command combos can be a time-consuming task.
51
+ A whole bunch of useful methods have been added,
52
+ preferably having the same apis or lookalikes, as in selenium.)
53
+
54
+ Some useful, often needed and simply required methods
55
+ ===================================================================
56
+
57
+ :py:meth:`~find` | find(text)
58
+ ----------------------------------------
59
+ Finds and returns a single element by text match.
60
+ By default, returns the first element found.
61
+ Much more powerful is the best_match flag,
62
+ although also much more expensive.
63
+ When no match is found, it will retry for <timeout> seconds (default: 10),
64
+ so this is also suitable to use as wait condition.
65
+
66
+ :py:meth:`~find` | find(text, best_match=True) or find(text, True)
67
+ -----------------------------------------------------------------------
68
+ Much more powerful (and expensive) than the above is
69
+ the use of the `find(text, best_match=True)` flag.
70
+ It will still return 1 element, but when multiple matches are found,
71
+ it picks the one having the most similar text length.
72
+ How would that help?
73
+ For example, you search for "login",
74
+ you'd probably want the "login" button element,
75
+ and not thousands of scripts/meta/headings,
76
+ which happens to contain a string of "login".
77
+
78
+ When no match is found, it will retry for <timeout> seconds (default: 10),
79
+ so this is also suitable to use as wait condition.
80
+
81
+ :py:meth:`~select` | select(selector)
82
+ ----------------------------------------
83
+ Finds and returns a single element by css selector match.
84
+ When no match is found, it will retry for <timeout> seconds (default: 10),
85
+ so this is also suitable to use as wait condition.
86
+
87
+ :py:meth:`~select_all` | select_all(selector)
88
+ ------------------------------------------------
89
+ Finds and returns all elements by css selector match.
90
+ When no match is found, it will retry for <timeout> seconds (default: 10),
91
+ so this is also suitable to use as wait condition.
92
+
93
+ await :py:obj:`Tab`
94
+ ---------------------------
95
+ Calling `await tab` will do a lot of stuff under the hood,
96
+ and ensures all references are up to date.
97
+ Also it allows for the script to "breathe",
98
+ as it is oftentime faster than your browser or webpage.
99
+ So whenever you get stuck and things crashes or element could not be found,
100
+ you should probably let it "breathe" by calling `await page`
101
+ and/or `await page.sleep()`.
102
+
103
+ It ensures :py:obj:`~url` will be updated to the most recent one,
104
+ which is quite important in some other methods.
105
+
106
+ Using other and custom CDP commands
107
+ ======================================================
108
+ Using the included cdp module, you can easily craft commands,
109
+ which will always return an generator object.
110
+ This generator object can be easily sent to the :py:meth:`~send` method.
111
+
112
+ :py:meth:`~send`
113
+ ---------------------------
114
+ This is probably the most important method,
115
+ although you won't ever call it, unless you want to go really custom.
116
+ The send method accepts a :py:obj:`cdp` command.
117
+ Each of which can be found in the cdp section.
118
+
119
+ When you import * from this package, cdp will be in your namespace,
120
+ and contains all domains/actions/events you can act upon.
121
+ """
122
+ browser: cdp_browser.Browser
123
+ _download_behavior: List[str] = None
124
+
125
+ def __init__(
126
+ self,
127
+ websocket_url: str,
128
+ target: cdp.target.TargetInfo,
129
+ browser: Optional["cdp_browser.Browser"] = None,
130
+ **kwargs,
131
+ ):
132
+ super().__init__(websocket_url, target, browser, **kwargs)
133
+ self.browser = browser
134
+ self._dom = None
135
+ self._window_id = None
136
+
137
+ @property
138
+ def inspector_url(self):
139
+ """
140
+ Get the inspector url.
141
+ This url can be used in another browser to show you
142
+ the devtools interface for current tab.
143
+ Useful for debugging and headless mode.
144
+ """
145
+ return f"http://{self.browser.config.host}:{self.browser.config.port}/devtools/inspector.html?ws={self.websocket_url[5:]}" # noqa
146
+
147
+ def inspector_open(self):
148
+ import webbrowser
149
+
150
+ webbrowser.open(self.inspector_url, new=2)
151
+
152
+ async def open_external_inspector(self):
153
+ """
154
+ Opens the system's browser containing the devtools inspector page
155
+ for this tab. Could be handy, especially to debug in headless mode.
156
+ """
157
+ import webbrowser
158
+
159
+ webbrowser.open(self.inspector_url)
160
+
161
+ async def find(
162
+ self,
163
+ text: str,
164
+ best_match: bool = False,
165
+ return_enclosing_element: bool = True,
166
+ timeout: Union[int, float] = 10,
167
+ ):
168
+ """
169
+ Find single element by text.
170
+ Can also be used to wait for such element to appear.
171
+ :param text:
172
+ Text to search for. Note: Script contents are also considered text.
173
+ :type text: str
174
+ :param best_match: :param best_match:
175
+ When True (default), it will return the element which has the most
176
+ comparable string length. This could help a lot. Eg:
177
+ If you search for "login", you probably want the login button element,
178
+ and not thousands of tags/scripts containing a "login" string.
179
+ When False, it returns just the first match (but is way faster).
180
+ :type best_match: bool
181
+ :param return_enclosing_element:
182
+ Since we deal with nodes instead of elements,
183
+ the find function most often returns so called text nodes,
184
+ which is actually a element of plain text,
185
+ which is the somehow imaginary "child" of a "span", "p", "script"
186
+ or any other elements which have text between their opening
187
+ and closing tags.
188
+ Most often when we search by text, we actually aim for the
189
+ element containing the text instead of a lousy plain text node,
190
+ so by default the containing element is returned.
191
+ There are exceptions. Eg:
192
+ Elements that use the "placeholder=" property.
193
+ :type return_enclosing_element: bool
194
+ :param timeout:
195
+ Raise timeout exception when after this many seconds nothing is found.
196
+ :type timeout: float,int
197
+ """
198
+ loop = asyncio.get_running_loop()
199
+ start_time = loop.time()
200
+ text = text.strip()
201
+ item = None
202
+ try:
203
+ item = await self.find_element_by_text(
204
+ text, best_match, return_enclosing_element
205
+ )
206
+ except (Exception, TypeError):
207
+ pass
208
+ while not item:
209
+ await self
210
+ item = await self.find_element_by_text(
211
+ text, best_match, return_enclosing_element
212
+ )
213
+ if loop.time() - start_time > timeout:
214
+ raise asyncio.TimeoutError(
215
+ "Time ran out while waiting for: {%s}" % text
216
+ )
217
+ await self.sleep(0.5)
218
+ return item
219
+
220
+ async def select(
221
+ self,
222
+ selector: str,
223
+ timeout: Union[int, float] = 10,
224
+ ) -> element.Element:
225
+ """
226
+ Find a single element by css selector.
227
+ Can also be used to wait for such an element to appear.
228
+ :param selector: css selector,
229
+ eg a[href], button[class*=close], a > img[src]
230
+ :type selector: str
231
+ :param timeout:
232
+ Raise timeout exception when after this many seconds nothing is found.
233
+ :type timeout: float,int
234
+ """
235
+ loop = asyncio.get_running_loop()
236
+ start_time = loop.time()
237
+ selector = selector.strip()
238
+ item = None
239
+ try:
240
+ item = await self.query_selector(selector)
241
+ except (Exception, TypeError):
242
+ pass
243
+ while not item:
244
+ await self
245
+ item = await self.query_selector(selector)
246
+ if loop.time() - start_time > timeout:
247
+ raise asyncio.TimeoutError(
248
+ "Time ran out while waiting for: {%s}" % selector
249
+ )
250
+ await self.sleep(0.5)
251
+ return item
252
+
253
+ async def find_all(
254
+ self,
255
+ text: str,
256
+ timeout: Union[int, float] = 10,
257
+ ) -> List[element.Element]:
258
+ """
259
+ Find multiple elements by text.
260
+ Can also be used to wait for such elements to appear.
261
+ :param text: Text to search for.
262
+ Note: Script contents are also considered text.
263
+ :type text: str
264
+ :param timeout:
265
+ Raise timeout exception when after this many seconds nothing is found.
266
+ :type timeout: float,int
267
+ """
268
+ loop = asyncio.get_running_loop()
269
+ now = loop.time()
270
+ text = text.strip()
271
+ items = []
272
+ try:
273
+ items = await self.find_elements_by_text(text)
274
+ except (Exception, TypeError):
275
+ pass
276
+ while not items:
277
+ await self
278
+ items = await self.find_elements_by_text(text)
279
+ if loop.time() - now > timeout:
280
+ raise asyncio.TimeoutError(
281
+ "Time ran out while waiting for: {%s}" % text
282
+ )
283
+ await self.sleep(0.5)
284
+ return items
285
+
286
+ async def select_all(
287
+ self,
288
+ selector: str,
289
+ timeout: Union[int, float] = 10,
290
+ include_frames=False,
291
+ ) -> List[element.Element]:
292
+ """
293
+ Find multiple elements by CSS Selector.
294
+ Can also be used to wait for such elements to appear.
295
+ :param selector: css selector,
296
+ eg a[href], button[class*=close], a > img[src]
297
+ :type selector: str
298
+ :param timeout:
299
+ Raise timeout exception when after this many seconds nothing is found.
300
+ :type timeout: float,int
301
+ :param include_frames: Whether to include results in iframes.
302
+ :type include_frames: bool
303
+ """
304
+ loop = asyncio.get_running_loop()
305
+ now = loop.time()
306
+ selector = selector.strip()
307
+ items = []
308
+ if include_frames:
309
+ frames = await self.query_selector_all("iframe")
310
+ # Unfortunately, asyncio.gather is not an option here
311
+ for fr in frames:
312
+ items.extend(await fr.query_selector_all(selector))
313
+ items.extend(await self.query_selector_all(selector))
314
+ while not items:
315
+ await self
316
+ items = await self.query_selector_all(selector)
317
+ if loop.time() - now > timeout:
318
+ raise asyncio.TimeoutError(
319
+ "Time ran out while waiting for: {%s}" % selector
320
+ )
321
+ await self.sleep(0.5)
322
+ return items
323
+
324
+ async def get(
325
+ self,
326
+ url="about:blank",
327
+ new_tab: bool = False,
328
+ new_window: bool = False,
329
+ ):
330
+ """
331
+ Top level get. Utilizes the first tab to retrieve the given url.
332
+ This is a convenience function known from selenium.
333
+ This function handles waits/sleeps and detects when DOM events fired,
334
+ so it's the safest way of navigating.
335
+ :param url: the url to navigate to
336
+ :param new_tab: open new tab
337
+ :param new_window: open new window
338
+ :return: Page
339
+ """
340
+ if not self.browser:
341
+ raise AttributeError(
342
+ "This page/tab has no browser attribute, "
343
+ "so you can't use get()"
344
+ )
345
+ if new_window and not new_tab:
346
+ new_tab = True
347
+ if new_tab:
348
+ return await self.browser.get(url, new_tab, new_window)
349
+ else:
350
+ frame_id, loader_id, *_ = await self.send(cdp.page.navigate(url))
351
+ await self
352
+ return self
353
+
354
+ async def query_selector_all(
355
+ self,
356
+ selector: str,
357
+ _node: Optional[Union[cdp.dom.Node, "element.Element"]] = None,
358
+ ):
359
+ """
360
+ Equivalent of JavaScript "document.querySelectorAll".
361
+ This is considered one of the main methods to use in this package.
362
+ It returns all matching :py:obj:`element.Element` objects.
363
+ :param selector: css selector.
364
+ (first time? => https://www.w3schools.com/cssref/css_selectors.php )
365
+ :type selector: str
366
+ :param _node: internal use
367
+ """
368
+ if not _node:
369
+ doc: cdp.dom.Node = await self.send(cdp.dom.get_document(-1, True))
370
+ else:
371
+ doc = _node
372
+ if _node.node_name == "IFRAME":
373
+ doc = _node.content_document
374
+ node_ids = []
375
+ try:
376
+ node_ids = await self.send(
377
+ cdp.dom.query_selector_all(doc.node_id, selector)
378
+ )
379
+ except ProtocolException as e:
380
+ if _node is not None:
381
+ if "could not find node" in e.message.lower():
382
+ if getattr(_node, "__last", None):
383
+ del _node.__last
384
+ return []
385
+ # If the supplied node is not found,
386
+ # then the DOM has changed since acquiring the element.
387
+ # Therefore, we need to update our node, and try again.
388
+ await _node.update()
389
+ _node.__last = (
390
+ True # Make sure this isn't turned into infinite loop.
391
+ )
392
+ return await self.query_selector_all(selector, _node)
393
+ else:
394
+ await self.send(cdp.dom.disable())
395
+ raise
396
+ if not node_ids:
397
+ return []
398
+ items = []
399
+ for nid in node_ids:
400
+ node = util.filter_recurse(doc, lambda n: n.node_id == nid)
401
+ # Pass along the retrieved document tree to improve performance.
402
+ if not node:
403
+ continue
404
+ elem = element.create(node, self, doc)
405
+ items.append(elem)
406
+ return items
407
+
408
+ async def query_selector(
409
+ self,
410
+ selector: str,
411
+ _node: Optional[Union[cdp.dom.Node, element.Element]] = None,
412
+ ):
413
+ """
414
+ Find a single element based on a CSS Selector string.
415
+ :param selector: CSS Selector(s)
416
+ :type selector: str
417
+ """
418
+ selector = selector.strip()
419
+ if not _node:
420
+ doc: cdp.dom.Node = await self.send(cdp.dom.get_document(-1, True))
421
+ else:
422
+ doc = _node
423
+ if _node.node_name == "IFRAME":
424
+ doc = _node.content_document
425
+ node_id = None
426
+ try:
427
+ node_id = await self.send(
428
+ cdp.dom.query_selector(doc.node_id, selector)
429
+ )
430
+ except ProtocolException as e:
431
+ if _node is not None:
432
+ if "could not find node" in e.message.lower():
433
+ if getattr(_node, "__last", None):
434
+ del _node.__last
435
+ return []
436
+ # If supplied node is not found,
437
+ # the dom has changed since acquiring the element,
438
+ # therefore, update our passed node and try again.
439
+ await _node.update()
440
+ _node.__last = (
441
+ True # Make sure this isn't turned into infinite loop.
442
+ )
443
+ return await self.query_selector(selector, _node)
444
+ else:
445
+ await self.send(cdp.dom.disable())
446
+ raise
447
+ if not node_id:
448
+ return
449
+ node = util.filter_recurse(doc, lambda n: n.node_id == node_id)
450
+ if not node:
451
+ return
452
+ return element.create(node, self, doc)
453
+
454
+ async def find_elements_by_text(
455
+ self,
456
+ text: str,
457
+ ) -> List[element.Element]:
458
+ """
459
+ Returns element which match the given text.
460
+ Note: This may (or will) also return any other element
461
+ (like inline scripts), which happen to contain that text.
462
+ :param text:
463
+ """
464
+ text = text.strip()
465
+ doc = await self.send(cdp.dom.get_document(-1, True))
466
+ search_id, nresult = await self.send(
467
+ cdp.dom.perform_search(text, True)
468
+ )
469
+ if nresult:
470
+ node_ids = await self.send(
471
+ cdp.dom.get_search_results(search_id, 0, nresult)
472
+ )
473
+ else:
474
+ node_ids = []
475
+ await self.send(cdp.dom.discard_search_results(search_id))
476
+ items = []
477
+ for nid in node_ids:
478
+ node = util.filter_recurse(doc, lambda n: n.node_id == nid)
479
+ if not node:
480
+ node = await self.send(cdp.dom.resolve_node(node_id=nid))
481
+ if not node:
482
+ continue
483
+ # remote_object = await self.send(
484
+ # cdp.dom.resolve_node(backend_node_id=node.backend_node_id)
485
+ # )
486
+ # node_id = await self.send(
487
+ # cdp.dom.request_node(object_id=remote_object.object_id)
488
+ # )
489
+ try:
490
+ elem = element.create(node, self, doc)
491
+ except BaseException:
492
+ continue
493
+ if elem.node_type == 3:
494
+ # If found element is a text node (which is plain text,
495
+ # and useless for our purpose), we return the parent element
496
+ # of the node (which is often a tag which can have text
497
+ # between their opening and closing tags (that is most tags,
498
+ # except for example "img" and "video", "br").
499
+ if not elem.parent:
500
+ # Check if parent actually has a parent
501
+ # and update it to be absolutely sure.
502
+ await elem.update()
503
+ items.append(
504
+ elem.parent or elem
505
+ ) # When there's no parent, use the text node itself.
506
+ continue
507
+ else:
508
+ # Add the element itself.
509
+ items.append(elem)
510
+ # Since we already fetched the entire doc, including shadow and frames,
511
+ # let's also search through the iframes.
512
+ iframes = util.filter_recurse_all(
513
+ doc, lambda node: node.node_name == "IFRAME"
514
+ )
515
+ if iframes:
516
+ iframes_elems = [
517
+ element.create(iframe, self, iframe.content_document)
518
+ for iframe in iframes
519
+ ]
520
+ for iframe_elem in iframes_elems:
521
+ if iframe_elem.content_document:
522
+ iframe_text_nodes = util.filter_recurse_all(
523
+ iframe_elem,
524
+ lambda node: node.node_type == 3 # noqa
525
+ and text.lower() in node.node_value.lower(),
526
+ )
527
+ if iframe_text_nodes:
528
+ iframe_text_elems = [
529
+ element.create(text_node, self, iframe_elem.tree)
530
+ for text_node in iframe_text_nodes
531
+ ]
532
+ items.extend(
533
+ text_node.parent for text_node in iframe_text_elems
534
+ )
535
+ await self.send(cdp.dom.disable())
536
+ return items or []
537
+
538
+ async def find_element_by_text(
539
+ self,
540
+ text: str,
541
+ best_match: Optional[bool] = False,
542
+ return_enclosing_element: Optional[bool] = True,
543
+ ) -> Union[element.Element, None]:
544
+ """
545
+ Finds and returns the first element containing <text>, or best match.
546
+ :param text:
547
+ :param best_match:
548
+ When True, which is MUCH more expensive (thus much slower),
549
+ will find the closest match based on length.
550
+ When searching for "login", you probably want the button element,
551
+ and not thousands of tags/scripts containing the "login" string.
552
+ :type best_match: bool
553
+ :param return_enclosing_element:
554
+ """
555
+ doc = await self.send(cdp.dom.get_document(-1, True))
556
+ text = text.strip()
557
+ search_id, nresult = await self.send(
558
+ cdp.dom.perform_search(text, True)
559
+ )
560
+ node_ids = await self.send(
561
+ cdp.dom.get_search_results(search_id, 0, nresult)
562
+ )
563
+ await self.send(cdp.dom.discard_search_results(search_id))
564
+ if not node_ids:
565
+ node_ids = []
566
+ items = []
567
+ for nid in node_ids:
568
+ node = util.filter_recurse(doc, lambda n: n.node_id == nid)
569
+ try:
570
+ elem = element.create(node, self, doc)
571
+ except BaseException:
572
+ continue
573
+ if elem.node_type == 3:
574
+ # If found element is a text node
575
+ # (which is plain text, and useless for our purpose),
576
+ # then return the parent element of the node
577
+ # (which is often a tag which can have text between their
578
+ # opening and closing tags (that is most tags,
579
+ # except for example "img" and "video", "br").
580
+ if not elem.parent:
581
+ # Check if parent has a parent, and update it to be sure.
582
+ await elem.update()
583
+ items.append(
584
+ elem.parent or elem
585
+ ) # When it really has no parent, use the text node itself
586
+ continue
587
+ else:
588
+ # Add the element itself
589
+ items.append(elem)
590
+ # Since the entire doc is already fetched, including shadow and frames,
591
+ # also search through the iframes.
592
+ iframes = util.filter_recurse_all(
593
+ doc, lambda node: node.node_name == "IFRAME"
594
+ )
595
+ if iframes:
596
+ iframes_elems = [
597
+ element.create(iframe, self, iframe.content_document)
598
+ for iframe in iframes
599
+ ]
600
+ for iframe_elem in iframes_elems:
601
+ iframe_text_nodes = util.filter_recurse_all(
602
+ iframe_elem,
603
+ lambda node: node.node_type == 3 # noqa
604
+ and text.lower() in node.node_value.lower(),
605
+ )
606
+ if iframe_text_nodes:
607
+ iframe_text_elems = [
608
+ element.create(text_node, self, iframe_elem.tree)
609
+ for text_node in iframe_text_nodes
610
+ ]
611
+ items.extend(
612
+ text_node.parent for text_node in iframe_text_elems
613
+ )
614
+ try:
615
+ if not items:
616
+ return
617
+ if best_match:
618
+ closest_by_length = min(
619
+ items, key=lambda el: abs(len(text) - len(el.text_all))
620
+ )
621
+ elem = closest_by_length or items[0]
622
+ return elem
623
+ else:
624
+ # Return the first result
625
+ for elem in items:
626
+ if elem:
627
+ return elem
628
+ finally:
629
+ await self.send(cdp.dom.disable())
630
+
631
+ async def back(self):
632
+ """History back"""
633
+ await self.send(cdp.runtime.evaluate("window.history.back()"))
634
+
635
+ async def forward(self):
636
+ """History forward"""
637
+ await self.send(cdp.runtime.evaluate("window.history.forward()"))
638
+
639
+ async def get_navigation_history(self):
640
+ """Get Navigation History"""
641
+ return await self.send(cdp.page.get_navigation_history())
642
+
643
+ async def reload(
644
+ self,
645
+ ignore_cache: Optional[bool] = True,
646
+ script_to_evaluate_on_load: Optional[str] = None,
647
+ ):
648
+ """
649
+ Reloads the page
650
+ :param ignore_cache: When set to True (default),
651
+ it ignores cache, and re-downloads the items.
652
+ :param script_to_evaluate_on_load: Script to run on load.
653
+ """
654
+ await self.send(
655
+ cdp.page.reload(
656
+ ignore_cache=ignore_cache,
657
+ script_to_evaluate_on_load=script_to_evaluate_on_load,
658
+ ),
659
+ )
660
+
661
+ async def evaluate(
662
+ self, expression: str, await_promise=False, return_by_value=True
663
+ ):
664
+ remote_object, errors = await self.send(
665
+ cdp.runtime.evaluate(
666
+ expression=expression,
667
+ user_gesture=True,
668
+ await_promise=await_promise,
669
+ return_by_value=return_by_value,
670
+ allow_unsafe_eval_blocked_by_csp=True,
671
+ )
672
+ )
673
+ if errors:
674
+ raise ProtocolException(errors)
675
+ if remote_object:
676
+ if return_by_value:
677
+ if remote_object.value:
678
+ return remote_object.value
679
+ else:
680
+ return remote_object, errors
681
+
682
+ async def js_dumps(
683
+ self, obj_name: str, return_by_value: Optional[bool] = True
684
+ ) -> Union[
685
+ Dict,
686
+ Tuple[cdp.runtime.RemoteObject, cdp.runtime.ExceptionDetails],
687
+ ]:
688
+ """
689
+ Dump Given js object with its properties and values as a dict.
690
+ Note: Complex objects might not be serializable,
691
+ therefore this method is not a "source of truth"
692
+ :param obj_name: the js object to dump
693
+ :type obj_name: str
694
+ :param return_by_value: If you want an tuple of cdp objects
695
+ (returnvalue, errors), then set this to False.
696
+ :type return_by_value: bool
697
+
698
+ Example
699
+ -------
700
+
701
+ x = await self.js_dumps('window')
702
+ print(x)
703
+ '...{
704
+ 'pageYOffset': 0,
705
+ 'visualViewport': {},
706
+ 'screenX': 10,
707
+ 'screenY': 10,
708
+ 'outerWidth': 1050,
709
+ 'outerHeight': 832,
710
+ 'devicePixelRatio': 1,
711
+ 'screenLeft': 10,
712
+ 'screenTop': 10,
713
+ 'styleMedia': {},
714
+ 'onsearch': None,
715
+ 'isSecureContext': True,
716
+ 'trustedTypes': {},
717
+ 'performance': {'timeOrigin': 1707823094767.9,
718
+ 'timing': {'connectStart': 0,
719
+ 'navigationStart': 1707823094768,
720
+ ]...
721
+ """
722
+ js_code_a = (
723
+ """
724
+ function ___dump(obj, _d = 0) {
725
+ let _typesA = ['object', 'function'];
726
+ let _typesB = ['number', 'string', 'boolean'];
727
+ if (_d == 2) {
728
+ console.log('maxdepth reached for ', obj);
729
+ return
730
+ }
731
+ let tmp = {}
732
+ for (let k in obj) {
733
+ if (obj[k] == window) continue;
734
+ let v;
735
+ try {
736
+ if (obj[k] === null
737
+ || obj[k] === undefined
738
+ || obj[k] === NaN) {
739
+ console.log('obj[k] is null or undefined or Nan',
740
+ k, '=>', obj[k])
741
+ tmp[k] = obj[k];
742
+ continue
743
+ }
744
+ } catch (e) {
745
+ tmp[k] = null;
746
+ continue
747
+ }
748
+ if (_typesB.includes(typeof obj[k])) {
749
+ tmp[k] = obj[k]
750
+ continue
751
+ }
752
+ try {
753
+ if (typeof obj[k] === 'function') {
754
+ tmp[k] = obj[k].toString()
755
+ continue
756
+ }
757
+ if (typeof obj[k] === 'object') {
758
+ tmp[k] = ___dump(obj[k], _d + 1);
759
+ continue
760
+ }
761
+ } catch (e) {}
762
+ try {
763
+ tmp[k] = JSON.stringify(obj[k])
764
+ continue
765
+ } catch (e) {
766
+ }
767
+ try {
768
+ tmp[k] = obj[k].toString();
769
+ continue
770
+ } catch (e) {}
771
+ }
772
+ return tmp
773
+ }
774
+ function ___dumpY(obj) {
775
+ var objKeys = (obj) => {
776
+ var [target, result] = [obj, []];
777
+ while (target !== null) {
778
+ result = result.concat(
779
+ Object.getOwnPropertyNames(target)
780
+ );
781
+ target = Object.getPrototypeOf(target);
782
+ }
783
+ return result;
784
+ }
785
+ return Object.fromEntries(
786
+ objKeys(obj).map(_ => [_, ___dump(obj[_])]))
787
+ }
788
+ ___dumpY( %s )
789
+ """
790
+ % obj_name
791
+ )
792
+ js_code_b = (
793
+ """
794
+ ((obj, visited = new WeakSet()) => {
795
+ if (visited.has(obj)) {
796
+ return {}
797
+ }
798
+ visited.add(obj)
799
+ var result = {}, _tmp;
800
+ for (var i in obj) {
801
+ try {
802
+ if (i === 'enabledPlugin'
803
+ || typeof obj[i] === 'function') {
804
+ continue;
805
+ } else if (typeof obj[i] === 'object') {
806
+ _tmp = recurse(obj[i], visited);
807
+ if (Object.keys(_tmp).length) {
808
+ result[i] = _tmp;
809
+ }
810
+ } else {
811
+ result[i] = obj[i];
812
+ }
813
+ } catch (error) {
814
+ // console.error('Error:', error);
815
+ }
816
+ }
817
+ return result;
818
+ })(%s)
819
+ """
820
+ % obj_name
821
+ )
822
+ # No self.evaluate here to prevent infinite loop on certain expressions
823
+ remote_object, exception_details = await self.send(
824
+ cdp.runtime.evaluate(
825
+ js_code_a,
826
+ await_promise=True,
827
+ return_by_value=return_by_value,
828
+ allow_unsafe_eval_blocked_by_csp=True,
829
+ )
830
+ )
831
+ if exception_details:
832
+ # Try second variant
833
+ remote_object, exception_details = await self.send(
834
+ cdp.runtime.evaluate(
835
+ js_code_b,
836
+ await_promise=True,
837
+ return_by_value=return_by_value,
838
+ allow_unsafe_eval_blocked_by_csp=True,
839
+ )
840
+ )
841
+ if exception_details:
842
+ raise ProtocolException(exception_details)
843
+ if return_by_value:
844
+ if remote_object.value:
845
+ return remote_object.value
846
+ else:
847
+ return remote_object, exception_details
848
+
849
+ async def close(self):
850
+ """Close the current target (ie: tab,window,page)"""
851
+ if self.target and self.target.target_id:
852
+ await self.send(
853
+ cdp.target.close_target(target_id=self.target.target_id)
854
+ )
855
+
856
+ async def get_window(self) -> Tuple[
857
+ cdp.browser.WindowID, cdp.browser.Bounds
858
+ ]:
859
+ """Get the window Bounds"""
860
+ window_id, bounds = await self.send(
861
+ cdp.browser.get_window_for_target(self.target_id)
862
+ )
863
+ return window_id, bounds
864
+
865
+ async def get_content(self):
866
+ """Gets the current page source content (html)"""
867
+ doc: cdp.dom.Node = await self.send(cdp.dom.get_document(-1, True))
868
+ return await self.send(
869
+ cdp.dom.get_outer_html(backend_node_id=doc.backend_node_id)
870
+ )
871
+
872
+ async def maximize(self):
873
+ """Maximize page/tab/window"""
874
+ return await self.set_window_state(state="maximize")
875
+
876
+ async def minimize(self):
877
+ """Minimize page/tab/window"""
878
+ return await self.set_window_state(state="minimize")
879
+
880
+ async def fullscreen(self):
881
+ """Minimize page/tab/window"""
882
+ return await self.set_window_state(state="fullscreen")
883
+
884
+ async def medimize(self):
885
+ return await self.set_window_state(state="normal")
886
+
887
+ async def set_window_size(self, left=0, top=0, width=1280, height=1024):
888
+ """
889
+ Set window size and position.
890
+ :param left:
891
+ Pixels from the left of the screen to the window top-left corner.
892
+ :param top:
893
+ Pixels from the top of the screen to the window top-left corner.
894
+ :param width: width of the window in pixels
895
+ :param height: height of the window in pixels
896
+ """
897
+ return await self.set_window_state(left, top, width, height)
898
+
899
+ async def activate(self):
900
+ """Active this target (Eg: tab, window, page)"""
901
+ await self.send(cdp.target.activate_target(self.target.target_id))
902
+
903
+ async def bring_to_front(self):
904
+ """Alias to self.activate"""
905
+ await self.activate()
906
+
907
+ async def set_window_state(
908
+ self, left=0, top=0, width=1280, height=720, state="normal"
909
+ ):
910
+ """
911
+ Sets the window size or state.
912
+ For state you can provide the full name like minimized, maximized,
913
+ normal, fullscreen, or something which leads to either of those,
914
+ like min, mini, mi, max, ma, maxi, full, fu, no, nor.
915
+ In case state is set other than "normal",
916
+ the left, top, width, and height are ignored.
917
+ :param left:
918
+ desired offset from left, in pixels
919
+ :type left: int
920
+ :param top:
921
+ desired offset from the top, in pixels
922
+ :type top: int
923
+ :param width:
924
+ desired width in pixels
925
+ :type width: int
926
+ :param height:
927
+ desired height in pixels
928
+ :type height: int
929
+ :param state:
930
+ can be one of the following strings:
931
+ - normal
932
+ - fullscreen
933
+ - maximized
934
+ - minimized
935
+ :type state: str
936
+ """
937
+ available_states = ["minimized", "maximized", "fullscreen", "normal"]
938
+ window_id: cdp.browser.WindowID
939
+ bounds: cdp.browser.Bounds
940
+ (window_id, bounds) = await self.get_window()
941
+ for state_name in available_states:
942
+ if all(x in state_name for x in state.lower()):
943
+ break
944
+ else:
945
+ raise NameError(
946
+ "could not determine any of %s from input '%s'"
947
+ % (",".join(available_states), state)
948
+ )
949
+ window_state = getattr(
950
+ cdp.browser.WindowState,
951
+ state_name.upper(),
952
+ cdp.browser.WindowState.NORMAL,
953
+ )
954
+ if window_state == cdp.browser.WindowState.NORMAL:
955
+ bounds = cdp.browser.Bounds(
956
+ left, top, width, height, window_state
957
+ )
958
+ else:
959
+ # min, max, full can only be used when current state == NORMAL,
960
+ # therefore, first switch to NORMAL
961
+ await self.set_window_state(state="normal")
962
+ bounds = cdp.browser.Bounds(window_state=window_state)
963
+
964
+ await self.send(
965
+ cdp.browser.set_window_bounds(window_id, bounds=bounds)
966
+ )
967
+
968
+ async def scroll_down(self, amount=25):
969
+ """
970
+ Scrolls the page down.
971
+ :param amount: Number in percentage.
972
+ 25 is a quarter of page, 50 half, and 1000 is 10x the page.
973
+ :type amount: int
974
+ """
975
+ window_id: cdp.browser.WindowID
976
+ bounds: cdp.browser.Bounds
977
+ (window_id, bounds) = await self.get_window()
978
+ await self.send(
979
+ cdp.input_.synthesize_scroll_gesture(
980
+ x=0,
981
+ y=0,
982
+ y_distance=-(bounds.height * (amount / 100)),
983
+ y_overscroll=0,
984
+ x_overscroll=0,
985
+ prevent_fling=True,
986
+ repeat_delay_ms=0,
987
+ speed=7777,
988
+ )
989
+ )
990
+
991
+ async def scroll_up(self, amount=25):
992
+ """
993
+ Scrolls the page up.
994
+ :param amount: Number in percentage.
995
+ 25 is a quarter of page, 50 half, and 1000 is 10x the page.
996
+ :type amount: int
997
+ """
998
+ window_id: cdp.browser.WindowID
999
+ bounds: cdp.browser.Bounds
1000
+ (window_id, bounds) = await self.get_window()
1001
+ await self.send(
1002
+ cdp.input_.synthesize_scroll_gesture(
1003
+ x=0,
1004
+ y=0,
1005
+ y_distance=(bounds.height * (amount / 100)),
1006
+ x_overscroll=0,
1007
+ prevent_fling=True,
1008
+ repeat_delay_ms=0,
1009
+ speed=7777,
1010
+ )
1011
+ )
1012
+
1013
+ async def wait_for(
1014
+ self,
1015
+ selector: Optional[str] = "",
1016
+ text: Optional[str] = "",
1017
+ timeout: Optional[Union[int, float]] = 10,
1018
+ ) -> element.Element:
1019
+ """
1020
+ Variant on query_selector_all and find_elements_by_text.
1021
+ This variant takes either selector or text,
1022
+ and will block until the requested element(s) are found.
1023
+ It will block for a maximum of <timeout> seconds,
1024
+ after which a TimeoutError will be raised.
1025
+ :param selector: css selector
1026
+ :param text: text
1027
+ :param timeout:
1028
+ :return: Element
1029
+ :raises: asyncio.TimeoutError
1030
+ """
1031
+ loop = asyncio.get_running_loop()
1032
+ now = loop.time()
1033
+ if selector:
1034
+ item = await self.query_selector(selector)
1035
+ while not item:
1036
+ item = await self.query_selector(selector)
1037
+ if loop.time() - now > timeout:
1038
+ raise asyncio.TimeoutError(
1039
+ "Time ran out while waiting for: {%s}" % selector
1040
+ )
1041
+ await self.sleep(0.5)
1042
+ return item
1043
+ if text:
1044
+ item = await self.find_element_by_text(text)
1045
+ while not item:
1046
+ item = await self.find_element_by_text(text)
1047
+ if loop.time() - now > timeout:
1048
+ raise asyncio.TimeoutError(
1049
+ "Time ran out while waiting for: {%s}" % text
1050
+ )
1051
+ await self.sleep(0.5)
1052
+ return item
1053
+
1054
+ async def download_file(
1055
+ self, url: str, filename: Optional[PathLike] = None
1056
+ ):
1057
+ """
1058
+ Downloads the file by the given url.
1059
+ :param url: The URL of the file.
1060
+ :param filename: The name for the file.
1061
+ If not specified, the name is composed from the url file name
1062
+ """
1063
+ if not self._download_behavior:
1064
+ directory_path = pathlib.Path.cwd() / "downloads"
1065
+ directory_path.mkdir(exist_ok=True)
1066
+ await self.set_download_path(directory_path)
1067
+
1068
+ warnings.warn(
1069
+ f"No download path set, so creating and using a default of "
1070
+ f"{directory_path}"
1071
+ )
1072
+ if not filename:
1073
+ filename = url.rsplit("/")[-1]
1074
+ filename = filename.split("?")[0]
1075
+ code = """
1076
+ (elem) => {
1077
+ async function _downloadFile(
1078
+ imageSrc,
1079
+ nameOfDownload,
1080
+ ) {
1081
+ const response = await fetch(imageSrc);
1082
+ const blobImage = await response.blob();
1083
+ const href = URL.createObjectURL(blobImage);
1084
+ const anchorElement = document.createElement('a');
1085
+ anchorElement.href = href;
1086
+ anchorElement.download = nameOfDownload;
1087
+ document.body.appendChild(anchorElement);
1088
+ anchorElement.click();
1089
+ setTimeout(() => {
1090
+ document.body.removeChild(anchorElement);
1091
+ window.URL.revokeObjectURL(href);
1092
+ }, 500);
1093
+ }
1094
+ _downloadFile('%s', '%s')
1095
+ }
1096
+ """ % (
1097
+ url,
1098
+ filename,
1099
+ )
1100
+ body = (await self.query_selector_all("body"))[0]
1101
+ await body.update()
1102
+ await self.send(
1103
+ cdp.runtime.call_function_on(
1104
+ code,
1105
+ object_id=body.object_id,
1106
+ arguments=[cdp.runtime.CallArgument(object_id=body.object_id)],
1107
+ )
1108
+ )
1109
+
1110
+ async def save_screenshot(
1111
+ self,
1112
+ filename: Optional[PathLike] = "auto",
1113
+ format: Optional[str] = "png",
1114
+ full_page: Optional[bool] = False,
1115
+ ) -> str:
1116
+ """
1117
+ Saves a screenshot of the page.
1118
+ This is not the same as :py:obj:`Element.save_screenshot`,
1119
+ which saves a screenshot of a single element only.
1120
+ :param filename: uses this as the save path
1121
+ :type filename: PathLike
1122
+ :param format: jpeg or png (defaults to jpeg)
1123
+ :type format: str
1124
+ :param full_page:
1125
+ When False (default), it captures the current viewport.
1126
+ When True, it captures the entire page.
1127
+ :type full_page: bool
1128
+ :return: The path/filename of the saved screenshot.
1129
+ :rtype: str
1130
+ """
1131
+ import urllib.parse
1132
+ import datetime
1133
+
1134
+ await self.sleep() # Update the target's URL
1135
+ path = None
1136
+ if format.lower() in ["jpg", "jpeg"]:
1137
+ ext = ".jpg"
1138
+ format = "jpeg"
1139
+ elif format.lower() in ["png"]:
1140
+ ext = ".png"
1141
+ format = "png"
1142
+ if not filename or filename == "auto":
1143
+ parsed = urllib.parse.urlparse(self.target.url)
1144
+ parts = parsed.path.split("/")
1145
+ last_part = parts[-1]
1146
+ last_part = last_part.rsplit("?", 1)[0]
1147
+ dt_str = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
1148
+ candidate = f"{parsed.hostname}__{last_part}_{dt_str}"
1149
+ path = pathlib.Path(candidate + ext) # noqa
1150
+ else:
1151
+ path = pathlib.Path(filename)
1152
+ path.parent.mkdir(parents=True, exist_ok=True)
1153
+ data = await self.send(
1154
+ cdp.page.capture_screenshot(
1155
+ format_=format, capture_beyond_viewport=full_page
1156
+ )
1157
+ )
1158
+ if not data:
1159
+ raise ProtocolException(
1160
+ "Could not take screenshot. "
1161
+ "Most possible cause is the page "
1162
+ "has not finished loading yet."
1163
+ )
1164
+ import base64
1165
+
1166
+ data_bytes = base64.b64decode(data)
1167
+ if not path:
1168
+ raise RuntimeError("Invalid filename or path: '%s'" % filename)
1169
+ path.write_bytes(data_bytes)
1170
+ return str(path)
1171
+
1172
+ async def set_download_path(self, path: PathLike):
1173
+ """
1174
+ Sets the download path.
1175
+ When not set, a default folder is used.
1176
+ :param path:
1177
+ """
1178
+ await self.send(
1179
+ cdp.browser.set_download_behavior(
1180
+ behavior="allow", download_path=str(path.resolve())
1181
+ )
1182
+ )
1183
+ self._download_behavior = ["allow", str(path.resolve())]
1184
+
1185
+ async def get_all_linked_sources(self) -> List["element.Element"]:
1186
+ """Get all elements of tag: link, a, img, scripts meta, video, audio"""
1187
+ all_assets = await self.query_selector_all(
1188
+ selector="a,link,img,script,meta"
1189
+ )
1190
+ return [element.create(asset, self) for asset in all_assets]
1191
+
1192
+ async def get_all_urls(self, absolute=True) -> List[str]:
1193
+ """
1194
+ Convenience function, which returns all links (a,link,img,script,meta).
1195
+ :param absolute:
1196
+ Try to build all the links in absolute form
1197
+ instead of "as is", often relative.
1198
+ :return: List of URLs.
1199
+ """
1200
+ import urllib.parse
1201
+
1202
+ res = []
1203
+ all_assets = await self.query_selector_all(
1204
+ selector="a,link,img,script,meta"
1205
+ )
1206
+ for asset in all_assets:
1207
+ if not absolute:
1208
+ res.append(asset.src or asset.href)
1209
+ else:
1210
+ for k, v in asset.attrs.items():
1211
+ if k in ("src", "href"):
1212
+ if "#" in v:
1213
+ continue
1214
+ if not any([_ in v for _ in ("http", "//", "/")]):
1215
+ continue
1216
+ abs_url = urllib.parse.urljoin(
1217
+ "/".join(self.url.rsplit("/")[:3]), v
1218
+ )
1219
+ if not abs_url.startswith(("http", "//", "ws")):
1220
+ continue
1221
+ res.append(abs_url)
1222
+ return res
1223
+
1224
+ async def verify_cf(self):
1225
+ """(An attempt)"""
1226
+ checkbox = None
1227
+ checkbox_sibling = await self.wait_for(text="verify you are human")
1228
+ if checkbox_sibling:
1229
+ parent = checkbox_sibling.parent
1230
+ while parent:
1231
+ checkbox = await parent.query_selector("input[type=checkbox]")
1232
+ if checkbox:
1233
+ break
1234
+ parent = parent.parent
1235
+ await checkbox.mouse_move()
1236
+ await checkbox.mouse_click()
1237
+
1238
+ async def get_document(self):
1239
+ return await self.send(cdp.dom.get_document())
1240
+
1241
+ async def get_flattened_document(self):
1242
+ return await self.send(cdp.dom.get_flattened_document())
1243
+
1244
+ async def get_local_storage(self):
1245
+ """
1246
+ Get local storage items as dict of strings.
1247
+ Proper deserialization may need to be done.
1248
+ """
1249
+ if not self.target.url:
1250
+ await self
1251
+ origin = "/".join(self.url.split("/", 3)[:-1])
1252
+ items = await self.send(
1253
+ cdp.dom_storage.get_dom_storage_items(
1254
+ cdp.dom_storage.StorageId(
1255
+ is_local_storage=True, security_origin=origin
1256
+ )
1257
+ )
1258
+ )
1259
+ retval = {}
1260
+ for item in items:
1261
+ retval[item[0]] = item[1]
1262
+ return retval
1263
+
1264
+ async def set_local_storage(self, items: dict):
1265
+ """
1266
+ Set local storage.
1267
+ Dict items must be strings.
1268
+ Simple types will be converted to strings automatically.
1269
+ :param items: dict containing {key:str, value:str}
1270
+ :type items: dict[str,str]
1271
+ """
1272
+ if not self.target.url:
1273
+ await self
1274
+ origin = "/".join(self.url.split("/", 3)[:-1])
1275
+ await asyncio.gather(
1276
+ *[
1277
+ self.send(
1278
+ cdp.dom_storage.set_dom_storage_item(
1279
+ storage_id=cdp.dom_storage.StorageId(
1280
+ is_local_storage=True, security_origin=origin
1281
+ ),
1282
+ key=str(key),
1283
+ value=str(val),
1284
+ )
1285
+ )
1286
+ for key, val in items.items()
1287
+ ]
1288
+ )
1289
+
1290
+ def __call__(
1291
+ self,
1292
+ text: Optional[str] = "",
1293
+ selector: Optional[str] = "",
1294
+ timeout: Optional[Union[int, float]] = 10,
1295
+ ):
1296
+ """
1297
+ Alias to query_selector_all or find_elements_by_text,
1298
+ depending on whether text= is set or selector= is set.
1299
+ :param selector: css selector string
1300
+ :type selector: str
1301
+ """
1302
+ return self.wait_for(text, selector, timeout)
1303
+
1304
+ def __eq__(self, other: Tab):
1305
+ try:
1306
+ return other.target == self.target
1307
+ except (AttributeError, TypeError):
1308
+ return False
1309
+
1310
+ def __getattr__(self, item):
1311
+ try:
1312
+ return getattr(self._target, item)
1313
+ except AttributeError:
1314
+ raise AttributeError(
1315
+ f'"{self.__class__.__name__}" has no attribute "%s"' % item
1316
+ )
1317
+
1318
+ def __repr__(self):
1319
+ extra = ""
1320
+ if self.target.url:
1321
+ extra = f"[url: {self.target.url}]"
1322
+ s = f"<{type(self).__name__} [{self.target_id}] [{self.type_}] {extra}>" # noqa
1323
+ return s