wavexis 2.2.6__tar.gz → 2.3.1__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 (214) hide show
  1. {wavexis-2.2.6 → wavexis-2.3.1}/CHANGELOG.md +16 -0
  2. {wavexis-2.2.6 → wavexis-2.3.1}/PKG-INFO +1 -1
  3. {wavexis-2.2.6 → wavexis-2.3.1}/pyproject.toml +1 -1
  4. wavexis-2.3.1/tests/unit/test_auto_wait.py +126 -0
  5. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_bidi_phase5.py +3 -1
  6. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/__init__.py +1 -1
  7. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/backend/base.py +9 -3
  8. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/backend/bidi.py +69 -7
  9. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/backend/cdp.py +46 -3
  10. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cleanup.py +3 -2
  11. {wavexis-2.2.6 → wavexis-2.3.1}/.dockerignore +0 -0
  12. {wavexis-2.2.6 → wavexis-2.3.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  13. {wavexis-2.2.6 → wavexis-2.3.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  14. {wavexis-2.2.6 → wavexis-2.3.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  15. {wavexis-2.2.6 → wavexis-2.3.1}/.github/workflows/ci.yml +0 -0
  16. {wavexis-2.2.6 → wavexis-2.3.1}/.github/workflows/docs.yml +0 -0
  17. {wavexis-2.2.6 → wavexis-2.3.1}/.github/workflows/release.yml +0 -0
  18. {wavexis-2.2.6 → wavexis-2.3.1}/.gitignore +0 -0
  19. {wavexis-2.2.6 → wavexis-2.3.1}/CODE_OF_CONDUCT.md +0 -0
  20. {wavexis-2.2.6 → wavexis-2.3.1}/CONTRIBUTING.md +0 -0
  21. {wavexis-2.2.6 → wavexis-2.3.1}/Dockerfile +0 -0
  22. {wavexis-2.2.6 → wavexis-2.3.1}/LICENSE +0 -0
  23. {wavexis-2.2.6 → wavexis-2.3.1}/README.md +0 -0
  24. {wavexis-2.2.6 → wavexis-2.3.1}/SECURITY.md +0 -0
  25. {wavexis-2.2.6 → wavexis-2.3.1}/docs/api/actions.md +0 -0
  26. {wavexis-2.2.6 → wavexis-2.3.1}/docs/api/backends.md +0 -0
  27. {wavexis-2.2.6 → wavexis-2.3.1}/docs/api/cli.md +0 -0
  28. {wavexis-2.2.6 → wavexis-2.3.1}/docs/api/config.md +0 -0
  29. {wavexis-2.2.6 → wavexis-2.3.1}/docs/api/exceptions.md +0 -0
  30. {wavexis-2.2.6 → wavexis-2.3.1}/docs/api/multi.md +0 -0
  31. {wavexis-2.2.6 → wavexis-2.3.1}/docs/api/plugins.md +0 -0
  32. {wavexis-2.2.6 → wavexis-2.3.1}/docs/api/serve.md +0 -0
  33. {wavexis-2.2.6 → wavexis-2.3.1}/docs/cookbook/auth.md +0 -0
  34. {wavexis-2.2.6 → wavexis-2.3.1}/docs/cookbook/ci-cd.md +0 -0
  35. {wavexis-2.2.6 → wavexis-2.3.1}/docs/cookbook/css.md +0 -0
  36. {wavexis-2.2.6 → wavexis-2.3.1}/docs/cookbook/debug.md +0 -0
  37. {wavexis-2.2.6 → wavexis-2.3.1}/docs/cookbook/record-replay.md +0 -0
  38. {wavexis-2.2.6 → wavexis-2.3.1}/docs/cookbook/scraping.md +0 -0
  39. {wavexis-2.2.6 → wavexis-2.3.1}/docs/cookbook/screenshots.md +0 -0
  40. {wavexis-2.2.6 → wavexis-2.3.1}/docs/cookbook/serve-mode.md +0 -0
  41. {wavexis-2.2.6 → wavexis-2.3.1}/docs/guide/assert.md +0 -0
  42. {wavexis-2.2.6 → wavexis-2.3.1}/docs/guide/backends.md +0 -0
  43. {wavexis-2.2.6 → wavexis-2.3.1}/docs/guide/commands.md +0 -0
  44. {wavexis-2.2.6 → wavexis-2.3.1}/docs/guide/init.md +0 -0
  45. {wavexis-2.2.6 → wavexis-2.3.1}/docs/guide/installation.md +0 -0
  46. {wavexis-2.2.6 → wavexis-2.3.1}/docs/guide/multi.md +0 -0
  47. {wavexis-2.2.6 → wavexis-2.3.1}/docs/guide/perf.md +0 -0
  48. {wavexis-2.2.6 → wavexis-2.3.1}/docs/guide/plugins.md +0 -0
  49. {wavexis-2.2.6 → wavexis-2.3.1}/docs/guide/quickstart.md +0 -0
  50. {wavexis-2.2.6 → wavexis-2.3.1}/docs/guide/raw.md +0 -0
  51. {wavexis-2.2.6 → wavexis-2.3.1}/docs/guide/repl.md +0 -0
  52. {wavexis-2.2.6 → wavexis-2.3.1}/docs/guide/troubleshooting.md +0 -0
  53. {wavexis-2.2.6 → wavexis-2.3.1}/docs/index.md +0 -0
  54. {wavexis-2.2.6 → wavexis-2.3.1}/mkdocs.yml +0 -0
  55. {wavexis-2.2.6 → wavexis-2.3.1}/stubs/bidiwave/__init__.pyi +0 -0
  56. {wavexis-2.2.6 → wavexis-2.3.1}/stubs/cdpwave/__init__.pyi +0 -0
  57. {wavexis-2.2.6 → wavexis-2.3.1}/tests/__init__.py +0 -0
  58. {wavexis-2.2.6 → wavexis-2.3.1}/tests/conftest.py +0 -0
  59. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/__init__.py +0 -0
  60. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_a11y.py +0 -0
  61. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_animation.py +0 -0
  62. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_backend_selection.py +0 -0
  63. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_browser.py +0 -0
  64. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_console.py +0 -0
  65. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_css.py +0 -0
  66. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_debug.py +0 -0
  67. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_dialog.py +0 -0
  68. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_dom.py +0 -0
  69. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_dom_snapshot.py +0 -0
  70. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_emulation.py +0 -0
  71. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_emulation_advanced.py +0 -0
  72. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_eval.py +0 -0
  73. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_har.py +0 -0
  74. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_input.py +0 -0
  75. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_media.py +0 -0
  76. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_multi.py +0 -0
  77. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_navigate.py +0 -0
  78. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_network.py +0 -0
  79. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_network_advanced.py +0 -0
  80. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_overlay.py +0 -0
  81. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_pdf.py +0 -0
  82. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_perf.py +0 -0
  83. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_permissions.py +0 -0
  84. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_raw.py +0 -0
  85. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_record.py +0 -0
  86. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_scrape.py +0 -0
  87. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_screenshot.py +0 -0
  88. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_security.py +0 -0
  89. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_serve.py +0 -0
  90. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_service_worker.py +0 -0
  91. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_storage.py +0 -0
  92. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_tabs.py +0 -0
  93. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_webaudio.py +0 -0
  94. {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_webauthn.py +0 -0
  95. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/__init__.py +0 -0
  96. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_abstract_backend_phase5.py +0 -0
  97. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_actions.py +0 -0
  98. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_actions_phase5.py +0 -0
  99. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_animation_action.py +0 -0
  100. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_assert_multi.py +0 -0
  101. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_auth.py +0 -0
  102. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_backend_manager.py +0 -0
  103. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_bidi_backend.py +0 -0
  104. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_bluetooth_action.py +0 -0
  105. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_cast_action.py +0 -0
  106. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_cleanup.py +0 -0
  107. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_cli_phase5.py +0 -0
  108. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_config.py +0 -0
  109. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_config_phase5.py +0 -0
  110. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_cookies_headers_actions.py +0 -0
  111. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_css_action.py +0 -0
  112. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_debug_action.py +0 -0
  113. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_dom_snapshot_action.py +0 -0
  114. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_download_screencast.py +0 -0
  115. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_emulation_action.py +0 -0
  116. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_error_handling.py +0 -0
  117. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_exceptions.py +0 -0
  118. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_format_record.py +0 -0
  119. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_global_options.py +0 -0
  120. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_init.py +0 -0
  121. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_input_action.py +0 -0
  122. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_interactions_crawl.py +0 -0
  123. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_manager.py +0 -0
  124. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_media_action.py +0 -0
  125. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_multi.py +0 -0
  126. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_multi_improvements.py +0 -0
  127. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_new_features.py +0 -0
  128. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_output.py +0 -0
  129. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_overlay_action.py +0 -0
  130. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_perf_action.py +0 -0
  131. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_perf_console.py +0 -0
  132. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_plugins.py +0 -0
  133. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_raw.py +0 -0
  134. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_record.py +0 -0
  135. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_repl.py +0 -0
  136. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_serve.py +0 -0
  137. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_service_worker_action.py +0 -0
  138. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_storage_action.py +0 -0
  139. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_webaudio_action.py +0 -0
  140. {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_webauthn_action.py +0 -0
  141. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/__main__.py +0 -0
  142. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/__init__.py +0 -0
  143. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/accessibility.py +0 -0
  144. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/animation.py +0 -0
  145. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/base.py +0 -0
  146. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/bluetooth.py +0 -0
  147. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/browser.py +0 -0
  148. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/cast.py +0 -0
  149. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/console.py +0 -0
  150. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/cookies.py +0 -0
  151. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/crawl.py +0 -0
  152. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/css.py +0 -0
  153. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/debug.py +0 -0
  154. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/dialog.py +0 -0
  155. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/dom.py +0 -0
  156. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/dom_snapshot.py +0 -0
  157. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/download.py +0 -0
  158. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/emulation.py +0 -0
  159. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/eval.py +0 -0
  160. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/extract.py +0 -0
  161. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/form.py +0 -0
  162. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/har.py +0 -0
  163. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/headers.py +0 -0
  164. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/input.py +0 -0
  165. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/lighthouse.py +0 -0
  166. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/media.py +0 -0
  167. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/multi.py +0 -0
  168. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/navigate.py +0 -0
  169. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/network.py +0 -0
  170. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/overlay.py +0 -0
  171. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/pdf.py +0 -0
  172. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/performance.py +0 -0
  173. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/permissions.py +0 -0
  174. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/record.py +0 -0
  175. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/scrape.py +0 -0
  176. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/screencast.py +0 -0
  177. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/screenshot.py +0 -0
  178. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/security.py +0 -0
  179. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/service_worker.py +0 -0
  180. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/session.py +0 -0
  181. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/storage.py +0 -0
  182. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/tabs.py +0 -0
  183. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/wait.py +0 -0
  184. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/webaudio.py +0 -0
  185. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/webauthn.py +0 -0
  186. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/websocket.py +0 -0
  187. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/auth.py +0 -0
  188. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/backend/__init__.py +0 -0
  189. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/backend/manager.py +0 -0
  190. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/__init__.py +0 -0
  191. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/_advanced.py +0 -0
  192. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/_capture.py +0 -0
  193. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/_config.py +0 -0
  194. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/_debug.py +0 -0
  195. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/_emulation.py +0 -0
  196. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/_experimental.py +0 -0
  197. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/_input.py +0 -0
  198. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/_navigation.py +0 -0
  199. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/_network.py +0 -0
  200. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/_perf.py +0 -0
  201. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/_serve.py +0 -0
  202. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/_session.py +0 -0
  203. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/_shared.py +0 -0
  204. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/_workflow.py +0 -0
  205. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/app.py +0 -0
  206. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/config.py +0 -0
  207. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/exceptions.py +0 -0
  208. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/init.py +0 -0
  209. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/multi.py +0 -0
  210. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/output.py +0 -0
  211. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/plugins.py +0 -0
  212. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/record.py +0 -0
  213. {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/repl.py +0 -0
  214. {wavexis-2.2.6 → wavexis-2.3.1}/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.3.1 — 2026-07-07
6
+
7
+ ### Added
8
+
9
+ - `auto_wait: bool = True` parameter in `click`, `fill`, `hover` (both backends)
10
+ - Pass `auto_wait=False` to skip element visibility polling for instant execution
11
+
12
+ ## v2.3.0 — 2026-07-07
13
+
14
+ ### Added
15
+
16
+ - Auto-waiting before `click`, `fill`, and `hover` in both CDP and BiDi backends
17
+ - `_wait_for_element` helper polls every 100ms until element exists and is visible (non-zero size)
18
+ - Configurable timeout (default 30s) via `WaitTimeoutError` with actionable hint
19
+ - Prevents `ElementNotFoundError` on dynamically loaded content
20
+
5
21
  ## v2.2.6 — 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.6
3
+ Version: 2.3.1
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.6"
7
+ version = "2.3.1"
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,126 @@
1
+ """Unit tests for auto-waiting before click/fill/hover."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from unittest.mock import AsyncMock, MagicMock
7
+
8
+ import pytest
9
+
10
+ from wavexis.exceptions import WaitTimeoutError
11
+
12
+
13
+ @pytest.mark.unit
14
+ class TestAutoWait:
15
+ """Tests for auto-waiting before input actions."""
16
+
17
+ def test_cdp_wait_for_element_found(self) -> None:
18
+ """Test _wait_for_element returns when element is visible."""
19
+ from wavexis.backend.cdp import CDPBackend
20
+
21
+ backend = CDPBackend()
22
+ backend._session = MagicMock()
23
+ backend._session.runtime = MagicMock()
24
+ backend._session.runtime.evaluate = AsyncMock(
25
+ return_value={"result": {"value": True}}
26
+ )
27
+ asyncio.run(backend._wait_for_element("#btn", timeout_ms=100))
28
+ assert backend._session.runtime.evaluate.call_count >= 1
29
+
30
+ def test_cdp_wait_for_element_timeout(self) -> None:
31
+ """Test _wait_for_element raises WaitTimeoutError on timeout."""
32
+ from wavexis.backend.cdp import CDPBackend
33
+
34
+ backend = CDPBackend()
35
+ backend._session = MagicMock()
36
+ backend._session.runtime = MagicMock()
37
+ backend._session.runtime.evaluate = AsyncMock(
38
+ return_value={"result": {"value": False}}
39
+ )
40
+ with pytest.raises(WaitTimeoutError):
41
+ asyncio.run(backend._wait_for_element("#missing", timeout_ms=50))
42
+
43
+ def test_cdp_click_calls_wait_for_element(self) -> None:
44
+ """Test that click calls _wait_for_element before acting."""
45
+ from wavexis.backend.cdp import CDPBackend
46
+
47
+ backend = CDPBackend()
48
+ backend._session = MagicMock()
49
+ backend._session.input = MagicMock()
50
+ backend._session.input.dispatch_mouse_event = AsyncMock()
51
+
52
+ call_order: list[str] = []
53
+
54
+ async def _mock_wait(selector: str, timeout_ms: int = 30000) -> None:
55
+ call_order.append("wait")
56
+
57
+ async def _mock_scroll(selector: str) -> None:
58
+ call_order.append("scroll")
59
+
60
+ async def _mock_box(selector: str) -> tuple[float, float]:
61
+ call_order.append("box")
62
+ return 50.0, 25.0
63
+
64
+ backend._wait_for_element = _mock_wait # type: ignore[assignment]
65
+ backend._scroll_into_view_if_needed = _mock_scroll # type: ignore[assignment]
66
+ backend._get_box_center = _mock_box # type: ignore[assignment]
67
+
68
+ asyncio.run(backend.click("#btn"))
69
+ assert call_order == ["wait", "scroll", "box"]
70
+
71
+ def test_cdp_click_auto_wait_false_skips_wait(self) -> None:
72
+ """Test that click with auto_wait=False skips _wait_for_element."""
73
+ from wavexis.backend.cdp import CDPBackend
74
+
75
+ backend = CDPBackend()
76
+ backend._session = MagicMock()
77
+ backend._session.input = MagicMock()
78
+ backend._session.input.dispatch_mouse_event = AsyncMock()
79
+
80
+ call_order: list[str] = []
81
+
82
+ async def _mock_wait(selector: str, timeout_ms: int = 30000) -> None:
83
+ call_order.append("wait")
84
+
85
+ async def _mock_scroll(selector: str) -> None:
86
+ call_order.append("scroll")
87
+
88
+ async def _mock_box(selector: str) -> tuple[float, float]:
89
+ call_order.append("box")
90
+ return 50.0, 25.0
91
+
92
+ backend._wait_for_element = _mock_wait # type: ignore[assignment]
93
+ backend._scroll_into_view_if_needed = _mock_scroll # type: ignore[assignment]
94
+ backend._get_box_center = _mock_box # type: ignore[assignment]
95
+
96
+ asyncio.run(backend.click("#btn", auto_wait=False))
97
+ assert "wait" not in call_order
98
+ assert call_order == ["scroll", "box"]
99
+
100
+ def test_bidi_wait_for_element_found(self) -> None:
101
+ """Test BiDi _wait_for_element returns when element is visible."""
102
+ from wavexis.backend.bidi import BiDiBackend
103
+
104
+ backend = BiDiBackend()
105
+ backend._client = MagicMock()
106
+ backend._context = MagicMock()
107
+ backend._client.script = MagicMock()
108
+ result = MagicMock()
109
+ result.value = True
110
+ backend._client.script.evaluate = AsyncMock(return_value=result)
111
+ asyncio.run(backend._wait_for_element("#btn", timeout_ms=100))
112
+ assert backend._client.script.evaluate.call_count >= 1
113
+
114
+ def test_bidi_wait_for_element_timeout(self) -> None:
115
+ """Test BiDi _wait_for_element raises WaitTimeoutError on timeout."""
116
+ from wavexis.backend.bidi import BiDiBackend
117
+
118
+ backend = BiDiBackend()
119
+ backend._client = MagicMock()
120
+ backend._context = MagicMock()
121
+ backend._client.script = MagicMock()
122
+ result = MagicMock()
123
+ result.value = False
124
+ backend._client.script.evaluate = AsyncMock(return_value=result)
125
+ with pytest.raises(WaitTimeoutError):
126
+ asyncio.run(backend._wait_for_element("#missing", timeout_ms=50))
@@ -20,7 +20,9 @@ class TestBiDiBackendPhase5:
20
20
  backend._client = MagicMock()
21
21
  backend._context = MagicMock()
22
22
  backend._client.script = MagicMock()
23
- backend._client.script.evaluate = AsyncMock()
23
+ result = MagicMock()
24
+ result.value = True
25
+ backend._client.script.evaluate = AsyncMock(return_value=result)
24
26
  backend._client._connection = MagicMock()
25
27
  backend._client._connection.send_command = AsyncMock()
26
28
  return backend
@@ -1,3 +1,3 @@
1
1
  """wavexis — CLI de automatización de navegador."""
2
2
 
3
- __version__ = "2.2.6"
3
+ __version__ = "2.3.1"
@@ -237,7 +237,11 @@ class AbstractBackend(ABC):
237
237
 
238
238
  @abstractmethod
239
239
  async def click(
240
- self, selector: str, button: str = "left", click_count: int = 1
240
+ self,
241
+ selector: str,
242
+ button: str = "left",
243
+ click_count: int = 1,
244
+ auto_wait: bool = True,
241
245
  ) -> None:
242
246
  """Click an element matching a CSS selector."""
243
247
 
@@ -246,7 +250,9 @@ class AbstractBackend(ABC):
246
250
  """Type text into an element, optionally with delay between keystrokes."""
247
251
 
248
252
  @abstractmethod
249
- async def fill(self, selector: str, value: str) -> None:
253
+ async def fill(
254
+ self, selector: str, value: str, auto_wait: bool = True
255
+ ) -> None:
250
256
  """Fill an input element with a value (replaces existing content)."""
251
257
 
252
258
  @abstractmethod
@@ -254,7 +260,7 @@ class AbstractBackend(ABC):
254
260
  """Select an option in a <select> element by value."""
255
261
 
256
262
  @abstractmethod
257
- async def hover(self, selector: str) -> None:
263
+ async def hover(self, selector: str, auto_wait: bool = True) -> None:
258
264
  """Hover over an element matching a CSS selector."""
259
265
 
260
266
  @abstractmethod
@@ -26,7 +26,7 @@ from wavexis.config import (
26
26
  ThrottleParams,
27
27
  WaitStrategy,
28
28
  )
29
- from wavexis.exceptions import SessionNotInitializedError
29
+ from wavexis.exceptions import SessionNotInitializedError, WaitTimeoutError
30
30
 
31
31
  try:
32
32
  from bidiwave import BiDiClient
@@ -807,6 +807,38 @@ class BiDiBackend(AbstractBackend):
807
807
 
808
808
  # ── Input ──────────────────────────────────────────────
809
809
 
810
+ async def _wait_for_element(self, selector: str, timeout_ms: int = 30000) -> None:
811
+ """Wait for an element to exist and be visible in the DOM.
812
+
813
+ Polls until the element matches, is attached, and has non-zero size.
814
+
815
+ Args:
816
+ selector: CSS selector for the target element.
817
+ timeout_ms: Maximum wait time in milliseconds.
818
+
819
+ Raises:
820
+ WaitTimeoutError: If the element is not found within the timeout.
821
+ """
822
+ import asyncio as _asyncio
823
+ import time as _time
824
+
825
+ client = self._require_client()
826
+ escaped = selector.replace("'", "\\'")
827
+ js = (
828
+ f"(function(){{var el=document.querySelector('{escaped}');"
829
+ f"if(!el)return false;"
830
+ f"var rect=el.getBoundingClientRect();"
831
+ f"return rect.width>0&&rect.height>0;}})()"
832
+ )
833
+ deadline = _time.monotonic() + timeout_ms / 1000
834
+ while _time.monotonic() < deadline:
835
+ result = await client.script.evaluate(self._context, js)
836
+ value = getattr(result, "value", None)
837
+ if value is True or value == "true":
838
+ return
839
+ await _asyncio.sleep(0.1)
840
+ raise WaitTimeoutError("selector", timeout_ms)
841
+
810
842
  async def _scroll_into_view_if_needed(self, selector: str) -> None:
811
843
  """Scroll element into view if it's not visible in the viewport."""
812
844
  client = self._require_client()
@@ -821,10 +853,23 @@ class BiDiBackend(AbstractBackend):
821
853
  await client.script.evaluate(self._context, js)
822
854
 
823
855
  async def click(
824
- self, selector: str, button: str = "left", click_count: int = 1
856
+ self,
857
+ selector: str,
858
+ button: str = "left",
859
+ click_count: int = 1,
860
+ auto_wait: bool = True,
825
861
  ) -> None:
826
- """Click an element via BiDi script.evaluate."""
862
+ """Click an element via BiDi script.evaluate.
863
+
864
+ Args:
865
+ selector: CSS selector for the target element.
866
+ button: Mouse button (unused in BiDi, for API compatibility).
867
+ click_count: Number of clicks to dispatch.
868
+ auto_wait: If True, wait for element to be visible before clicking.
869
+ """
827
870
  client = self._require_client()
871
+ if auto_wait:
872
+ await self._wait_for_element(selector)
828
873
  await self._scroll_into_view_if_needed(selector)
829
874
  escaped = selector.replace("'", "\\'")
830
875
  js = (
@@ -854,9 +899,19 @@ class BiDiBackend(AbstractBackend):
854
899
  if delay > 0:
855
900
  await _asyncio.sleep(delay / 1000)
856
901
 
857
- async def fill(self, selector: str, value: str) -> None:
858
- """Fill an input element with a value via BiDi."""
902
+ async def fill(
903
+ self, selector: str, value: str, auto_wait: bool = True
904
+ ) -> None:
905
+ """Fill an input element with a value via BiDi.
906
+
907
+ Args:
908
+ selector: CSS selector for the target element.
909
+ value: Value to set in the input field.
910
+ auto_wait: If True, wait for element to be visible before filling.
911
+ """
859
912
  client = self._require_client()
913
+ if auto_wait:
914
+ await self._wait_for_element(selector)
860
915
  await self._scroll_into_view_if_needed(selector)
861
916
  escaped = selector.replace("'", "\\'")
862
917
  escaped_val = value.replace("\\", "\\\\").replace("'", "\\'")
@@ -872,9 +927,16 @@ class BiDiBackend(AbstractBackend):
872
927
  js = f"document.querySelector('{escaped}').value = '{escaped_val}'"
873
928
  await self._client.script.evaluate(self._context, js)
874
929
 
875
- async def hover(self, selector: str) -> None:
876
- """Hover over an element via BiDi script.evaluate."""
930
+ async def hover(self, selector: str, auto_wait: bool = True) -> None:
931
+ """Hover over an element via BiDi script.evaluate.
932
+
933
+ Args:
934
+ selector: CSS selector for the target element.
935
+ auto_wait: If True, wait for element to be visible before hovering.
936
+ """
877
937
  client = self._require_client()
938
+ if auto_wait:
939
+ await self._wait_for_element(selector)
878
940
  await self._scroll_into_view_if_needed(selector)
879
941
  escaped = selector.replace("'", "\\'")
880
942
  js = (
@@ -1026,6 +1026,34 @@ class CDPBackend(AbstractBackend):
1026
1026
  cy = (min(ys) + max(ys)) / 2
1027
1027
  return cx, cy
1028
1028
 
1029
+ async def _wait_for_element(self, selector: str, timeout_ms: int = 30000) -> None:
1030
+ """Wait for an element to exist and be visible in the DOM.
1031
+
1032
+ Polls until the element matches, is attached, and has non-zero size.
1033
+
1034
+ Args:
1035
+ selector: CSS selector for the target element.
1036
+ timeout_ms: Maximum wait time in milliseconds.
1037
+
1038
+ Raises:
1039
+ WaitTimeoutError: If the element is not found within the timeout.
1040
+ """
1041
+ session = self._require_session()
1042
+ escaped = selector.replace("'", "\\'")
1043
+ js = (
1044
+ f"(function(){{var el=document.querySelector('{escaped}');"
1045
+ f"if(!el)return false;"
1046
+ f"var rect=el.getBoundingClientRect();"
1047
+ f"return rect.width>0&&rect.height>0;}})()"
1048
+ )
1049
+ deadline = time.monotonic() + timeout_ms / 1000
1050
+ while time.monotonic() < deadline:
1051
+ result = await session.runtime.evaluate(js)
1052
+ if result.get("result", {}).get("value") is True:
1053
+ return
1054
+ await asyncio.sleep(0.1)
1055
+ raise WaitTimeoutError("selector", timeout_ms)
1056
+
1029
1057
  async def _scroll_into_view_if_needed(self, selector: str) -> None:
1030
1058
  """Scroll element into view if it's not visible in the viewport.
1031
1059
 
@@ -1044,7 +1072,11 @@ class CDPBackend(AbstractBackend):
1044
1072
  await session.runtime.evaluate(js)
1045
1073
 
1046
1074
  async def click(
1047
- self, selector: str, button: str = "left", click_count: int = 1
1075
+ self,
1076
+ selector: str,
1077
+ button: str = "left",
1078
+ click_count: int = 1,
1079
+ auto_wait: bool = True,
1048
1080
  ) -> None:
1049
1081
  """Click an element matching a CSS selector.
1050
1082
 
@@ -1052,8 +1084,11 @@ class CDPBackend(AbstractBackend):
1052
1084
  selector: CSS selector for the target element.
1053
1085
  button: Mouse button — "left", "right", or "middle".
1054
1086
  click_count: Number of clicks to dispatch.
1087
+ auto_wait: If True, wait for element to be visible before clicking.
1055
1088
  """
1056
1089
  session = self._require_session()
1090
+ if auto_wait:
1091
+ await self._wait_for_element(selector)
1057
1092
  await self._scroll_into_view_if_needed(selector)
1058
1093
  x, y = await self._get_box_center(selector)
1059
1094
  btn_map = {"left": "left", "right": "right", "middle": "middle"}
@@ -1084,14 +1119,19 @@ class CDPBackend(AbstractBackend):
1084
1119
  if delay > 0:
1085
1120
  await asyncio.sleep(delay / 1000)
1086
1121
 
1087
- async def fill(self, selector: str, value: str) -> None:
1122
+ async def fill(
1123
+ self, selector: str, value: str, auto_wait: bool = True
1124
+ ) -> None:
1088
1125
  """Fill an input element with a value (replaces existing content).
1089
1126
 
1090
1127
  Args:
1091
1128
  selector: CSS selector for the target element.
1092
1129
  value: Value to set in the input field.
1130
+ auto_wait: If True, wait for element to be visible before filling.
1093
1131
  """
1094
1132
  session = self._require_session()
1133
+ if auto_wait:
1134
+ await self._wait_for_element(selector)
1095
1135
  await self._scroll_into_view_if_needed(selector)
1096
1136
  escaped = selector.replace("'", "\\'")
1097
1137
  js = (
@@ -1125,13 +1165,16 @@ class CDPBackend(AbstractBackend):
1125
1165
  if not result.get("result", {}).get("value"):
1126
1166
  raise ElementNotFoundError(selector)
1127
1167
 
1128
- async def hover(self, selector: str) -> None:
1168
+ async def hover(self, selector: str, auto_wait: bool = True) -> None:
1129
1169
  """Hover over an element matching a CSS selector.
1130
1170
 
1131
1171
  Args:
1132
1172
  selector: CSS selector for the target element.
1173
+ auto_wait: If True, wait for element to be visible before hovering.
1133
1174
  """
1134
1175
  session = self._require_session()
1176
+ if auto_wait:
1177
+ await self._wait_for_element(selector)
1135
1178
  await self._scroll_into_view_if_needed(selector)
1136
1179
  x, y = await self._get_box_center(selector)
1137
1180
  await session.input.dispatch_mouse_event(
@@ -23,8 +23,9 @@ _SIGNAL_NAMES: dict[int, str] = {
23
23
  signal.SIGTERM: "SIGTERM",
24
24
  }
25
25
 
26
- with contextlib.suppress(AttributeError):
27
- _SIGNAL_NAMES[signal.SIGBREAK] = "SIGBREAK"
26
+ _sigbreak = getattr(signal, "SIGBREAK", None)
27
+ if _sigbreak is not None:
28
+ _SIGNAL_NAMES[_sigbreak] = "SIGBREAK"
28
29
 
29
30
 
30
31
  def register_backend(backend: Any) -> None:
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