wavexis 2.3.2__tar.gz → 2.3.3__tar.gz

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 (218) hide show
  1. {wavexis-2.3.2 → wavexis-2.3.3}/CHANGELOG.md +11 -0
  2. {wavexis-2.3.2 → wavexis-2.3.3}/PKG-INFO +1 -1
  3. {wavexis-2.3.2 → wavexis-2.3.3}/pyproject.toml +1 -1
  4. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_backend_manager.py +18 -0
  5. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_manager.py +7 -0
  6. wavexis-2.3.3/tests/unit/test_shadow.py +232 -0
  7. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/__init__.py +1 -1
  8. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/backend/base.py +39 -0
  9. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/backend/bidi.py +129 -0
  10. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/backend/cdp.py +127 -0
  11. wavexis-2.3.3/wavexis/cli/_shadow.py +135 -0
  12. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/cli/app.py +1 -0
  13. {wavexis-2.3.2 → wavexis-2.3.3}/.dockerignore +0 -0
  14. {wavexis-2.3.2 → wavexis-2.3.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  15. {wavexis-2.3.2 → wavexis-2.3.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  16. {wavexis-2.3.2 → wavexis-2.3.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  17. {wavexis-2.3.2 → wavexis-2.3.3}/.github/workflows/ci.yml +0 -0
  18. {wavexis-2.3.2 → wavexis-2.3.3}/.github/workflows/docs.yml +0 -0
  19. {wavexis-2.3.2 → wavexis-2.3.3}/.github/workflows/release.yml +0 -0
  20. {wavexis-2.3.2 → wavexis-2.3.3}/.gitignore +0 -0
  21. {wavexis-2.3.2 → wavexis-2.3.3}/CODE_OF_CONDUCT.md +0 -0
  22. {wavexis-2.3.2 → wavexis-2.3.3}/CONTRIBUTING.md +0 -0
  23. {wavexis-2.3.2 → wavexis-2.3.3}/Dockerfile +0 -0
  24. {wavexis-2.3.2 → wavexis-2.3.3}/LICENSE +0 -0
  25. {wavexis-2.3.2 → wavexis-2.3.3}/README.md +0 -0
  26. {wavexis-2.3.2 → wavexis-2.3.3}/SECURITY.md +0 -0
  27. {wavexis-2.3.2 → wavexis-2.3.3}/docs/api/actions.md +0 -0
  28. {wavexis-2.3.2 → wavexis-2.3.3}/docs/api/backends.md +0 -0
  29. {wavexis-2.3.2 → wavexis-2.3.3}/docs/api/cli.md +0 -0
  30. {wavexis-2.3.2 → wavexis-2.3.3}/docs/api/config.md +0 -0
  31. {wavexis-2.3.2 → wavexis-2.3.3}/docs/api/exceptions.md +0 -0
  32. {wavexis-2.3.2 → wavexis-2.3.3}/docs/api/multi.md +0 -0
  33. {wavexis-2.3.2 → wavexis-2.3.3}/docs/api/plugins.md +0 -0
  34. {wavexis-2.3.2 → wavexis-2.3.3}/docs/api/serve.md +0 -0
  35. {wavexis-2.3.2 → wavexis-2.3.3}/docs/cookbook/auth.md +0 -0
  36. {wavexis-2.3.2 → wavexis-2.3.3}/docs/cookbook/ci-cd.md +0 -0
  37. {wavexis-2.3.2 → wavexis-2.3.3}/docs/cookbook/css.md +0 -0
  38. {wavexis-2.3.2 → wavexis-2.3.3}/docs/cookbook/debug.md +0 -0
  39. {wavexis-2.3.2 → wavexis-2.3.3}/docs/cookbook/record-replay.md +0 -0
  40. {wavexis-2.3.2 → wavexis-2.3.3}/docs/cookbook/scraping.md +0 -0
  41. {wavexis-2.3.2 → wavexis-2.3.3}/docs/cookbook/screenshots.md +0 -0
  42. {wavexis-2.3.2 → wavexis-2.3.3}/docs/cookbook/serve-mode.md +0 -0
  43. {wavexis-2.3.2 → wavexis-2.3.3}/docs/guide/assert.md +0 -0
  44. {wavexis-2.3.2 → wavexis-2.3.3}/docs/guide/backends.md +0 -0
  45. {wavexis-2.3.2 → wavexis-2.3.3}/docs/guide/commands.md +0 -0
  46. {wavexis-2.3.2 → wavexis-2.3.3}/docs/guide/init.md +0 -0
  47. {wavexis-2.3.2 → wavexis-2.3.3}/docs/guide/installation.md +0 -0
  48. {wavexis-2.3.2 → wavexis-2.3.3}/docs/guide/multi.md +0 -0
  49. {wavexis-2.3.2 → wavexis-2.3.3}/docs/guide/perf.md +0 -0
  50. {wavexis-2.3.2 → wavexis-2.3.3}/docs/guide/plugins.md +0 -0
  51. {wavexis-2.3.2 → wavexis-2.3.3}/docs/guide/quickstart.md +0 -0
  52. {wavexis-2.3.2 → wavexis-2.3.3}/docs/guide/raw.md +0 -0
  53. {wavexis-2.3.2 → wavexis-2.3.3}/docs/guide/repl.md +0 -0
  54. {wavexis-2.3.2 → wavexis-2.3.3}/docs/guide/troubleshooting.md +0 -0
  55. {wavexis-2.3.2 → wavexis-2.3.3}/docs/index.md +0 -0
  56. {wavexis-2.3.2 → wavexis-2.3.3}/mkdocs.yml +0 -0
  57. {wavexis-2.3.2 → wavexis-2.3.3}/stubs/bidiwave/__init__.pyi +0 -0
  58. {wavexis-2.3.2 → wavexis-2.3.3}/stubs/cdpwave/__init__.pyi +0 -0
  59. {wavexis-2.3.2 → wavexis-2.3.3}/tests/__init__.py +0 -0
  60. {wavexis-2.3.2 → wavexis-2.3.3}/tests/conftest.py +0 -0
  61. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/__init__.py +0 -0
  62. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_a11y.py +0 -0
  63. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_animation.py +0 -0
  64. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_backend_selection.py +0 -0
  65. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_browser.py +0 -0
  66. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_console.py +0 -0
  67. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_css.py +0 -0
  68. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_debug.py +0 -0
  69. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_dialog.py +0 -0
  70. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_dom.py +0 -0
  71. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_dom_snapshot.py +0 -0
  72. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_emulation.py +0 -0
  73. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_emulation_advanced.py +0 -0
  74. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_eval.py +0 -0
  75. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_har.py +0 -0
  76. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_input.py +0 -0
  77. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_media.py +0 -0
  78. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_multi.py +0 -0
  79. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_navigate.py +0 -0
  80. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_network.py +0 -0
  81. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_network_advanced.py +0 -0
  82. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_overlay.py +0 -0
  83. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_pdf.py +0 -0
  84. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_perf.py +0 -0
  85. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_permissions.py +0 -0
  86. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_raw.py +0 -0
  87. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_record.py +0 -0
  88. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_scrape.py +0 -0
  89. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_screenshot.py +0 -0
  90. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_security.py +0 -0
  91. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_serve.py +0 -0
  92. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_service_worker.py +0 -0
  93. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_storage.py +0 -0
  94. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_tabs.py +0 -0
  95. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_webaudio.py +0 -0
  96. {wavexis-2.3.2 → wavexis-2.3.3}/tests/integration/test_webauthn.py +0 -0
  97. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/__init__.py +0 -0
  98. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_abstract_backend_phase5.py +0 -0
  99. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_actions.py +0 -0
  100. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_actions_phase5.py +0 -0
  101. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_animation_action.py +0 -0
  102. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_assert_multi.py +0 -0
  103. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_auth.py +0 -0
  104. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_auto_wait.py +0 -0
  105. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_bidi_backend.py +0 -0
  106. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_bidi_phase5.py +0 -0
  107. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_bluetooth_action.py +0 -0
  108. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_cast_action.py +0 -0
  109. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_cleanup.py +0 -0
  110. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_cli_phase5.py +0 -0
  111. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_config.py +0 -0
  112. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_config_phase5.py +0 -0
  113. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_cookies_headers_actions.py +0 -0
  114. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_css_action.py +0 -0
  115. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_debug_action.py +0 -0
  116. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_dom_snapshot_action.py +0 -0
  117. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_download_screencast.py +0 -0
  118. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_emulation_action.py +0 -0
  119. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_error_handling.py +0 -0
  120. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_exceptions.py +0 -0
  121. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_format_record.py +0 -0
  122. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_global_options.py +0 -0
  123. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_iframe.py +0 -0
  124. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_init.py +0 -0
  125. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_input_action.py +0 -0
  126. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_interactions_crawl.py +0 -0
  127. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_media_action.py +0 -0
  128. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_multi.py +0 -0
  129. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_multi_improvements.py +0 -0
  130. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_new_features.py +0 -0
  131. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_output.py +0 -0
  132. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_overlay_action.py +0 -0
  133. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_perf_action.py +0 -0
  134. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_perf_console.py +0 -0
  135. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_plugins.py +0 -0
  136. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_raw.py +0 -0
  137. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_record.py +0 -0
  138. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_repl.py +0 -0
  139. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_serve.py +0 -0
  140. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_service_worker_action.py +0 -0
  141. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_storage_action.py +0 -0
  142. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_webaudio_action.py +0 -0
  143. {wavexis-2.3.2 → wavexis-2.3.3}/tests/unit/test_webauthn_action.py +0 -0
  144. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/__main__.py +0 -0
  145. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/__init__.py +0 -0
  146. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/accessibility.py +0 -0
  147. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/animation.py +0 -0
  148. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/base.py +0 -0
  149. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/bluetooth.py +0 -0
  150. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/browser.py +0 -0
  151. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/cast.py +0 -0
  152. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/console.py +0 -0
  153. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/cookies.py +0 -0
  154. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/crawl.py +0 -0
  155. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/css.py +0 -0
  156. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/debug.py +0 -0
  157. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/dialog.py +0 -0
  158. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/dom.py +0 -0
  159. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/dom_snapshot.py +0 -0
  160. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/download.py +0 -0
  161. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/emulation.py +0 -0
  162. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/eval.py +0 -0
  163. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/extract.py +0 -0
  164. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/form.py +0 -0
  165. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/har.py +0 -0
  166. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/headers.py +0 -0
  167. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/input.py +0 -0
  168. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/lighthouse.py +0 -0
  169. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/media.py +0 -0
  170. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/multi.py +0 -0
  171. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/navigate.py +0 -0
  172. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/network.py +0 -0
  173. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/overlay.py +0 -0
  174. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/pdf.py +0 -0
  175. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/performance.py +0 -0
  176. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/permissions.py +0 -0
  177. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/record.py +0 -0
  178. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/scrape.py +0 -0
  179. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/screencast.py +0 -0
  180. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/screenshot.py +0 -0
  181. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/security.py +0 -0
  182. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/service_worker.py +0 -0
  183. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/session.py +0 -0
  184. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/storage.py +0 -0
  185. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/tabs.py +0 -0
  186. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/wait.py +0 -0
  187. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/webaudio.py +0 -0
  188. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/webauthn.py +0 -0
  189. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/actions/websocket.py +0 -0
  190. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/auth.py +0 -0
  191. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/backend/__init__.py +0 -0
  192. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/backend/manager.py +0 -0
  193. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/cleanup.py +0 -0
  194. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/cli/__init__.py +0 -0
  195. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/cli/_advanced.py +0 -0
  196. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/cli/_capture.py +0 -0
  197. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/cli/_config.py +0 -0
  198. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/cli/_debug.py +0 -0
  199. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/cli/_emulation.py +0 -0
  200. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/cli/_experimental.py +0 -0
  201. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/cli/_iframe.py +0 -0
  202. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/cli/_input.py +0 -0
  203. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/cli/_navigation.py +0 -0
  204. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/cli/_network.py +0 -0
  205. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/cli/_perf.py +0 -0
  206. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/cli/_serve.py +0 -0
  207. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/cli/_session.py +0 -0
  208. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/cli/_shared.py +0 -0
  209. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/cli/_workflow.py +0 -0
  210. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/config.py +0 -0
  211. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/exceptions.py +0 -0
  212. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/init.py +0 -0
  213. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/multi.py +0 -0
  214. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/output.py +0 -0
  215. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/plugins.py +0 -0
  216. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/record.py +0 -0
  217. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/repl.py +0 -0
  218. {wavexis-2.3.2 → wavexis-2.3.3}/wavexis/serve.py +0 -0
@@ -2,6 +2,17 @@
2
2
 
3
3
  All notable changes to wavexis are documented in this file.
4
4
 
5
+ ## v2.3.3 — 2026-07-07
6
+
7
+ ### Added
8
+
9
+ - Shadow DOM support: `shadow_click`, `shadow_fill`, `shadow_eval` in both CDP and BiDi backends
10
+ - `wavexis shadow click/fill/eval` CLI command with `--selectors` (comma-separated piercing chain)
11
+ - `_build_shadow_pierce_js` helper generates JS that traverses `shadowRoot` boundaries
12
+ - Auto-waiting inside shadow DOM via `_wait_for_element_in_shadow` with polling and configurable timeout
13
+ - `--no-wait` flag to skip auto-waiting for shadow interactions
14
+ - Events dispatched with `composed: true` to cross shadow boundaries
15
+
5
16
  ## v2.3.2 — 2026-07-07
6
17
 
7
18
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavexis
3
- Version: 2.3.2
3
+ Version: 2.3.3
4
4
  Summary: Browser automation CLI — wraps cdpwave and bidiwave, no Node.js, no Chromium download
5
5
  Project-URL: Homepage, https://github.com/MathiasPaulenko/wavexis
6
6
  Project-URL: Repository, https://github.com/MathiasPaulenko/wavexis
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "wavexis"
7
- version = "2.3.2"
7
+ version = "2.3.3"
8
8
  description = "Browser automation CLI — wraps cdpwave and bidiwave, no Node.js, no Chromium download"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -268,6 +268,24 @@ class DummyBackend(AbstractBackend):
268
268
  """Iframe fill."""
269
269
  pass
270
270
 
271
+ async def shadow_eval(
272
+ self, selectors: list[str], expression: str, await_promise: bool = False
273
+ ) -> object:
274
+ """Shadow eval."""
275
+ return None
276
+
277
+ async def shadow_click(
278
+ self, selectors: list[str], auto_wait: bool = True
279
+ ) -> None:
280
+ """Shadow click."""
281
+ pass
282
+
283
+ async def shadow_fill(
284
+ self, selectors: list[str], value: str, auto_wait: bool = True
285
+ ) -> None:
286
+ """Shadow fill."""
287
+ pass
288
+
271
289
  async def throttle_network(self, params: object) -> None:
272
290
  """Throttle network."""
273
291
  pass
@@ -269,6 +269,13 @@ class FakeBackend(AbstractBackend):
269
269
  """Iframe click."""
270
270
  async def iframe_fill(self, iframe_selector, selector, value, auto_wait=True):
271
271
  """Iframe fill."""
272
+ async def shadow_eval(self, selectors, expression, await_promise=False):
273
+ """Shadow eval."""
274
+ return None
275
+ async def shadow_click(self, selectors, auto_wait=True):
276
+ """Shadow click."""
277
+ async def shadow_fill(self, selectors, value, auto_wait=True):
278
+ """Shadow fill."""
272
279
 
273
280
 
274
281
  class TestBackendManager:
@@ -0,0 +1,232 @@
1
+ """Unit tests for Shadow DOM support — shadow_eval, shadow_click, shadow_fill."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import Any
7
+ from unittest.mock import AsyncMock, MagicMock
8
+
9
+ import pytest
10
+
11
+ from wavexis.exceptions import ElementNotFoundError
12
+
13
+
14
+ def _make_cdp_backend() -> Any:
15
+ """Create a CDPBackend with mocked session."""
16
+ from wavexis.backend.cdp import CDPBackend
17
+
18
+ backend = CDPBackend()
19
+ backend._session = MagicMock()
20
+ backend._session.runtime = MagicMock()
21
+ backend._session.runtime.evaluate = AsyncMock(
22
+ return_value={"result": {"value": True}}
23
+ )
24
+ return backend
25
+
26
+
27
+ def _make_bidi_backend() -> Any:
28
+ """Create a BiDiBackend with mocked client."""
29
+ from wavexis.backend.bidi import BiDiBackend
30
+
31
+ backend = BiDiBackend()
32
+ backend._client = MagicMock()
33
+ backend._context = MagicMock()
34
+ backend._client.script = MagicMock()
35
+ result = MagicMock()
36
+ result.value = True
37
+ backend._client.script.evaluate = AsyncMock(return_value=result)
38
+ return backend
39
+
40
+
41
+ @pytest.mark.unit
42
+ class TestShadowEval:
43
+ """Tests for shadow_eval in both backends."""
44
+
45
+ def test_cdp_shadow_eval_returns_value(self) -> None:
46
+ """Test CDP shadow_eval returns the evaluated value."""
47
+ backend = _make_cdp_backend()
48
+ backend._session.runtime.evaluate = AsyncMock(
49
+ return_value={"result": {"value": 42}}
50
+ )
51
+ result = asyncio.run(
52
+ backend.shadow_eval(["my-component", "button"], "1 + 1")
53
+ )
54
+ assert result == 42
55
+
56
+ def test_cdp_shadow_eval_null_element_not_found(self) -> None:
57
+ """Test CDP shadow_eval returns None when element is not found."""
58
+ backend = _make_cdp_backend()
59
+ backend._session.runtime.evaluate = AsyncMock(
60
+ return_value={"result": {"value": None}}
61
+ )
62
+ result = asyncio.run(
63
+ backend.shadow_eval(["missing", "button"], "this.textContent")
64
+ )
65
+ assert result is None
66
+
67
+ def test_bidi_shadow_eval_returns_value(self) -> None:
68
+ """Test BiDi shadow_eval returns the evaluated value."""
69
+ backend = _make_bidi_backend()
70
+ result_mock = MagicMock()
71
+ result_mock.value = "hello"
72
+ backend._client.script.evaluate = AsyncMock(return_value=result_mock)
73
+ result = asyncio.run(
74
+ backend.shadow_eval(["my-component", "button"], "this.textContent")
75
+ )
76
+ assert result == "hello"
77
+
78
+
79
+ @pytest.mark.unit
80
+ class TestShadowClick:
81
+ """Tests for shadow_click in both backends."""
82
+
83
+ def test_cdp_shadow_click_success(self) -> None:
84
+ """Test CDP shadow_click dispatches click inside shadow DOM."""
85
+ backend = _make_cdp_backend()
86
+ backend._session.runtime.evaluate = AsyncMock(
87
+ return_value={"result": {"value": True}}
88
+ )
89
+ asyncio.run(backend.shadow_click(["my-component", "button"], auto_wait=False))
90
+ backend._session.runtime.evaluate.assert_called()
91
+
92
+ def test_cdp_shadow_click_element_not_found(self) -> None:
93
+ """Test CDP shadow_click raises ElementNotFoundError when element missing."""
94
+ backend = _make_cdp_backend()
95
+ backend._session.runtime.evaluate = AsyncMock(
96
+ return_value={"result": {"value": False}}
97
+ )
98
+ with pytest.raises(ElementNotFoundError):
99
+ asyncio.run(backend.shadow_click(["missing", "btn"], auto_wait=False))
100
+
101
+ def test_bidi_shadow_click_success(self) -> None:
102
+ """Test BiDi shadow_click dispatches click inside shadow DOM."""
103
+ backend = _make_bidi_backend()
104
+ result_mock = MagicMock()
105
+ result_mock.value = True
106
+ backend._client.script.evaluate = AsyncMock(return_value=result_mock)
107
+ asyncio.run(backend.shadow_click(["my-component", "button"], auto_wait=False))
108
+ backend._client.script.evaluate.assert_called()
109
+
110
+ def test_bidi_shadow_click_element_not_found(self) -> None:
111
+ """Test BiDi shadow_click raises ElementNotFoundError when element missing."""
112
+ backend = _make_bidi_backend()
113
+ result_mock = MagicMock()
114
+ result_mock.value = False
115
+ backend._client.script.evaluate = AsyncMock(return_value=result_mock)
116
+ with pytest.raises(ElementNotFoundError):
117
+ asyncio.run(backend.shadow_click(["missing", "btn"], auto_wait=False))
118
+
119
+
120
+ @pytest.mark.unit
121
+ class TestShadowFill:
122
+ """Tests for shadow_fill in both backends."""
123
+
124
+ def test_cdp_shadow_fill_success(self) -> None:
125
+ """Test CDP shadow_fill sets value inside shadow DOM."""
126
+ backend = _make_cdp_backend()
127
+ backend._session.runtime.evaluate = AsyncMock(
128
+ return_value={"result": {"value": True}}
129
+ )
130
+ asyncio.run(
131
+ backend.shadow_fill(["my-component", "input"], "test value", auto_wait=False)
132
+ )
133
+ backend._session.runtime.evaluate.assert_called()
134
+
135
+ def test_cdp_shadow_fill_element_not_found(self) -> None:
136
+ """Test CDP shadow_fill raises ElementNotFoundError when element missing."""
137
+ backend = _make_cdp_backend()
138
+ backend._session.runtime.evaluate = AsyncMock(
139
+ return_value={"result": {"value": False}}
140
+ )
141
+ with pytest.raises(ElementNotFoundError):
142
+ asyncio.run(
143
+ backend.shadow_fill(["missing", "input"], "val", auto_wait=False)
144
+ )
145
+
146
+ def test_bidi_shadow_fill_success(self) -> None:
147
+ """Test BiDi shadow_fill sets value inside shadow DOM."""
148
+ backend = _make_bidi_backend()
149
+ result_mock = MagicMock()
150
+ result_mock.value = True
151
+ backend._client.script.evaluate = AsyncMock(return_value=result_mock)
152
+ asyncio.run(
153
+ backend.shadow_fill(["my-component", "input"], "test value", auto_wait=False)
154
+ )
155
+ backend._client.script.evaluate.assert_called()
156
+
157
+ def test_bidi_shadow_fill_element_not_found(self) -> None:
158
+ """Test BiDi shadow_fill raises ElementNotFoundError when element missing."""
159
+ backend = _make_bidi_backend()
160
+ result_mock = MagicMock()
161
+ result_mock.value = False
162
+ backend._client.script.evaluate = AsyncMock(return_value=result_mock)
163
+ with pytest.raises(ElementNotFoundError):
164
+ asyncio.run(
165
+ backend.shadow_fill(["missing", "input"], "val", auto_wait=False)
166
+ )
167
+
168
+
169
+ @pytest.mark.unit
170
+ class TestShadowAutoWait:
171
+ """Tests for auto_wait in shadow operations."""
172
+
173
+ def test_cdp_shadow_click_auto_wait_false_skips_wait(self) -> None:
174
+ """Test that shadow_click with auto_wait=False skips _wait_for_element_in_shadow."""
175
+ backend = _make_cdp_backend()
176
+ call_order: list[str] = []
177
+
178
+ async def _mock_wait(selectors: list[str], timeout_ms: int = 30000) -> None:
179
+ call_order.append("wait")
180
+
181
+ backend._wait_for_element_in_shadow = _mock_wait # type: ignore[assignment]
182
+ backend._session.runtime.evaluate = AsyncMock(
183
+ return_value={"result": {"value": True}}
184
+ )
185
+
186
+ asyncio.run(backend.shadow_click(["my-component", "btn"], auto_wait=False))
187
+ assert "wait" not in call_order
188
+
189
+ def test_cdp_shadow_click_auto_wait_true_calls_wait(self) -> None:
190
+ """Test that shadow_click with auto_wait=True calls _wait_for_element_in_shadow."""
191
+ backend = _make_cdp_backend()
192
+ call_order: list[str] = []
193
+
194
+ async def _mock_wait(selectors: list[str], timeout_ms: int = 30000) -> None:
195
+ call_order.append("wait")
196
+
197
+ backend._wait_for_element_in_shadow = _mock_wait # type: ignore[assignment]
198
+ backend._session.runtime.evaluate = AsyncMock(
199
+ return_value={"result": {"value": True}}
200
+ )
201
+
202
+ asyncio.run(backend.shadow_click(["my-component", "btn"], auto_wait=True))
203
+ assert "wait" in call_order
204
+
205
+
206
+ @pytest.mark.unit
207
+ class TestBuildShadowPierceJs:
208
+ """Tests for _build_shadow_pierce_js helper."""
209
+
210
+ def test_single_selector(self) -> None:
211
+ """Test JS generation for a single selector (no shadow piercing)."""
212
+ from wavexis.backend.cdp import CDPBackend
213
+
214
+ js = CDPBackend._build_shadow_pierce_js(["#my-element"])
215
+ assert "document.querySelector" in js
216
+ assert "shadowRoot" not in js
217
+
218
+ def test_nested_selectors(self) -> None:
219
+ """Test JS generation for nested shadow piercing."""
220
+ from wavexis.backend.cdp import CDPBackend
221
+
222
+ js = CDPBackend._build_shadow_pierce_js(["my-component", "inner", "button"])
223
+ assert js.count("shadowRoot") == 4
224
+ assert "inner" in js
225
+ assert "button" in js
226
+
227
+ def test_selector_escaping(self) -> None:
228
+ """Test that single quotes in selectors are escaped."""
229
+ from wavexis.backend.cdp import CDPBackend
230
+
231
+ js = CDPBackend._build_shadow_pierce_js(["[data-test='foo']"])
232
+ assert "\\'" in js
@@ -1,3 +1,3 @@
1
1
  """wavexis — CLI de automatización de navegador."""
2
2
 
3
- __version__ = "2.3.2"
3
+ __version__ = "2.3.3"
@@ -323,6 +323,45 @@ class AbstractBackend(ABC):
323
323
  auto_wait: If True, wait for element to be visible before filling.
324
324
  """
325
325
 
326
+ # ── Shadow DOM ──────────────────────────────────────────
327
+
328
+ @abstractmethod
329
+ async def shadow_eval(
330
+ self, selectors: list[str], expression: str, await_promise: bool = False
331
+ ) -> Any:
332
+ """Evaluate a JavaScript expression inside a shadow DOM tree.
333
+
334
+ Args:
335
+ selectors: List of CSS selectors piercing shadow boundaries.
336
+ selectors[0] is in the main document, selectors[1] in
337
+ selectors[0].shadowRoot, and so on.
338
+ expression: JavaScript expression to evaluate in the shadow context.
339
+ await_promise: Whether to await a returned Promise.
340
+ """
341
+
342
+ @abstractmethod
343
+ async def shadow_click(
344
+ self, selectors: list[str], auto_wait: bool = True
345
+ ) -> None:
346
+ """Click an element inside a shadow DOM tree.
347
+
348
+ Args:
349
+ selectors: List of CSS selectors piercing shadow boundaries.
350
+ auto_wait: If True, wait for element to be visible before clicking.
351
+ """
352
+
353
+ @abstractmethod
354
+ async def shadow_fill(
355
+ self, selectors: list[str], value: str, auto_wait: bool = True
356
+ ) -> None:
357
+ """Fill an input element inside a shadow DOM tree.
358
+
359
+ Args:
360
+ selectors: List of CSS selectors piercing shadow boundaries.
361
+ value: Value to set in the input field.
362
+ auto_wait: If True, wait for element to be visible before filling.
363
+ """
364
+
326
365
  # ── Network advanced ───────────────────────────────────
327
366
 
328
367
  @abstractmethod
@@ -1128,6 +1128,135 @@ class BiDiBackend(AbstractBackend):
1128
1128
  if not result_value:
1129
1129
  raise ElementNotFoundError(selector)
1130
1130
 
1131
+ # ── Shadow DOM ──────────────────────────────────────────
1132
+
1133
+ @staticmethod
1134
+ def _build_shadow_pierce_js(selectors: list[str]) -> str:
1135
+ """Build JS that pierces shadow boundaries via a selector chain."""
1136
+ escaped = [s.replace("'", "\\'") for s in selectors]
1137
+ parts = [f"var el=document.querySelector('{escaped[0]}')"]
1138
+ for sel in escaped[1:]:
1139
+ parts.append(
1140
+ f"if(!el||!el.shadowRoot)return null;"
1141
+ f"el=el.shadowRoot.querySelector('{sel}')"
1142
+ )
1143
+ parts.append("return el")
1144
+ body = ";".join(parts)
1145
+ return f"(function(){{{body};}})()"
1146
+
1147
+ async def shadow_eval(
1148
+ self, selectors: list[str], expression: str, await_promise: bool = False
1149
+ ) -> Any:
1150
+ """Evaluate a JavaScript expression inside a shadow DOM tree.
1151
+
1152
+ Args:
1153
+ selectors: List of CSS selectors piercing shadow boundaries.
1154
+ expression: JavaScript expression to evaluate in the shadow context.
1155
+ await_promise: Whether to await a returned Promise.
1156
+
1157
+ Returns:
1158
+ The evaluation result value.
1159
+ """
1160
+ client = self._require_client()
1161
+ pierce_js = self._build_shadow_pierce_js(selectors)
1162
+ escaped_expr = expression.replace("\\", "\\\\").replace("'", "\\'")
1163
+ js = (
1164
+ f"(function(){{var el=({pierce_js});"
1165
+ f"if(!el)return null;"
1166
+ f"return (function(){{{escaped_expr}}}).call(el);}})()"
1167
+ )
1168
+ result = await client.script.evaluate(
1169
+ self._context, js, await_promise=await_promise
1170
+ )
1171
+ if hasattr(result, "value"):
1172
+ return result.value
1173
+ return result
1174
+
1175
+ async def _wait_for_element_in_shadow(
1176
+ self, selectors: list[str], timeout_ms: int = 30000
1177
+ ) -> None:
1178
+ """Wait for an element inside a shadow DOM tree to exist and be visible.
1179
+
1180
+ Args:
1181
+ selectors: List of CSS selectors piercing shadow boundaries.
1182
+ timeout_ms: Maximum wait time in milliseconds.
1183
+
1184
+ Raises:
1185
+ WaitTimeoutError: If the element is not found within the timeout.
1186
+ """
1187
+ import asyncio as _asyncio
1188
+ import time as _time
1189
+
1190
+ client = self._require_client()
1191
+ pierce_js = self._build_shadow_pierce_js(selectors)
1192
+ js = (
1193
+ f"(function(){{var el=({pierce_js});"
1194
+ f"if(!el)return false;"
1195
+ f"var rect=el.getBoundingClientRect();"
1196
+ f"return rect.width>0&&rect.height>0;}})()"
1197
+ )
1198
+ deadline = _time.monotonic() + timeout_ms / 1000
1199
+ while _time.monotonic() < deadline:
1200
+ result = await client.script.evaluate(self._context, js)
1201
+ value = getattr(result, "value", None)
1202
+ if value is True or value == "true":
1203
+ return
1204
+ await _asyncio.sleep(0.1)
1205
+ raise WaitTimeoutError("selector", timeout_ms)
1206
+
1207
+ async def shadow_click(
1208
+ self, selectors: list[str], auto_wait: bool = True
1209
+ ) -> None:
1210
+ """Click an element inside a shadow DOM tree.
1211
+
1212
+ Args:
1213
+ selectors: List of CSS selectors piercing shadow boundaries.
1214
+ auto_wait: If True, wait for element to be visible before clicking.
1215
+ """
1216
+ client = self._require_client()
1217
+ if auto_wait:
1218
+ await self._wait_for_element_in_shadow(selectors)
1219
+ pierce_js = self._build_shadow_pierce_js(selectors)
1220
+ js = (
1221
+ f"(function(){{var el=({pierce_js});"
1222
+ f"if(!el)return false;"
1223
+ f"el.scrollIntoView({{block:'center',behavior:'instant'}});"
1224
+ f"el.dispatchEvent(new MouseEvent('click',{{bubbles:true,composed:true}}));"
1225
+ f"return true;}})()"
1226
+ )
1227
+ result = await client.script.evaluate(self._context, js)
1228
+ result_value = getattr(result, "value", None)
1229
+ if not result_value:
1230
+ raise ElementNotFoundError(" -> ".join(selectors))
1231
+
1232
+ async def shadow_fill(
1233
+ self, selectors: list[str], value: str, auto_wait: bool = True
1234
+ ) -> None:
1235
+ """Fill an input element inside a shadow DOM tree.
1236
+
1237
+ Args:
1238
+ selectors: List of CSS selectors piercing shadow boundaries.
1239
+ value: Value to set in the input field.
1240
+ auto_wait: If True, wait for element to be visible before filling.
1241
+ """
1242
+ client = self._require_client()
1243
+ if auto_wait:
1244
+ await self._wait_for_element_in_shadow(selectors)
1245
+ pierce_js = self._build_shadow_pierce_js(selectors)
1246
+ escaped_val = value.replace("\\", "\\\\").replace("'", "\\'")
1247
+ js = (
1248
+ f"(function(){{var el=({pierce_js});"
1249
+ f"if(!el)return false;"
1250
+ f"el.focus();el.value='{escaped_val}';"
1251
+ f"el.dispatchEvent(new Event('input',{{bubbles:true,composed:true}}));"
1252
+ f"el.dispatchEvent(new Event('change',{{bubbles:true,composed:true}}));"
1253
+ f"return true;}})()"
1254
+ )
1255
+ result = await client.script.evaluate(self._context, js)
1256
+ result_value = getattr(result, "value", None)
1257
+ if not result_value:
1258
+ raise ElementNotFoundError(" -> ".join(selectors))
1259
+
1131
1260
  async def block_requests(self, patterns: list[str]) -> None:
1132
1261
  """Block requests matching URL patterns (partial BiDi support)."""
1133
1262
  if self._client is None:
@@ -1369,6 +1369,133 @@ class CDPBackend(AbstractBackend):
1369
1369
  if not result.get("result", {}).get("value"):
1370
1370
  raise ElementNotFoundError(selector)
1371
1371
 
1372
+ # ── Shadow DOM ──────────────────────────────────────────
1373
+
1374
+ @staticmethod
1375
+ def _build_shadow_pierce_js(selectors: list[str]) -> str:
1376
+ """Build JS that pierces shadow boundaries via a selector chain.
1377
+
1378
+ Args:
1379
+ selectors: List of CSS selectors. selectors[0] in document,
1380
+ each subsequent selector in the previous element's shadowRoot.
1381
+
1382
+ Returns:
1383
+ JavaScript IIFE that returns the final element or null.
1384
+ """
1385
+ escaped = [s.replace("'", "\\'") for s in selectors]
1386
+ parts = [f"var el=document.querySelector('{escaped[0]}')"]
1387
+ for sel in escaped[1:]:
1388
+ parts.append(
1389
+ f"if(!el||!el.shadowRoot)return null;"
1390
+ f"el=el.shadowRoot.querySelector('{sel}')"
1391
+ )
1392
+ parts.append("return el")
1393
+ body = ";".join(parts)
1394
+ return f"(function(){{{body};}})()"
1395
+
1396
+ async def shadow_eval(
1397
+ self, selectors: list[str], expression: str, await_promise: bool = False
1398
+ ) -> Any:
1399
+ """Evaluate a JavaScript expression inside a shadow DOM tree.
1400
+
1401
+ Args:
1402
+ selectors: List of CSS selectors piercing shadow boundaries.
1403
+ expression: JavaScript expression to evaluate in the shadow context.
1404
+ await_promise: Whether to await a returned Promise.
1405
+
1406
+ Returns:
1407
+ The evaluation result value.
1408
+ """
1409
+ session = self._require_session()
1410
+ pierce_js = self._build_shadow_pierce_js(selectors)
1411
+ escaped_expr = expression.replace("\\", "\\\\").replace("'", "\\'")
1412
+ js = (
1413
+ f"(function(){{var el=({pierce_js});"
1414
+ f"if(!el)return null;"
1415
+ f"return (function(){{{escaped_expr}}}).call(el);}})()"
1416
+ )
1417
+ result = await session.runtime.evaluate(js, await_promise=await_promise)
1418
+ return result.get("result", {}).get("value")
1419
+
1420
+ async def _wait_for_element_in_shadow(
1421
+ self, selectors: list[str], timeout_ms: int = 30000
1422
+ ) -> None:
1423
+ """Wait for an element inside a shadow DOM tree to exist and be visible.
1424
+
1425
+ Args:
1426
+ selectors: List of CSS selectors piercing shadow boundaries.
1427
+ timeout_ms: Maximum wait time in milliseconds.
1428
+
1429
+ Raises:
1430
+ WaitTimeoutError: If the element is not found within the timeout.
1431
+ """
1432
+ session = self._require_session()
1433
+ pierce_js = self._build_shadow_pierce_js(selectors)
1434
+ js = (
1435
+ f"(function(){{var el=({pierce_js});"
1436
+ f"if(!el)return false;"
1437
+ f"var rect=el.getBoundingClientRect();"
1438
+ f"return rect.width>0&&rect.height>0;}})()"
1439
+ )
1440
+ deadline = time.monotonic() + timeout_ms / 1000
1441
+ while time.monotonic() < deadline:
1442
+ result = await session.runtime.evaluate(js)
1443
+ if result.get("result", {}).get("value") is True:
1444
+ return
1445
+ await asyncio.sleep(0.1)
1446
+ raise WaitTimeoutError("selector", timeout_ms)
1447
+
1448
+ async def shadow_click(
1449
+ self, selectors: list[str], auto_wait: bool = True
1450
+ ) -> None:
1451
+ """Click an element inside a shadow DOM tree.
1452
+
1453
+ Args:
1454
+ selectors: List of CSS selectors piercing shadow boundaries.
1455
+ auto_wait: If True, wait for element to be visible before clicking.
1456
+ """
1457
+ session = self._require_session()
1458
+ if auto_wait:
1459
+ await self._wait_for_element_in_shadow(selectors)
1460
+ pierce_js = self._build_shadow_pierce_js(selectors)
1461
+ js = (
1462
+ f"(function(){{var el=({pierce_js});"
1463
+ f"if(!el)return false;"
1464
+ f"el.scrollIntoView({{block:'center',behavior:'instant'}});"
1465
+ f"el.dispatchEvent(new MouseEvent('click',{{bubbles:true,composed:true}}));"
1466
+ f"return true;}})()"
1467
+ )
1468
+ result = await session.runtime.evaluate(js)
1469
+ if not result.get("result", {}).get("value"):
1470
+ raise ElementNotFoundError(" -> ".join(selectors))
1471
+
1472
+ async def shadow_fill(
1473
+ self, selectors: list[str], value: str, auto_wait: bool = True
1474
+ ) -> None:
1475
+ """Fill an input element inside a shadow DOM tree.
1476
+
1477
+ Args:
1478
+ selectors: List of CSS selectors piercing shadow boundaries.
1479
+ value: Value to set in the input field.
1480
+ auto_wait: If True, wait for element to be visible before filling.
1481
+ """
1482
+ session = self._require_session()
1483
+ if auto_wait:
1484
+ await self._wait_for_element_in_shadow(selectors)
1485
+ pierce_js = self._build_shadow_pierce_js(selectors)
1486
+ escaped_val = value.replace("\\", "\\\\").replace("'", "\\'")
1487
+ js = (
1488
+ f"(function(){{var el=({pierce_js});"
1489
+ f"if(!el)return false;"
1490
+ f"el.focus();el.value='{escaped_val}';"
1491
+ f"el.dispatchEvent(new Event('input',{{bubbles:true,composed:true}}));"
1492
+ f"el.dispatchEvent(new Event('change',{{bubbles:true,composed:true}}));"
1493
+ f"return true;}})()"
1494
+ )
1495
+ result = await session.runtime.evaluate(js)
1496
+ if not result.get("result", {}).get("value"):
1497
+ raise ElementNotFoundError(" -> ".join(selectors))
1498
+
1372
1499
  # ── Network advanced ───────────────────────────────────
1373
1500
 
1374
1501
  async def block_requests(self, patterns: list[str]) -> None: