wavexis 2.2.0__tar.gz → 2.2.2__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 (211) hide show
  1. {wavexis-2.2.0 → wavexis-2.2.2}/CHANGELOG.md +16 -0
  2. {wavexis-2.2.0 → wavexis-2.2.2}/PKG-INFO +1 -1
  3. {wavexis-2.2.0 → wavexis-2.2.2}/pyproject.toml +1 -1
  4. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_global_options.py +16 -1
  5. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/__init__.py +1 -1
  6. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/backend/bidi.py +22 -9
  7. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/backend/cdp.py +33 -5
  8. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/_shared.py +9 -0
  9. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/config.py +2 -0
  10. {wavexis-2.2.0 → wavexis-2.2.2}/.dockerignore +0 -0
  11. {wavexis-2.2.0 → wavexis-2.2.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  12. {wavexis-2.2.0 → wavexis-2.2.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  13. {wavexis-2.2.0 → wavexis-2.2.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  14. {wavexis-2.2.0 → wavexis-2.2.2}/.github/workflows/ci.yml +0 -0
  15. {wavexis-2.2.0 → wavexis-2.2.2}/.github/workflows/docs.yml +0 -0
  16. {wavexis-2.2.0 → wavexis-2.2.2}/.github/workflows/release.yml +0 -0
  17. {wavexis-2.2.0 → wavexis-2.2.2}/.gitignore +0 -0
  18. {wavexis-2.2.0 → wavexis-2.2.2}/CODE_OF_CONDUCT.md +0 -0
  19. {wavexis-2.2.0 → wavexis-2.2.2}/CONTRIBUTING.md +0 -0
  20. {wavexis-2.2.0 → wavexis-2.2.2}/Dockerfile +0 -0
  21. {wavexis-2.2.0 → wavexis-2.2.2}/LICENSE +0 -0
  22. {wavexis-2.2.0 → wavexis-2.2.2}/README.md +0 -0
  23. {wavexis-2.2.0 → wavexis-2.2.2}/SECURITY.md +0 -0
  24. {wavexis-2.2.0 → wavexis-2.2.2}/docs/api/actions.md +0 -0
  25. {wavexis-2.2.0 → wavexis-2.2.2}/docs/api/backends.md +0 -0
  26. {wavexis-2.2.0 → wavexis-2.2.2}/docs/api/cli.md +0 -0
  27. {wavexis-2.2.0 → wavexis-2.2.2}/docs/api/config.md +0 -0
  28. {wavexis-2.2.0 → wavexis-2.2.2}/docs/api/exceptions.md +0 -0
  29. {wavexis-2.2.0 → wavexis-2.2.2}/docs/api/multi.md +0 -0
  30. {wavexis-2.2.0 → wavexis-2.2.2}/docs/api/plugins.md +0 -0
  31. {wavexis-2.2.0 → wavexis-2.2.2}/docs/api/serve.md +0 -0
  32. {wavexis-2.2.0 → wavexis-2.2.2}/docs/cookbook/auth.md +0 -0
  33. {wavexis-2.2.0 → wavexis-2.2.2}/docs/cookbook/ci-cd.md +0 -0
  34. {wavexis-2.2.0 → wavexis-2.2.2}/docs/cookbook/css.md +0 -0
  35. {wavexis-2.2.0 → wavexis-2.2.2}/docs/cookbook/debug.md +0 -0
  36. {wavexis-2.2.0 → wavexis-2.2.2}/docs/cookbook/record-replay.md +0 -0
  37. {wavexis-2.2.0 → wavexis-2.2.2}/docs/cookbook/scraping.md +0 -0
  38. {wavexis-2.2.0 → wavexis-2.2.2}/docs/cookbook/screenshots.md +0 -0
  39. {wavexis-2.2.0 → wavexis-2.2.2}/docs/cookbook/serve-mode.md +0 -0
  40. {wavexis-2.2.0 → wavexis-2.2.2}/docs/guide/assert.md +0 -0
  41. {wavexis-2.2.0 → wavexis-2.2.2}/docs/guide/backends.md +0 -0
  42. {wavexis-2.2.0 → wavexis-2.2.2}/docs/guide/commands.md +0 -0
  43. {wavexis-2.2.0 → wavexis-2.2.2}/docs/guide/init.md +0 -0
  44. {wavexis-2.2.0 → wavexis-2.2.2}/docs/guide/installation.md +0 -0
  45. {wavexis-2.2.0 → wavexis-2.2.2}/docs/guide/multi.md +0 -0
  46. {wavexis-2.2.0 → wavexis-2.2.2}/docs/guide/perf.md +0 -0
  47. {wavexis-2.2.0 → wavexis-2.2.2}/docs/guide/plugins.md +0 -0
  48. {wavexis-2.2.0 → wavexis-2.2.2}/docs/guide/quickstart.md +0 -0
  49. {wavexis-2.2.0 → wavexis-2.2.2}/docs/guide/raw.md +0 -0
  50. {wavexis-2.2.0 → wavexis-2.2.2}/docs/guide/repl.md +0 -0
  51. {wavexis-2.2.0 → wavexis-2.2.2}/docs/guide/troubleshooting.md +0 -0
  52. {wavexis-2.2.0 → wavexis-2.2.2}/docs/index.md +0 -0
  53. {wavexis-2.2.0 → wavexis-2.2.2}/mkdocs.yml +0 -0
  54. {wavexis-2.2.0 → wavexis-2.2.2}/stubs/bidiwave/__init__.pyi +0 -0
  55. {wavexis-2.2.0 → wavexis-2.2.2}/stubs/cdpwave/__init__.pyi +0 -0
  56. {wavexis-2.2.0 → wavexis-2.2.2}/tests/__init__.py +0 -0
  57. {wavexis-2.2.0 → wavexis-2.2.2}/tests/conftest.py +0 -0
  58. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/__init__.py +0 -0
  59. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_a11y.py +0 -0
  60. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_animation.py +0 -0
  61. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_backend_selection.py +0 -0
  62. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_browser.py +0 -0
  63. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_console.py +0 -0
  64. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_css.py +0 -0
  65. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_debug.py +0 -0
  66. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_dialog.py +0 -0
  67. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_dom.py +0 -0
  68. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_dom_snapshot.py +0 -0
  69. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_emulation.py +0 -0
  70. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_emulation_advanced.py +0 -0
  71. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_eval.py +0 -0
  72. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_har.py +0 -0
  73. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_input.py +0 -0
  74. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_media.py +0 -0
  75. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_multi.py +0 -0
  76. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_navigate.py +0 -0
  77. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_network.py +0 -0
  78. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_network_advanced.py +0 -0
  79. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_overlay.py +0 -0
  80. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_pdf.py +0 -0
  81. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_perf.py +0 -0
  82. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_permissions.py +0 -0
  83. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_raw.py +0 -0
  84. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_record.py +0 -0
  85. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_scrape.py +0 -0
  86. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_screenshot.py +0 -0
  87. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_security.py +0 -0
  88. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_serve.py +0 -0
  89. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_service_worker.py +0 -0
  90. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_storage.py +0 -0
  91. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_tabs.py +0 -0
  92. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_webaudio.py +0 -0
  93. {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_webauthn.py +0 -0
  94. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/__init__.py +0 -0
  95. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_abstract_backend_phase5.py +0 -0
  96. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_actions.py +0 -0
  97. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_actions_phase5.py +0 -0
  98. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_animation_action.py +0 -0
  99. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_assert_multi.py +0 -0
  100. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_auth.py +0 -0
  101. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_backend_manager.py +0 -0
  102. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_bidi_backend.py +0 -0
  103. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_bidi_phase5.py +0 -0
  104. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_bluetooth_action.py +0 -0
  105. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_cast_action.py +0 -0
  106. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_cli_phase5.py +0 -0
  107. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_config.py +0 -0
  108. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_config_phase5.py +0 -0
  109. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_cookies_headers_actions.py +0 -0
  110. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_css_action.py +0 -0
  111. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_debug_action.py +0 -0
  112. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_dom_snapshot_action.py +0 -0
  113. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_download_screencast.py +0 -0
  114. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_emulation_action.py +0 -0
  115. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_error_handling.py +0 -0
  116. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_exceptions.py +0 -0
  117. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_format_record.py +0 -0
  118. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_init.py +0 -0
  119. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_input_action.py +0 -0
  120. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_interactions_crawl.py +0 -0
  121. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_manager.py +0 -0
  122. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_media_action.py +0 -0
  123. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_multi.py +0 -0
  124. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_multi_improvements.py +0 -0
  125. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_new_features.py +0 -0
  126. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_output.py +0 -0
  127. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_overlay_action.py +0 -0
  128. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_perf_action.py +0 -0
  129. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_perf_console.py +0 -0
  130. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_plugins.py +0 -0
  131. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_raw.py +0 -0
  132. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_record.py +0 -0
  133. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_repl.py +0 -0
  134. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_serve.py +0 -0
  135. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_service_worker_action.py +0 -0
  136. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_storage_action.py +0 -0
  137. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_webaudio_action.py +0 -0
  138. {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_webauthn_action.py +0 -0
  139. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/__main__.py +0 -0
  140. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/__init__.py +0 -0
  141. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/accessibility.py +0 -0
  142. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/animation.py +0 -0
  143. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/base.py +0 -0
  144. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/bluetooth.py +0 -0
  145. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/browser.py +0 -0
  146. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/cast.py +0 -0
  147. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/console.py +0 -0
  148. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/cookies.py +0 -0
  149. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/crawl.py +0 -0
  150. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/css.py +0 -0
  151. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/debug.py +0 -0
  152. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/dialog.py +0 -0
  153. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/dom.py +0 -0
  154. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/dom_snapshot.py +0 -0
  155. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/download.py +0 -0
  156. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/emulation.py +0 -0
  157. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/eval.py +0 -0
  158. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/extract.py +0 -0
  159. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/form.py +0 -0
  160. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/har.py +0 -0
  161. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/headers.py +0 -0
  162. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/input.py +0 -0
  163. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/lighthouse.py +0 -0
  164. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/media.py +0 -0
  165. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/multi.py +0 -0
  166. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/navigate.py +0 -0
  167. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/network.py +0 -0
  168. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/overlay.py +0 -0
  169. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/pdf.py +0 -0
  170. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/performance.py +0 -0
  171. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/permissions.py +0 -0
  172. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/record.py +0 -0
  173. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/scrape.py +0 -0
  174. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/screencast.py +0 -0
  175. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/screenshot.py +0 -0
  176. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/security.py +0 -0
  177. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/service_worker.py +0 -0
  178. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/session.py +0 -0
  179. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/storage.py +0 -0
  180. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/tabs.py +0 -0
  181. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/wait.py +0 -0
  182. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/webaudio.py +0 -0
  183. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/webauthn.py +0 -0
  184. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/websocket.py +0 -0
  185. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/auth.py +0 -0
  186. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/backend/__init__.py +0 -0
  187. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/backend/base.py +0 -0
  188. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/backend/manager.py +0 -0
  189. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/__init__.py +0 -0
  190. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/_advanced.py +0 -0
  191. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/_capture.py +0 -0
  192. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/_config.py +0 -0
  193. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/_debug.py +0 -0
  194. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/_emulation.py +0 -0
  195. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/_experimental.py +0 -0
  196. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/_input.py +0 -0
  197. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/_navigation.py +0 -0
  198. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/_network.py +0 -0
  199. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/_perf.py +0 -0
  200. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/_serve.py +0 -0
  201. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/_session.py +0 -0
  202. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/_workflow.py +0 -0
  203. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/app.py +0 -0
  204. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/exceptions.py +0 -0
  205. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/init.py +0 -0
  206. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/multi.py +0 -0
  207. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/output.py +0 -0
  208. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/plugins.py +0 -0
  209. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/record.py +0 -0
  210. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/repl.py +0 -0
  211. {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/serve.py +0 -0
@@ -2,6 +2,22 @@
2
2
 
3
3
  All notable changes to wavexis are documented in this file.
4
4
 
5
+ ## v2.2.2 — 2026-07-07
6
+
7
+ ### Added
8
+
9
+ - Auto scroll-into-view before `click`, `fill`, and `hover` in both CDP and BiDi backends
10
+ - `_scroll_into_view_if_needed` helper checks if element is in viewport and scrolls if needed
11
+
12
+ ## v2.2.1 — 2026-07-07
13
+
14
+ ### Added
15
+
16
+ - `--browser-url` global flag to connect to an existing browser (e.g. `--browser-url ws://localhost:9222`)
17
+ - `browser_url` field in `BrowserOptions` dataclass
18
+ - `browser_url` support in `config.yml` (`browser_url: ws://localhost:9222`)
19
+ - `CDPBackend.launch()` now uses `CDPClient.connect()` when `browser_url` is set instead of launching a new browser
20
+
5
21
  ## v2.2.0 — 2026-07-07
6
22
 
7
23
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavexis
3
- Version: 2.2.0
3
+ Version: 2.2.2
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.2.0"
7
+ version = "2.2.2"
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" }
@@ -31,6 +31,11 @@ class TestBrowserOptions:
31
31
  assert opts.proxy is None
32
32
  assert opts.timeout == 30000
33
33
  assert opts.user_data_dir is None
34
+ assert opts.browser_url is None
35
+
36
+ def test_with_browser_url(self) -> None:
37
+ opts = BrowserOptions(browser_url="ws://localhost:9222")
38
+ assert opts.browser_url == "ws://localhost:9222"
34
39
 
35
40
  def test_with_user_data_dir(self) -> None:
36
41
  opts = BrowserOptions(user_data_dir="/tmp/wavexis-profile")
@@ -63,6 +68,13 @@ class TestBrowserOptionsHelper:
63
68
  assert opts.timeout == 30000
64
69
  assert opts.proxy is None
65
70
  assert opts.user_data_dir is None
71
+ assert opts.browser_url is None
72
+
73
+ def test_browser_url_options(self) -> None:
74
+ ctx = _fresh_ctx()
75
+ ctx.browser_url = "ws://localhost:9222"
76
+ opts = _cli._browser_options()
77
+ assert opts.browser_url == "ws://localhost:9222"
66
78
 
67
79
  def test_user_data_dir_options(self) -> None:
68
80
  ctx = _fresh_ctx()
@@ -101,6 +113,7 @@ class TestLoadGlobalConfig:
101
113
  assert ctx.timeout == 30000
102
114
  assert ctx.proxy is None
103
115
  assert ctx.user_data_dir is None
116
+ assert ctx.browser_url is None
104
117
 
105
118
  def test_loads_config(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
106
119
  config_dir = tmp_path / ".wavexis"
@@ -111,7 +124,8 @@ class TestLoadGlobalConfig:
111
124
  "headless: false\n"
112
125
  "timeout: 60000\n"
113
126
  "proxy: http://proxy:9090\n"
114
- "user_data_dir: /tmp/profile\n",
127
+ "user_data_dir: /tmp/profile\n"
128
+ "browser_url: ws://localhost:9222\n",
115
129
  encoding="utf-8",
116
130
  )
117
131
  monkeypatch.setattr(Path, "home", lambda: tmp_path)
@@ -124,6 +138,7 @@ class TestLoadGlobalConfig:
124
138
  assert ctx.timeout == 60000
125
139
  assert ctx.proxy == "http://proxy:9090"
126
140
  assert ctx.user_data_dir == "/tmp/profile"
141
+ assert ctx.browser_url == "ws://localhost:9222"
127
142
 
128
143
  def test_cli_overrides_config(
129
144
  self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
@@ -1,3 +1,3 @@
1
1
  """wavexis — CLI de automatización de navegador."""
2
2
 
3
- __version__ = "2.2.0"
3
+ __version__ = "2.2.2"
@@ -807,19 +807,32 @@ class BiDiBackend(AbstractBackend):
807
807
 
808
808
  # ── Input ──────────────────────────────────────────────
809
809
 
810
+ async def _scroll_into_view_if_needed(self, selector: str) -> None:
811
+ """Scroll element into view if it's not visible in the viewport."""
812
+ client = self._require_client()
813
+ escaped = selector.replace("'", "\\'")
814
+ js = (
815
+ f"(function(){{var el=document.querySelector('{escaped}');"
816
+ f"if(!el)return;var rect=el.getBoundingClientRect();"
817
+ f"if(rect.top<0||rect.bottom>window.innerHeight||"
818
+ f"rect.left<0||rect.right>window.innerWidth)"
819
+ f"el.scrollIntoView({{block:'center',behavior:'instant'}});}})()"
820
+ )
821
+ await client.script.evaluate(self._context, js)
822
+
810
823
  async def click(
811
824
  self, selector: str, button: str = "left", click_count: int = 1
812
825
  ) -> None:
813
826
  """Click an element via BiDi script.evaluate."""
814
- if self._client is None or self._context is None:
815
- raise RuntimeError("BiDiBackend not launched. Call launch() first.")
827
+ client = self._require_client()
828
+ await self._scroll_into_view_if_needed(selector)
816
829
  escaped = selector.replace("'", "\\'")
817
830
  js = (
818
831
  f"document.querySelector('{escaped}')"
819
832
  f".dispatchEvent(new MouseEvent('click',{{bubbles:true}}))"
820
833
  )
821
834
  for _ in range(click_count):
822
- await self._client.script.evaluate(self._context, js)
835
+ await client.script.evaluate(self._context, js)
823
836
 
824
837
  async def type_text(self, selector: str, text: str, delay: int = 0) -> None:
825
838
  """Type text into an element via BiDi."""
@@ -843,12 +856,12 @@ class BiDiBackend(AbstractBackend):
843
856
 
844
857
  async def fill(self, selector: str, value: str) -> None:
845
858
  """Fill an input element with a value via BiDi."""
846
- if self._client is None or self._context is None:
847
- raise RuntimeError("BiDiBackend not launched. Call launch() first.")
859
+ client = self._require_client()
860
+ await self._scroll_into_view_if_needed(selector)
848
861
  escaped = selector.replace("'", "\\'")
849
862
  escaped_val = value.replace("\\", "\\\\").replace("'", "\\'")
850
863
  js = f"document.querySelector('{escaped}').value = '{escaped_val}'"
851
- await self._client.script.evaluate(self._context, js)
864
+ await client.script.evaluate(self._context, js)
852
865
 
853
866
  async def select_option(self, selector: str, value: str) -> None:
854
867
  """Select an option in a <select> element by value via BiDi."""
@@ -861,15 +874,15 @@ class BiDiBackend(AbstractBackend):
861
874
 
862
875
  async def hover(self, selector: str) -> None:
863
876
  """Hover over an element via BiDi script.evaluate."""
864
- if self._client is None or self._context is None:
865
- raise RuntimeError("BiDiBackend not launched. Call launch() first.")
877
+ client = self._require_client()
878
+ await self._scroll_into_view_if_needed(selector)
866
879
  escaped = selector.replace("'", "\\'")
867
880
  js = (
868
881
  f"var el=document.querySelector('{escaped}');"
869
882
  f"el.dispatchEvent(new MouseEvent('mouseover',{{bubbles:true}}));"
870
883
  f"el.dispatchEvent(new MouseEvent('mousemove',{{bubbles:true}}))"
871
884
  )
872
- await self._client.script.evaluate(self._context, js)
885
+ await client.script.evaluate(self._context, js)
873
886
 
874
887
  async def key_press(self, key: str) -> None:
875
888
  """Press a keyboard key via BiDi script.evaluate."""
@@ -83,11 +83,19 @@ class CDPBackend(AbstractBackend):
83
83
  if options.proxy:
84
84
  extra_args.append(f"--proxy-server={options.proxy}")
85
85
 
86
- self._client = await CDPClient.launch(
87
- headless=options.headless,
88
- user_data_dir=options.user_data_dir, # type: ignore[call-arg]
89
- extra_args=extra_args if extra_args else None,
90
- )
86
+ if options.browser_url:
87
+ from urllib.parse import urlparse
88
+
89
+ parsed = urlparse(options.browser_url)
90
+ host = parsed.hostname or "localhost"
91
+ port = parsed.port or 9222
92
+ self._client = await CDPClient.connect(host=host, port=port) # type: ignore[attr-defined]
93
+ else:
94
+ self._client = await CDPClient.launch(
95
+ headless=options.headless,
96
+ user_data_dir=options.user_data_dir, # type: ignore[call-arg]
97
+ extra_args=extra_args if extra_args else None,
98
+ )
91
99
  self._session = await self._client.new_page()
92
100
 
93
101
  if options.user_agent:
@@ -1018,6 +1026,23 @@ class CDPBackend(AbstractBackend):
1018
1026
  cy = (min(ys) + max(ys)) / 2
1019
1027
  return cx, cy
1020
1028
 
1029
+ async def _scroll_into_view_if_needed(self, selector: str) -> None:
1030
+ """Scroll element into view if it's not visible in the viewport.
1031
+
1032
+ Args:
1033
+ selector: CSS selector for the target element.
1034
+ """
1035
+ session = self._require_session()
1036
+ escaped = selector.replace("'", "\\'")
1037
+ js = (
1038
+ f"(function(){{var el=document.querySelector('{escaped}');"
1039
+ f"if(!el)return;var rect=el.getBoundingClientRect();"
1040
+ f"if(rect.top<0||rect.bottom>window.innerHeight||"
1041
+ f"rect.left<0||rect.right>window.innerWidth)"
1042
+ f"el.scrollIntoView({{block:'center',behavior:'instant'}});}})()"
1043
+ )
1044
+ await session.runtime.evaluate(js)
1045
+
1021
1046
  async def click(
1022
1047
  self, selector: str, button: str = "left", click_count: int = 1
1023
1048
  ) -> None:
@@ -1029,6 +1054,7 @@ class CDPBackend(AbstractBackend):
1029
1054
  click_count: Number of clicks to dispatch.
1030
1055
  """
1031
1056
  session = self._require_session()
1057
+ await self._scroll_into_view_if_needed(selector)
1032
1058
  x, y = await self._get_box_center(selector)
1033
1059
  btn_map = {"left": "left", "right": "right", "middle": "middle"}
1034
1060
  btn = btn_map.get(button, "left")
@@ -1066,6 +1092,7 @@ class CDPBackend(AbstractBackend):
1066
1092
  value: Value to set in the input field.
1067
1093
  """
1068
1094
  session = self._require_session()
1095
+ await self._scroll_into_view_if_needed(selector)
1069
1096
  escaped = selector.replace("'", "\\'")
1070
1097
  js = (
1071
1098
  f"(function(){{var el=document.querySelector('{escaped}');"
@@ -1105,6 +1132,7 @@ class CDPBackend(AbstractBackend):
1105
1132
  selector: CSS selector for the target element.
1106
1133
  """
1107
1134
  session = self._require_session()
1135
+ await self._scroll_into_view_if_needed(selector)
1108
1136
  x, y = await self._get_box_center(selector)
1109
1137
  await session.input.dispatch_mouse_event(
1110
1138
  type_="mouseMoved", x=x, y=y
@@ -76,6 +76,7 @@ class CLIContext:
76
76
  timeout: int = 30000
77
77
  proxy: str | None = None
78
78
  user_data_dir: str | None = None
79
+ browser_url: str | None = None
79
80
 
80
81
 
81
82
  _ctx: contextvars.ContextVar[CLIContext | None] = contextvars.ContextVar(
@@ -116,6 +117,8 @@ def _load_global_config() -> None:
116
117
  ctx.proxy = str(raw["proxy"])
117
118
  if "user_data_dir" in raw:
118
119
  ctx.user_data_dir = str(raw["user_data_dir"])
120
+ if "browser_url" in raw:
121
+ ctx.browser_url = str(raw["browser_url"])
119
122
  except (OSError, ValueError, TypeError, yaml.YAMLError) as exc:
120
123
  if ctx.verbose:
121
124
  _echo(f"Warning: failed to load config from {config_path}: {exc}")
@@ -144,6 +147,9 @@ def main_callback(
144
147
  user_data_dir: str | None = typer.Option(
145
148
  None, "--user-data-dir", help="Path to persistent browser profile directory"
146
149
  ),
150
+ browser_url: str | None = typer.Option(
151
+ None, "--browser-url", help="Connect to existing browser (e.g. ws://localhost:9222)"
152
+ ),
147
153
  version: bool = typer.Option(
148
154
  False, "--version", help="Print wavexis version and exit"
149
155
  ),
@@ -162,6 +168,8 @@ def main_callback(
162
168
  ctx.proxy = proxy
163
169
  if user_data_dir:
164
170
  ctx.user_data_dir = user_data_dir
171
+ if browser_url:
172
+ ctx.browser_url = browser_url
165
173
  if version:
166
174
  from wavexis import __version__
167
175
 
@@ -236,6 +244,7 @@ def _browser_options() -> BrowserOptions:
236
244
  timeout=ctx.timeout,
237
245
  proxy=ctx.proxy,
238
246
  user_data_dir=ctx.user_data_dir,
247
+ browser_url=ctx.browser_url,
239
248
  )
240
249
 
241
250
 
@@ -147,6 +147,7 @@ class BrowserOptions:
147
147
  proxy: Proxy server URL (e.g. http://proxy:8080 or socks5://proxy:1080).
148
148
  timeout: Default navigation timeout in milliseconds.
149
149
  user_data_dir: Path to a persistent user data directory for browser profiles.
150
+ browser_url: URL of an existing browser to connect to (e.g. ws://localhost:9222).
150
151
  """
151
152
 
152
153
  headless: bool = True
@@ -157,6 +158,7 @@ class BrowserOptions:
157
158
  proxy: str | None = None
158
159
  timeout: int = 30000
159
160
  user_data_dir: str | None = None
161
+ browser_url: str | None = None
160
162
 
161
163
 
162
164
  @dataclass
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
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