wavexis 2.3.1__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.1 → wavexis-2.3.3}/CHANGELOG.md +21 -0
  2. {wavexis-2.3.1 → wavexis-2.3.3}/PKG-INFO +1 -1
  3. {wavexis-2.3.1 → wavexis-2.3.3}/pyproject.toml +1 -1
  4. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_backend_manager.py +47 -3
  5. wavexis-2.3.3/tests/unit/test_iframe.py +203 -0
  6. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_manager.py +17 -3
  7. wavexis-2.3.3/tests/unit/test_shadow.py +232 -0
  8. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/__init__.py +1 -1
  9. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/backend/base.py +78 -0
  10. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/backend/bidi.py +260 -2
  11. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/backend/cdp.py +244 -0
  12. wavexis-2.3.3/wavexis/cli/_iframe.py +132 -0
  13. wavexis-2.3.3/wavexis/cli/_shadow.py +135 -0
  14. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/cli/app.py +2 -0
  15. {wavexis-2.3.1 → wavexis-2.3.3}/.dockerignore +0 -0
  16. {wavexis-2.3.1 → wavexis-2.3.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  17. {wavexis-2.3.1 → wavexis-2.3.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  18. {wavexis-2.3.1 → wavexis-2.3.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  19. {wavexis-2.3.1 → wavexis-2.3.3}/.github/workflows/ci.yml +0 -0
  20. {wavexis-2.3.1 → wavexis-2.3.3}/.github/workflows/docs.yml +0 -0
  21. {wavexis-2.3.1 → wavexis-2.3.3}/.github/workflows/release.yml +0 -0
  22. {wavexis-2.3.1 → wavexis-2.3.3}/.gitignore +0 -0
  23. {wavexis-2.3.1 → wavexis-2.3.3}/CODE_OF_CONDUCT.md +0 -0
  24. {wavexis-2.3.1 → wavexis-2.3.3}/CONTRIBUTING.md +0 -0
  25. {wavexis-2.3.1 → wavexis-2.3.3}/Dockerfile +0 -0
  26. {wavexis-2.3.1 → wavexis-2.3.3}/LICENSE +0 -0
  27. {wavexis-2.3.1 → wavexis-2.3.3}/README.md +0 -0
  28. {wavexis-2.3.1 → wavexis-2.3.3}/SECURITY.md +0 -0
  29. {wavexis-2.3.1 → wavexis-2.3.3}/docs/api/actions.md +0 -0
  30. {wavexis-2.3.1 → wavexis-2.3.3}/docs/api/backends.md +0 -0
  31. {wavexis-2.3.1 → wavexis-2.3.3}/docs/api/cli.md +0 -0
  32. {wavexis-2.3.1 → wavexis-2.3.3}/docs/api/config.md +0 -0
  33. {wavexis-2.3.1 → wavexis-2.3.3}/docs/api/exceptions.md +0 -0
  34. {wavexis-2.3.1 → wavexis-2.3.3}/docs/api/multi.md +0 -0
  35. {wavexis-2.3.1 → wavexis-2.3.3}/docs/api/plugins.md +0 -0
  36. {wavexis-2.3.1 → wavexis-2.3.3}/docs/api/serve.md +0 -0
  37. {wavexis-2.3.1 → wavexis-2.3.3}/docs/cookbook/auth.md +0 -0
  38. {wavexis-2.3.1 → wavexis-2.3.3}/docs/cookbook/ci-cd.md +0 -0
  39. {wavexis-2.3.1 → wavexis-2.3.3}/docs/cookbook/css.md +0 -0
  40. {wavexis-2.3.1 → wavexis-2.3.3}/docs/cookbook/debug.md +0 -0
  41. {wavexis-2.3.1 → wavexis-2.3.3}/docs/cookbook/record-replay.md +0 -0
  42. {wavexis-2.3.1 → wavexis-2.3.3}/docs/cookbook/scraping.md +0 -0
  43. {wavexis-2.3.1 → wavexis-2.3.3}/docs/cookbook/screenshots.md +0 -0
  44. {wavexis-2.3.1 → wavexis-2.3.3}/docs/cookbook/serve-mode.md +0 -0
  45. {wavexis-2.3.1 → wavexis-2.3.3}/docs/guide/assert.md +0 -0
  46. {wavexis-2.3.1 → wavexis-2.3.3}/docs/guide/backends.md +0 -0
  47. {wavexis-2.3.1 → wavexis-2.3.3}/docs/guide/commands.md +0 -0
  48. {wavexis-2.3.1 → wavexis-2.3.3}/docs/guide/init.md +0 -0
  49. {wavexis-2.3.1 → wavexis-2.3.3}/docs/guide/installation.md +0 -0
  50. {wavexis-2.3.1 → wavexis-2.3.3}/docs/guide/multi.md +0 -0
  51. {wavexis-2.3.1 → wavexis-2.3.3}/docs/guide/perf.md +0 -0
  52. {wavexis-2.3.1 → wavexis-2.3.3}/docs/guide/plugins.md +0 -0
  53. {wavexis-2.3.1 → wavexis-2.3.3}/docs/guide/quickstart.md +0 -0
  54. {wavexis-2.3.1 → wavexis-2.3.3}/docs/guide/raw.md +0 -0
  55. {wavexis-2.3.1 → wavexis-2.3.3}/docs/guide/repl.md +0 -0
  56. {wavexis-2.3.1 → wavexis-2.3.3}/docs/guide/troubleshooting.md +0 -0
  57. {wavexis-2.3.1 → wavexis-2.3.3}/docs/index.md +0 -0
  58. {wavexis-2.3.1 → wavexis-2.3.3}/mkdocs.yml +0 -0
  59. {wavexis-2.3.1 → wavexis-2.3.3}/stubs/bidiwave/__init__.pyi +0 -0
  60. {wavexis-2.3.1 → wavexis-2.3.3}/stubs/cdpwave/__init__.pyi +0 -0
  61. {wavexis-2.3.1 → wavexis-2.3.3}/tests/__init__.py +0 -0
  62. {wavexis-2.3.1 → wavexis-2.3.3}/tests/conftest.py +0 -0
  63. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/__init__.py +0 -0
  64. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_a11y.py +0 -0
  65. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_animation.py +0 -0
  66. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_backend_selection.py +0 -0
  67. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_browser.py +0 -0
  68. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_console.py +0 -0
  69. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_css.py +0 -0
  70. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_debug.py +0 -0
  71. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_dialog.py +0 -0
  72. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_dom.py +0 -0
  73. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_dom_snapshot.py +0 -0
  74. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_emulation.py +0 -0
  75. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_emulation_advanced.py +0 -0
  76. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_eval.py +0 -0
  77. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_har.py +0 -0
  78. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_input.py +0 -0
  79. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_media.py +0 -0
  80. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_multi.py +0 -0
  81. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_navigate.py +0 -0
  82. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_network.py +0 -0
  83. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_network_advanced.py +0 -0
  84. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_overlay.py +0 -0
  85. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_pdf.py +0 -0
  86. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_perf.py +0 -0
  87. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_permissions.py +0 -0
  88. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_raw.py +0 -0
  89. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_record.py +0 -0
  90. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_scrape.py +0 -0
  91. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_screenshot.py +0 -0
  92. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_security.py +0 -0
  93. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_serve.py +0 -0
  94. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_service_worker.py +0 -0
  95. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_storage.py +0 -0
  96. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_tabs.py +0 -0
  97. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_webaudio.py +0 -0
  98. {wavexis-2.3.1 → wavexis-2.3.3}/tests/integration/test_webauthn.py +0 -0
  99. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/__init__.py +0 -0
  100. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_abstract_backend_phase5.py +0 -0
  101. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_actions.py +0 -0
  102. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_actions_phase5.py +0 -0
  103. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_animation_action.py +0 -0
  104. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_assert_multi.py +0 -0
  105. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_auth.py +0 -0
  106. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_auto_wait.py +0 -0
  107. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_bidi_backend.py +0 -0
  108. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_bidi_phase5.py +0 -0
  109. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_bluetooth_action.py +0 -0
  110. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_cast_action.py +0 -0
  111. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_cleanup.py +0 -0
  112. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_cli_phase5.py +0 -0
  113. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_config.py +0 -0
  114. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_config_phase5.py +0 -0
  115. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_cookies_headers_actions.py +0 -0
  116. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_css_action.py +0 -0
  117. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_debug_action.py +0 -0
  118. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_dom_snapshot_action.py +0 -0
  119. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_download_screencast.py +0 -0
  120. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_emulation_action.py +0 -0
  121. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_error_handling.py +0 -0
  122. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_exceptions.py +0 -0
  123. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_format_record.py +0 -0
  124. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_global_options.py +0 -0
  125. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_init.py +0 -0
  126. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_input_action.py +0 -0
  127. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_interactions_crawl.py +0 -0
  128. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_media_action.py +0 -0
  129. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_multi.py +0 -0
  130. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_multi_improvements.py +0 -0
  131. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_new_features.py +0 -0
  132. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_output.py +0 -0
  133. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_overlay_action.py +0 -0
  134. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_perf_action.py +0 -0
  135. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_perf_console.py +0 -0
  136. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_plugins.py +0 -0
  137. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_raw.py +0 -0
  138. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_record.py +0 -0
  139. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_repl.py +0 -0
  140. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_serve.py +0 -0
  141. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_service_worker_action.py +0 -0
  142. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_storage_action.py +0 -0
  143. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_webaudio_action.py +0 -0
  144. {wavexis-2.3.1 → wavexis-2.3.3}/tests/unit/test_webauthn_action.py +0 -0
  145. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/__main__.py +0 -0
  146. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/__init__.py +0 -0
  147. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/accessibility.py +0 -0
  148. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/animation.py +0 -0
  149. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/base.py +0 -0
  150. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/bluetooth.py +0 -0
  151. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/browser.py +0 -0
  152. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/cast.py +0 -0
  153. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/console.py +0 -0
  154. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/cookies.py +0 -0
  155. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/crawl.py +0 -0
  156. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/css.py +0 -0
  157. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/debug.py +0 -0
  158. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/dialog.py +0 -0
  159. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/dom.py +0 -0
  160. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/dom_snapshot.py +0 -0
  161. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/download.py +0 -0
  162. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/emulation.py +0 -0
  163. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/eval.py +0 -0
  164. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/extract.py +0 -0
  165. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/form.py +0 -0
  166. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/har.py +0 -0
  167. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/headers.py +0 -0
  168. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/input.py +0 -0
  169. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/lighthouse.py +0 -0
  170. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/media.py +0 -0
  171. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/multi.py +0 -0
  172. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/navigate.py +0 -0
  173. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/network.py +0 -0
  174. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/overlay.py +0 -0
  175. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/pdf.py +0 -0
  176. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/performance.py +0 -0
  177. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/permissions.py +0 -0
  178. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/record.py +0 -0
  179. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/scrape.py +0 -0
  180. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/screencast.py +0 -0
  181. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/screenshot.py +0 -0
  182. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/security.py +0 -0
  183. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/service_worker.py +0 -0
  184. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/session.py +0 -0
  185. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/storage.py +0 -0
  186. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/tabs.py +0 -0
  187. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/wait.py +0 -0
  188. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/webaudio.py +0 -0
  189. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/webauthn.py +0 -0
  190. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/actions/websocket.py +0 -0
  191. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/auth.py +0 -0
  192. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/backend/__init__.py +0 -0
  193. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/backend/manager.py +0 -0
  194. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/cleanup.py +0 -0
  195. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/cli/__init__.py +0 -0
  196. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/cli/_advanced.py +0 -0
  197. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/cli/_capture.py +0 -0
  198. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/cli/_config.py +0 -0
  199. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/cli/_debug.py +0 -0
  200. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/cli/_emulation.py +0 -0
  201. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/cli/_experimental.py +0 -0
  202. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/cli/_input.py +0 -0
  203. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/cli/_navigation.py +0 -0
  204. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/cli/_network.py +0 -0
  205. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/cli/_perf.py +0 -0
  206. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/cli/_serve.py +0 -0
  207. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/cli/_session.py +0 -0
  208. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/cli/_shared.py +0 -0
  209. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/cli/_workflow.py +0 -0
  210. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/config.py +0 -0
  211. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/exceptions.py +0 -0
  212. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/init.py +0 -0
  213. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/multi.py +0 -0
  214. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/output.py +0 -0
  215. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/plugins.py +0 -0
  216. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/record.py +0 -0
  217. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/repl.py +0 -0
  218. {wavexis-2.3.1 → wavexis-2.3.3}/wavexis/serve.py +0 -0
@@ -2,6 +2,27 @@
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
+
16
+ ## v2.3.2 — 2026-07-07
17
+
18
+ ### Added
19
+
20
+ - iframe support: `iframe_click`, `iframe_fill`, `iframe_eval` in both CDP and BiDi backends
21
+ - `wavexis iframe click/fill/eval` CLI command with `--iframe`, `--selector`, `--value`, `--expression` options
22
+ - Auto-waiting inside iframes via `_wait_for_element_in_iframe` with polling and configurable timeout
23
+ - `--no-wait` flag to skip auto-waiting for iframe interactions
24
+ - Same-origin iframe support via `contentDocument` access
25
+
5
26
  ## v2.3.1 — 2026-07-07
6
27
 
7
28
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavexis
3
- Version: 2.3.1
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.1"
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" }
@@ -206,7 +206,13 @@ class DummyBackend(AbstractBackend):
206
206
  """Set dark mode."""
207
207
  pass
208
208
 
209
- async def click(self, selector: str, button: str = "left", click_count: int = 1) -> None:
209
+ async def click(
210
+ self,
211
+ selector: str,
212
+ button: str = "left",
213
+ click_count: int = 1,
214
+ auto_wait: bool = True,
215
+ ) -> None:
210
216
  """Click."""
211
217
  pass
212
218
 
@@ -214,7 +220,9 @@ class DummyBackend(AbstractBackend):
214
220
  """Type text."""
215
221
  pass
216
222
 
217
- async def fill(self, selector: str, value: str) -> None:
223
+ async def fill(
224
+ self, selector: str, value: str, auto_wait: bool = True
225
+ ) -> None:
218
226
  """Fill."""
219
227
  pass
220
228
 
@@ -222,7 +230,7 @@ class DummyBackend(AbstractBackend):
222
230
  """Select option."""
223
231
  pass
224
232
 
225
- async def hover(self, selector: str) -> None:
233
+ async def hover(self, selector: str, auto_wait: bool = True) -> None:
226
234
  """Hover."""
227
235
  pass
228
236
 
@@ -242,6 +250,42 @@ class DummyBackend(AbstractBackend):
242
250
  """Set files."""
243
251
  pass
244
252
 
253
+ async def iframe_eval(
254
+ self, iframe_selector: str, expression: str, await_promise: bool = False
255
+ ) -> object:
256
+ """Iframe eval."""
257
+ return None
258
+
259
+ async def iframe_click(
260
+ self, iframe_selector: str, selector: str, auto_wait: bool = True
261
+ ) -> None:
262
+ """Iframe click."""
263
+ pass
264
+
265
+ async def iframe_fill(
266
+ self, iframe_selector: str, selector: str, value: str, auto_wait: bool = True
267
+ ) -> None:
268
+ """Iframe fill."""
269
+ pass
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
+
245
289
  async def throttle_network(self, params: object) -> None:
246
290
  """Throttle network."""
247
291
  pass
@@ -0,0 +1,203 @@
1
+ """Unit tests for iframe support — iframe_eval, iframe_click, iframe_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 TestIframeEval:
43
+ """Tests for iframe_eval in both backends."""
44
+
45
+ def test_cdp_iframe_eval_returns_value(self) -> None:
46
+ """Test CDP iframe_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.iframe_eval("#myframe", "1 + 1", await_promise=False)
53
+ )
54
+ assert result == 42
55
+
56
+ def test_cdp_iframe_eval_null_iframe_not_found(self) -> None:
57
+ """Test CDP iframe_eval returns None when iframe 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.iframe_eval("#missing", "document.title")
64
+ )
65
+ assert result is None
66
+
67
+ def test_bidi_iframe_eval_returns_value(self) -> None:
68
+ """Test BiDi iframe_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.iframe_eval("#myframe", "document.title")
75
+ )
76
+ assert result == "hello"
77
+
78
+
79
+ @pytest.mark.unit
80
+ class TestIframeClick:
81
+ """Tests for iframe_click in both backends."""
82
+
83
+ def test_cdp_iframe_click_success(self) -> None:
84
+ """Test CDP iframe_click dispatches click inside iframe."""
85
+ backend = _make_cdp_backend()
86
+ backend._session.runtime.evaluate = AsyncMock(
87
+ return_value={"result": {"value": True}}
88
+ )
89
+ asyncio.run(backend.iframe_click("#myframe", "#btn", auto_wait=False))
90
+ backend._session.runtime.evaluate.assert_called()
91
+
92
+ def test_cdp_iframe_click_element_not_found(self) -> None:
93
+ """Test CDP iframe_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.iframe_click("#myframe", "#missing", auto_wait=False))
100
+
101
+ def test_bidi_iframe_click_success(self) -> None:
102
+ """Test BiDi iframe_click dispatches click inside iframe."""
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.iframe_click("#myframe", "#btn", auto_wait=False))
108
+ backend._client.script.evaluate.assert_called()
109
+
110
+ def test_bidi_iframe_click_element_not_found(self) -> None:
111
+ """Test BiDi iframe_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.iframe_click("#myframe", "#missing", auto_wait=False))
118
+
119
+
120
+ @pytest.mark.unit
121
+ class TestIframeFill:
122
+ """Tests for iframe_fill in both backends."""
123
+
124
+ def test_cdp_iframe_fill_success(self) -> None:
125
+ """Test CDP iframe_fill sets value inside iframe."""
126
+ backend = _make_cdp_backend()
127
+ backend._session.runtime.evaluate = AsyncMock(
128
+ return_value={"result": {"value": True}}
129
+ )
130
+ asyncio.run(
131
+ backend.iframe_fill("#myframe", "#input", "test value", auto_wait=False)
132
+ )
133
+ backend._session.runtime.evaluate.assert_called()
134
+
135
+ def test_cdp_iframe_fill_element_not_found(self) -> None:
136
+ """Test CDP iframe_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.iframe_fill("#myframe", "#missing", "val", auto_wait=False)
144
+ )
145
+
146
+ def test_bidi_iframe_fill_success(self) -> None:
147
+ """Test BiDi iframe_fill sets value inside iframe."""
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.iframe_fill("#myframe", "#input", "test value", auto_wait=False)
154
+ )
155
+ backend._client.script.evaluate.assert_called()
156
+
157
+ def test_bidi_iframe_fill_element_not_found(self) -> None:
158
+ """Test BiDi iframe_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.iframe_fill("#myframe", "#missing", "val", auto_wait=False)
166
+ )
167
+
168
+
169
+ @pytest.mark.unit
170
+ class TestIframeAutoWait:
171
+ """Tests for auto_wait in iframe operations."""
172
+
173
+ def test_cdp_iframe_click_auto_wait_false_skips_wait(self) -> None:
174
+ """Test that iframe_click with auto_wait=False skips _wait_for_element_in_iframe."""
175
+ backend = _make_cdp_backend()
176
+ call_order: list[str] = []
177
+
178
+ async def _mock_wait(iframe_sel: str, sel: str, timeout_ms: int = 30000) -> None:
179
+ call_order.append("wait")
180
+
181
+ backend._wait_for_element_in_iframe = _mock_wait # type: ignore[assignment]
182
+ backend._session.runtime.evaluate = AsyncMock(
183
+ return_value={"result": {"value": True}}
184
+ )
185
+
186
+ asyncio.run(backend.iframe_click("#myframe", "#btn", auto_wait=False))
187
+ assert "wait" not in call_order
188
+
189
+ def test_cdp_iframe_click_auto_wait_true_calls_wait(self) -> None:
190
+ """Test that iframe_click with auto_wait=True calls _wait_for_element_in_iframe."""
191
+ backend = _make_cdp_backend()
192
+ call_order: list[str] = []
193
+
194
+ async def _mock_wait(iframe_sel: str, sel: str, timeout_ms: int = 30000) -> None:
195
+ call_order.append("wait")
196
+
197
+ backend._wait_for_element_in_iframe = _mock_wait # type: ignore[assignment]
198
+ backend._session.runtime.evaluate = AsyncMock(
199
+ return_value={"result": {"value": True}}
200
+ )
201
+
202
+ asyncio.run(backend.iframe_click("#myframe", "#btn", auto_wait=True))
203
+ assert "wait" in call_order
@@ -102,15 +102,15 @@ class FakeBackend(AbstractBackend):
102
102
  """Set timezone."""
