wavexis 2.2.6__tar.gz → 2.3.0__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.0}/CHANGELOG.md +9 -0
  2. {wavexis-2.2.6 → wavexis-2.3.0}/PKG-INFO +1 -1
  3. {wavexis-2.2.6 → wavexis-2.3.0}/pyproject.toml +1 -1
  4. wavexis-2.3.0/tests/unit/test_auto_wait.py +97 -0
  5. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_bidi_phase5.py +3 -1
  6. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/__init__.py +1 -1
  7. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/backend/bidi.py +36 -1
  8. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/backend/cdp.py +31 -0
  9. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cleanup.py +3 -2
  10. {wavexis-2.2.6 → wavexis-2.3.0}/.dockerignore +0 -0
  11. {wavexis-2.2.6 → wavexis-2.3.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  12. {wavexis-2.2.6 → wavexis-2.3.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  13. {wavexis-2.2.6 → wavexis-2.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  14. {wavexis-2.2.6 → wavexis-2.3.0}/.github/workflows/ci.yml +0 -0
  15. {wavexis-2.2.6 → wavexis-2.3.0}/.github/workflows/docs.yml +0 -0
  16. {wavexis-2.2.6 → wavexis-2.3.0}/.github/workflows/release.yml +0 -0
  17. {wavexis-2.2.6 → wavexis-2.3.0}/.gitignore +0 -0
  18. {wavexis-2.2.6 → wavexis-2.3.0}/CODE_OF_CONDUCT.md +0 -0
  19. {wavexis-2.2.6 → wavexis-2.3.0}/CONTRIBUTING.md +0 -0
  20. {wavexis-2.2.6 → wavexis-2.3.0}/Dockerfile +0 -0
  21. {wavexis-2.2.6 → wavexis-2.3.0}/LICENSE +0 -0
  22. {wavexis-2.2.6 → wavexis-2.3.0}/README.md +0 -0
  23. {wavexis-2.2.6 → wavexis-2.3.0}/SECURITY.md +0 -0
  24. {wavexis-2.2.6 → wavexis-2.3.0}/docs/api/actions.md +0 -0
  25. {wavexis-2.2.6 → wavexis-2.3.0}/docs/api/backends.md +0 -0
  26. {wavexis-2.2.6 → wavexis-2.3.0}/docs/api/cli.md +0 -0
  27. {wavexis-2.2.6 → wavexis-2.3.0}/docs/api/config.md +0 -0
  28. {wavexis-2.2.6 → wavexis-2.3.0}/docs/api/exceptions.md +0 -0
  29. {wavexis-2.2.6 → wavexis-2.3.0}/docs/api/multi.md +0 -0
  30. {wavexis-2.2.6 → wavexis-2.3.0}/docs/api/plugins.md +0 -0
  31. {wavexis-2.2.6 → wavexis-2.3.0}/docs/api/serve.md +0 -0
  32. {wavexis-2.2.6 → wavexis-2.3.0}/docs/cookbook/auth.md +0 -0
  33. {wavexis-2.2.6 → wavexis-2.3.0}/docs/cookbook/ci-cd.md +0 -0
  34. {wavexis-2.2.6 → wavexis-2.3.0}/docs/cookbook/css.md +0 -0
  35. {wavexis-2.2.6 → wavexis-2.3.0}/docs/cookbook/debug.md +0 -0
  36. {wavexis-2.2.6 → wavexis-2.3.0}/docs/cookbook/record-replay.md +0 -0
  37. {wavexis-2.2.6 → wavexis-2.3.0}/docs/cookbook/scraping.md +0 -0
  38. {wavexis-2.2.6 → wavexis-2.3.0}/docs/cookbook/screenshots.md +0 -0
  39. {wavexis-2.2.6 → wavexis-2.3.0}/docs/cookbook/serve-mode.md +0 -0
  40. {wavexis-2.2.6 → wavexis-2.3.0}/docs/guide/assert.md +0 -0
  41. {wavexis-2.2.6 → wavexis-2.3.0}/docs/guide/backends.md +0 -0
  42. {wavexis-2.2.6 → wavexis-2.3.0}/docs/guide/commands.md +0 -0
  43. {wavexis-2.2.6 → wavexis-2.3.0}/docs/guide/init.md +0 -0
  44. {wavexis-2.2.6 → wavexis-2.3.0}/docs/guide/installation.md +0 -0
  45. {wavexis-2.2.6 → wavexis-2.3.0}/docs/guide/multi.md +0 -0
  46. {wavexis-2.2.6 → wavexis-2.3.0}/docs/guide/perf.md +0 -0
  47. {wavexis-2.2.6 → wavexis-2.3.0}/docs/guide/plugins.md +0 -0
  48. {wavexis-2.2.6 → wavexis-2.3.0}/docs/guide/quickstart.md +0 -0
  49. {wavexis-2.2.6 → wavexis-2.3.0}/docs/guide/raw.md +0 -0
  50. {wavexis-2.2.6 → wavexis-2.3.0}/docs/guide/repl.md +0 -0
  51. {wavexis-2.2.6 → wavexis-2.3.0}/docs/guide/troubleshooting.md +0 -0
  52. {wavexis-2.2.6 → wavexis-2.3.0}/docs/index.md +0 -0
  53. {wavexis-2.2.6 → wavexis-2.3.0}/mkdocs.yml +0 -0
  54. {wavexis-2.2.6 → wavexis-2.3.0}/stubs/bidiwave/__init__.pyi +0 -0
  55. {wavexis-2.2.6 → wavexis-2.3.0}/stubs/cdpwave/__init__.pyi +0 -0
  56. {wavexis-2.2.6 → wavexis-2.3.0}/tests/__init__.py +0 -0
  57. {wavexis-2.2.6 → wavexis-2.3.0}/tests/conftest.py +0 -0
  58. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/__init__.py +0 -0
  59. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_a11y.py +0 -0
  60. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_animation.py +0 -0
  61. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_backend_selection.py +0 -0
  62. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_browser.py +0 -0
  63. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_console.py +0 -0
  64. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_css.py +0 -0
  65. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_debug.py +0 -0
  66. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_dialog.py +0 -0
  67. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_dom.py +0 -0
  68. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_dom_snapshot.py +0 -0
  69. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_emulation.py +0 -0
  70. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_emulation_advanced.py +0 -0
  71. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_eval.py +0 -0
  72. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_har.py +0 -0
  73. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_input.py +0 -0
  74. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_media.py +0 -0
  75. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_multi.py +0 -0
  76. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_navigate.py +0 -0
  77. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_network.py +0 -0
  78. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_network_advanced.py +0 -0
  79. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_overlay.py +0 -0
  80. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_pdf.py +0 -0
  81. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_perf.py +0 -0
  82. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_permissions.py +0 -0
  83. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_raw.py +0 -0
  84. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_record.py +0 -0
  85. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_scrape.py +0 -0
  86. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_screenshot.py +0 -0
  87. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_security.py +0 -0
  88. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_serve.py +0 -0
  89. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_service_worker.py +0 -0
  90. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_storage.py +0 -0
  91. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_tabs.py +0 -0
  92. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_webaudio.py +0 -0
  93. {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_webauthn.py +0 -0
  94. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/__init__.py +0 -0
  95. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_abstract_backend_phase5.py +0 -0
  96. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_actions.py +0 -0
  97. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_actions_phase5.py +0 -0
  98. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_animation_action.py +0 -0
  99. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_assert_multi.py +0 -0
  100. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_auth.py +0 -0
  101. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_backend_manager.py +0 -0
  102. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_bidi_backend.py +0 -0
  103. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_bluetooth_action.py +0 -0
  104. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_cast_action.py +0 -0
  105. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_cleanup.py +0 -0
  106. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_cli_phase5.py +0 -0
  107. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_config.py +0 -0
  108. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_config_phase5.py +0 -0
  109. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_cookies_headers_actions.py +0 -0
  110. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_css_action.py +0 -0
  111. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_debug_action.py +0 -0
  112. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_dom_snapshot_action.py +0 -0
  113. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_download_screencast.py +0 -0
  114. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_emulation_action.py +0 -0
  115. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_error_handling.py +0 -0
  116. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_exceptions.py +0 -0
  117. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_format_record.py +0 -0
  118. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_global_options.py +0 -0
  119. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_init.py +0 -0
  120. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_input_action.py +0 -0
  121. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_interactions_crawl.py +0 -0
  122. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_manager.py +0 -0
  123. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_media_action.py +0 -0
  124. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_multi.py +0 -0
  125. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_multi_improvements.py +0 -0
  126. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_new_features.py +0 -0
  127. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_output.py +0 -0
  128. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_overlay_action.py +0 -0
  129. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_perf_action.py +0 -0
  130. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_perf_console.py +0 -0
  131. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_plugins.py +0 -0
  132. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_raw.py +0 -0
  133. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_record.py +0 -0
  134. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_repl.py +0 -0
  135. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_serve.py +0 -0
  136. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_service_worker_action.py +0 -0
  137. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_storage_action.py +0 -0
  138. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_webaudio_action.py +0 -0
  139. {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_webauthn_action.py +0 -0
  140. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/__main__.py +0 -0
  141. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/__init__.py +0 -0
  142. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/accessibility.py +0 -0
  143. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/animation.py +0 -0
  144. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/base.py +0 -0
  145. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/bluetooth.py +0 -0
  146. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/browser.py +0 -0
  147. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/cast.py +0 -0
  148. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/console.py +0 -0
  149. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/cookies.py +0 -0
  150. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/crawl.py +0 -0
  151. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/css.py +0 -0
  152. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/debug.py +0 -0
  153. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/dialog.py +0 -0
  154. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/dom.py +0 -0
  155. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/dom_snapshot.py +0 -0
  156. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/download.py +0 -0
  157. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/emulation.py +0 -0
  158. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/eval.py +0 -0
  159. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/extract.py +0 -0
  160. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/form.py +0 -0
  161. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/har.py +0 -0
  162. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/headers.py +0 -0
  163. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/input.py +0 -0
  164. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/lighthouse.py +0 -0
  165. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/media.py +0 -0
  166. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/multi.py +0 -0
  167. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/navigate.py +0 -0
  168. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/network.py +0 -0
  169. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/overlay.py +0 -0
  170. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/pdf.py +0 -0
  171. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/performance.py +0 -0
  172. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/permissions.py +0 -0
  173. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/record.py +0 -0
  174. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/scrape.py +0 -0
  175. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/screencast.py +0 -0
  176. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/screenshot.py +0 -0
  177. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/security.py +0 -0
  178. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/service_worker.py +0 -0
  179. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/session.py +0 -0
  180. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/storage.py +0 -0
  181. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/tabs.py +0 -0
  182. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/wait.py +0 -0
  183. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/webaudio.py +0 -0
  184. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/webauthn.py +0 -0
  185. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/websocket.py +0 -0
  186. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/auth.py +0 -0
  187. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/backend/__init__.py +0 -0
  188. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/backend/base.py +0 -0
  189. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/backend/manager.py +0 -0
  190. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/__init__.py +0 -0
  191. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/_advanced.py +0 -0
  192. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/_capture.py +0 -0
  193. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/_config.py +0 -0
  194. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/_debug.py +0 -0
  195. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/_emulation.py +0 -0
  196. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/_experimental.py +0 -0
  197. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/_input.py +0 -0
  198. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/_navigation.py +0 -0
  199. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/_network.py +0 -0
  200. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/_perf.py +0 -0
  201. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/_serve.py +0 -0
  202. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/_session.py +0 -0
  203. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/_shared.py +0 -0
  204. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/_workflow.py +0 -0
  205. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/app.py +0 -0
  206. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/config.py +0 -0
  207. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/exceptions.py +0 -0
  208. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/init.py +0 -0
  209. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/multi.py +0 -0
  210. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/output.py +0 -0
  211. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/plugins.py +0 -0
  212. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/record.py +0 -0
  213. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/repl.py +0 -0
  214. {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/serve.py +0 -0
@@ -2,6 +2,15 @@
2
2
 
3
3
  All notable changes to wavexis are documented in this file.
4
4
 
5
+ ## v2.3.0 — 2026-07-07
6
+
7
+ ### Added
8
+
9
+ - Auto-waiting before `click`, `fill`, and `hover` in both CDP and BiDi backends
10
+ - `_wait_for_element` helper polls every 100ms until element exists and is visible (non-zero size)
11
+ - Configurable timeout (default 30s) via `WaitTimeoutError` with actionable hint
12
+ - Prevents `ElementNotFoundError` on dynamically loaded content
13
+
5
14
  ## v2.2.6 — 2026-07-07
6
15
 
7
16
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavexis
3
- Version: 2.2.6
3
+ Version: 2.3.0
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.0"
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,97 @@
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_bidi_wait_for_element_found(self) -> None:
72
+ """Test BiDi _wait_for_element returns when element is visible."""
73
+ from wavexis.backend.bidi import BiDiBackend
74
+
75
+ backend = BiDiBackend()
76
+ backend._client = MagicMock()
77
+ backend._context = MagicMock()
78
+ backend._client.script = MagicMock()
79
+ result = MagicMock()
80
+ result.value = True
81
+ backend._client.script.evaluate = AsyncMock(return_value=result)
82
+ asyncio.run(backend._wait_for_element("#btn", timeout_ms=100))
83
+ assert backend._client.script.evaluate.call_count >= 1
84
+
85
+ def test_bidi_wait_for_element_timeout(self) -> None:
86
+ """Test BiDi _wait_for_element raises WaitTimeoutError on timeout."""
87
+ from wavexis.backend.bidi import BiDiBackend
88
+
89
+ backend = BiDiBackend()
90
+ backend._client = MagicMock()
91
+ backend._context = MagicMock()
92
+ backend._client.script = MagicMock()
93
+ result = MagicMock()
94
+ result.value = False
95
+ backend._client.script.evaluate = AsyncMock(return_value=result)
96
+ with pytest.raises(WaitTimeoutError):
97
+ 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.0"
@@ -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()
@@ -825,6 +857,7 @@ class BiDiBackend(AbstractBackend):
825
857
  ) -> None:
826
858
  """Click an element via BiDi script.evaluate."""
827
859
  client = self._require_client()
860
+ await self._wait_for_element(selector)
828
861
  await self._scroll_into_view_if_needed(selector)
829
862
  escaped = selector.replace("'", "\\'")
830
863
  js = (
@@ -857,6 +890,7 @@ class BiDiBackend(AbstractBackend):
857
890
  async def fill(self, selector: str, value: str) -> None:
858
891
  """Fill an input element with a value via BiDi."""
859
892
  client = self._require_client()
893
+ await self._wait_for_element(selector)
860
894
  await self._scroll_into_view_if_needed(selector)
861
895
  escaped = selector.replace("'", "\\'")
862
896
  escaped_val = value.replace("\\", "\\\\").replace("'", "\\'")
@@ -875,6 +909,7 @@ class BiDiBackend(AbstractBackend):
875
909
  async def hover(self, selector: str) -> None:
876
910
  """Hover over an element via BiDi script.evaluate."""
877
911
  client = self._require_client()
912
+ await self._wait_for_element(selector)
878
913
  await self._scroll_into_view_if_needed(selector)
879
914
  escaped = selector.replace("'", "\\'")
880
915
  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
 
@@ -1054,6 +1082,7 @@ class CDPBackend(AbstractBackend):
1054
1082
  click_count: Number of clicks to dispatch.
1055
1083
  """
1056
1084
  session = self._require_session()
1085
+ await self._wait_for_element(selector)
1057
1086
  await self._scroll_into_view_if_needed(selector)
1058
1087
  x, y = await self._get_box_center(selector)
1059
1088
  btn_map = {"left": "left", "right": "right", "middle": "middle"}
@@ -1092,6 +1121,7 @@ class CDPBackend(AbstractBackend):
1092
1121
  value: Value to set in the input field.
1093
1122
  """
1094
1123
  session = self._require_session()
1124
+ await self._wait_for_element(selector)
1095
1125
  await self._scroll_into_view_if_needed(selector)
1096
1126
  escaped = selector.replace("'", "\\'")
1097
1127
  js = (
@@ -1132,6 +1162,7 @@ class CDPBackend(AbstractBackend):
1132
1162
  selector: CSS selector for the target element.
1133
1163
  """
1134
1164
  session = self._require_session()
1165
+ await self._wait_for_element(selector)
1135
1166
  await self._scroll_into_view_if_needed(selector)
1136
1167
  x, y = await self._get_box_center(selector)
1137
1168
  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
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