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.
- sbase/__init__.py +1 -0
- sbase/steps.py +7 -0
- seleniumbase/__init__.py +16 -7
- seleniumbase/__version__.py +1 -1
- seleniumbase/behave/behave_sb.py +97 -32
- seleniumbase/common/decorators.py +16 -7
- seleniumbase/config/proxy_list.py +3 -3
- seleniumbase/config/settings.py +4 -0
- seleniumbase/console_scripts/logo_helper.py +47 -8
- seleniumbase/console_scripts/run.py +345 -335
- seleniumbase/console_scripts/sb_behave_gui.py +5 -12
- seleniumbase/console_scripts/sb_caseplans.py +6 -13
- seleniumbase/console_scripts/sb_commander.py +5 -12
- seleniumbase/console_scripts/sb_install.py +62 -54
- seleniumbase/console_scripts/sb_mkchart.py +13 -20
- seleniumbase/console_scripts/sb_mkdir.py +11 -17
- seleniumbase/console_scripts/sb_mkfile.py +69 -43
- seleniumbase/console_scripts/sb_mkpres.py +13 -20
- seleniumbase/console_scripts/sb_mkrec.py +88 -21
- seleniumbase/console_scripts/sb_objectify.py +30 -30
- seleniumbase/console_scripts/sb_print.py +5 -12
- seleniumbase/console_scripts/sb_recorder.py +16 -11
- seleniumbase/core/browser_launcher.py +1658 -221
- seleniumbase/core/detect_b_ver.py +7 -8
- seleniumbase/core/log_helper.py +42 -27
- seleniumbase/core/mysql.py +1 -4
- seleniumbase/core/proxy_helper.py +35 -30
- seleniumbase/core/recorder_helper.py +24 -5
- seleniumbase/core/sb_cdp.py +1951 -0
- seleniumbase/core/sb_driver.py +162 -8
- seleniumbase/core/settings_parser.py +6 -0
- seleniumbase/core/style_sheet.py +10 -0
- seleniumbase/extensions/recorder.zip +0 -0
- seleniumbase/fixtures/base_case.py +1234 -632
- seleniumbase/fixtures/constants.py +10 -1
- seleniumbase/fixtures/js_utils.py +171 -144
- seleniumbase/fixtures/page_actions.py +177 -13
- seleniumbase/fixtures/page_utils.py +25 -53
- seleniumbase/fixtures/shared_utils.py +97 -11
- seleniumbase/js_code/active_css_js.py +1 -1
- seleniumbase/js_code/recorder_js.py +1 -1
- seleniumbase/plugins/base_plugin.py +2 -3
- seleniumbase/plugins/driver_manager.py +340 -65
- seleniumbase/plugins/pytest_plugin.py +276 -47
- seleniumbase/plugins/sb_manager.py +412 -99
- seleniumbase/plugins/selenium_plugin.py +122 -17
- seleniumbase/translate/translator.py +0 -7
- seleniumbase/undetected/__init__.py +59 -52
- seleniumbase/undetected/cdp.py +0 -1
- seleniumbase/undetected/cdp_driver/__init__.py +1 -0
- seleniumbase/undetected/cdp_driver/_contradict.py +110 -0
- seleniumbase/undetected/cdp_driver/browser.py +829 -0
- seleniumbase/undetected/cdp_driver/cdp_util.py +458 -0
- seleniumbase/undetected/cdp_driver/config.py +334 -0
- seleniumbase/undetected/cdp_driver/connection.py +639 -0
- seleniumbase/undetected/cdp_driver/element.py +1168 -0
- seleniumbase/undetected/cdp_driver/tab.py +1323 -0
- seleniumbase/undetected/dprocess.py +4 -7
- seleniumbase/undetected/options.py +6 -8
- seleniumbase/undetected/patcher.py +11 -13
- seleniumbase/undetected/reactor.py +0 -1
- seleniumbase/undetected/webelement.py +16 -3
- {seleniumbase-4.24.10.dist-info → seleniumbase-4.33.15.dist-info}/LICENSE +1 -1
- {seleniumbase-4.24.10.dist-info → seleniumbase-4.33.15.dist-info}/METADATA +299 -252
- {seleniumbase-4.24.10.dist-info → seleniumbase-4.33.15.dist-info}/RECORD +68 -70
- {seleniumbase-4.24.10.dist-info → seleniumbase-4.33.15.dist-info}/WHEEL +1 -1
- sbase/ReadMe.txt +0 -2
- seleniumbase/ReadMe.md +0 -25
- seleniumbase/common/ReadMe.md +0 -71
- seleniumbase/console_scripts/ReadMe.md +0 -731
- seleniumbase/drivers/ReadMe.md +0 -27
- seleniumbase/extensions/ReadMe.md +0 -12
- seleniumbase/masterqa/ReadMe.md +0 -61
- seleniumbase/resources/ReadMe.md +0 -31
- seleniumbase/resources/favicon.ico +0 -0
- seleniumbase/utilities/selenium_grid/ReadMe.md +0 -84
- seleniumbase/utilities/selenium_ide/ReadMe.md +0 -111
- {seleniumbase-4.24.10.dist-info → seleniumbase-4.33.15.dist-info}/entry_points.txt +0 -0
- {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
|