103
103
  async def set_dark_mode(self, enabled):
104
104
  """Set dark mode."""
105
- async def click(self, selector, button="left", click_count=1):
105
+ async def click(self, selector, button="left", click_count=1, auto_wait=True):
106
106
  """Click."""
107
107
  async def type_text(self, selector, text, delay=0):
108
108
  """Type text."""
109
- async def fill(self, selector, value):
109
+ async def fill(self, selector, value, auto_wait=True):
110
110
  """Fill."""
111
111
  async def select_option(self, selector, value):
112
112
  """Select option."""
113
- async def hover(self, selector):
113
+ async def hover(self, selector, auto_wait=True):
114
114
  """Hover."""
115
115
  async def key_press(self, key):
116
116
  """Key press."""
@@ -262,6 +262,20 @@ class FakeBackend(AbstractBackend):
262
262
  """Bluetooth stop."""
263
263
  async def set_files(self, selector, files):
264
264
  """Set files."""
265
+ async def iframe_eval(self, iframe_selector, expression, await_promise=False):
266
+ """Iframe eval."""
267
+ return None
268
+ async def iframe_click(self, iframe_selector, selector, auto_wait=True):
269
+ """Iframe click."""
270
+ async def iframe_fill(self, iframe_selector, selector, value, auto_wait=True):
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."""
265
279
 
266
280
 
267
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.1"
3
+ __version__ = "2.3.3"