wavexis 2.2.1__tar.gz → 2.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. {wavexis-2.2.1 → wavexis-2.2.3}/CHANGELOG.md +15 -0
  2. {wavexis-2.2.1 → wavexis-2.2.3}/PKG-INFO +1 -1
  3. {wavexis-2.2.1 → wavexis-2.2.3}/pyproject.toml +1 -1
  4. wavexis-2.2.3/tests/unit/test_cleanup.py +53 -0
  5. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/__init__.py +1 -1
  6. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/backend/bidi.py +22 -9
  7. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/backend/cdp.py +20 -0
  8. wavexis-2.2.3/wavexis/cleanup.py +96 -0
  9. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/cli/_shared.py +9 -2
  10. {wavexis-2.2.1 → wavexis-2.2.3}/.dockerignore +0 -0
  11. {wavexis-2.2.1 → wavexis-2.2.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  12. {wavexis-2.2.1 → wavexis-2.2.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  13. {wavexis-2.2.1 → wavexis-2.2.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  14. {wavexis-2.2.1 → wavexis-2.2.3}/.github/workflows/ci.yml +0 -0
  15. {wavexis-2.2.1 → wavexis-2.2.3}/.github/workflows/docs.yml +0 -0
  16. {wavexis-2.2.1 → wavexis-2.2.3}/.github/workflows/release.yml +0 -0
  17. {wavexis-2.2.1 → wavexis-2.2.3}/.gitignore +0 -0
  18. {wavexis-2.2.1 → wavexis-2.2.3}/CODE_OF_CONDUCT.md +0 -0
  19. {wavexis-2.2.1 → wavexis-2.2.3}/CONTRIBUTING.md +0 -0
  20. {wavexis-2.2.1 → wavexis-2.2.3}/Dockerfile +0 -0
  21. {wavexis-2.2.1 → wavexis-2.2.3}/LICENSE +0 -0
  22. {wavexis-2.2.1 → wavexis-2.2.3}/README.md +0 -0
  23. {wavexis-2.2.1 → wavexis-2.2.3}/SECURITY.md +0 -0
  24. {wavexis-2.2.1 → wavexis-2.2.3}/docs/api/actions.md +0 -0
  25. {wavexis-2.2.1 → wavexis-2.2.3}/docs/api/backends.md +0 -0
  26. {wavexis-2.2.1 → wavexis-2.2.3}/docs/api/cli.md +0 -0
  27. {wavexis-2.2.1 → wavexis-2.2.3}/docs/api/config.md +0 -0
  28. {wavexis-2.2.1 → wavexis-2.2.3}/docs/api/exceptions.md +0 -0
  29. {wavexis-2.2.1 → wavexis-2.2.3}/docs/api/multi.md +0 -0
  30. {wavexis-2.2.1 → wavexis-2.2.3}/docs/api/plugins.md +0 -0
  31. {wavexis-2.2.1 → wavexis-2.2.3}/docs/api/serve.md +0 -0
  32. {wavexis-2.2.1 → wavexis-2.2.3}/docs/cookbook/auth.md +0 -0
  33. {wavexis-2.2.1 → wavexis-2.2.3}/docs/cookbook/ci-cd.md +0 -0
  34. {wavexis-2.2.1 → wavexis-2.2.3}/docs/cookbook/css.md +0 -0
  35. {wavexis-2.2.1 → wavexis-2.2.3}/docs/cookbook/debug.md +0 -0
  36. {wavexis-2.2.1 → wavexis-2.2.3}/docs/cookbook/record-replay.md +0 -0
  37. {wavexis-2.2.1 → wavexis-2.2.3}/docs/cookbook/scraping.md +0 -0
  38. {wavexis-2.2.1 → wavexis-2.2.3}/docs/cookbook/screenshots.md +0 -0
  39. {wavexis-2.2.1 → wavexis-2.2.3}/docs/cookbook/serve-mode.md +0 -0
  40. {wavexis-2.2.1 → wavexis-2.2.3}/docs/guide/assert.md +0 -0
  41. {wavexis-2.2.1 → wavexis-2.2.3}/docs/guide/backends.md +0 -0
  42. {wavexis-2.2.1 → wavexis-2.2.3}/docs/guide/commands.md +0 -0
  43. {wavexis-2.2.1 → wavexis-2.2.3}/docs/guide/init.md +0 -0
  44. {wavexis-2.2.1 → wavexis-2.2.3}/docs/guide/installation.md +0 -0
  45. {wavexis-2.2.1 → wavexis-2.2.3}/docs/guide/multi.md +0 -0
  46. {wavexis-2.2.1 → wavexis-2.2.3}/docs/guide/perf.md +0 -0
  47. {wavexis-2.2.1 → wavexis-2.2.3}/docs/guide/plugins.md +0 -0
  48. {wavexis-2.2.1 → wavexis-2.2.3}/docs/guide/quickstart.md +0 -0
  49. {wavexis-2.2.1 → wavexis-2.2.3}/docs/guide/raw.md +0 -0
  50. {wavexis-2.2.1 → wavexis-2.2.3}/docs/guide/repl.md +0 -0
  51. {wavexis-2.2.1 → wavexis-2.2.3}/docs/guide/troubleshooting.md +0 -0
  52. {wavexis-2.2.1 → wavexis-2.2.3}/docs/index.md +0 -0
  53. {wavexis-2.2.1 → wavexis-2.2.3}/mkdocs.yml +0 -0
  54. {wavexis-2.2.1 → wavexis-2.2.3}/stubs/bidiwave/__init__.pyi +0 -0
  55. {wavexis-2.2.1 → wavexis-2.2.3}/stubs/cdpwave/__init__.pyi +0 -0
  56. {wavexis-2.2.1 → wavexis-2.2.3}/tests/__init__.py +0 -0
  57. {wavexis-2.2.1 → wavexis-2.2.3}/tests/conftest.py +0 -0
  58. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/__init__.py +0 -0
  59. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_a11y.py +0 -0
  60. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_animation.py +0 -0
  61. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_backend_selection.py +0 -0
  62. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_browser.py +0 -0
  63. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_console.py +0 -0
  64. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_css.py +0 -0
  65. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_debug.py +0 -0
  66. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_dialog.py +0 -0
  67. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_dom.py +0 -0
  68. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_dom_snapshot.py +0 -0
  69. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_emulation.py +0 -0
  70. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_emulation_advanced.py +0 -0
  71. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_eval.py +0 -0
  72. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_har.py +0 -0
  73. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_input.py +0 -0
  74. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_media.py +0 -0
  75. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_multi.py +0 -0
  76. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_navigate.py +0 -0
  77. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_network.py +0 -0
  78. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_network_advanced.py +0 -0
  79. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_overlay.py +0 -0
  80. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_pdf.py +0 -0
  81. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_perf.py +0 -0
  82. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_permissions.py +0 -0
  83. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_raw.py +0 -0
  84. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_record.py +0 -0
  85. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_scrape.py +0 -0
  86. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_screenshot.py +0 -0
  87. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_security.py +0 -0
  88. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_serve.py +0 -0
  89. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_service_worker.py +0 -0
  90. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_storage.py +0 -0
  91. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_tabs.py +0 -0
  92. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_webaudio.py +0 -0
  93. {wavexis-2.2.1 → wavexis-2.2.3}/tests/integration/test_webauthn.py +0 -0
  94. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/__init__.py +0 -0
  95. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_abstract_backend_phase5.py +0 -0
  96. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_actions.py +0 -0
  97. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_actions_phase5.py +0 -0
  98. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_animation_action.py +0 -0
  99. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_assert_multi.py +0 -0
  100. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_auth.py +0 -0
  101. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_backend_manager.py +0 -0
  102. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_bidi_backend.py +0 -0
  103. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_bidi_phase5.py +0 -0
  104. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_bluetooth_action.py +0 -0
  105. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_cast_action.py +0 -0
  106. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_cli_phase5.py +0 -0
  107. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_config.py +0 -0
  108. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_config_phase5.py +0 -0
  109. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_cookies_headers_actions.py +0 -0
  110. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_css_action.py +0 -0
  111. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_debug_action.py +0 -0
  112. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_dom_snapshot_action.py +0 -0
  113. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_download_screencast.py +0 -0
  114. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_emulation_action.py +0 -0
  115. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_error_handling.py +0 -0
  116. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_exceptions.py +0 -0
  117. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_format_record.py +0 -0
  118. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_global_options.py +0 -0
  119. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_init.py +0 -0
  120. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_input_action.py +0 -0
  121. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_interactions_crawl.py +0 -0
  122. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_manager.py +0 -0
  123. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_media_action.py +0 -0
  124. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_multi.py +0 -0
  125. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_multi_improvements.py +0 -0
  126. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_new_features.py +0 -0
  127. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_output.py +0 -0
  128. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_overlay_action.py +0 -0
  129. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_perf_action.py +0 -0
  130. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_perf_console.py +0 -0
  131. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_plugins.py +0 -0
  132. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_raw.py +0 -0
  133. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_record.py +0 -0
  134. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_repl.py +0 -0
  135. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_serve.py +0 -0
  136. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_service_worker_action.py +0 -0
  137. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_storage_action.py +0 -0
  138. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_webaudio_action.py +0 -0
  139. {wavexis-2.2.1 → wavexis-2.2.3}/tests/unit/test_webauthn_action.py +0 -0
  140. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/__main__.py +0 -0
  141. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/__init__.py +0 -0
  142. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/accessibility.py +0 -0
  143. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/animation.py +0 -0
  144. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/base.py +0 -0
  145. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/bluetooth.py +0 -0
  146. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/browser.py +0 -0
  147. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/cast.py +0 -0
  148. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/console.py +0 -0
  149. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/cookies.py +0 -0
  150. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/crawl.py +0 -0
  151. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/css.py +0 -0
  152. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/debug.py +0 -0
  153. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/dialog.py +0 -0
  154. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/dom.py +0 -0
  155. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/dom_snapshot.py +0 -0
  156. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/download.py +0 -0
  157. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/emulation.py +0 -0
  158. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/eval.py +0 -0
  159. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/extract.py +0 -0
  160. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/form.py +0 -0
  161. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/har.py +0 -0
  162. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/headers.py +0 -0
  163. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/input.py +0 -0
  164. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/lighthouse.py +0 -0
  165. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/media.py +0 -0
  166. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/multi.py +0 -0
  167. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/navigate.py +0 -0
  168. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/network.py +0 -0
  169. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/overlay.py +0 -0
  170. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/pdf.py +0 -0
  171. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/performance.py +0 -0
  172. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/permissions.py +0 -0
  173. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/record.py +0 -0
  174. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/scrape.py +0 -0
  175. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/screencast.py +0 -0
  176. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/screenshot.py +0 -0
  177. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/security.py +0 -0
  178. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/service_worker.py +0 -0
  179. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/session.py +0 -0
  180. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/storage.py +0 -0
  181. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/tabs.py +0 -0
  182. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/wait.py +0 -0
  183. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/webaudio.py +0 -0
  184. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/webauthn.py +0 -0
  185. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/actions/websocket.py +0 -0
  186. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/auth.py +0 -0
  187. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/backend/__init__.py +0 -0
  188. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/backend/base.py +0 -0
  189. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/backend/manager.py +0 -0
  190. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/cli/__init__.py +0 -0
  191. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/cli/_advanced.py +0 -0
  192. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/cli/_capture.py +0 -0
  193. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/cli/_config.py +0 -0
  194. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/cli/_debug.py +0 -0
  195. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/cli/_emulation.py +0 -0
  196. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/cli/_experimental.py +0 -0
  197. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/cli/_input.py +0 -0
  198. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/cli/_navigation.py +0 -0
  199. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/cli/_network.py +0 -0
  200. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/cli/_perf.py +0 -0
  201. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/cli/_serve.py +0 -0
  202. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/cli/_session.py +0 -0
  203. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/cli/_workflow.py +0 -0
  204. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/cli/app.py +0 -0
  205. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/config.py +0 -0
  206. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/exceptions.py +0 -0
  207. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/init.py +0 -0
  208. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/multi.py +0 -0
  209. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/output.py +0 -0
  210. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/plugins.py +0 -0
  211. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/record.py +0 -0
  212. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/repl.py +0 -0
  213. {wavexis-2.2.1 → wavexis-2.2.3}/wavexis/serve.py +0 -0
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to wavexis are documented in this file.
4
4
 
5
+ ## v2.2.3 — 2026-07-07
6
+
7
+ ### Added
8
+
9
+ - Resource cleanup module (`wavexis.cleanup`) with `atexit` and signal handlers
10
+ - Backends are automatically registered for cleanup on crash or signal (SIGINT, SIGTERM, SIGBREAK)
11
+ - Orphaned browser processes are closed even when the process is killed unexpectedly
12
+
13
+ ## v2.2.2 — 2026-07-07
14
+
15
+ ### Added
16
+
17
+ - Auto scroll-into-view before `click`, `fill`, and `hover` in both CDP and BiDi backends
18
+ - `_scroll_into_view_if_needed` helper checks if element is in viewport and scrolls if needed
19
+
5
20
  ## v2.2.1 — 2026-07-07
6
21
 
7
22
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavexis
3
- Version: 2.2.1
3
+ Version: 2.2.3
4
4
  Summary: Browser automation CLI — wraps cdpwave and bidiwave, no Node.js, no Chromium download
5
5
  Project-URL: Homepage, https://github.com/MathiasPaulenko/wavexis
6
6
  Project-URL: Repository, https://github.com/MathiasPaulenko/wavexis
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "wavexis"
7
- version = "2.2.1"
7
+ version = "2.2.3"
8
8
  description = "Browser automation CLI — wraps cdpwave and bidiwave, no Node.js, no Chromium download"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -0,0 +1,53 @@
1
+ """Unit tests for resource cleanup module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from unittest.mock import AsyncMock, MagicMock
6
+
7
+ import pytest
8
+
9
+ import wavexis.cleanup as cleanup_mod
10
+ from wavexis.cleanup import (
11
+ _cleanup_sync,
12
+ register_backend,
13
+ unregister_backend,
14
+ )
15
+
16
+
17
+ @pytest.mark.unit
18
+ class TestCleanup:
19
+ """Tests for browser cleanup on crash."""
20
+
21
+ def setup_method(self) -> None:
22
+ """Reset cleanup state before each test."""
23
+ cleanup_mod._cleanup_done = False
24
+ cleanup_mod._registered_backends.clear()
25
+
26
+ def test_register_and_unregister(self) -> None:
27
+ backend = MagicMock()
28
+ register_backend(backend)
29
+ unregister_backend(backend)
30
+
31
+ def test_cleanup_with_no_backends(self) -> None:
32
+ _cleanup_sync()
33
+
34
+ def test_cleanup_closes_registered_backends(self) -> None:
35
+ backend = MagicMock()
36
+ backend.close = AsyncMock()
37
+ register_backend(backend)
38
+ _cleanup_sync()
39
+ backend.close.assert_called_once()
40
+
41
+ def test_cleanup_is_idempotent(self) -> None:
42
+ backend = MagicMock()
43
+ backend.close = AsyncMock()
44
+ register_backend(backend)
45
+ _cleanup_sync()
46
+ _cleanup_sync()
47
+ backend.close.assert_called_once()
48
+
49
+ def test_cleanup_swallows_errors(self) -> None:
50
+ backend = MagicMock()
51
+ backend.close = AsyncMock(side_effect=RuntimeError("boom"))
52
+ register_backend(backend)
53
+ _cleanup_sync()
@@ -1,3 +1,3 @@
1
1
  """wavexis — CLI de automatización de navegador."""
2
2
 
3
- __version__ = "2.2.1"
3
+ __version__ = "2.2.3"
@@ -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."""
@@ -1026,6 +1026,23 @@ class CDPBackend(AbstractBackend):
1026
1026
  cy = (min(ys) + max(ys)) / 2
1027
1027
  return cx, cy
1028
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
+
1029
1046
  async def click(
1030
1047
  self, selector: str, button: str = "left", click_count: int = 1
1031
1048
  ) -> None:
@@ -1037,6 +1054,7 @@ class CDPBackend(AbstractBackend):
1037
1054
  click_count: Number of clicks to dispatch.
1038
1055
  """
1039
1056
  session = self._require_session()
1057
+ await self._scroll_into_view_if_needed(selector)
1040
1058
  x, y = await self._get_box_center(selector)
1041
1059
  btn_map = {"left": "left", "right": "right", "middle": "middle"}
1042
1060
  btn = btn_map.get(button, "left")
@@ -1074,6 +1092,7 @@ class CDPBackend(AbstractBackend):
1074
1092
  value: Value to set in the input field.
1075
1093
  """
1076
1094
  session = self._require_session()
1095
+ await self._scroll_into_view_if_needed(selector)
1077
1096
  escaped = selector.replace("'", "\\'")
1078
1097
  js = (
1079
1098
  f"(function(){{var el=document.querySelector('{escaped}');"
@@ -1113,6 +1132,7 @@ class CDPBackend(AbstractBackend):
1113
1132
  selector: CSS selector for the target element.
1114
1133
  """
1115
1134
  session = self._require_session()
1135
+ await self._scroll_into_view_if_needed(selector)
1116
1136
  x, y = await self._get_box_center(selector)
1117
1137
  await session.input.dispatch_mouse_event(
1118
1138
  type_="mouseMoved", x=x, y=y
@@ -0,0 +1,96 @@
1
+ """Resource cleanup for orphaned browser processes.
2
+
3
+ Registers atexit and signal handlers to ensure browser backends are closed
4
+ even when the process crashes or is interrupted.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import atexit
10
+ import contextlib
11
+ import logging
12
+ import signal
13
+ import sys
14
+ from typing import Any
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ _registered_backends: list[Any] = []
19
+ _cleanup_done = False
20
+
21
+ _SIGNAL_NAMES: dict[int, str] = {
22
+ signal.SIGINT: "SIGINT",
23
+ signal.SIGTERM: "SIGTERM",
24
+ }
25
+
26
+ with contextlib.suppress(AttributeError):
27
+ _SIGNAL_NAMES[signal.SIGBREAK] = "SIGBREAK"
28
+
29
+
30
+ def register_backend(backend: Any) -> None:
31
+ """Register a backend for cleanup on exit.
32
+
33
+ Args:
34
+ backend: A backend instance with an async ``close()`` method.
35
+ """
36
+ _registered_backends.append(backend)
37
+
38
+
39
+ def unregister_backend(backend: Any) -> None:
40
+ """Unregister a backend after it has been closed normally.
41
+
42
+ Args:
43
+ backend: The backend instance to remove from cleanup tracking.
44
+ """
45
+ if backend in _registered_backends:
46
+ _registered_backends.remove(backend)
47
+
48
+
49
+ def _cleanup_sync() -> None:
50
+ """Synchronously attempt to close all registered backends.
51
+
52
+ Uses ``asyncio.run`` if there are backends to close.
53
+ """
54
+ global _cleanup_done
55
+ if _cleanup_done:
56
+ return
57
+ _cleanup_done = True
58
+
59
+ if not _registered_backends:
60
+ return
61
+
62
+ import asyncio
63
+
64
+ async def _close_all() -> None:
65
+ for backend in _registered_backends:
66
+ with contextlib.suppress(Exception):
67
+ await backend.close()
68
+
69
+ with contextlib.suppress(Exception):
70
+ asyncio.run(_close_all())
71
+
72
+ _registered_backends.clear()
73
+
74
+
75
+ def _signal_handler(signum: int, frame: Any) -> None:
76
+ """Handle signals by cleaning up and re-raising.
77
+
78
+ Args:
79
+ signum: Signal number received.
80
+ frame: Current stack frame.
81
+ """
82
+ _cleanup_sync()
83
+ name = _SIGNAL_NAMES.get(signum, str(signum))
84
+ sys.stderr.write(f"\nwavexis: received {name}, cleaning up…\n")
85
+ sys.exit(128 + signum)
86
+
87
+
88
+ def _setup_signal_handlers() -> None:
89
+ """Register signal handlers for cleanup."""
90
+ for sig in _SIGNAL_NAMES:
91
+ with contextlib.suppress(OSError, ValueError):
92
+ signal.signal(sig, _signal_handler)
93
+
94
+
95
+ atexit.register(_cleanup_sync)
96
+ _setup_signal_handlers()
@@ -11,6 +11,7 @@ from typing import Any
11
11
  import typer
12
12
 
13
13
  from wavexis.backend.manager import get_manager
14
+ from wavexis.cleanup import register_backend, unregister_backend
14
15
  from wavexis.config import (
15
16
  DEVICE_PRESETS,
16
17
  BrowserOptions,
@@ -51,6 +52,7 @@ __all__ = [
51
52
  "_write_json_output",
52
53
  "app",
53
54
  "get_manager",
55
+ "unregister_backend",
54
56
  ]
55
57
 
56
58
  EXIT_SUCCESS = 0
@@ -229,8 +231,13 @@ def _run_async(coro: Any) -> Any:
229
231
 
230
232
 
231
233
  def _get_backend() -> Any:
232
- """Select a backend using the preferred backend if set."""
233
- return get_manager().select(_get_ctx().preferred_backend)
234
+ """Select a backend using the preferred backend if set.
235
+
236
+ Registers the backend for automatic cleanup on crash or signal.
237
+ """
238
+ backend = get_manager().select(_get_ctx().preferred_backend)
239
+ register_backend(backend)
240
+ return backend
234
241
 
235
242
 
236
243
  def _browser_options() -> BrowserOptions:
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
File without changes