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