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.
Files changed (242) hide show
  1. parsek_cdp/__init__.py +55 -0
  2. parsek_cdp/_logging.py +49 -0
  3. parsek_cdp/cdp/__init__.py +247 -0
  4. parsek_cdp/cdp/accessibility/__init__.py +15 -0
  5. parsek_cdp/cdp/accessibility/events.py +35 -0
  6. parsek_cdp/cdp/accessibility/functions.py +202 -0
  7. parsek_cdp/cdp/accessibility/types.py +235 -0
  8. parsek_cdp/cdp/animation/__init__.py +15 -0
  9. parsek_cdp/cdp/animation/events.py +52 -0
  10. parsek_cdp/cdp/animation/functions.py +140 -0
  11. parsek_cdp/cdp/animation/types.py +113 -0
  12. parsek_cdp/cdp/audits/__init__.py +15 -0
  13. parsek_cdp/cdp/audits/events.py +21 -0
  14. parsek_cdp/cdp/audits/functions.py +95 -0
  15. parsek_cdp/cdp/audits/types.py +1051 -0
  16. parsek_cdp/cdp/autofill/__init__.py +15 -0
  17. parsek_cdp/cdp/autofill/events.py +27 -0
  18. parsek_cdp/cdp/autofill/functions.py +64 -0
  19. parsek_cdp/cdp/autofill/types.py +108 -0
  20. parsek_cdp/cdp/backgroundservice/__init__.py +15 -0
  21. parsek_cdp/cdp/backgroundservice/events.py +40 -0
  22. parsek_cdp/cdp/backgroundservice/functions.py +59 -0
  23. parsek_cdp/cdp/backgroundservice/types.py +63 -0
  24. parsek_cdp/cdp/bluetoothemulation/__init__.py +15 -0
  25. parsek_cdp/cdp/bluetoothemulation/events.py +70 -0
  26. parsek_cdp/cdp/bluetoothemulation/functions.py +247 -0
  27. parsek_cdp/cdp/bluetoothemulation/types.py +120 -0
  28. parsek_cdp/cdp/browser/__init__.py +15 -0
  29. parsek_cdp/cdp/browser/events.py +46 -0
  30. parsek_cdp/cdp/browser/functions.py +350 -0
  31. parsek_cdp/cdp/browser/types.py +158 -0
  32. parsek_cdp/cdp/cachestorage/__init__.py +15 -0
  33. parsek_cdp/cdp/cachestorage/events.py +13 -0
  34. parsek_cdp/cdp/cachestorage/functions.py +126 -0
  35. parsek_cdp/cdp/cachestorage/types.py +89 -0
  36. parsek_cdp/cdp/cast/__init__.py +15 -0
  37. parsek_cdp/cdp/cast/events.py +38 -0
  38. parsek_cdp/cdp/cast/functions.py +77 -0
  39. parsek_cdp/cdp/cast/types.py +23 -0
  40. parsek_cdp/cdp/console/__init__.py +15 -0
  41. parsek_cdp/cdp/console/events.py +22 -0
  42. parsek_cdp/cdp/console/functions.py +36 -0
  43. parsek_cdp/cdp/console/types.py +30 -0
  44. parsek_cdp/cdp/css/__init__.py +15 -0
  45. parsek_cdp/cdp/css/events.py +79 -0
  46. parsek_cdp/cdp/css/functions.py +788 -0
  47. parsek_cdp/cdp/css/types.py +742 -0
  48. parsek_cdp/cdp/debugger/__init__.py +15 -0
  49. parsek_cdp/cdp/debugger/events.py +164 -0
  50. parsek_cdp/cdp/debugger/functions.py +663 -0
  51. parsek_cdp/cdp/debugger/types.py +171 -0
  52. parsek_cdp/cdp/deviceaccess/__init__.py +15 -0
  53. parsek_cdp/cdp/deviceaccess/events.py +30 -0
  54. parsek_cdp/cdp/deviceaccess/functions.py +54 -0
  55. parsek_cdp/cdp/deviceaccess/types.py +28 -0
  56. parsek_cdp/cdp/deviceorientation/__init__.py +15 -0
  57. parsek_cdp/cdp/deviceorientation/events.py +13 -0
  58. parsek_cdp/cdp/deviceorientation/functions.py +35 -0
  59. parsek_cdp/cdp/deviceorientation/types.py +10 -0
  60. parsek_cdp/cdp/dom/__init__.py +15 -0
  61. parsek_cdp/cdp/dom/events.py +228 -0
  62. parsek_cdp/cdp/dom/functions.py +1033 -0
  63. parsek_cdp/cdp/dom/types.py +293 -0
  64. parsek_cdp/cdp/domdebugger/__init__.py +15 -0
  65. parsek_cdp/cdp/domdebugger/events.py +13 -0
  66. parsek_cdp/cdp/domdebugger/functions.py +155 -0
  67. parsek_cdp/cdp/domdebugger/types.py +57 -0
  68. parsek_cdp/cdp/domsnapshot/__init__.py +15 -0
  69. parsek_cdp/cdp/domsnapshot/events.py +13 -0
  70. parsek_cdp/cdp/domsnapshot/functions.py +111 -0
  71. parsek_cdp/cdp/domsnapshot/types.py +312 -0
  72. parsek_cdp/cdp/domstorage/__init__.py +15 -0
  73. parsek_cdp/cdp/domstorage/events.py +60 -0
  74. parsek_cdp/cdp/domstorage/functions.py +79 -0
  75. parsek_cdp/cdp/domstorage/types.py +30 -0
  76. parsek_cdp/cdp/emulation/__init__.py +15 -0
  77. parsek_cdp/cdp/emulation/events.py +17 -0
  78. parsek_cdp/cdp/emulation/functions.py +688 -0
  79. parsek_cdp/cdp/emulation/types.py +305 -0
  80. parsek_cdp/cdp/eventbreakpoints/__init__.py +15 -0
  81. parsek_cdp/cdp/eventbreakpoints/events.py +13 -0
  82. parsek_cdp/cdp/eventbreakpoints/functions.py +41 -0
  83. parsek_cdp/cdp/eventbreakpoints/types.py +10 -0
  84. parsek_cdp/cdp/extensions/__init__.py +15 -0
  85. parsek_cdp/cdp/extensions/events.py +13 -0
  86. parsek_cdp/cdp/extensions/functions.py +118 -0
  87. parsek_cdp/cdp/extensions/types.py +19 -0
  88. parsek_cdp/cdp/fedcm/__init__.py +15 -0
  89. parsek_cdp/cdp/fedcm/events.py +45 -0
  90. parsek_cdp/cdp/fedcm/functions.py +93 -0
  91. parsek_cdp/cdp/fedcm/types.py +72 -0
  92. parsek_cdp/cdp/fetch/__init__.py +15 -0
  93. parsek_cdp/cdp/fetch/events.py +82 -0
  94. parsek_cdp/cdp/fetch/functions.py +215 -0
  95. parsek_cdp/cdp/fetch/types.py +81 -0
  96. parsek_cdp/cdp/filesystem/__init__.py +15 -0
  97. parsek_cdp/cdp/filesystem/events.py +13 -0
  98. parsek_cdp/cdp/filesystem/functions.py +36 -0
  99. parsek_cdp/cdp/filesystem/types.py +54 -0
  100. parsek_cdp/cdp/headlessexperimental/__init__.py +15 -0
  101. parsek_cdp/cdp/headlessexperimental/events.py +13 -0
  102. parsek_cdp/cdp/headlessexperimental/functions.py +77 -0
  103. parsek_cdp/cdp/headlessexperimental/types.py +24 -0
  104. parsek_cdp/cdp/heapprofiler/__init__.py +15 -0
  105. parsek_cdp/cdp/heapprofiler/events.py +65 -0
  106. parsek_cdp/cdp/heapprofiler/functions.py +193 -0
  107. parsek_cdp/cdp/heapprofiler/types.py +57 -0
  108. parsek_cdp/cdp/indexeddb/__init__.py +15 -0
  109. parsek_cdp/cdp/indexeddb/events.py +13 -0
  110. parsek_cdp/cdp/indexeddb/functions.py +232 -0
  111. parsek_cdp/cdp/indexeddb/types.py +120 -0
  112. parsek_cdp/cdp/input/__init__.py +15 -0
  113. parsek_cdp/cdp/input/events.py +25 -0
  114. parsek_cdp/cdp/input/functions.py +361 -0
  115. parsek_cdp/cdp/input/types.py +87 -0
  116. parsek_cdp/cdp/inspector/__init__.py +15 -0
  117. parsek_cdp/cdp/inspector/events.py +41 -0
  118. parsek_cdp/cdp/inspector/functions.py +27 -0
  119. parsek_cdp/cdp/inspector/types.py +10 -0
  120. parsek_cdp/cdp/io/__init__.py +15 -0
  121. parsek_cdp/cdp/io/events.py +13 -0
  122. parsek_cdp/cdp/io/functions.py +77 -0
  123. parsek_cdp/cdp/io/types.py +13 -0
  124. parsek_cdp/cdp/layertree/__init__.py +15 -0
  125. parsek_cdp/cdp/layertree/events.py +36 -0
  126. parsek_cdp/cdp/layertree/functions.py +178 -0
  127. parsek_cdp/cdp/layertree/types.py +104 -0
  128. parsek_cdp/cdp/log/__init__.py +15 -0
  129. parsek_cdp/cdp/log/events.py +22 -0
  130. parsek_cdp/cdp/log/functions.py +54 -0
  131. parsek_cdp/cdp/log/types.py +57 -0
  132. parsek_cdp/cdp/media/__init__.py +15 -0
  133. parsek_cdp/cdp/media/events.py +87 -0
  134. parsek_cdp/cdp/media/functions.py +27 -0
  135. parsek_cdp/cdp/media/types.py +101 -0
  136. parsek_cdp/cdp/memory/__init__.py +15 -0
  137. parsek_cdp/cdp/memory/events.py +13 -0
  138. parsek_cdp/cdp/memory/functions.py +164 -0
  139. parsek_cdp/cdp/memory/types.py +71 -0
  140. parsek_cdp/cdp/mixins/__init__.py +21 -0
  141. parsek_cdp/cdp/mixins/datatype.py +123 -0
  142. parsek_cdp/cdp/mixins/event.py +78 -0
  143. parsek_cdp/cdp/mixins/namespace.py +31 -0
  144. parsek_cdp/cdp/network/__init__.py +15 -0
  145. parsek_cdp/cdp/network/events.py +726 -0
  146. parsek_cdp/cdp/network/functions.py +734 -0
  147. parsek_cdp/cdp/network/types.py +1247 -0
  148. parsek_cdp/cdp/overlay/__init__.py +15 -0
  149. parsek_cdp/cdp/overlay/events.py +54 -0
  150. parsek_cdp/cdp/overlay/functions.py +407 -0
  151. parsek_cdp/cdp/overlay/types.py +328 -0
  152. parsek_cdp/cdp/page/__init__.py +15 -0
  153. parsek_cdp/cdp/page/events.py +416 -0
  154. parsek_cdp/cdp/page/functions.py +1081 -0
  155. parsek_cdp/cdp/page/types.py +1088 -0
  156. parsek_cdp/cdp/performance/__init__.py +15 -0
  157. parsek_cdp/cdp/performance/events.py +24 -0
  158. parsek_cdp/cdp/performance/functions.py +63 -0
  159. parsek_cdp/cdp/performance/types.py +22 -0
  160. parsek_cdp/cdp/performancetimeline/__init__.py +15 -0
  161. parsek_cdp/cdp/performancetimeline/events.py +22 -0
  162. parsek_cdp/cdp/performancetimeline/functions.py +30 -0
  163. parsek_cdp/cdp/performancetimeline/types.py +85 -0
  164. parsek_cdp/cdp/preload/__init__.py +15 -0
  165. parsek_cdp/cdp/preload/events.py +116 -0
  166. parsek_cdp/cdp/preload/functions.py +25 -0
  167. parsek_cdp/cdp/preload/types.py +267 -0
  168. parsek_cdp/cdp/profiler/__init__.py +15 -0
  169. parsek_cdp/cdp/profiler/events.py +64 -0
  170. parsek_cdp/cdp/profiler/functions.py +134 -0
  171. parsek_cdp/cdp/profiler/types.py +105 -0
  172. parsek_cdp/cdp/pwa/__init__.py +15 -0
  173. parsek_cdp/cdp/pwa/events.py +13 -0
  174. parsek_cdp/cdp/pwa/functions.py +196 -0
  175. parsek_cdp/cdp/pwa/types.py +45 -0
  176. parsek_cdp/cdp/runtime/__init__.py +15 -0
  177. parsek_cdp/cdp/runtime/events.py +123 -0
  178. parsek_cdp/cdp/runtime/functions.py +552 -0
  179. parsek_cdp/cdp/runtime/types.py +319 -0
  180. parsek_cdp/cdp/schema/__init__.py +15 -0
  181. parsek_cdp/cdp/schema/events.py +13 -0
  182. parsek_cdp/cdp/schema/functions.py +32 -0
  183. parsek_cdp/cdp/schema/types.py +22 -0
  184. parsek_cdp/cdp/security/__init__.py +15 -0
  185. parsek_cdp/cdp/security/events.py +64 -0
  186. parsek_cdp/cdp/security/functions.py +66 -0
  187. parsek_cdp/cdp/security/types.py +168 -0
  188. parsek_cdp/cdp/serviceworker/__init__.py +15 -0
  189. parsek_cdp/cdp/serviceworker/events.py +43 -0
  190. parsek_cdp/cdp/serviceworker/functions.py +115 -0
  191. parsek_cdp/cdp/serviceworker/types.py +95 -0
  192. parsek_cdp/cdp/storage/__init__.py +15 -0
  193. parsek_cdp/cdp/storage/events.py +288 -0
  194. parsek_cdp/cdp/storage/functions.py +571 -0
  195. parsek_cdp/cdp/storage/types.py +656 -0
  196. parsek_cdp/cdp/systeminfo/__init__.py +15 -0
  197. parsek_cdp/cdp/systeminfo/events.py +13 -0
  198. parsek_cdp/cdp/systeminfo/functions.py +75 -0
  199. parsek_cdp/cdp/systeminfo/types.py +132 -0
  200. parsek_cdp/cdp/target/__init__.py +15 -0
  201. parsek_cdp/cdp/target/events.py +109 -0
  202. parsek_cdp/cdp/target/functions.py +424 -0
  203. parsek_cdp/cdp/target/types.py +83 -0
  204. parsek_cdp/cdp/tethering/__init__.py +15 -0
  205. parsek_cdp/cdp/tethering/events.py +22 -0
  206. parsek_cdp/cdp/tethering/functions.py +35 -0
  207. parsek_cdp/cdp/tethering/types.py +10 -0
  208. parsek_cdp/cdp/tracing/__init__.py +15 -0
  209. parsek_cdp/cdp/tracing/events.py +61 -0
  210. parsek_cdp/cdp/tracing/functions.py +119 -0
  211. parsek_cdp/cdp/tracing/types.py +81 -0
  212. parsek_cdp/cdp/webaudio/__init__.py +15 -0
  213. parsek_cdp/cdp/webaudio/events.py +184 -0
  214. parsek_cdp/cdp/webaudio/functions.py +51 -0
  215. parsek_cdp/cdp/webaudio/types.py +155 -0
  216. parsek_cdp/cdp/webauthn/__init__.py +15 -0
  217. parsek_cdp/cdp/webauthn/events.py +69 -0
  218. parsek_cdp/cdp/webauthn/functions.py +214 -0
  219. parsek_cdp/cdp/webauthn/types.py +96 -0
  220. parsek_cdp/core/__init__.py +18 -0
  221. parsek_cdp/core/browser.py +240 -0
  222. parsek_cdp/core/browser_context.py +58 -0
  223. parsek_cdp/core/element.py +405 -0
  224. parsek_cdp/core/feature.py +376 -0
  225. parsek_cdp/core/frame.py +351 -0
  226. parsek_cdp/core/pagable.py +175 -0
  227. parsek_cdp/core/page.py +163 -0
  228. parsek_cdp/core/target.py +490 -0
  229. parsek_cdp/features/__init__.py +14 -0
  230. parsek_cdp/features/request_listener/__init__.py +256 -0
  231. parsek_cdp/features/request_listener/events.py +48 -0
  232. parsek_cdp/features/request_listener/types.py +235 -0
  233. parsek_cdp/generate.py +642 -0
  234. parsek_cdp/parsek/__init__.py +18 -0
  235. parsek_cdp/parsek/events.py +38 -0
  236. parsek_cdp/parsek/types.py +36 -0
  237. parsek_cdp/py.typed +0 -0
  238. parsek_cdp-0.1.0.dist-info/METADATA +277 -0
  239. parsek_cdp-0.1.0.dist-info/RECORD +242 -0
  240. parsek_cdp-0.1.0.dist-info/WHEEL +5 -0
  241. parsek_cdp-0.1.0.dist-info/licenses/LICENSE +201 -0
  242. 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__