wavexis 2.3.4__tar.gz → 2.3.5__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 (221) hide show
  1. {wavexis-2.3.4 → wavexis-2.3.5}/CHANGELOG.md +12 -0
  2. {wavexis-2.3.4 → wavexis-2.3.5}/PKG-INFO +1 -1
  3. {wavexis-2.3.4 → wavexis-2.3.5}/pyproject.toml +1 -1
  4. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_backend_manager.py +16 -0
  5. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_manager.py +7 -0
  6. wavexis-2.3.5/tests/unit/test_nl.py +180 -0
  7. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/__init__.py +1 -1
  8. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/backend/base.py +43 -0
  9. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/backend/bidi.py +108 -0
  10. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/backend/cdp.py +108 -0
  11. wavexis-2.3.5/wavexis/cli/_nl.py +102 -0
  12. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/cli/app.py +1 -0
  13. {wavexis-2.3.4 → wavexis-2.3.5}/.dockerignore +0 -0
  14. {wavexis-2.3.4 → wavexis-2.3.5}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  15. {wavexis-2.3.4 → wavexis-2.3.5}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  16. {wavexis-2.3.4 → wavexis-2.3.5}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  17. {wavexis-2.3.4 → wavexis-2.3.5}/.github/workflows/ci.yml +0 -0
  18. {wavexis-2.3.4 → wavexis-2.3.5}/.github/workflows/docs.yml +0 -0
  19. {wavexis-2.3.4 → wavexis-2.3.5}/.github/workflows/release.yml +0 -0
  20. {wavexis-2.3.4 → wavexis-2.3.5}/.gitignore +0 -0
  21. {wavexis-2.3.4 → wavexis-2.3.5}/CODE_OF_CONDUCT.md +0 -0
  22. {wavexis-2.3.4 → wavexis-2.3.5}/CONTRIBUTING.md +0 -0
  23. {wavexis-2.3.4 → wavexis-2.3.5}/Dockerfile +0 -0
  24. {wavexis-2.3.4 → wavexis-2.3.5}/LICENSE +0 -0
  25. {wavexis-2.3.4 → wavexis-2.3.5}/README.md +0 -0
  26. {wavexis-2.3.4 → wavexis-2.3.5}/SECURITY.md +0 -0
  27. {wavexis-2.3.4 → wavexis-2.3.5}/docs/api/actions.md +0 -0
  28. {wavexis-2.3.4 → wavexis-2.3.5}/docs/api/backends.md +0 -0
  29. {wavexis-2.3.4 → wavexis-2.3.5}/docs/api/cli.md +0 -0
  30. {wavexis-2.3.4 → wavexis-2.3.5}/docs/api/config.md +0 -0
  31. {wavexis-2.3.4 → wavexis-2.3.5}/docs/api/exceptions.md +0 -0
  32. {wavexis-2.3.4 → wavexis-2.3.5}/docs/api/multi.md +0 -0
  33. {wavexis-2.3.4 → wavexis-2.3.5}/docs/api/plugins.md +0 -0
  34. {wavexis-2.3.4 → wavexis-2.3.5}/docs/api/serve.md +0 -0
  35. {wavexis-2.3.4 → wavexis-2.3.5}/docs/cookbook/auth.md +0 -0
  36. {wavexis-2.3.4 → wavexis-2.3.5}/docs/cookbook/ci-cd.md +0 -0
  37. {wavexis-2.3.4 → wavexis-2.3.5}/docs/cookbook/css.md +0 -0
  38. {wavexis-2.3.4 → wavexis-2.3.5}/docs/cookbook/debug.md +0 -0
  39. {wavexis-2.3.4 → wavexis-2.3.5}/docs/cookbook/record-replay.md +0 -0
  40. {wavexis-2.3.4 → wavexis-2.3.5}/docs/cookbook/scraping.md +0 -0
  41. {wavexis-2.3.4 → wavexis-2.3.5}/docs/cookbook/screenshots.md +0 -0
  42. {wavexis-2.3.4 → wavexis-2.3.5}/docs/cookbook/serve-mode.md +0 -0
  43. {wavexis-2.3.4 → wavexis-2.3.5}/docs/guide/assert.md +0 -0
  44. {wavexis-2.3.4 → wavexis-2.3.5}/docs/guide/backends.md +0 -0
  45. {wavexis-2.3.4 → wavexis-2.3.5}/docs/guide/commands.md +0 -0
  46. {wavexis-2.3.4 → wavexis-2.3.5}/docs/guide/init.md +0 -0
  47. {wavexis-2.3.4 → wavexis-2.3.5}/docs/guide/installation.md +0 -0
  48. {wavexis-2.3.4 → wavexis-2.3.5}/docs/guide/multi.md +0 -0
  49. {wavexis-2.3.4 → wavexis-2.3.5}/docs/guide/perf.md +0 -0
  50. {wavexis-2.3.4 → wavexis-2.3.5}/docs/guide/plugins.md +0 -0
  51. {wavexis-2.3.4 → wavexis-2.3.5}/docs/guide/quickstart.md +0 -0
  52. {wavexis-2.3.4 → wavexis-2.3.5}/docs/guide/raw.md +0 -0
  53. {wavexis-2.3.4 → wavexis-2.3.5}/docs/guide/repl.md +0 -0
  54. {wavexis-2.3.4 → wavexis-2.3.5}/docs/guide/troubleshooting.md +0 -0
  55. {wavexis-2.3.4 → wavexis-2.3.5}/docs/index.md +0 -0
  56. {wavexis-2.3.4 → wavexis-2.3.5}/mkdocs.yml +0 -0
  57. {wavexis-2.3.4 → wavexis-2.3.5}/stubs/bidiwave/__init__.pyi +0 -0
  58. {wavexis-2.3.4 → wavexis-2.3.5}/stubs/cdpwave/__init__.pyi +0 -0
  59. {wavexis-2.3.4 → wavexis-2.3.5}/tests/__init__.py +0 -0
  60. {wavexis-2.3.4 → wavexis-2.3.5}/tests/conftest.py +0 -0
  61. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/__init__.py +0 -0
  62. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_a11y.py +0 -0
  63. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_animation.py +0 -0
  64. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_backend_selection.py +0 -0
  65. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_browser.py +0 -0
  66. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_console.py +0 -0
  67. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_css.py +0 -0
  68. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_debug.py +0 -0
  69. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_dialog.py +0 -0
  70. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_dom.py +0 -0
  71. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_dom_snapshot.py +0 -0
  72. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_emulation.py +0 -0
  73. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_emulation_advanced.py +0 -0
  74. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_eval.py +0 -0
  75. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_har.py +0 -0
  76. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_input.py +0 -0
  77. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_media.py +0 -0
  78. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_multi.py +0 -0
  79. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_navigate.py +0 -0
  80. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_network.py +0 -0
  81. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_network_advanced.py +0 -0
  82. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_overlay.py +0 -0
  83. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_pdf.py +0 -0
  84. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_perf.py +0 -0
  85. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_permissions.py +0 -0
  86. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_raw.py +0 -0
  87. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_record.py +0 -0
  88. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_scrape.py +0 -0
  89. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_screenshot.py +0 -0
  90. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_security.py +0 -0
  91. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_serve.py +0 -0
  92. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_service_worker.py +0 -0
  93. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_storage.py +0 -0
  94. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_tabs.py +0 -0
  95. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_webaudio.py +0 -0
  96. {wavexis-2.3.4 → wavexis-2.3.5}/tests/integration/test_webauthn.py +0 -0
  97. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/__init__.py +0 -0
  98. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_abstract_backend_phase5.py +0 -0
  99. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_actions.py +0 -0
  100. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_actions_phase5.py +0 -0
  101. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_animation_action.py +0 -0
  102. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_assert_multi.py +0 -0
  103. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_auth.py +0 -0
  104. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_auto_wait.py +0 -0
  105. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_bidi_backend.py +0 -0
  106. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_bidi_phase5.py +0 -0
  107. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_bluetooth_action.py +0 -0
  108. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_cast_action.py +0 -0
  109. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_cleanup.py +0 -0
  110. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_cli_phase5.py +0 -0
  111. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_config.py +0 -0
  112. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_config_phase5.py +0 -0
  113. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_cookies_headers_actions.py +0 -0
  114. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_css_action.py +0 -0
  115. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_debug_action.py +0 -0
  116. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_dom_snapshot_action.py +0 -0
  117. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_download_screencast.py +0 -0
  118. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_emulation_action.py +0 -0
  119. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_error_handling.py +0 -0
  120. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_exceptions.py +0 -0
  121. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_format_record.py +0 -0
  122. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_global_options.py +0 -0
  123. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_iframe.py +0 -0
  124. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_init.py +0 -0
  125. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_input_action.py +0 -0
  126. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_interactions_crawl.py +0 -0
  127. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_media_action.py +0 -0
  128. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_multi.py +0 -0
  129. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_multi_improvements.py +0 -0
  130. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_new_features.py +0 -0
  131. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_output.py +0 -0
  132. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_overlay_action.py +0 -0
  133. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_perf_action.py +0 -0
  134. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_perf_console.py +0 -0
  135. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_plugins.py +0 -0
  136. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_raw.py +0 -0
  137. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_record.py +0 -0
  138. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_repl.py +0 -0
  139. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_serve.py +0 -0
  140. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_service_worker_action.py +0 -0
  141. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_shadow.py +0 -0
  142. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_storage_action.py +0 -0
  143. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_suggest_locator.py +0 -0
  144. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_webaudio_action.py +0 -0
  145. {wavexis-2.3.4 → wavexis-2.3.5}/tests/unit/test_webauthn_action.py +0 -0
  146. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/__main__.py +0 -0
  147. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/__init__.py +0 -0
  148. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/accessibility.py +0 -0
  149. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/animation.py +0 -0
  150. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/base.py +0 -0
  151. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/bluetooth.py +0 -0
  152. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/browser.py +0 -0
  153. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/cast.py +0 -0
  154. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/console.py +0 -0
  155. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/cookies.py +0 -0
  156. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/crawl.py +0 -0
  157. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/css.py +0 -0
  158. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/debug.py +0 -0
  159. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/dialog.py +0 -0
  160. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/dom.py +0 -0
  161. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/dom_snapshot.py +0 -0
  162. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/download.py +0 -0
  163. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/emulation.py +0 -0
  164. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/eval.py +0 -0
  165. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/extract.py +0 -0
  166. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/form.py +0 -0
  167. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/har.py +0 -0
  168. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/headers.py +0 -0
  169. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/input.py +0 -0
  170. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/lighthouse.py +0 -0
  171. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/media.py +0 -0
  172. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/multi.py +0 -0
  173. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/navigate.py +0 -0
  174. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/network.py +0 -0
  175. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/overlay.py +0 -0
  176. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/pdf.py +0 -0
  177. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/performance.py +0 -0
  178. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/permissions.py +0 -0
  179. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/record.py +0 -0
  180. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/scrape.py +0 -0
  181. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/screencast.py +0 -0
  182. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/screenshot.py +0 -0
  183. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/security.py +0 -0
  184. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/service_worker.py +0 -0
  185. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/session.py +0 -0
  186. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/storage.py +0 -0
  187. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/tabs.py +0 -0
  188. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/wait.py +0 -0
  189. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/webaudio.py +0 -0
  190. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/webauthn.py +0 -0
  191. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/actions/websocket.py +0 -0
  192. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/auth.py +0 -0
  193. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/backend/__init__.py +0 -0
  194. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/backend/manager.py +0 -0
  195. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/cleanup.py +0 -0
  196. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/cli/__init__.py +0 -0
  197. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/cli/_advanced.py +0 -0
  198. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/cli/_capture.py +0 -0
  199. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/cli/_config.py +0 -0
  200. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/cli/_debug.py +0 -0
  201. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/cli/_emulation.py +0 -0
  202. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/cli/_experimental.py +0 -0
  203. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/cli/_iframe.py +0 -0
  204. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/cli/_input.py +0 -0
  205. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/cli/_navigation.py +0 -0
  206. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/cli/_network.py +0 -0
  207. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/cli/_perf.py +0 -0
  208. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/cli/_serve.py +0 -0
  209. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/cli/_session.py +0 -0
  210. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/cli/_shadow.py +0 -0
  211. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/cli/_shared.py +0 -0
  212. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/cli/_workflow.py +0 -0
  213. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/config.py +0 -0
  214. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/exceptions.py +0 -0
  215. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/init.py +0 -0
  216. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/multi.py +0 -0
  217. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/output.py +0 -0
  218. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/plugins.py +0 -0
  219. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/record.py +0 -0
  220. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/repl.py +0 -0
  221. {wavexis-2.3.4 → wavexis-2.3.5}/wavexis/serve.py +0 -0
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to wavexis are documented in this file.
4
4
 
