wavexis 2.3.3__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.3 → wavexis-2.3.4}/CHANGELOG.md +10 -0
  2. {wavexis-2.3.3 → wavexis-2.3.4}/PKG-INFO +1 -1
  3. {wavexis-2.3.3 → wavexis-2.3.4}/pyproject.toml +1 -1
  4. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_backend_manager.py +6 -0
  5. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_manager.py +3 -0
  6. wavexis-2.3.4/tests/unit/test_suggest_locator.py +110 -0
  7. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/__init__.py +1 -1
  8. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/backend/base.py +17 -0
  9. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/backend/bidi.py +58 -0
  10. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/backend/cdp.py +58 -0
  11. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_capture.py +18 -2
  12. {wavexis-2.3.3 → wavexis-2.3.4}/.dockerignore +0 -0
  13. {wavexis-2.3.3 → wavexis-2.3.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  14. {wavexis-2.3.3 → wavexis-2.3.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  15. {wavexis-2.3.3 → wavexis-2.3.4}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  16. {wavexis-2.3.3 → wavexis-2.3.4}/.github/workflows/ci.yml +0 -0
  17. {wavexis-2.3.3 → wavexis-2.3.4}/.github/workflows/docs.yml +0 -0
  18. {wavexis-2.3.3 → wavexis-2.3.4}/.github/workflows/release.yml +0 -0
  19. {wavexis-2.3.3 → wavexis-2.3.4}/.gitignore +0 -0
  20. {wavexis-2.3.3 → wavexis-2.3.4}/CODE_OF_CONDUCT.md +0 -0
  21. {wavexis-2.3.3 → wavexis-2.3.4}/CONTRIBUTING.md +0 -0
  22. {wavexis-2.3.3 → wavexis-2.3.4}/Dockerfile +0 -0
  23. {wavexis-2.3.3 → wavexis-2.3.4}/LICENSE +0 -0
  24. {wavexis-2.3.3 → wavexis-2.3.4}/README.md +0 -0
  25. {wavexis-2.3.3 → wavexis-2.3.4}/SECURITY.md +0 -0
  26. {wavexis-2.3.3 → wavexis-2.3.4}/docs/api/actions.md +0 -0
  27. {wavexis-2.3.3 → wavexis-2.3.4}/docs/api/backends.md +0 -0
  28. {wavexis-2.3.3 → wavexis-2.3.4}/docs/api/cli.md +0 -0
  29. {wavexis-2.3.3 → wavexis-2.3.4}/docs/api/config.md +0 -0
  30. {wavexis-2.3.3 → wavexis-2.3.4}/docs/api/exceptions.md +0 -0
  31. {wavexis-2.3.3 → wavexis-2.3.4}/docs/api/multi.md +0 -0
  32. {wavexis-2.3.3 → wavexis-2.3.4}/docs/api/plugins.md +0 -0
  33. {wavexis-2.3.3 → wavexis-2.3.4}/docs/api/serve.md +0 -0
  34. {wavexis-2.3.3 → wavexis-2.3.4}/docs/cookbook/auth.md +0 -0
  35. {wavexis-2.3.3 → wavexis-2.3.4}/docs/cookbook/ci-cd.md +0 -0
  36. {wavexis-2.3.3 → wavexis-2.3.4}/docs/cookbook/css.md +0 -0
  37. {wavexis-2.3.3 → wavexis-2.3.4}/docs/cookbook/debug.md +0 -0
  38. {wavexis-2.3.3 → wavexis-2.3.4}/docs/cookbook/record-replay.md +0 -0
  39. {wavexis-2.3.3 → wavexis-2.3.4}/docs/cookbook/scraping.md +0 -0
  40. {wavexis-2.3.3 → wavexis-2.3.4}/docs/cookbook/screenshots.md +0 -0
  41. {wavexis-2.3.3 → wavexis-2.3.4}/docs/cookbook/serve-mode.md +0 -0
  42. {wavexis-2.3.3 → wavexis-2.3.4}/docs/guide/assert.md +0 -0
  43. {wavexis-2.3.3 → wavexis-2.3.4}/docs/guide/backends.md +0 -0
  44. {wavexis-2.3.3 → wavexis-2.3.4}/docs/guide/commands.md +0 -0
  45. {wavexis-2.3.3 → wavexis-2.3.4}/docs/guide/init.md +0 -0
  46. {wavexis-2.3.3 → wavexis-2.3.4}/docs/guide/installation.md +0 -0
  47. {wavexis-2.3.3 → wavexis-2.3.4}/docs/guide/multi.md +0 -0
  48. {wavexis-2.3.3 → wavexis-2.3.4}/docs/guide/perf.md +0 -0
  49. {wavexis-2.3.3 → wavexis-2.3.4}/docs/guide/plugins.md +0 -0
  50. {wavexis-2.3.3 → wavexis-2.3.4}/docs/guide/quickstart.md +0 -0
  51. {wavexis-2.3.3 → wavexis-2.3.4}/docs/guide/raw.md +0 -0
  52. {wavexis-2.3.3 → wavexis-2.3.4}/docs/guide/repl.md +0 -0
  53. {wavexis-2.3.3 → wavexis-2.3.4}/docs/guide/troubleshooting.md +0 -0
  54. {wavexis-2.3.3 → wavexis-2.3.4}/docs/index.md +0 -0
  55. {wavexis-2.3.3 → wavexis-2.3.4}/mkdocs.yml +0 -0
  56. {wavexis-2.3.3 → wavexis-2.3.4}/stubs/bidiwave/__init__.pyi +0 -0
  57. {wavexis-2.3.3 → wavexis-2.3.4}/stubs/cdpwave/__init__.pyi +0 -0
  58. {wavexis-2.3.3 → wavexis-2.3.4}/tests/__init__.py +0 -0
  59. {wavexis-2.3.3 → wavexis-2.3.4}/tests/conftest.py +0 -0
  60. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/__init__.py +0 -0
  61. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_a11y.py +0 -0
  62. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_animation.py +0 -0
  63. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_backend_selection.py +0 -0
  64. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_browser.py +0 -0
  65. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_console.py +0 -0
  66. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_css.py +0 -0
  67. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_debug.py +0 -0
  68. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_dialog.py +0 -0
  69. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_dom.py +0 -0
  70. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_dom_snapshot.py +0 -0
  71. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_emulation.py +0 -0
  72. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_emulation_advanced.py +0 -0
  73. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_eval.py +0 -0
  74. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_har.py +0 -0
  75. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_input.py +0 -0
  76. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_media.py +0 -0
  77. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_multi.py +0 -0
  78. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_navigate.py +0 -0
  79. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_network.py +0 -0
  80. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_network_advanced.py +0 -0
  81. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_overlay.py +0 -0
  82. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_pdf.py +0 -0
  83. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_perf.py +0 -0
  84. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_permissions.py +0 -0
  85. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_raw.py +0 -0
  86. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_record.py +0 -0
  87. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_scrape.py +0 -0
  88. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_screenshot.py +0 -0
  89. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_security.py +0 -0
  90. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_serve.py +0 -0
  91. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_service_worker.py +0 -0
  92. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_storage.py +0 -0
  93. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_tabs.py +0 -0
  94. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_webaudio.py +0 -0
  95. {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_webauthn.py +0 -0
  96. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/__init__.py +0 -0
  97. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_abstract_backend_phase5.py +0 -0
  98. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_actions.py +0 -0
  99. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_actions_phase5.py +0 -0
  100. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_animation_action.py +0 -0
  101. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_assert_multi.py +0 -0
  102. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_auth.py +0 -0
  103. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_auto_wait.py +0 -0
  104. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_bidi_backend.py +0 -0
  105. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_bidi_phase5.py +0 -0
  106. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_bluetooth_action.py +0 -0
  107. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_cast_action.py +0 -0
  108. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_cleanup.py +0 -0
  109. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_cli_phase5.py +0 -0
  110. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_config.py +0 -0
  111. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_config_phase5.py +0 -0
  112. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_cookies_headers_actions.py +0 -0
  113. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_css_action.py +0 -0
  114. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_debug_action.py +0 -0
  115. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_dom_snapshot_action.py +0 -0
  116. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_download_screencast.py +0 -0
  117. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_emulation_action.py +0 -0
  118. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_error_handling.py +0 -0
  119. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_exceptions.py +0 -0
  120. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_format_record.py +0 -0
  121. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_global_options.py +0 -0
  122. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_iframe.py +0 -0
  123. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_init.py +0 -0
  124. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_input_action.py +0 -0
  125. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_interactions_crawl.py +0 -0
  126. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_media_action.py +0 -0
  127. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_multi.py +0 -0
  128. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_multi_improvements.py +0 -0
  129. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_new_features.py +0 -0
  130. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_output.py +0 -0
  131. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_overlay_action.py +0 -0
  132. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_perf_action.py +0 -0
  133. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_perf_console.py +0 -0
  134. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_plugins.py +0 -0
  135. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_raw.py +0 -0
  136. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_record.py +0 -0
  137. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_repl.py +0 -0
  138. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_serve.py +0 -0
  139. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_service_worker_action.py +0 -0
  140. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_shadow.py +0 -0
  141. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_storage_action.py +0 -0
  142. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_webaudio_action.py +0 -0
  143. {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_webauthn_action.py +0 -0
  144. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/__main__.py +0 -0
  145. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/__init__.py +0 -0
  146. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/accessibility.py +0 -0
  147. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/animation.py +0 -0
  148. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/base.py +0 -0
  149. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/bluetooth.py +0 -0
  150. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/browser.py +0 -0
  151. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/cast.py +0 -0
  152. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/console.py +0 -0
  153. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/cookies.py +0 -0
  154. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/crawl.py +0 -0
  155. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/css.py +0 -0
  156. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/debug.py +0 -0
  157. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/dialog.py +0 -0
  158. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/dom.py +0 -0
  159. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/dom_snapshot.py +0 -0
  160. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/download.py +0 -0
  161. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/emulation.py +0 -0
  162. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/eval.py +0 -0
  163. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/extract.py +0 -0
  164. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/form.py +0 -0
  165. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/har.py +0 -0
  166. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/headers.py +0 -0
  167. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/input.py +0 -0
  168. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/lighthouse.py +0 -0
  169. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/media.py +0 -0
  170. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/multi.py +0 -0
  171. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/navigate.py +0 -0
  172. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/network.py +0 -0
  173. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/overlay.py +0 -0
  174. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/pdf.py +0 -0
  175. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/performance.py +0 -0
  176. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/permissions.py +0 -0
  177. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/record.py +0 -0
  178. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/scrape.py +0 -0
  179. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/screencast.py +0 -0
  180. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/screenshot.py +0 -0
  181. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/security.py +0 -0
  182. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/service_worker.py +0 -0
  183. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/session.py +0 -0
  184. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/storage.py +0 -0
  185. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/tabs.py +0 -0
  186. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/wait.py +0 -0
  187. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/webaudio.py +0 -0
  188. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/webauthn.py +0 -0
  189. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/websocket.py +0 -0
  190. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/auth.py +0 -0
  191. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/backend/__init__.py +0 -0
  192. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/backend/manager.py +0 -0
  193. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cleanup.py +0 -0
  194. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/__init__.py +0 -0
  195. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_advanced.py +0 -0
  196. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_config.py +0 -0
  197. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_debug.py +0 -0
  198. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_emulation.py +0 -0
  199. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_experimental.py +0 -0
  200. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_iframe.py +0 -0
  201. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_input.py +0 -0
  202. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_navigation.py +0 -0
  203. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_network.py +0 -0
  204. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_perf.py +0 -0
  205. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_serve.py +0 -0
  206. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_session.py +0 -0
  207. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_shadow.py +0 -0
  208. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_shared.py +0 -0
  209. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_workflow.py +0 -0
  210. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/app.py +0 -0
  211. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/config.py +0 -0
  212. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/exceptions.py +0 -0
  213. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/init.py +0 -0
  214. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/multi.py +0 -0
  215. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/output.py +0 -0
  216. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/plugins.py +0 -0
  217. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/record.py +0 -0
  218. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/repl.py +0 -0
  219. {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/serve.py +0 -0
@@ -2,6 +2,16 @@
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
+
5
15
  ## v2.3.3 — 2026-07-07
6
16
 
7
17
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavexis
3
- Version: 2.3.3
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.3"
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 {}
@@ -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):
@@ -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.3"
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
@@ -537,6 +537,64 @@ class BiDiBackend(AbstractBackend):
537
537
  js = f"window.scrollTo({x},{y})"
538
538
  await self._client.script.evaluate(self._context, js)
539
539
 
540
+ async def suggest_locator(
541
+ self, selector: str, all: bool = False
542
+ ) -> list[str] | str:
543
+ """Suggest the best CSS selector for an element.
544
+
545
+ Args:
546
+ selector: CSS selector for the target element.
547
+ all: If True, return multiple suggestions; otherwise just the best one.
548
+
549
+ Returns:
550
+ List of selector strings when all=True, single best selector when all=False.
551
+ """
552
+ client = self._require_client()
553
+ escaped = selector.replace("'", "\\'")
554
+ js = self._suggest_locator_js(escaped)
555
+ result = await client.script.evaluate(self._context, js)
556
+ raw = getattr(result, "value", None)
557
+ if not raw:
558
+ raise ElementNotFoundError(selector)
559
+ suggestions: list[str] = json.loads(raw)
560
+ if all:
561
+ return suggestions
562
+ return suggestions[0] if suggestions else selector
563
+
564
+ @staticmethod
565
+ def _suggest_locator_js(escaped: str) -> str:
566
+ """Build JS that generates CSS selector suggestions for an element."""
567
+ return (
568
+ f"(function(){{"
569
+ f"var el=document.querySelector('{escaped}');"
570
+ f"if(!el)return null;"
571
+ f"var s=[];var t=el.tagName.toLowerCase();"
572
+ f"var id=el.id;"
573
+ f"var tid=el.getAttribute('data-testid')"
574
+ f"||el.getAttribute('data-test-id')"
575
+ f"||el.getAttribute('data-cy');"
576
+ f"var aria=el.getAttribute('aria-label');"
577
+ f"var role=el.getAttribute('role');"
578
+ f"var txt=(el.textContent||'').trim().substring(0,50);"
579
+ f"var cls=Array.from(el.classList);"
580
+ f"if(id)s.push('#'+CSS.escape(id));"
581
+ f"if(tid)s.push('[data-testid=\"'+tid+'\"]');"
582
+ f"if(aria)s.push(t+'[aria-label=\"'+aria+'\"]');"
583
+ f"if(role)s.push(t+'[role=\"'+role+'\"]');"
584
+ f"if(txt&&txt.length<30)s.push(t+':has-text(\"'+txt+'\"]');"
585
+ f"if(cls.length>0)s.push(t+'.'+cls.join('.'));"
586
+ f"var p=el.parentElement;"
587
+ f"if(p&&p.id)s.push('#'+CSS.escape(p.id)+' > '+t);"
588
+ f"var sib=p?Array.from(p.children)"
589
+ f".filter(function(c){{return c.tagName===el.tagName}}):[];"
590
+ f"if(sib.length>1){{"
591
+ f"var i=sib.indexOf(el)+1;"
592
+ f"s.push(t+':nth-of-type('+i+')');}}"
593
+ f"s.push(t);"
594
+ f"return JSON.stringify(s);"
595
+ f"}})()"
596
+ )
597
+
540
598
  async def capture_har(self, params: HarParams) -> dict[str, Any]:
541
599
  """Capture HAR data via CDP Network domain.
542
600
 
@@ -644,6 +644,64 @@ class CDPBackend(AbstractBackend):
644
644
  js = f"window.scrollBy({x}, {y})"
645
645
  await session.runtime.evaluate(js)
646
646
 
647
+ async def suggest_locator(
648
+ self, selector: str, all: bool = False
649
+ ) -> list[str] | str:
650
+ """Suggest the best CSS selector for an element.
651
+
652
+ Args:
653
+ selector: CSS selector for the target element.
654
+ all: If True, return multiple suggestions; otherwise just the best one.
655
+
656
+ Returns:
657
+ List of selector strings when all=True, single best selector when all=False.
658
+ """
659
+ session = self._require_session()
660
+ escaped = selector.replace("'", "\\'")
661
+ js = self._suggest_locator_js(escaped)
662
+ result = await session.runtime.evaluate(js)
663
+ raw = result.get("result", {}).get("value")
664
+ if not raw:
665
+ raise ElementNotFoundError(selector)
666
+ suggestions: list[str] = json.loads(raw)
667
+ if all:
668
+ return suggestions
669
+ return suggestions[0] if suggestions else selector
670
+
671
+ @staticmethod
672
+ def _suggest_locator_js(escaped: str) -> str:
673
+ """Build JS that generates CSS selector suggestions for an element."""
674
+ return (
675
+ f"(function(){{"
676
+ f"var el=document.querySelector('{escaped}');"
677
+ f"if(!el)return null;"
678
+ f"var s=[];var t=el.tagName.toLowerCase();"
679
+ f"var id=el.id;"
680
+ f"var tid=el.getAttribute('data-testid')"
681
+ f"||el.getAttribute('data-test-id')"
682
+ f"||el.getAttribute('data-cy');"
683
+ f"var aria=el.getAttribute('aria-label');"
684
+ f"var role=el.getAttribute('role');"
685
+ f"var txt=(el.textContent||'').trim().substring(0,50);"
686
+ f"var cls=Array.from(el.classList);"
687
+ f"if(id)s.push('#'+CSS.escape(id));"
688
+ f"if(tid)s.push('[data-testid=\"'+tid+'\"]');"
689
+ f"if(aria)s.push(t+'[aria-label=\"'+aria+'\"]');"
690
+ f"if(role)s.push(t+'[role=\"'+role+'\"]');"
691
+ f"if(txt&&txt.length<30)s.push(t+':has-text(\"'+txt+'\"]');"
692
+ f"if(cls.length>0)s.push(t+'.'+cls.join('.'));"
693
+ f"var p=el.parentElement;"
694
+ f"if(p&&p.id)s.push('#'+CSS.escape(p.id)+' > '+t);"
695
+ f"var sib=p?Array.from(p.children)"
696
+ f".filter(function(c){{return c.tagName===el.tagName}}):[];"
697
+ f"if(sib.length>1){{"
698
+ f"var i=sib.indexOf(el)+1;"
699
+ f"s.push(t+':nth-of-type('+i+')');}}"
700
+ f"s.push(t);"
701
+ f"return JSON.stringify(s);"
702
+ f"}})()"
703
+ )
704
+
647
705
  # ── Network ────────────────────────────────────────────
648
706
 
649
707
  async def capture_har(self, params: HarParams) -> dict[str, Any]:
@@ -274,14 +274,20 @@ def dom(
274
274
  "get",
275
275
  "--action",
276
276
  "-a",
277
- help="DOM action: get, query, attr, remove_attr, remove, focus, scroll",
277
+ help=(
278
+ "DOM action: get, query, attr, remove_attr, remove, focus, scroll, "
279
+ "suggest_locator"
280
+ ),
278
281
  ),
279
282
  selector: str = typer.Option("", "--selector", "-s", help="CSS selector"),
280
283
  output: str | None = typer.Option(
281
284
  None, "--output", "-o", help="Output file path"
282
285
  ),
283
286
  outer: bool = typer.Option(True, "--outer/--inner", help="Outer or inner HTML"),
284
- all: bool = typer.Option(False, "--all", help="Query all matching elements"),
287
+ all: bool = typer.Option(
288
+ False, "--all",
289
+ help="Query all matching elements or all locator suggestions",
290
+ ),
285
291
  attribute: str | None = typer.Option(
286
292
  None, "--attribute", help="Attribute name for get/set/remove"
287
293
  ),
@@ -302,6 +308,13 @@ def dom(
302
308
  typer.echo(f"Output saved to {output}")
303
309
  else:
304
310
  typer.echo(result)
311
+ elif isinstance(result, list):
312
+ if output:
313
+ Output.write_json(result, output)
314
+ typer.echo(f"Output saved to {output}")
315
+ else:
316
+ for item in result:
317
+ typer.echo(item)
305
318
  elif result is not None:
306
319
  if output:
307
320
  Output.write_json(result, output)
@@ -324,6 +337,9 @@ async def _dom(
324
337
  backend = _get_backend()
325
338
  try:
326
339
  await backend.launch(_browser_options())
340
+ if action == "suggest_locator":
341
+ await backend.navigate(url, WaitStrategy(strategy="load"))
342
+ return await backend.suggest_locator(selector, all=all)
327
343
  params = DOMParams(
328
344
  url=url,
329
345
  action=action,
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes