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