5
+ ## v2.3.5 — 2026-07-07
6
+
7
+ ### Added
8
+
9
+ - Natural language selector: `find_by_text`, `nl_click`, `nl_fill` in both CDP and BiDi backends
10
+ - `wavexis nl click/fill/find` CLI command with natural language queries
11
+ - Fuzzy text matching scoring: exact match (100), contains query (80), query contains text (60), word overlap (up to 50)
12
+ - Searches textContent, aria-label, placeholder, title, alt, and value attributes
13
+ - Returns best CSS selector for the matched element
14
+ - `--all` flag on `nl find` returns all matches ranked by score
15
+ - `--no-wait` flag to skip auto-waiting
16
+
5
17
  ## v2.3.4 — 2026-07-07
6
18
 
7
19
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavexis
3
- Version: 2.3.4
3
+ Version: 2.3.5
4
4
  Summary: Browser automation CLI — wraps cdpwave and bidiwave, no Node.js, no Chromium download
5
5
  Project-URL: Homepage, https://github.com/MathiasPaulenko/wavexis
6
6
  Project-URL: Repository, https://github.com/MathiasPaulenko/wavexis
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "wavexis"
7
- version = "2.3.4"
7
+ version = "2.3.5"
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" }
@@ -138,6 +138,22 @@ class DummyBackend(AbstractBackend):
138
138
  """Suggest locator."""
