wavexis 2.3.2__tar.gz → 2.3.4__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 (219) hide show
  1. {wavexis-2.3.2 → wavexis-2.3.4}/CHANGELOG.md +21 -0
  2. {wavexis-2.3.2 → wavexis-2.3.4}/PKG-INFO +1 -1
  3. {wavexis-2.3.2 → wavexis-2.3.4}/pyproject.toml +1 -1
  4. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_backend_manager.py +24 -0
  5. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_manager.py +10 -0
  6. wavexis-2.3.4/tests/unit/test_shadow.py +232 -0
  7. wavexis-2.3.4/tests/unit/test_suggest_locator.py +110 -0
  8. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/__init__.py +1 -1
  9. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/backend/base.py +56 -0
  10. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/backend/bidi.py +187 -0
  11. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/backend/cdp.py +185 -0
  12. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_capture.py +18 -2
  13. wavexis-2.3.4/wavexis/cli/_shadow.py +135 -0
  14. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/app.py +1 -0
  15. {wavexis-2.3.2 → wavexis-2.3.4}/.dockerignore +0 -0
  16. {wavexis-2.3.2 → wavexis-2.3.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  17. {wavexis-2.3.2 → wavexis-2.3.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  18. {wavexis-2.3.2 → wavexis-2.3.4}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  19. {wavexis-2.3.2 → wavexis-2.3.4}/.github/workflows/ci.yml +0 -0
  20. {wavexis-2.3.2 → wavexis-2.3.4}/.github/workflows/docs.yml +0 -0
  21. {wavexis-2.3.2 → wavexis-2.3.4}/.github/workflows/release.yml +0 -0
  22. {wavexis-2.3.2 → wavexis-2.3.4}/.gitignore +0 -0
  23. {wavexis-2.3.2 → wavexis-2.3.4}/CODE_OF_CONDUCT.md +0 -0
  24. {wavexis-2.3.2 → wavexis-2.3.4}/CONTRIBUTING.md +0 -0
  25. {wavexis-2.3.2 → wavexis-2.3.4}/Dockerfile +0 -0
  26. {wavexis-2.3.2 → wavexis-2.3.4}/LICENSE +0 -0
  27. {wavexis-2.3.2 → wavexis-2.3.4}/README.md +0 -0
  28. {wavexis-2.3.2 → wavexis-2.3.4}/SECURITY.md +0 -0
  29. {wavexis-2.3.2 → wavexis-2.3.4}/docs/api/actions.md +0 -0
  30. {wavexis-2.3.2 → wavexis-2.3.4}/docs/api/backends.md +0 -0
  31. {wavexis-2.3.2 → wavexis-2.3.4}/docs/api/cli.md +0 -0
  32. {wavexis-2.3.2 → wavexis-2.3.4}/docs/api/config.md +0 -0
  33. {wavexis-2.3.2 → wavexis-2.3.4}/docs/api/exceptions.md +0 -0
  34. {wavexis-2.3.2 → wavexis-2.3.4}/docs/api/multi.md +0 -0
  35. {wavexis-2.3.2 → wavexis-2.3.4}/docs/api/plugins.md +0 -0
  36. {wavexis-2.3.2 → wavexis-2.3.4}/docs/api/serve.md +0 -0
  37. {wavexis-2.3.2 → wavexis-2.3.4}/docs/cookbook/auth.md +0 -0
  38. {wavexis-2.3.2 → wavexis-2.3.4}/docs/cookbook/ci-cd.md +0 -0
  39. {wavexis-2.3.2 → wavexis-2.3.4}/docs/cookbook/css.md +0 -0
  40. {wavexis-2.3.2 → wavexis-2.3.4}/docs/cookbook/debug.md +0 -0
  41. {wavexis-2.3.2 → wavexis-2.3.4}/docs/cookbook/record-replay.md +0 -0
  42. {wavexis-2.3.2 → wavexis-2.3.4}/docs/cookbook/scraping.md +0 -0
  43. {wavexis-2.3.2 → wavexis-2.3.4}/docs/cookbook/screenshots.md +0 -0
  44. {wavexis-2.3.2 → wavexis-2.3.4}/docs/cookbook/serve-mode.md +0 -0
  45. {wavexis-2.3.2 → wavexis-2.3.4}/docs/guide/assert.md +0 -0
  46. {wavexis-2.3.2 → wavexis-2.3.4}/docs/guide/backends.md +0 -0
  47. {wavexis-2.3.2 → wavexis-2.3.4}/docs/guide/commands.md +0 -0
  48. {wavexis-2.3.2 → wavexis-2.3.4}/docs/guide/init.md +0 -0
  49. {wavexis-2.3.2 → wavexis-2.3.4}/docs/guide/installation.md +0 -0
  50. {wavexis-2.3.2 → wavexis-2.3.4}/docs/guide/multi.md +0 -0
  51. {wavexis-2.3.2 → wavexis-2.3.4}/docs/guide/perf.md +0 -0
  52. {wavexis-2.3.2 → wavexis-2.3.4}/docs/guide/plugins.md +0 -0
  53. {wavexis-2.3.2 → wavexis-2.3.4}/docs/guide/quickstart.md +0 -0
  54. {wavexis-2.3.2 → wavexis-2.3.4}/docs/guide/raw.md +0 -0
  55. {wavexis-2.3.2 → wavexis-2.3.4}/docs/guide/repl.md +0 -0
  56. {wavexis-2.3.2 → wavexis-2.3.4}/docs/guide/troubleshooting.md +0 -0
  57. {wavexis-2.3.2 → wavexis-2.3.4}/docs/index.md +0 -0
  58. {wavexis-2.3.2 → wavexis-2.3.4}/mkdocs.yml +0 -0
  59. {wavexis-2.3.2 → wavexis-2.3.4}/stubs/bidiwave/__init__.pyi +0 -0
  60. {wavexis-2.3.2 → wavexis-2.3.4}/stubs/cdpwave/__init__.pyi +0 -0
  61. {wavexis-2.3.2 → wavexis-2.3.4}/tests/__init__.py +0 -0
  62. {wavexis-2.3.2 → wavexis-2.3.4}/tests/conftest.py +0 -0
  63. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/__init__.py +0 -0
  64. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_a11y.py +0 -0
  65. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_animation.py +0 -0
  66. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_backend_selection.py +0 -0
  67. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_browser.py +0 -0
  68. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_console.py +0 -0
  69. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_css.py +0 -0
  70. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_debug.py +0 -0
  71. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_dialog.py +0 -0
  72. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_dom.py +0 -0
  73. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_dom_snapshot.py +0 -0
  74. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_emulation.py +0 -0
  75. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_emulation_advanced.py +0 -0
  76. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_eval.py +0 -0
  77. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_har.py +0 -0
  78. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_input.py +0 -0
  79. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_media.py +0 -0
  80. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_multi.py +0 -0
  81. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_navigate.py +0 -0
  82. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_network.py +0 -0
  83. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_network_advanced.py +0 -0
  84. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_overlay.py +0 -0
  85. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_pdf.py +0 -0
  86. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_perf.py +0 -0
  87. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_permissions.py +0 -0
  88. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_raw.py +0 -0
  89. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_record.py +0 -0
  90. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_scrape.py +0 -0
  91. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_screenshot.py +0 -0
  92. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_security.py +0 -0
  93. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_serve.py +0 -0
  94. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_service_worker.py +0 -0
  95. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_storage.py +0 -0
  96. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_tabs.py +0 -0
  97. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_webaudio.py +0 -0
  98. {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_webauthn.py +0 -0
  99. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/__init__.py +0 -0
  100. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_abstract_backend_phase5.py +0 -0
  101. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_actions.py +0 -0
  102. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_actions_phase5.py +0 -0
  103. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_animation_action.py +0 -0
  104. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_assert_multi.py +0 -0
  105. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_auth.py +0 -0
  106. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_auto_wait.py +0 -0
  107. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_bidi_backend.py +0 -0
  108. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_bidi_phase5.py +0 -0
  109. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_bluetooth_action.py +0 -0
  110. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_cast_action.py +0 -0
  111. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_cleanup.py +0 -0
  112. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_cli_phase5.py +0 -0
  113. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_config.py +0 -0
  114. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_config_phase5.py +0 -0
  115. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_cookies_headers_actions.py +0 -0
  116. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_css_action.py +0 -0
  117. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_debug_action.py +0 -0
  118. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_dom_snapshot_action.py +0 -0
  119. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_download_screencast.py +0 -0
  120. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_emulation_action.py +0 -0
  121. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_error_handling.py +0 -0
  122. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_exceptions.py +0 -0
  123. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_format_record.py +0 -0
  124. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_global_options.py +0 -0
  125. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_iframe.py +0 -0
  126. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_init.py +0 -0
  127. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_input_action.py +0 -0
  128. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_interactions_crawl.py +0 -0
  129. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_media_action.py +0 -0
  130. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_multi.py +0 -0
  131. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_multi_improvements.py +0 -0
  132. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_new_features.py +0 -0
  133. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_output.py +0 -0
  134. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_overlay_action.py +0 -0
  135. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_perf_action.py +0 -0
  136. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_perf_console.py +0 -0
  137. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_plugins.py +0 -0
  138. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_raw.py +0 -0
  139. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_record.py +0 -0
  140. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_repl.py +0 -0
  141. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_serve.py +0 -0
  142. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_service_worker_action.py +0 -0
  143. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_storage_action.py +0 -0
  144. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_webaudio_action.py +0 -0
  145. {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_webauthn_action.py +0 -0
  146. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/__main__.py +0 -0
  147. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/__init__.py +0 -0
  148. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/accessibility.py +0 -0
  149. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/animation.py +0 -0
  150. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/base.py +0 -0
  151. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/bluetooth.py +0 -0
  152. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/browser.py +0 -0
  153. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/cast.py +0 -0
  154. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/console.py +0 -0
  155. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/cookies.py +0 -0
  156. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/crawl.py +0 -0
  157. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/css.py +0 -0
  158. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/debug.py +0 -0
  159. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/dialog.py +0 -0
  160. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/dom.py +0 -0
  161. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/dom_snapshot.py +0 -0
  162. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/download.py +0 -0
  163. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/emulation.py +0 -0
  164. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/eval.py +0 -0
  165. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/extract.py +0 -0
  166. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/form.py +0 -0
  167. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/har.py +0 -0
  168. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/headers.py +0 -0
  169. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/input.py +0 -0
  170. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/lighthouse.py +0 -0
  171. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/media.py +0 -0
  172. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/multi.py +0 -0
  173. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/navigate.py +0 -0
  174. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/network.py +0 -0
  175. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/overlay.py +0 -0
  176. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/pdf.py +0 -0
  177. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/performance.py +0 -0
  178. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/permissions.py +0 -0
  179. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/record.py +0 -0
  180. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/scrape.py +0 -0
  181. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/screencast.py +0 -0
  182. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/screenshot.py +0 -0
  183. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/security.py +0 -0
  184. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/service_worker.py +0 -0
  185. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/session.py +0 -0
  186. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/storage.py +0 -0
  187. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/tabs.py +0 -0
  188. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/wait.py +0 -0
  189. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/webaudio.py +0 -0
  190. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/webauthn.py +0 -0
  191. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/websocket.py +0 -0
  192. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/auth.py +0 -0
  193. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/backend/__init__.py +0 -0
  194. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/backend/manager.py +0 -0
  195. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cleanup.py +0 -0
  196. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/__init__.py +0 -0
  197. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_advanced.py +0 -0
  198. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_config.py +0 -0
  199. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_debug.py +0 -0
  200. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_emulation.py +0 -0
  201. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_experimental.py +0 -0
  202. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_iframe.py +0 -0
  203. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_input.py +0 -0
  204. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_navigation.py +0 -0
  205. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_network.py +0 -0
  206. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_perf.py +0 -0
  207. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_serve.py +0 -0
  208. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_session.py +0 -0
  209. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_shared.py +0 -0
  210. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_workflow.py +0 -0
  211. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/config.py +0 -0
  212. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/exceptions.py +0 -0
  213. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/init.py +0 -0
  214. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/multi.py +0 -0
  215. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/output.py +0 -0
  216. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/plugins.py +0 -0
  217. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/record.py +0 -0
  218. {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/repl.py +0 -0
  219. {wavexis-2.3.2 → wavexis-2.3.4}/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.4 — 2026-07-07
6
+
7
+ ### Added
8
+
9
+ - Generate locator: `suggest_locator` method in both CDP and BiDi backends
10
+ - `wavexis dom <url> --action suggest_locator -s <selector>` CLI command
11
+ - `--all` flag to return all suggestions ranked by specificity
12
+ - Priority order: `#id` > `[data-testid]` > `[aria-label]` > `[role]` > `:has-text()` > `tag.classes` > `parent > tag` > `:nth-of-type()` > `tag`
13
+ - Uses `CSS.escape()` for safe selector generation
14
+
15
+ ## v2.3.3 — 2026-07-07
16
+
17
+ ### Added
18
+
19
+ - Shadow DOM support: `shadow_click`, `shadow_fill`, `shadow_eval` in both CDP and BiDi backends
20
+ - `wavexis shadow click/fill/eval` CLI command with `--selectors` (comma-separated piercing chain)
21
+ - `_build_shadow_pierce_js` helper generates JS that traverses `shadowRoot` boundaries
22
+ - Auto-waiting inside shadow DOM via `_wait_for_element_in_shadow` with polling and configurable timeout
23
+ - `--no-wait` flag to skip auto-waiting for shadow interactions
24
+ - Events dispatched with `composed: true` to cross shadow boundaries
25
+
5
26
  ## v2.3.2 — 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.2
3
+ Version: 2.3.4
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.4"
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" }
@@ -132,6 +132,12 @@ class DummyBackend(AbstractBackend):
132
132
  """Dom scroll."""
133
133
  pass
134
134
 
135
+ async def suggest_locator(
136
+ self, selector: str, all: bool = False
137
+ ) -> list[str] | str:
138
+ """Suggest locator."""
139
+ return selector
140
+
135
141
  async def capture_har(self, params: object) -> dict:
136
142
  """Capture har."""
137
143
  return {}
@@ -268,6 +274,24 @@ class DummyBackend(AbstractBackend):
268
274
  """Iframe fill."""
269
275
  pass
270
276
 
277
+ async def shadow_eval(
278
+ self, selectors: list[str], expression: str, await_promise: bool = False
279
+ ) -> object:
280
+ """Shadow eval."""
281
+ return None
282
+
283
+ async def shadow_click(
284
+ self, selectors: list[str], auto_wait: bool = True
285
+ ) -> None:
286
+ """Shadow click."""
287
+ pass
288
+
289
+ async def shadow_fill(
290
+ self, selectors: list[str], value: str, auto_wait: bool = True
291
+ ) -> None:
292
+ """Shadow fill."""
293
+ pass
294
+
271
295
  async def throttle_network(self, params: object) -> None:
272
296
  """Throttle network."""
273
297
  pass
@@ -66,6 +66,9 @@ class FakeBackend(AbstractBackend):
66
66
  """Dom focus."""
67
67
  async def dom_scroll(self, selector=None, x=0, y=0):
68
68
  """Dom scroll."""
69
+ async def suggest_locator(self, selector, all=False):
70
+ """Suggest locator."""
71
+ return selector
69
72
  async def capture_har(self, params):
70
73
  """Capture har."""
71
74
  async def get_cookies(self):
@@ -269,6 +272,13 @@ class FakeBackend(AbstractBackend):
269
272
  """Iframe click."""
270
273
  async def iframe_fill(self, iframe_selector, selector, value, auto_wait=True):
271
274
  """Iframe fill."""
275
+ async def shadow_eval(self, selectors, expression, await_promise=False):
276
+ """Shadow eval."""
277
+ return None
278
+ async def shadow_click(self, selectors, auto_wait=True):
279
+ """Shadow click."""
280
+ async def shadow_fill(self, selectors, value, auto_wait=True):
281
+ """Shadow fill."""
272
282
 
273
283
 
274
284
  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
@@ -0,0 +1,110 @@
1
+ """Unit tests for suggest_locator in both backends."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ from typing import Any
8
+ from unittest.mock import AsyncMock, MagicMock
9
+
10
+ import pytest
11
+
12
+ from wavexis.exceptions import ElementNotFoundError
13
+
14
+
15
+ def _make_cdp_backend() -> Any:
16
+ """Create a CDPBackend with mocked session."""
17
+ from wavexis.backend.cdp import CDPBackend
18
+
19
+ backend = CDPBackend()
20
+ backend._session = MagicMock()
21
+ backend._session.runtime = MagicMock()
22
+ backend._session.runtime.evaluate = AsyncMock(
23
+ return_value={"result": {"value": None}}
24
+ )
25
+ return backend
26
+
27
+
28
+ def _make_bidi_backend() -> Any:
29
+ """Create a BiDiBackend with mocked client."""
30
+ from wavexis.backend.bidi import BiDiBackend
31
+
32
+ backend = BiDiBackend()
33
+ backend._client = MagicMock()
34
+ backend._context = MagicMock()
35
+ backend._client.script = MagicMock()
36
+ result = MagicMock()
37
+ result.value = None
38
+ backend._client.script.evaluate = AsyncMock(return_value=result)
39
+ return backend
40
+
41
+
42
+ @pytest.mark.unit
43
+ class TestSuggestLocatorCDP:
44
+ """Tests for suggest_locator in CDP backend."""
45
+
46
+ def test_returns_best_selector(self) -> None:
47
+ """Test that suggest_locator returns the first (best) suggestion."""
48
+ backend = _make_cdp_backend()
49
+ suggestions = ["#login-btn", "button.primary", "button"]
50
+ backend._session.runtime.evaluate = AsyncMock(
51
+ return_value={"result": {"value": json.dumps(suggestions)}}
52
+ )
53
+ result = asyncio.run(backend.suggest_locator("#login-btn"))
54
+ assert result == "#login-btn"
55
+
56
+ def test_returns_all_suggestions(self) -> None:
57
+ """Test that suggest_locator with all=True returns all suggestions."""
58
+ backend = _make_cdp_backend()
59
+ suggestions = ["#my-id", "[data-testid='btn']", "button.clickable", "button"]
60
+ backend._session.runtime.evaluate = AsyncMock(
61
+ return_value={"result": {"value": json.dumps(suggestions)}}
62
+ )
63
+ result = asyncio.run(backend.suggest_locator("button", all=True))
64
+ assert isinstance(result, list)
65
+ assert len(result) == 4
66
+ assert result[0] == "#my-id"
67
+
68
+ def test_raises_on_element_not_found(self) -> None:
69
+ """Test that suggest_locator raises ElementNotFoundError when element missing."""
70
+ backend = _make_cdp_backend()
71
+ backend._session.runtime.evaluate = AsyncMock(
72
+ return_value={"result": {"value": None}}
73
+ )
74
+ with pytest.raises(ElementNotFoundError):
75
+ asyncio.run(backend.suggest_locator("#missing"))
76
+
77
+
78
+ @pytest.mark.unit
79
+ class TestSuggestLocatorBiDi:
80
+ """Tests for suggest_locator in BiDi backend."""
81
+
82
+ def test_returns_best_selector(self) -> None:
83
+ """Test that suggest_locator returns the first (best) suggestion."""
84
+ backend = _make_bidi_backend()
85
+ suggestions = ["#submit", "button[type='submit']", "button"]
86
+ result_mock = MagicMock()
87
+ result_mock.value = json.dumps(suggestions)
88
+ backend._client.script.evaluate = AsyncMock(return_value=result_mock)
89
+ result = asyncio.run(backend.suggest_locator("#submit"))
90
+ assert result == "#submit"
91
+
92
+ def test_returns_all_suggestions(self) -> None:
93
+ """Test that suggest_locator with all=True returns all suggestions."""
94
+ backend = _make_bidi_backend()
95
+ suggestions = ["#email", "[data-testid='email']", "input.email", "input"]
96
+ result_mock = MagicMock()
97
+ result_mock.value = json.dumps(suggestions)
98
+ backend._client.script.evaluate = AsyncMock(return_value=result_mock)
99
+ result = asyncio.run(backend.suggest_locator("input", all=True))
100
+ assert isinstance(result, list)
101
+ assert len(result) == 4
102
+
103
+ def test_raises_on_element_not_found(self) -> None:
104
+ """Test that suggest_locator raises ElementNotFoundError when element missing."""
105
+ backend = _make_bidi_backend()
106
+ result_mock = MagicMock()
107
+ result_mock.value = None
108
+ backend._client.script.evaluate = AsyncMock(return_value=result_mock)
109
+ with pytest.raises(ElementNotFoundError):
110
+ asyncio.run(backend.suggest_locator("#missing"))
@@ -1,3 +1,3 @@
1
1
  """wavexis — CLI de automatización de navegador."""
2
2
 
3
- __version__ = "2.3.2"
3
+ __version__ = "2.3.4"
@@ -149,6 +149,23 @@ class AbstractBackend(ABC):
149
149
  ) -> None:
150
150
  """Scroll to an element or by offset."""
151
151
 
152
+ @abstractmethod
153
+ async def suggest_locator(
154
+ self, selector: str, all: bool = False
155
+ ) -> list[str] | str:
156
+ """Suggest the best CSS selector for an element.
157
+
158
+ Analyzes the element and generates optimal selectors in priority order:
159
+ id > data-testid > aria-label > text > tag.classes > nth-of-type chain.
160
+
161
+ Args:
162
+ selector: CSS selector for the target element.
163
+ all: If True, return multiple suggestions; otherwise just the best one.
164
+
165
+ Returns:
166
+ List of selector strings when all=True, single best selector when all=False.
167
+ """
168
+
152
169
  # ── Network ────────────────────────────────────────────
153
170
 
154
171
  @abstractmethod
@@ -323,6 +340,45 @@ class AbstractBackend(ABC):
323
340
  auto_wait: If True, wait for element to be visible before filling.
324
341
  """
325
342
 
343
+ # ── Shadow DOM ──────────────────────────────────────────
344
+
345
+ @abstractmethod
346
+ async def shadow_eval(
347
+ self, selectors: list[str], expression: str, await_promise: bool = False
348
+ ) -> Any:
349
+ """Evaluate a JavaScript expression inside a shadow DOM tree.
350
+
351
+ Args:
352
+ selectors: List of CSS selectors piercing shadow boundaries.
353
+ selectors[0] is in the main document, selectors[1] in
354
+ selectors[0].shadowRoot, and so on.
355
+ expression: JavaScript expression to evaluate in the shadow context.
356
+ await_promise: Whether to await a returned Promise.
357
+ """
358
+
359
+ @abstractmethod
360
+ async def shadow_click(
361
+ self, selectors: list[str], auto_wait: bool = True
362
+ ) -> None:
363
+ """Click an element inside a shadow DOM tree.
364
+
365
+ Args:
366
+ selectors: List of CSS selectors piercing shadow boundaries.
367
+ auto_wait: If True, wait for element to be visible before clicking.
368
+ """
369
+
370
+ @abstractmethod
371
+ async def shadow_fill(
372
+ self, selectors: list[str], value: str, auto_wait: bool = True
373
+ ) -> None:
374
+ """Fill an input element inside a shadow DOM tree.
375
+
376
+ Args:
377
+ selectors: List of CSS selectors piercing shadow boundaries.
378
+ value: Value to set in the input field.
379
+ auto_wait: If True, wait for element to be visible before filling.
380
+ """
381
+
326
382
  # ── Network advanced ───────────────────────────────────
327
383
 
328
384
  @abstractmethod