parsek-cdp 0.1.0__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.
- parsek_cdp/__init__.py +55 -0
- parsek_cdp/_logging.py +49 -0
- parsek_cdp/cdp/__init__.py +247 -0
- parsek_cdp/cdp/accessibility/__init__.py +15 -0
- parsek_cdp/cdp/accessibility/events.py +35 -0
- parsek_cdp/cdp/accessibility/functions.py +202 -0
- parsek_cdp/cdp/accessibility/types.py +235 -0
- parsek_cdp/cdp/animation/__init__.py +15 -0
- parsek_cdp/cdp/animation/events.py +52 -0
- parsek_cdp/cdp/animation/functions.py +140 -0
- parsek_cdp/cdp/animation/types.py +113 -0
- parsek_cdp/cdp/audits/__init__.py +15 -0
- parsek_cdp/cdp/audits/events.py +21 -0
- parsek_cdp/cdp/audits/functions.py +95 -0
- parsek_cdp/cdp/audits/types.py +1051 -0
- parsek_cdp/cdp/autofill/__init__.py +15 -0
- parsek_cdp/cdp/autofill/events.py +27 -0
- parsek_cdp/cdp/autofill/functions.py +64 -0
- parsek_cdp/cdp/autofill/types.py +108 -0
- parsek_cdp/cdp/backgroundservice/__init__.py +15 -0
- parsek_cdp/cdp/backgroundservice/events.py +40 -0
- parsek_cdp/cdp/backgroundservice/functions.py +59 -0
- parsek_cdp/cdp/backgroundservice/types.py +63 -0
- parsek_cdp/cdp/bluetoothemulation/__init__.py +15 -0
- parsek_cdp/cdp/bluetoothemulation/events.py +70 -0
- parsek_cdp/cdp/bluetoothemulation/functions.py +247 -0
- parsek_cdp/cdp/bluetoothemulation/types.py +120 -0
- parsek_cdp/cdp/browser/__init__.py +15 -0
- parsek_cdp/cdp/browser/events.py +46 -0
- parsek_cdp/cdp/browser/functions.py +350 -0
- parsek_cdp/cdp/browser/types.py +158 -0
- parsek_cdp/cdp/cachestorage/__init__.py +15 -0
- parsek_cdp/cdp/cachestorage/events.py +13 -0
- parsek_cdp/cdp/cachestorage/functions.py +126 -0
- parsek_cdp/cdp/cachestorage/types.py +89 -0
- parsek_cdp/cdp/cast/__init__.py +15 -0
- parsek_cdp/cdp/cast/events.py +38 -0
- parsek_cdp/cdp/cast/functions.py +77 -0
- parsek_cdp/cdp/cast/types.py +23 -0
- parsek_cdp/cdp/console/__init__.py +15 -0
- parsek_cdp/cdp/console/events.py +22 -0
- parsek_cdp/cdp/console/functions.py +36 -0
- parsek_cdp/cdp/console/types.py +30 -0
- parsek_cdp/cdp/css/__init__.py +15 -0
- parsek_cdp/cdp/css/events.py +79 -0
- parsek_cdp/cdp/css/functions.py +788 -0
- parsek_cdp/cdp/css/types.py +742 -0
- parsek_cdp/cdp/debugger/__init__.py +15 -0
- parsek_cdp/cdp/debugger/events.py +164 -0
- parsek_cdp/cdp/debugger/functions.py +663 -0
- parsek_cdp/cdp/debugger/types.py +171 -0
- parsek_cdp/cdp/deviceaccess/__init__.py +15 -0
- parsek_cdp/cdp/deviceaccess/events.py +30 -0
- parsek_cdp/cdp/deviceaccess/functions.py +54 -0
- parsek_cdp/cdp/deviceaccess/types.py +28 -0
- parsek_cdp/cdp/deviceorientation/__init__.py +15 -0
- parsek_cdp/cdp/deviceorientation/events.py +13 -0
- parsek_cdp/cdp/deviceorientation/functions.py +35 -0
- parsek_cdp/cdp/deviceorientation/types.py +10 -0
- parsek_cdp/cdp/dom/__init__.py +15 -0
- parsek_cdp/cdp/dom/events.py +228 -0
- parsek_cdp/cdp/dom/functions.py +1033 -0
- parsek_cdp/cdp/dom/types.py +293 -0
- parsek_cdp/cdp/domdebugger/__init__.py +15 -0
- parsek_cdp/cdp/domdebugger/events.py +13 -0
- parsek_cdp/cdp/domdebugger/functions.py +155 -0
- parsek_cdp/cdp/domdebugger/types.py +57 -0
- parsek_cdp/cdp/domsnapshot/__init__.py +15 -0
- parsek_cdp/cdp/domsnapshot/events.py +13 -0
- parsek_cdp/cdp/domsnapshot/functions.py +111 -0
- parsek_cdp/cdp/domsnapshot/types.py +312 -0
- parsek_cdp/cdp/domstorage/__init__.py +15 -0
- parsek_cdp/cdp/domstorage/events.py +60 -0
- parsek_cdp/cdp/domstorage/functions.py +79 -0
- parsek_cdp/cdp/domstorage/types.py +30 -0
- parsek_cdp/cdp/emulation/__init__.py +15 -0
- parsek_cdp/cdp/emulation/events.py +17 -0
- parsek_cdp/cdp/emulation/functions.py +688 -0
- parsek_cdp/cdp/emulation/types.py +305 -0
- parsek_cdp/cdp/eventbreakpoints/__init__.py +15 -0
- parsek_cdp/cdp/eventbreakpoints/events.py +13 -0
- parsek_cdp/cdp/eventbreakpoints/functions.py +41 -0
- parsek_cdp/cdp/eventbreakpoints/types.py +10 -0
- parsek_cdp/cdp/extensions/__init__.py +15 -0
- parsek_cdp/cdp/extensions/events.py +13 -0
- parsek_cdp/cdp/extensions/functions.py +118 -0
- parsek_cdp/cdp/extensions/types.py +19 -0
- parsek_cdp/cdp/fedcm/__init__.py +15 -0
- parsek_cdp/cdp/fedcm/events.py +45 -0
- parsek_cdp/cdp/fedcm/functions.py +93 -0
- parsek_cdp/cdp/fedcm/types.py +72 -0
- parsek_cdp/cdp/fetch/__init__.py +15 -0
- parsek_cdp/cdp/fetch/events.py +82 -0
- parsek_cdp/cdp/fetch/functions.py +215 -0
- parsek_cdp/cdp/fetch/types.py +81 -0
- parsek_cdp/cdp/filesystem/__init__.py +15 -0
- parsek_cdp/cdp/filesystem/events.py +13 -0
- parsek_cdp/cdp/filesystem/functions.py +36 -0
- parsek_cdp/cdp/filesystem/types.py +54 -0
- parsek_cdp/cdp/headlessexperimental/__init__.py +15 -0
- parsek_cdp/cdp/headlessexperimental/events.py +13 -0
- parsek_cdp/cdp/headlessexperimental/functions.py +77 -0
- parsek_cdp/cdp/headlessexperimental/types.py +24 -0
- parsek_cdp/cdp/heapprofiler/__init__.py +15 -0
- parsek_cdp/cdp/heapprofiler/events.py +65 -0
- parsek_cdp/cdp/heapprofiler/functions.py +193 -0
- parsek_cdp/cdp/heapprofiler/types.py +57 -0
- parsek_cdp/cdp/indexeddb/__init__.py +15 -0
- parsek_cdp/cdp/indexeddb/events.py +13 -0
- parsek_cdp/cdp/indexeddb/functions.py +232 -0
- parsek_cdp/cdp/indexeddb/types.py +120 -0
- parsek_cdp/cdp/input/__init__.py +15 -0
- parsek_cdp/cdp/input/events.py +25 -0
- parsek_cdp/cdp/input/functions.py +361 -0
- parsek_cdp/cdp/input/types.py +87 -0
- parsek_cdp/cdp/inspector/__init__.py +15 -0
- parsek_cdp/cdp/inspector/events.py +41 -0
- parsek_cdp/cdp/inspector/functions.py +27 -0
- parsek_cdp/cdp/inspector/types.py +10 -0
- parsek_cdp/cdp/io/__init__.py +15 -0
- parsek_cdp/cdp/io/events.py +13 -0
- parsek_cdp/cdp/io/functions.py +77 -0
- parsek_cdp/cdp/io/types.py +13 -0
- parsek_cdp/cdp/layertree/__init__.py +15 -0
- parsek_cdp/cdp/layertree/events.py +36 -0
- parsek_cdp/cdp/layertree/functions.py +178 -0
- parsek_cdp/cdp/layertree/types.py +104 -0
- parsek_cdp/cdp/log/__init__.py +15 -0
- parsek_cdp/cdp/log/events.py +22 -0
- parsek_cdp/cdp/log/functions.py +54 -0
- parsek_cdp/cdp/log/types.py +57 -0
- parsek_cdp/cdp/media/__init__.py +15 -0
- parsek_cdp/cdp/media/events.py +87 -0
- parsek_cdp/cdp/media/functions.py +27 -0
- parsek_cdp/cdp/media/types.py +101 -0
- parsek_cdp/cdp/memory/__init__.py +15 -0
- parsek_cdp/cdp/memory/events.py +13 -0
- parsek_cdp/cdp/memory/functions.py +164 -0
- parsek_cdp/cdp/memory/types.py +71 -0
- parsek_cdp/cdp/mixins/__init__.py +21 -0
- parsek_cdp/cdp/mixins/datatype.py +123 -0
- parsek_cdp/cdp/mixins/event.py +78 -0
- parsek_cdp/cdp/mixins/namespace.py +31 -0
- parsek_cdp/cdp/network/__init__.py +15 -0
- parsek_cdp/cdp/network/events.py +726 -0
- parsek_cdp/cdp/network/functions.py +734 -0
- parsek_cdp/cdp/network/types.py +1247 -0
- parsek_cdp/cdp/overlay/__init__.py +15 -0
- parsek_cdp/cdp/overlay/events.py +54 -0
- parsek_cdp/cdp/overlay/functions.py +407 -0
- parsek_cdp/cdp/overlay/types.py +328 -0
- parsek_cdp/cdp/page/__init__.py +15 -0
- parsek_cdp/cdp/page/events.py +416 -0
- parsek_cdp/cdp/page/functions.py +1081 -0
- parsek_cdp/cdp/page/types.py +1088 -0
- parsek_cdp/cdp/performance/__init__.py +15 -0
- parsek_cdp/cdp/performance/events.py +24 -0
- parsek_cdp/cdp/performance/functions.py +63 -0
- parsek_cdp/cdp/performance/types.py +22 -0
- parsek_cdp/cdp/performancetimeline/__init__.py +15 -0
- parsek_cdp/cdp/performancetimeline/events.py +22 -0
- parsek_cdp/cdp/performancetimeline/functions.py +30 -0
- parsek_cdp/cdp/performancetimeline/types.py +85 -0
- parsek_cdp/cdp/preload/__init__.py +15 -0
- parsek_cdp/cdp/preload/events.py +116 -0
- parsek_cdp/cdp/preload/functions.py +25 -0
- parsek_cdp/cdp/preload/types.py +267 -0
- parsek_cdp/cdp/profiler/__init__.py +15 -0
- parsek_cdp/cdp/profiler/events.py +64 -0
- parsek_cdp/cdp/profiler/functions.py +134 -0
- parsek_cdp/cdp/profiler/types.py +105 -0
- parsek_cdp/cdp/pwa/__init__.py +15 -0
- parsek_cdp/cdp/pwa/events.py +13 -0
- parsek_cdp/cdp/pwa/functions.py +196 -0
- parsek_cdp/cdp/pwa/types.py +45 -0
- parsek_cdp/cdp/runtime/__init__.py +15 -0
- parsek_cdp/cdp/runtime/events.py +123 -0
- parsek_cdp/cdp/runtime/functions.py +552 -0
- parsek_cdp/cdp/runtime/types.py +319 -0
- parsek_cdp/cdp/schema/__init__.py +15 -0
- parsek_cdp/cdp/schema/events.py +13 -0
- parsek_cdp/cdp/schema/functions.py +32 -0
- parsek_cdp/cdp/schema/types.py +22 -0
- parsek_cdp/cdp/security/__init__.py +15 -0
- parsek_cdp/cdp/security/events.py +64 -0
- parsek_cdp/cdp/security/functions.py +66 -0
- parsek_cdp/cdp/security/types.py +168 -0
- parsek_cdp/cdp/serviceworker/__init__.py +15 -0
- parsek_cdp/cdp/serviceworker/events.py +43 -0
- parsek_cdp/cdp/serviceworker/functions.py +115 -0
- parsek_cdp/cdp/serviceworker/types.py +95 -0
- parsek_cdp/cdp/storage/__init__.py +15 -0
- parsek_cdp/cdp/storage/events.py +288 -0
- parsek_cdp/cdp/storage/functions.py +571 -0
- parsek_cdp/cdp/storage/types.py +656 -0
- parsek_cdp/cdp/systeminfo/__init__.py +15 -0
- parsek_cdp/cdp/systeminfo/events.py +13 -0
- parsek_cdp/cdp/systeminfo/functions.py +75 -0
- parsek_cdp/cdp/systeminfo/types.py +132 -0
- parsek_cdp/cdp/target/__init__.py +15 -0
- parsek_cdp/cdp/target/events.py +109 -0
- parsek_cdp/cdp/target/functions.py +424 -0
- parsek_cdp/cdp/target/types.py +83 -0
- parsek_cdp/cdp/tethering/__init__.py +15 -0
- parsek_cdp/cdp/tethering/events.py +22 -0
- parsek_cdp/cdp/tethering/functions.py +35 -0
- parsek_cdp/cdp/tethering/types.py +10 -0
- parsek_cdp/cdp/tracing/__init__.py +15 -0
- parsek_cdp/cdp/tracing/events.py +61 -0
- parsek_cdp/cdp/tracing/functions.py +119 -0
- parsek_cdp/cdp/tracing/types.py +81 -0
- parsek_cdp/cdp/webaudio/__init__.py +15 -0
- parsek_cdp/cdp/webaudio/events.py +184 -0
- parsek_cdp/cdp/webaudio/functions.py +51 -0
- parsek_cdp/cdp/webaudio/types.py +155 -0
- parsek_cdp/cdp/webauthn/__init__.py +15 -0
- parsek_cdp/cdp/webauthn/events.py +69 -0
- parsek_cdp/cdp/webauthn/functions.py +214 -0
- parsek_cdp/cdp/webauthn/types.py +96 -0
- parsek_cdp/core/__init__.py +18 -0
- parsek_cdp/core/browser.py +240 -0
- parsek_cdp/core/browser_context.py +58 -0
- parsek_cdp/core/element.py +405 -0
- parsek_cdp/core/feature.py +376 -0
- parsek_cdp/core/frame.py +351 -0
- parsek_cdp/core/pagable.py +175 -0
- parsek_cdp/core/page.py +163 -0
- parsek_cdp/core/target.py +490 -0
- parsek_cdp/features/__init__.py +14 -0
- parsek_cdp/features/request_listener/__init__.py +256 -0
- parsek_cdp/features/request_listener/events.py +48 -0
- parsek_cdp/features/request_listener/types.py +235 -0
- parsek_cdp/generate.py +642 -0
- parsek_cdp/parsek/__init__.py +18 -0
- parsek_cdp/parsek/events.py +38 -0
- parsek_cdp/parsek/types.py +36 -0
- parsek_cdp/py.typed +0 -0
- parsek_cdp-0.1.0.dist-info/METADATA +277 -0
- parsek_cdp-0.1.0.dist-info/RECORD +242 -0
- parsek_cdp-0.1.0.dist-info/WHEEL +5 -0
- parsek_cdp-0.1.0.dist-info/licenses/LICENSE +201 -0
- parsek_cdp-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Browser contexts -- isolated, incognito-like profiles within one browser.
|
|
2
|
+
|
|
3
|
+
A :class:`BrowserContext` groups pages that share cookies/storage and are
|
|
4
|
+
isolated from other contexts. The context with ``id is None`` is the browser's
|
|
5
|
+
default context.
|
|
6
|
+
|
|
7
|
+
A context is *not* a separate CDP session: its commands go through the browser
|
|
8
|
+
session, so :attr:`cdp` and :attr:`connection` delegate to the browser. All the
|
|
9
|
+
page/tab handling is inherited from :class:`PagableTarget`; pages added here
|
|
10
|
+
bubble to the browser via :attr:`_parent`.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import TYPE_CHECKING, Optional
|
|
16
|
+
|
|
17
|
+
from .pagable import PagableTarget
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from .browser import Browser
|
|
21
|
+
from .page import Page
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BrowserContext[T: Page](PagableTarget[T]):
|
|
25
|
+
"""A browser context (incognito-like profile)."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, browser: "Browser[T]", context_id: Optional[str] = None):
|
|
28
|
+
super().__init__()
|
|
29
|
+
self.browser = browser
|
|
30
|
+
self.id = context_id
|
|
31
|
+
self.page_class: type[T] = browser.page_class
|
|
32
|
+
self._parent = browser
|
|
33
|
+
self._cdp = browser.cdp
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def _scope_context_id(self) -> Optional[str]:
|
|
37
|
+
return self.id
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def is_default(self) -> bool:
|
|
41
|
+
return self.id is None
|
|
42
|
+
|
|
43
|
+
def _bind_page(self, page: T) -> None:
|
|
44
|
+
page.browser = self.browser
|
|
45
|
+
page.browser_context = self
|
|
46
|
+
|
|
47
|
+
async def close(self) -> None:
|
|
48
|
+
"""Dispose the context (and its pages). No-op for the default context."""
|
|
49
|
+
if self.id is None:
|
|
50
|
+
return
|
|
51
|
+
await self.browser.cdp.Target.dispose_browser_context(
|
|
52
|
+
browser_context_id=self.id
|
|
53
|
+
)
|
|
54
|
+
self.browser._forget_context(self.id)
|
|
55
|
+
|
|
56
|
+
def __repr__(self) -> str:
|
|
57
|
+
kind = "default" if self.is_default else self.id
|
|
58
|
+
return f"<BrowserContext {kind} pages={len(self.pages)}>"
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
"""A DOM element handle bound to a :class:`~parsek_cdp.core.frames.Frame`.
|
|
2
|
+
|
|
3
|
+
Returned by :meth:`Frame.select` / :meth:`Frame.select_all`. Wraps a DOM
|
|
4
|
+
``nodeId`` and offers the actions you usually want on a node: real pointer
|
|
5
|
+
:meth:`click` / :meth:`mouse_click` (dispatched through the ``Input`` domain at
|
|
6
|
+
the node's on-screen centre), keyboard input (:meth:`fill` / :meth:`type` /
|
|
7
|
+
:meth:`press`), a :meth:`is_visible` check, text/attribute reads, and a generic
|
|
8
|
+
:meth:`apply` to run a JS function with the node as ``this``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import functools
|
|
15
|
+
import inspect
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
from ..cdp import DOM, Runtime
|
|
19
|
+
from ..cdp.input.types import MouseButton
|
|
20
|
+
from .frame import Frame
|
|
21
|
+
from .target import ProtocolError
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from ..cdp import CDP
|
|
25
|
+
|
|
26
|
+
_KEY_DEFINITIONS: dict[str, tuple[int, str, str]] = {
|
|
27
|
+
"Enter": (13, "Enter", "\r"),
|
|
28
|
+
"Tab": (9, "Tab", "\t"),
|
|
29
|
+
"Backspace": (8, "Backspace", ""),
|
|
30
|
+
"Delete": (46, "Delete", ""),
|
|
31
|
+
"Escape": (27, "Escape", ""),
|
|
32
|
+
"Space": (32, "Space", " "),
|
|
33
|
+
"ArrowLeft": (37, "ArrowLeft", ""),
|
|
34
|
+
"ArrowUp": (38, "ArrowUp", ""),
|
|
35
|
+
"ArrowRight": (39, "ArrowRight", ""),
|
|
36
|
+
"ArrowDown": (40, "ArrowDown", ""),
|
|
37
|
+
"Home": (36, "Home", ""),
|
|
38
|
+
"End": (35, "End", ""),
|
|
39
|
+
"PageUp": (33, "PageUp", ""),
|
|
40
|
+
"PageDown": (34, "PageDown", ""),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _dom_target(args, kwargs):
|
|
45
|
+
"""Find the :class:`Target` to drive DOM on, from a ``@with_dom`` call's args.
|
|
46
|
+
|
|
47
|
+
Both shapes a ``@with_dom`` method can take are covered: an instance method
|
|
48
|
+
(``self`` is an :class:`Element`) and the ``get_node`` classmethod (``cls``
|
|
49
|
+
plus a ``frame``, passed positionally or by keyword).
|
|
50
|
+
"""
|
|
51
|
+
for obj in (*args, *kwargs.values()):
|
|
52
|
+
if isinstance(obj, Element):
|
|
53
|
+
return obj._frame.target
|
|
54
|
+
if isinstance(obj, Frame):
|
|
55
|
+
return obj.target
|
|
56
|
+
raise TypeError("@with_dom needs an Element or Frame argument")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _dom_enabled(args, kwargs):
|
|
60
|
+
"""Return the ``domain_enabled(DOM)`` context for a ``@with_dom`` call."""
|
|
61
|
+
target = _dom_target(args, kwargs)
|
|
62
|
+
return target.domain_enabled(target.cdp.DOM)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def with_dom(method):
|
|
66
|
+
"""Run a DOM-touching method with the ``DOM`` domain enabled.
|
|
67
|
+
|
|
68
|
+
DOM starts disabled (see :meth:`Target.connect`), so any method that issues
|
|
69
|
+
``DOM.*`` commands must enable it first. Decorating with ``@with_dom`` does
|
|
70
|
+
that automatically; nested decorated calls are a no-op because
|
|
71
|
+
:meth:`Target.domain_enabled` only flips a domain that is off. Works for
|
|
72
|
+
coroutine methods, async-generator methods (those that ``yield``), and the
|
|
73
|
+
``get_node`` classmethod (place ``@with_dom`` *below* ``@classmethod``).
|
|
74
|
+
|
|
75
|
+
Note ``getDocument`` is *not* done here -- it reassigns the whole node-id
|
|
76
|
+
map, so :meth:`Element.refresh` calls it only when it actually needs to mint
|
|
77
|
+
a fresh ``nodeId`` (see there).
|
|
78
|
+
"""
|
|
79
|
+
if inspect.isasyncgenfunction(method):
|
|
80
|
+
|
|
81
|
+
@functools.wraps(method)
|
|
82
|
+
async def gen_wrapper(*args, **kwargs):
|
|
83
|
+
async with _dom_enabled(args, kwargs):
|
|
84
|
+
async for item in method(*args, **kwargs):
|
|
85
|
+
yield item
|
|
86
|
+
|
|
87
|
+
return gen_wrapper
|
|
88
|
+
|
|
89
|
+
@functools.wraps(method)
|
|
90
|
+
async def wrapper(*args, **kwargs):
|
|
91
|
+
async with _dom_enabled(args, kwargs):
|
|
92
|
+
return await method(*args, **kwargs)
|
|
93
|
+
|
|
94
|
+
return wrapper
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class Element:
|
|
98
|
+
"""A handle to a single DOM node in a frame."""
|
|
99
|
+
|
|
100
|
+
_object_id: Runtime.RemoteObjectId | None
|
|
101
|
+
|
|
102
|
+
def __init__(self, node_info: DOM.Node, frame: Frame) -> None:
|
|
103
|
+
self._node = node_info
|
|
104
|
+
self._frame = frame
|
|
105
|
+
self._object_id = None
|
|
106
|
+
self._children: dict[DOM.BackendNodeId, Element] = {}
|
|
107
|
+
self._generation = -1
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def text(self):
|
|
111
|
+
return "".join(
|
|
112
|
+
[self._node.node_value]
|
|
113
|
+
+ [child.text for child in self.children if child._node.node_type == 3]
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def attributes(self):
|
|
118
|
+
attrs = self._node.attributes or []
|
|
119
|
+
keys = attrs[0::2]
|
|
120
|
+
values = attrs[1::2]
|
|
121
|
+
return dict(zip(keys, values))
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def id(self):
|
|
125
|
+
return self._node.node_id
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def backend_id(self):
|
|
129
|
+
return self._node.backend_node_id
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def children(self):
|
|
133
|
+
return list(self._children.values())
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def shadow_roots(self):
|
|
137
|
+
roots = [
|
|
138
|
+
Element(sh_root, self._frame) for sh_root in self._node.shadow_roots or []
|
|
139
|
+
]
|
|
140
|
+
for child in self.children:
|
|
141
|
+
roots.extend(child.shadow_roots)
|
|
142
|
+
return roots
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
async def get_node(
|
|
146
|
+
cls,
|
|
147
|
+
frame: Frame,
|
|
148
|
+
*,
|
|
149
|
+
node_id: DOM.NodeId | None = None,
|
|
150
|
+
backend_node_id: DOM.BackendNodeId | None = None,
|
|
151
|
+
object_id: Runtime.RemoteObjectId | None = None,
|
|
152
|
+
):
|
|
153
|
+
kwargs = dict(
|
|
154
|
+
node_id=node_id,
|
|
155
|
+
backend_node_id=backend_node_id,
|
|
156
|
+
object_id=object_id,
|
|
157
|
+
)
|
|
158
|
+
kwargs = dict(filter(lambda row: row[1] is not None, kwargs.items()))
|
|
159
|
+
if len(kwargs.values()) == 0:
|
|
160
|
+
raise ValueError("No identifier provided")
|
|
161
|
+
node = await frame.cdp.DOM.describe_node(
|
|
162
|
+
**kwargs, # pyright: ignore[reportArgumentType]
|
|
163
|
+
depth=-1,
|
|
164
|
+
pierce=True,
|
|
165
|
+
)
|
|
166
|
+
return node.node
|
|
167
|
+
|
|
168
|
+
@classmethod
|
|
169
|
+
async def describe_node(
|
|
170
|
+
cls,
|
|
171
|
+
frame: Frame,
|
|
172
|
+
*,
|
|
173
|
+
node_id: DOM.NodeId | None = None,
|
|
174
|
+
backend_node_id: DOM.BackendNodeId | None = None,
|
|
175
|
+
object_id: Runtime.RemoteObjectId | None = None,
|
|
176
|
+
):
|
|
177
|
+
node = await cls.get_node(
|
|
178
|
+
frame,
|
|
179
|
+
node_id=node_id,
|
|
180
|
+
backend_node_id=backend_node_id,
|
|
181
|
+
object_id=object_id,
|
|
182
|
+
)
|
|
183
|
+
return cls._from_node(node, frame)
|
|
184
|
+
|
|
185
|
+
@classmethod
|
|
186
|
+
def _from_node(cls, node: DOM.Node, frame: Frame) -> "Element":
|
|
187
|
+
"""Build an :class:`Element` tree from an already-fetched node.
|
|
188
|
+
|
|
189
|
+
``get_node`` uses ``depth=-1``, so ``node`` already carries its whole
|
|
190
|
+
subtree in ``node.children`` -- no further CDP round-trips are needed.
|
|
191
|
+
"""
|
|
192
|
+
instance = cls(node, frame)
|
|
193
|
+
for child in node.children or []:
|
|
194
|
+
instance._children[child.backend_node_id] = cls._from_node(child, frame)
|
|
195
|
+
return instance
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def _cdp(self) -> "CDP":
|
|
199
|
+
return self._frame.cdp
|
|
200
|
+
|
|
201
|
+
@with_dom
|
|
202
|
+
async def refresh(self):
|
|
203
|
+
"""Re-read this node's subtree and ensure it carries a live ``nodeId``.
|
|
204
|
+
|
|
205
|
+
``backendNodeId`` is stable across ``DOM.enable``/``disable``; ``nodeId``
|
|
206
|
+
is not. ``describeNode(backendNodeId)`` doubles as the validity check:
|
|
207
|
+
it returns the live ``nodeId`` when the node is currently pushed to the
|
|
208
|
+
frontend, or ``0`` when it isn't (e.g. a ``disable`` wiped the node map).
|
|
209
|
+
Only in that ``0`` case do we pay for ``getDocument`` (to repopulate the
|
|
210
|
+
map -- which ``push`` requires) + ``pushNodesByBackendIdsToFrontend`` (to
|
|
211
|
+
mint a fresh id); a still-live id needs neither.
|
|
212
|
+
"""
|
|
213
|
+
cdp = self._cdp
|
|
214
|
+
if self._generation != self._frame._dom_generation:
|
|
215
|
+
self._object_id = None
|
|
216
|
+
await cdp.DOM.get_document(depth=0)
|
|
217
|
+
node_id = (
|
|
218
|
+
await cdp.DOM.push_nodes_by_backend_ids_to_frontend(
|
|
219
|
+
backend_node_ids=[self.backend_id]
|
|
220
|
+
)
|
|
221
|
+
).node_ids[0]
|
|
222
|
+
else:
|
|
223
|
+
node_id = self.id
|
|
224
|
+
self._node = (
|
|
225
|
+
await cdp.DOM.describe_node(node_id=node_id, depth=-1, pierce=True)
|
|
226
|
+
).node
|
|
227
|
+
self._generation = self._frame._dom_generation
|
|
228
|
+
self._children.clear()
|
|
229
|
+
for child in self._node.children or []:
|
|
230
|
+
self._children[child.backend_node_id] = self._from_node(child, self._frame)
|
|
231
|
+
|
|
232
|
+
@with_dom
|
|
233
|
+
async def query_selector(self, selector: str) -> Element | None:
|
|
234
|
+
try:
|
|
235
|
+
await self.refresh()
|
|
236
|
+
data = await self._cdp.DOM.query_selector(
|
|
237
|
+
node_id=self.id, selector=selector
|
|
238
|
+
)
|
|
239
|
+
return await self.describe_node(node_id=data.node_id, frame=self._frame)
|
|
240
|
+
except ProtocolError:
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
@with_dom
|
|
244
|
+
async def query_selector_all(self, selector: str):
|
|
245
|
+
await self.refresh()
|
|
246
|
+
data = await self._cdp.DOM.query_selector_all(
|
|
247
|
+
node_id=self.id, selector=selector
|
|
248
|
+
)
|
|
249
|
+
for node_id in data.node_ids:
|
|
250
|
+
yield await self.describe_node(node_id=node_id, frame=self._frame)
|
|
251
|
+
|
|
252
|
+
@with_dom
|
|
253
|
+
async def scroll_into_view(self) -> None:
|
|
254
|
+
"""Best-effort scroll so the node is on screen (ignored if it fails)."""
|
|
255
|
+
try:
|
|
256
|
+
await self._cdp.DOM.scroll_into_view_if_needed(
|
|
257
|
+
backend_node_id=self.backend_id
|
|
258
|
+
)
|
|
259
|
+
except Exception:
|
|
260
|
+
pass
|
|
261
|
+
|
|
262
|
+
@with_dom
|
|
263
|
+
async def center(self) -> tuple[float, float]:
|
|
264
|
+
"""Return the viewport ``(x, y)`` of the node's content-box centre."""
|
|
265
|
+
await self.refresh()
|
|
266
|
+
model = (
|
|
267
|
+
await self._cdp.DOM.get_box_model(backend_node_id=self.backend_id)
|
|
268
|
+
).model
|
|
269
|
+
quad = model.content # [x1,y1, x2,y2, x3,y3, x4,y4]
|
|
270
|
+
xs = quad[0::2]
|
|
271
|
+
ys = quad[1::2]
|
|
272
|
+
return sum(xs) / len(xs), sum(ys) / len(ys)
|
|
273
|
+
|
|
274
|
+
@with_dom
|
|
275
|
+
async def mouse_click(
|
|
276
|
+
self, button: MouseButton = MouseButton.LEFT, *, count: int = 1
|
|
277
|
+
) -> None:
|
|
278
|
+
"""Click the element with a real pointer sequence (move, press, release)."""
|
|
279
|
+
await self.scroll_into_view()
|
|
280
|
+
x, y = await self.center()
|
|
281
|
+
inp = self._cdp.Input
|
|
282
|
+
await inp.dispatch_mouse_event(type_="mouseMoved", x=x, y=y)
|
|
283
|
+
await inp.dispatch_mouse_event(
|
|
284
|
+
type_="mousePressed", x=x, y=y, button=button, click_count=count
|
|
285
|
+
)
|
|
286
|
+
await inp.dispatch_mouse_event(
|
|
287
|
+
type_="mouseReleased", x=x, y=y, button=button, click_count=count
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
click = mouse_click
|
|
291
|
+
|
|
292
|
+
@with_dom
|
|
293
|
+
async def focus(self) -> None:
|
|
294
|
+
"""Give the node keyboard focus (so key/text events land on it)."""
|
|
295
|
+
await self._cdp.DOM.focus(backend_node_id=self.backend_id)
|
|
296
|
+
|
|
297
|
+
async def fill(self, value: str) -> None:
|
|
298
|
+
"""Focus the node, clear any existing value, then enter ``value``.
|
|
299
|
+
|
|
300
|
+
Works for both form controls (``<input>`` / ``<textarea>``) and
|
|
301
|
+
``contenteditable`` nodes. Mirrors Playwright's ``fill``: fast (one
|
|
302
|
+
``insertText`` rather than per-key events) and fires an ``input`` event,
|
|
303
|
+
so reactive frameworks pick up the change.
|
|
304
|
+
"""
|
|
305
|
+
await self.focus()
|
|
306
|
+
await self.apply(
|
|
307
|
+
"function () {"
|
|
308
|
+
" if (this.value !== undefined) { this.value = ''; }"
|
|
309
|
+
" else if (this.isContentEditable) { this.textContent = ''; }"
|
|
310
|
+
" this.dispatchEvent(new Event('input', { bubbles: true }));"
|
|
311
|
+
"}"
|
|
312
|
+
)
|
|
313
|
+
if value:
|
|
314
|
+
await self._cdp.Input.insert_text(text=value)
|
|
315
|
+
|
|
316
|
+
async def type(self, text: str, *, delay: float = 0) -> None:
|
|
317
|
+
"""Type ``text`` one character at a time, dispatching real key events.
|
|
318
|
+
|
|
319
|
+
Slower than :meth:`fill` but exercises ``keydown`` / ``keyup`` handlers
|
|
320
|
+
(autocomplete, key-by-key validation, ...). ``delay`` is the pause in
|
|
321
|
+
seconds between characters. Does not focus or clear -- call
|
|
322
|
+
:meth:`focus` / :meth:`fill` first if needed.
|
|
323
|
+
"""
|
|
324
|
+
inp = self._cdp.Input
|
|
325
|
+
for char in text:
|
|
326
|
+
await inp.dispatch_key_event(type_="keyDown", text=char, key=char)
|
|
327
|
+
await inp.dispatch_key_event(type_="keyUp", key=char)
|
|
328
|
+
if delay:
|
|
329
|
+
await asyncio.sleep(delay)
|
|
330
|
+
|
|
331
|
+
async def press(self, key: str) -> None:
|
|
332
|
+
"""Press a single key, e.g. ``"Enter"``, ``"Tab"`` or a character.
|
|
333
|
+
|
|
334
|
+
Named keys (see :data:`_KEY_DEFINITIONS`) carry their virtual-key code;
|
|
335
|
+
any other single character is sent as plain text.
|
|
336
|
+
"""
|
|
337
|
+
inp = self._cdp.Input
|
|
338
|
+
if key in _KEY_DEFINITIONS:
|
|
339
|
+
vk, code, text = _KEY_DEFINITIONS[key]
|
|
340
|
+
down: dict[str, Any] = {
|
|
341
|
+
"key": key,
|
|
342
|
+
"code": code,
|
|
343
|
+
"windows_virtual_key_code": vk,
|
|
344
|
+
}
|
|
345
|
+
if text:
|
|
346
|
+
down["text"] = text
|
|
347
|
+
await inp.dispatch_key_event(type_="keyDown", **down)
|
|
348
|
+
await inp.dispatch_key_event(type_="keyUp", key=key, code=code)
|
|
349
|
+
else:
|
|
350
|
+
await inp.dispatch_key_event(type_="keyDown", text=key, key=key)
|
|
351
|
+
await inp.dispatch_key_event(type_="keyUp", key=key)
|
|
352
|
+
|
|
353
|
+
@with_dom
|
|
354
|
+
async def is_visible(self) -> bool:
|
|
355
|
+
"""Whether the node is rendered and visually perceivable.
|
|
356
|
+
|
|
357
|
+
Visible means it has a non-empty layout box (so ``display: none``,
|
|
358
|
+
detached and zero-size nodes are excluded) and is not hidden by
|
|
359
|
+
``visibility`` or ``opacity: 0``. Never raises -- a detached/unstylable
|
|
360
|
+
node is reported as not visible.
|
|
361
|
+
"""
|
|
362
|
+
try:
|
|
363
|
+
model = (await self._cdp.DOM.get_box_model(node_id=self.id)).model
|
|
364
|
+
except Exception:
|
|
365
|
+
return False # no layout box: display:none, detached, or not rendered
|
|
366
|
+
if model is None or not model.width or not model.height:
|
|
367
|
+
return False
|
|
368
|
+
try:
|
|
369
|
+
async with self._frame.target.domain_enabled(self._frame.target.cdp.CSS):
|
|
370
|
+
styles = (
|
|
371
|
+
await self._cdp.CSS.get_computed_style_for_node(node_id=self.id)
|
|
372
|
+
).computed_style
|
|
373
|
+
except Exception:
|
|
374
|
+
return True
|
|
375
|
+
props = {p.name: p.value for p in styles}
|
|
376
|
+
if props.get("visibility") in ("hidden", "collapse"):
|
|
377
|
+
return False
|
|
378
|
+
try:
|
|
379
|
+
if float(props.get("opacity", "1")) <= 0:
|
|
380
|
+
return False
|
|
381
|
+
except ValueError:
|
|
382
|
+
pass
|
|
383
|
+
return True
|
|
384
|
+
|
|
385
|
+
@with_dom
|
|
386
|
+
async def _object(self) -> Runtime.RemoteObjectId | None:
|
|
387
|
+
"""Resolve (once) a JS object id for this node, for ``callFunctionOn``."""
|
|
388
|
+
if self._object_id is None:
|
|
389
|
+
resolved = await self._cdp.DOM.resolve_node(node_id=self.id)
|
|
390
|
+
self._object_id = resolved.object.object_id
|
|
391
|
+
return self._object_id
|
|
392
|
+
|
|
393
|
+
async def apply(self, function_declaration: str) -> Any:
|
|
394
|
+
"""Call ``function_declaration`` (e.g. ``"el => el.click()"``) on the node."""
|
|
395
|
+
result = await self._cdp.Runtime.call_function_on(
|
|
396
|
+
function_declaration=function_declaration,
|
|
397
|
+
object_id=await self._object(),
|
|
398
|
+
return_by_value=True,
|
|
399
|
+
)
|
|
400
|
+
return result.result.value if result.result else None
|
|
401
|
+
|
|
402
|
+
def __str__(self) -> str:
|
|
403
|
+
return f"<Element node={self.id} back_id={self.backend_id}>"
|
|
404
|
+
|
|
405
|
+
__repr__ = __str__
|