139
139
  return selector
140
140
 
141
+ async def find_by_text(
142
+ self, query: str, all: bool = False
143
+ ) -> list[str] | str:
144
+ """Find by text."""
145
+ return query
146
+
147
+ async def nl_click(self, query: str, auto_wait: bool = True) -> None:
148
+ """NL click."""
149
+ pass
150
+
151
+ async def nl_fill(
152
+ self, query: str, value: str, auto_wait: bool = True
153
+ ) -> None:
154
+ """NL fill."""
155
+ pass
156
+
141
157
  async def capture_har(self, params: object) -> dict:
142
158
  """Capture har."""
143
159
  return {}
@@ -69,6 +69,13 @@ class FakeBackend(AbstractBackend):
69
69
  async def suggest_locator(self, selector, all=False):
70
70
  """Suggest locator."""
71
71
  return selector
72
+ async def find_by_text(self, query, all=False):
73
+ """Find by text."""
74
+ return query
75
+ async def nl_click(self, query, auto_wait=True):
76
+ """NL click."""
77
+ async def nl_fill(self, query, value, auto_wait=True):
78
+ """NL fill."""
72
79
  async def capture_har(self, params):
73
80
  """Capture har."""
74
81
  async def get_cookies(self):
@@ -0,0 +1,180 @@
1
+ """Unit tests for natural language selector — find_by_text, nl_click, nl_fill."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ from typing import Any
8
+ from unittest.mock import AsyncMock, MagicMock
9
+
10
+ import pytest
11
+
12
+ from wavexis.exceptions import ElementNotFoundError
13
+
14
+
15
+ def _make_cdp_backend() -> Any:
16
+ """Create a CDPBackend with mocked session."""
17
+ from wavexis.backend.cdp import CDPBackend
18
+
19
+ backend = CDPBackend()
20
+ backend._session = MagicMock()
21
+ backend._session.runtime = MagicMock()
22
+ backend._session.runtime.evaluate = AsyncMock(
23
+ return_value={"result": {"value": None}}
24
+ )
25
+ return backend
26
+
27
+
28
+ def _make_bidi_backend() -> Any:
29
+ """Create a BiDiBackend with mocked client."""
30
+ from wavexis.backend.bidi import BiDiBackend
31
+
32
+ backend = BiDiBackend()
33
+ backend._client = MagicMock()
34
+ backend._context = MagicMock()
35
+ backend._client.script = MagicMock()
36
+ result = MagicMock()
37
+ result.value = None
38
+ backend._client.script.evaluate = AsyncMock(return_value=result)
39
+ return backend
40
+
41
+
42
+ @pytest.mark.unit
43
+ class TestFindByTextCDP:
44
+ """Tests for find_by_text in CDP backend."""
45
+
46
+ def test_returns_best_selector(self) -> None:
47
+ """Test that find_by_text returns the best match."""
48
+ backend = _make_cdp_backend()
49
+ selectors = ["#login-btn", "button.primary"]
50
+ backend._session.runtime.evaluate = AsyncMock(
51
+ return_value={"result": {"value": json.dumps(selectors)}}
52
+ )
53
+ result = asyncio.run(backend.find_by_text("login button"))
54
+ assert result == "#login-btn"
55
+
56
+ def test_returns_all_selectors(self) -> None:
57
+ """Test that find_by_text with all=True returns all matches."""
58
+ backend = _make_cdp_backend()
59
+ selectors = ["#login-btn", "button.primary", "button"]
60
+ backend._session.runtime.evaluate = AsyncMock(
61
+ return_value={"result": {"value": json.dumps(selectors)}}
62
+ )
63
+ result = asyncio.run(backend.find_by_text("login", all=True))
64
+ assert isinstance(result, list)
65
+ assert len(result) == 3
66
+
67
+ def test_raises_on_no_match(self) -> None:
68
+ """Test that find_by_text raises ElementNotFoundError when no match."""
69
+ backend = _make_cdp_backend()
70
+ backend._session.runtime.evaluate = AsyncMock(
71
+ return_value={"result": {"value": json.dumps([])}}
72
+ )
73
+ with pytest.raises(ElementNotFoundError):
74
+ asyncio.run(backend.find_by_text("nonexistent"))
75
+
76
+ def test_raises_on_null_result(self) -> None:
77
+ """Test that find_by_text raises when JS returns null."""
78
+ backend = _make_cdp_backend()
79
+ backend._session.runtime.evaluate = AsyncMock(
80
+ return_value={"result": {"value": None}}
81
+ )
82
+ with pytest.raises(ElementNotFoundError):
83
+ asyncio.run(backend.find_by_text("anything"))
84
+
85
+
86
+ @pytest.mark.unit
87
+ class TestFindByTextBiDi:
88
+ """Tests for find_by_text in BiDi backend."""
89
+
90
+ def test_returns_best_selector(self) -> None:
91
+ """Test that find_by_text returns the best match."""
92
+ backend = _make_bidi_backend()
93
+ selectors = ["#email-input", "input.email"]
94
+ result_mock = MagicMock()
95
+ result_mock.value = json.dumps(selectors)
96
+ backend._client.script.evaluate = AsyncMock(return_value=result_mock)
97
+ result = asyncio.run(backend.find_by_text("email field"))
98
+ assert result == "#email-input"
99
+
100
+ def test_returns_all_selectors(self) -> None:
101
+ """Test that find_by_text with all=True returns all matches."""
102
+ backend = _make_bidi_backend()
103
+ selectors = ["#email", "input[placeholder='email']", "input"]
104
+ result_mock = MagicMock()
105
+ result_mock.value = json.dumps(selectors)
106
+ backend._client.script.evaluate = AsyncMock(return_value=result_mock)
107
+ result = asyncio.run(backend.find_by_text("email", all=True))
108
+ assert isinstance(result, list)
109
+ assert len(result) == 3
110
+
111
+ def test_raises_on_no_match(self) -> None:
112
+ """Test that find_by_text raises ElementNotFoundError when no match."""
113
+ backend = _make_bidi_backend()
114
+ result_mock = MagicMock()
115
+ result_mock.value = json.dumps([])
116
+ backend._client.script.evaluate = AsyncMock(return_value=result_mock)
117
+ with pytest.raises(ElementNotFoundError):
118
+ asyncio.run(backend.find_by_text("nonexistent"))
119
+
120
+
121
+ @pytest.mark.unit
122
+ class TestNlClick:
123
+ """Tests for nl_click in both backends."""
124
+
125
+ def test_cdp_nl_click_calls_click(self) -> None:
126
+ """Test that nl_click finds element and calls click."""
127
+ backend = _make_cdp_backend()
128
+ selectors = ["#submit-btn"]
129
+ backend._session.runtime.evaluate = AsyncMock(
130
+ return_value={"result": {"value": json.dumps(selectors)}}
131
+ )
132
+ backend.click = AsyncMock()
133
+ asyncio.run(backend.nl_click("submit button", auto_wait=False))
134
+ backend.click.assert_called_once_with(
135
+ "#submit-btn", auto_wait=False
136
+ )
137
+
138
+ def test_bidi_nl_click_calls_click(self) -> None:
139
+ """Test that nl_click finds element and calls click."""
140
+ backend = _make_bidi_backend()
141
+ selectors = ["#submit-btn"]
142
+ result_mock = MagicMock()
143
+ result_mock.value = json.dumps(selectors)
144
+ backend._client.script.evaluate = AsyncMock(return_value=result_mock)
145
+ backend.click = AsyncMock()
146
+ asyncio.run(backend.nl_click("submit button", auto_wait=False))
147
+ backend.click.assert_called_once_with(
148
+ "#submit-btn", auto_wait=False
149
+ )
150
+
151
+
152
+ @pytest.mark.unit
153
+ class TestNlFill:
154
+ """Tests for nl_fill in both backends."""
155
+
156
+ def test_cdp_nl_fill_calls_fill(self) -> None:
157
+ """Test that nl_fill finds element and calls fill."""
158
+ backend = _make_cdp_backend()
159
+ selectors = ["#email-field"]
160
+ backend._session.runtime.evaluate = AsyncMock(
161
+ return_value={"result": {"value": json.dumps(selectors)}}
162
+ )
163
+ backend.fill = AsyncMock()
164
+ asyncio.run(backend.nl_fill("email field", "test@test.com", auto_wait=False))
165
+ backend.fill.assert_called_once_with(
166
+ "#email-field", "test@test.com", auto_wait=False
167
+ )
168
+
169
+ def test_bidi_nl_fill_calls_fill(self) -> None:
170
+ """Test that nl_fill finds element and calls fill."""
171
+ backend = _make_bidi_backend()
172
+ selectors = ["#email-field"]
173
+ result_mock = MagicMock()
174
+ result_mock.value = json.dumps(selectors)
175
+ backend._client.script.evaluate = AsyncMock(return_value=result_mock)
176
+ backend.fill = AsyncMock()
177
+ asyncio.run(backend.nl_fill("email field", "test@test.com", auto_wait=False))
178
+ backend.fill.assert_called_once_with(
179
+ "#email-field", "test@test.com", auto_wait=False
180
+ )
@@ -1,3 +1,3 @@
1
1
  """wavexis — CLI de automatización de navegador."""
2
2
 
3
- __version__ = "2.3.4"
3
+ __version__ = "2.3.5"
@@ -166,6 +166,49 @@ class AbstractBackend(ABC):
166
166
  List of selector strings when all=True, single best selector when all=False.
167
167
  """
168
168
 
169
+ @abstractmethod
170
+ async def find_by_text(
171
+ self, query: str, all: bool = False
172
+ ) -> list[str] | str:
173
+ """Find elements by natural language text query.
174
+
175
+ Searches all visible elements by text content, aria-label,
176
+ placeholder, title, and alt text using fuzzy matching.
177
+
178
+ Args:
179
+ query: Natural language query (e.g. "the login button").
180
+ all: If True, return all matches; otherwise just the best one.
181
+
182
+ Returns:
183
+ List of CSS selector strings when all=True, single best when all=False.
184
+
185
+ Raises:
186
+ ElementNotFoundError: If no element matches the query.
187
+ """
188
+
189
+ @abstractmethod
190
+ async def nl_click(
191
+ self, query: str, auto_wait: bool = True
192
+ ) -> None:
193
+ """Click an element found by natural language text query.
194
+
195
+ Args:
196
+ query: Natural language query (e.g. "login button").
197
+ auto_wait: If True, wait for element to be visible before clicking.
198
+ """
199
+
200
+ @abstractmethod
201
+ async def nl_fill(
202
+ self, query: str, value: str, auto_wait: bool = True
203
+ ) -> None:
204
+ """Fill an input element found by natural language text query.
205
+
206
+ Args:
207
+ query: Natural language query (e.g. "email field").
208
+ value: Value to set in the input field.
209
+ auto_wait: If True, wait for element to be visible before filling.
210
+ """
211
+
169
212
  # ── Network ────────────────────────────────────────────
170
213
 
171
214
  @abstractmethod
@@ -595,6 +595,114 @@ class BiDiBackend(AbstractBackend):
595
595
  f"}})()"
596
596
  )
597
597
 
598
+ @staticmethod
599
+ def _find_by_text_js(query: str) -> str:
600
+ """Build JS that finds elements by natural language text query."""
601
+ escaped = query.replace("\\", "\\\\").replace("'", "\\'")
602
+ return (
603
+ f"(function(){{"
604
+ f"var q='{escaped}'.toLowerCase().trim();"
605
+ f"var words=q.split(/\\s+/);"
606
+ f"var els=Array.from(document.querySelectorAll('*'));"
607
+ f"var results=[];"
608
+ f"for(var i=0;i<els.length;i++){{"
609
+ f"var el=els[i];"
610
+ f"var rect=el.getBoundingClientRect();"
611
+ f"if(rect.width===0||rect.height===0)continue;"
612
+ f"var texts=["
613
+ f"(el.textContent||'').trim(),"
614
+ f"el.getAttribute('aria-label')||'',"
615
+ f"el.getAttribute('placeholder')||'',"
616
+ f"el.getAttribute('title')||'',"
617
+ f"el.getAttribute('alt')||'',"
618
+ f"el.getAttribute('value')||''"
619
+ f"].map(function(t){{return t.toLowerCase()}});"
620
+ f"var bestScore=0;"
621
+ f"for(var j=0;j<texts.length;j++){{"
622
+ f"var t=texts[j];if(!t)continue;"
623
+ f"if(t===q){{bestScore=100;break;}}"
624
+ f"if(t.indexOf(q)>=0){{bestScore=Math.max(bestScore,80);}}"
625
+ f"if(q.indexOf(t)>=0&&t.length>3){{bestScore=Math.max(bestScore,60);}}"
626
+ f"var matched=0;"
627
+ f"for(var k=0;k<words.length;k++){{"
628
+ f"if(t.indexOf(words[k])>=0)matched++;"
629
+ f"}}"
630
+ f"if(matched>0)bestScore=Math.max(bestScore,"
631
+ f"Math.round(matched/words.length*50));"
632
+ f"}}"
633
+ f"if(bestScore>0){{"
634
+ f"var tag=el.tagName.toLowerCase();"
635
+ f"var sel=tag;"
636
+ f"if(el.id)sel='#'+CSS.escape(el.id);"
637
+ f"else if(el.getAttribute('data-testid'))"
638
+ f"sel='[data-testid=\"'+el.getAttribute('data-testid')+'\"]';"
639
+ f"else if(el.getAttribute('aria-label'))"
640
+ f"sel=tag+'[aria-label=\"'+el.getAttribute('aria-label')+'\"]';"
641
+ f"else if(el.classList.length>0)"
642
+ f"sel=tag+'.'+Array.from(el.classList).join('.');"
643
+ f"results.push({{score:bestScore,sel:sel}});"
644
+ f"}}"
645
+ f"}}"
646
+ f"results.sort(function(a,b){{return b.score-a.score}});"
647
+ f"return JSON.stringify(results.map(function(r){{return r.sel}}));"
648
+ f"}})()"
649
+ )
650
+
651
+ async def find_by_text(
652
+ self, query: str, all: bool = False
653
+ ) -> list[str] | str:
654
+ """Find elements by natural language text query.
655
+
656
+ Args:
657
+ query: Natural language query (e.g. "the login button").
658
+ all: If True, return all matches; otherwise just the best one.
659
+
660
+ Returns:
661
+ List of CSS selector strings when all=True, single best when all=False.
662
+
663
+ Raises:
664
+ ElementNotFoundError: If no element matches the query.
665
+ """
666
+ client = self._require_client()
667
+ js = self._find_by_text_js(query)
668
+ result = await client.script.evaluate(self._context, js)
669
+ raw = getattr(result, "value", None)
670
+ if not raw:
671
+ raise ElementNotFoundError(query)
672
+ selectors: list[str] = json.loads(raw)
673
+ if not selectors:
674
+ raise ElementNotFoundError(query)
675
+ if all:
676
+ return selectors
677
+ return selectors[0]
678
+
679
+ async def nl_click(
680
+ self, query: str, auto_wait: bool = True
681
+ ) -> None:
682
+ """Click an element found by natural language text query.
683
+
684
+ Args:
685
+ query: Natural language query (e.g. "login button").
686
+ auto_wait: If True, wait for element to be visible before clicking.
687
+ """
688
+ selector = await self.find_by_text(query)
689
+ assert isinstance(selector, str)
690
+ await self.click(selector, auto_wait=auto_wait)
691
+
692
+ async def nl_fill(
693
+ self, query: str, value: str, auto_wait: bool = True
694
+ ) -> None:
695
+ """Fill an input element found by natural language text query.
696
+
697
+ Args:
698
+ query: Natural language query (e.g. "email field").
699
+ value: Value to set in the input field.
700
+ auto_wait: If True, wait for element to be visible before filling.
701
+ """
702
+ selector = await self.find_by_text(query)
703
+ assert isinstance(selector, str)
704
+ await self.fill(selector, value, auto_wait=auto_wait)
705
+
598
706
  async def capture_har(self, params: HarParams) -> dict[str, Any]:
599
707
  """Capture HAR data via CDP Network domain.
600
708
 
@@ -702,6 +702,114 @@ class CDPBackend(AbstractBackend):
702
702
  f"}})()"
703
703
  )
704
704
 
705
+ @staticmethod
706
+ def _find_by_text_js(query: str) -> str:
707
+ """Build JS that finds elements by natural language text query."""
708
+ escaped = query.replace("\\", "\\\\").replace("'", "\\'")
709
+ return (
710
+ f"(function(){{"
711
+ f"var q='{escaped}'.toLowerCase().trim();"
712
+ f"var words=q.split(/\\s+/);"
713
+ f"var els=Array.from(document.querySelectorAll('*'));"
714
+ f"var results=[];"
715
+ f"for(var i=0;i<els.length;i++){{"
716
+ f"var el=els[i];"
717
+ f"var rect=el.getBoundingClientRect();"
718
+ f"if(rect.width===0||rect.height===0)continue;"
719
+ f"var texts=["
720
+ f"(el.textContent||'').trim(),"
721
+ f"el.getAttribute('aria-label')||'',"
722
+ f"el.getAttribute('placeholder')||'',"
723
+ f"el.getAttribute('title')||'',"
724
+ f"el.getAttribute('alt')||'',"
725
+ f"el.getAttribute('value')||''"
726
+ f"].map(function(t){{return t.toLowerCase()}});"
727
+ f"var bestScore=0;"
728
+ f"for(var j=0;j<texts.length;j++){{"
729
+ f"var t=texts[j];if(!t)continue;"
730
+ f"if(t===q){{bestScore=100;break;}}"
731
+ f"if(t.indexOf(q)>=0){{bestScore=Math.max(bestScore,80);}}"
732
+ f"if(q.indexOf(t)>=0&&t.length>3){{bestScore=Math.max(bestScore,60);}}"
733
+ f"var matched=0;"
734
+ f"for(var k=0;k<words.length;k++){{"
735
+ f"if(t.indexOf(words[k])>=0)matched++;"
736
+ f"}}"
737
+ f"if(matched>0)bestScore=Math.max(bestScore,"
738
+ f"Math.round(matched/words.length*50));"
739
+ f"}}"
740
+ f"if(bestScore>0){{"
741
+ f"var tag=el.tagName.toLowerCase();"
742
+ f"var sel=tag;"
743
+ f"if(el.id)sel='#'+CSS.escape(el.id);"
744
+ f"else if(el.getAttribute('data-testid'))"
745
+ f"sel='[data-testid=\"'+el.getAttribute('data-testid')+'\"]';"
746
+ f"else if(el.getAttribute('aria-label'))"
747
+ f"sel=tag+'[aria-label=\"'+el.getAttribute('aria-label')+'\"]';"
748
+ f"else if(el.classList.length>0)"
749
+ f"sel=tag+'.'+Array.from(el.classList).join('.');"
750
+ f"results.push({{score:bestScore,sel:sel}});"
751
+ f"}}"
752
+ f"}}"
753
+ f"results.sort(function(a,b){{return b.score-a.score}});"
754
+ f"return JSON.stringify(results.map(function(r){{return r.sel}}));"
755
+ f"}})()"
756
+ )
757
+
758
+ async def find_by_text(
759
+ self, query: str, all: bool = False
760
+ ) -> list[str] | str:
761
+ """Find elements by natural language text query.
762
+
763
+ Args:
764
+ query: Natural language query (e.g. "the login button").
765
+ all: If True, return all matches; otherwise just the best one.
766
+
767
+ Returns:
768
+ List of CSS selector strings when all=True, single best when all=False.
769
+
770
+ Raises:
771
+ ElementNotFoundError: If no element matches the query.
772
+ """
773
+ session = self._require_session()
774
+ js = self._find_by_text_js(query)
775
+ result = await session.runtime.evaluate(js)
776
+ raw = result.get("result", {}).get("value")
777
+ if not raw:
778
+ raise ElementNotFoundError(query)
779
+ selectors: list[str] = json.loads(raw)
780
+ if not selectors:
781
+ raise ElementNotFoundError(query)
782
+ if all:
783
+ return selectors
784
+ return selectors[0]
785
+
786
+ async def nl_click(
787
+ self, query: str, auto_wait: bool = True
788
+ ) -> None:
789
+ """Click an element found by natural language text query.
790
+
791
+ Args:
792
+ query: Natural language query (e.g. "login button").
793
+ auto_wait: If True, wait for element to be visible before clicking.
794
+ """
795
+ selector = await self.find_by_text(query)
796
+ assert isinstance(selector, str)
797
+ await self.click(selector, auto_wait=auto_wait)
798
+
799
+ async def nl_fill(
800
+ self, query: str, value: str, auto_wait: bool = True
801
+ ) -> None:
802
+ """Fill an input element found by natural language text query.
803
+
804
+ Args:
805
+ query: Natural language query (e.g. "email field").
806
+ value: Value to set in the input field.
807
+ auto_wait: If True, wait for element to be visible before filling.
808
+ """
809
+ selector = await self.find_by_text(query)
810
+ assert isinstance(selector, str)
811
+ await self.fill(selector, value, auto_wait=auto_wait)
812
+
705
813
  # ── Network ────────────────────────────────────────────
706
814
 
707
815
  async def capture_har(self, params: HarParams) -> dict[str, Any]:
@@ -0,0 +1,102 @@
1
+ """Natural language selector commands for wavexis CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__ = ["nl"]
6
+
7
+ import typer
8
+
9
+ from wavexis.cli._shared import (
10
+ _browser_options,
11
+ _get_backend,
12
+ _run_async,
13
+ app,
14
+ )
15
+ from wavexis.config import WaitStrategy
16
+
17
+
18
+ @app.command()
19
+ def nl(
20
+ action: str = typer.Argument(
21
+ ..., help="Natural language action: click, fill, find"
22
+ ),
23
+ url: str = typer.Argument(..., help="URL to navigate to"),
24
+ query: str = typer.Argument(
25
+ ..., help='Natural language query (e.g. "the login button")'
26
+ ),
27
+ value: str = typer.Option(
28
+ "", "--value", "-v", help="Value to fill (for fill)"
29
+ ),
30
+ no_wait: bool = typer.Option(
31
+ False, "--no-wait", help="Skip auto-waiting for element visibility"
32
+ ),
33
+ all: bool = typer.Option(
34
+ False, "--all", help="Return all matches (for find)"
35
+ ),
36
+ ) -> None:
37
+ """Interact with elements using natural language text queries.
38
+
39
+ \b
40
+ Click: wavexis nl click https://example.com "the login button"
41
+ Fill: wavexis nl fill https://example.com "email field" -v "user@example.com"
42
+ Find: wavexis nl find https://example.com "submit button" --all
43
+ """
44
+ if action == "click":
45
+ _run_async(_nl_click(url, query, not no_wait))
46
+ typer.echo(f"Clicked element matching '{query}'")
47
+
48
+ elif action == "fill":
49
+ _run_async(_nl_fill(url, query, value, not no_wait))
50
+ typer.echo(f"Filled element matching '{query}' with '{value}'")
51
+
52
+ elif action == "find":
53
+ result = _run_async(_nl_find(url, query, all))
54
+ if isinstance(result, list):
55
+ for sel in result:
56
+ typer.echo(sel)
57
+ else:
58
+ typer.echo(result)
59
+
60
+ else:
61
+ typer.echo(
62
+ f"Error: unknown nl action '{action}'. "
63
+ "Use click, fill, or find.",
64
+ err=True,
65
+ )
66
+ raise typer.Exit(1)
67
+
68
+
69
+ async def _nl_click(url: str, query: str, auto_wait: bool) -> None:
70
+ """Async helper for natural language click."""
71
+ backend = _get_backend()
72
+ try:
73
+ await backend.launch(_browser_options())
74
+ await backend.navigate(url, WaitStrategy(strategy="load"))
75
+ await backend.nl_click(query, auto_wait=auto_wait)
76
+ finally:
77
+ await backend.close()
78
+
79
+
80
+ async def _nl_fill(
81
+ url: str, query: str, value: str, auto_wait: bool
82
+ ) -> None:
83
+ """Async helper for natural language fill."""
84
+ backend = _get_backend()
85
+ try:
86
+ await backend.launch(_browser_options())
87
+ await backend.navigate(url, WaitStrategy(strategy="load"))
88
+ await backend.nl_fill(query, value, auto_wait=auto_wait)
89
+ finally:
90
+ await backend.close()
91
+
92
+
93
+ async def _nl_find(url: str, query: str, all: bool) -> list[str] | str:
94
+ """Async helper for natural language find."""
95
+ backend = _get_backend()
96
+ try:
97
+ await backend.launch(_browser_options())
98
+ await backend.navigate(url, WaitStrategy(strategy="load"))
99
+ result: list[str] | str = await backend.find_by_text(query, all=all)
100
+ return result
101
+ finally:
102
+ await backend.close()
@@ -10,6 +10,7 @@ from wavexis.cli._iframe import * # noqa: F401, F403
10
10
  from wavexis.cli._input import * # noqa: F401, F403
11
11
  from wavexis.cli._navigation import * # noqa: F401, F403
12
12
  from wavexis.cli._network import * # noqa: F401, F403
13
+ from wavexis.cli._nl import * # noqa: F401, F403
13
14
  from wavexis.cli._perf import * # noqa: F401, F403
14
15
  from wavexis.cli._serve import * # noqa: F401, F403
15
16
  from wavexis.cli._session import * # noqa: F401, F403
File without changes
File without changes
File without changes