wavexis 2.3.3__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.3 → wavexis-2.3.5}/CHANGELOG.md +22 -0
  2. {wavexis-2.3.3 → wavexis-2.3.5}/PKG-INFO +1 -1
  3. {wavexis-2.3.3 → wavexis-2.3.5}/pyproject.toml +1 -1
  4. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_backend_manager.py +22 -0
  5. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_manager.py +10 -0
  6. wavexis-2.3.5/tests/unit/test_nl.py +180 -0
  7. wavexis-2.3.5/tests/unit/test_suggest_locator.py +110 -0
  8. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/__init__.py +1 -1
  9. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/backend/base.py +60 -0
  10. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/backend/bidi.py +166 -0
  11. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/backend/cdp.py +166 -0
  12. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/cli/_capture.py +18 -2
  13. wavexis-2.3.5/wavexis/cli/_nl.py +102 -0
  14. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/cli/app.py +1 -0
  15. {wavexis-2.3.3 → wavexis-2.3.5}/.dockerignore +0 -0
  16. {wavexis-2.3.3 → wavexis-2.3.5}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  17. {wavexis-2.3.3 → wavexis-2.3.5}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  18. {wavexis-2.3.3 → wavexis-2.3.5}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  19. {wavexis-2.3.3 → wavexis-2.3.5}/.github/workflows/ci.yml +0 -0
  20. {wavexis-2.3.3 → wavexis-2.3.5}/.github/workflows/docs.yml +0 -0
  21. {wavexis-2.3.3 → wavexis-2.3.5}/.github/workflows/release.yml +0 -0
  22. {wavexis-2.3.3 → wavexis-2.3.5}/.gitignore +0 -0
  23. {wavexis-2.3.3 → wavexis-2.3.5}/CODE_OF_CONDUCT.md +0 -0
  24. {wavexis-2.3.3 → wavexis-2.3.5}/CONTRIBUTING.md +0 -0
  25. {wavexis-2.3.3 → wavexis-2.3.5}/Dockerfile +0 -0
  26. {wavexis-2.3.3 → wavexis-2.3.5}/LICENSE +0 -0
  27. {wavexis-2.3.3 → wavexis-2.3.5}/README.md +0 -0
  28. {wavexis-2.3.3 → wavexis-2.3.5}/SECURITY.md +0 -0
  29. {wavexis-2.3.3 → wavexis-2.3.5}/docs/api/actions.md +0 -0
  30. {wavexis-2.3.3 → wavexis-2.3.5}/docs/api/backends.md +0 -0
  31. {wavexis-2.3.3 → wavexis-2.3.5}/docs/api/cli.md +0 -0
  32. {wavexis-2.3.3 → wavexis-2.3.5}/docs/api/config.md +0 -0
  33. {wavexis-2.3.3 → wavexis-2.3.5}/docs/api/exceptions.md +0 -0
  34. {wavexis-2.3.3 → wavexis-2.3.5}/docs/api/multi.md +0 -0
  35. {wavexis-2.3.3 → wavexis-2.3.5}/docs/api/plugins.md +0 -0
  36. {wavexis-2.3.3 → wavexis-2.3.5}/docs/api/serve.md +0 -0
  37. {wavexis-2.3.3 → wavexis-2.3.5}/docs/cookbook/auth.md +0 -0
  38. {wavexis-2.3.3 → wavexis-2.3.5}/docs/cookbook/ci-cd.md +0 -0
  39. {wavexis-2.3.3 → wavexis-2.3.5}/docs/cookbook/css.md +0 -0
  40. {wavexis-2.3.3 → wavexis-2.3.5}/docs/cookbook/debug.md +0 -0
  41. {wavexis-2.3.3 → wavexis-2.3.5}/docs/cookbook/record-replay.md +0 -0
  42. {wavexis-2.3.3 → wavexis-2.3.5}/docs/cookbook/scraping.md +0 -0
  43. {wavexis-2.3.3 → wavexis-2.3.5}/docs/cookbook/screenshots.md +0 -0
  44. {wavexis-2.3.3 → wavexis-2.3.5}/docs/cookbook/serve-mode.md +0 -0
  45. {wavexis-2.3.3 → wavexis-2.3.5}/docs/guide/assert.md +0 -0
  46. {wavexis-2.3.3 → wavexis-2.3.5}/docs/guide/backends.md +0 -0
  47. {wavexis-2.3.3 → wavexis-2.3.5}/docs/guide/commands.md +0 -0
  48. {wavexis-2.3.3 → wavexis-2.3.5}/docs/guide/init.md +0 -0
  49. {wavexis-2.3.3 → wavexis-2.3.5}/docs/guide/installation.md +0 -0
  50. {wavexis-2.3.3 → wavexis-2.3.5}/docs/guide/multi.md +0 -0
  51. {wavexis-2.3.3 → wavexis-2.3.5}/docs/guide/perf.md +0 -0
  52. {wavexis-2.3.3 → wavexis-2.3.5}/docs/guide/plugins.md +0 -0
  53. {wavexis-2.3.3 → wavexis-2.3.5}/docs/guide/quickstart.md +0 -0
  54. {wavexis-2.3.3 → wavexis-2.3.5}/docs/guide/raw.md +0 -0
  55. {wavexis-2.3.3 → wavexis-2.3.5}/docs/guide/repl.md +0 -0
  56. {wavexis-2.3.3 → wavexis-2.3.5}/docs/guide/troubleshooting.md +0 -0
  57. {wavexis-2.3.3 → wavexis-2.3.5}/docs/index.md +0 -0
  58. {wavexis-2.3.3 → wavexis-2.3.5}/mkdocs.yml +0 -0
  59. {wavexis-2.3.3 → wavexis-2.3.5}/stubs/bidiwave/__init__.pyi +0 -0
  60. {wavexis-2.3.3 → wavexis-2.3.5}/stubs/cdpwave/__init__.pyi +0 -0
  61. {wavexis-2.3.3 → wavexis-2.3.5}/tests/__init__.py +0 -0
  62. {wavexis-2.3.3 → wavexis-2.3.5}/tests/conftest.py +0 -0
  63. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/__init__.py +0 -0
  64. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_a11y.py +0 -0
  65. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_animation.py +0 -0
  66. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_backend_selection.py +0 -0
  67. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_browser.py +0 -0
  68. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_console.py +0 -0
  69. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_css.py +0 -0
  70. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_debug.py +0 -0
  71. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_dialog.py +0 -0
  72. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_dom.py +0 -0
  73. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_dom_snapshot.py +0 -0
  74. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_emulation.py +0 -0
  75. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_emulation_advanced.py +0 -0
  76. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_eval.py +0 -0
  77. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_har.py +0 -0
  78. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_input.py +0 -0
  79. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_media.py +0 -0
  80. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_multi.py +0 -0
  81. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_navigate.py +0 -0
  82. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_network.py +0 -0
  83. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_network_advanced.py +0 -0
  84. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_overlay.py +0 -0
  85. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_pdf.py +0 -0
  86. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_perf.py +0 -0
  87. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_permissions.py +0 -0
  88. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_raw.py +0 -0
  89. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_record.py +0 -0
  90. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_scrape.py +0 -0
  91. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_screenshot.py +0 -0
  92. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_security.py +0 -0
  93. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_serve.py +0 -0
  94. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_service_worker.py +0 -0
  95. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_storage.py +0 -0
  96. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_tabs.py +0 -0
  97. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_webaudio.py +0 -0
  98. {wavexis-2.3.3 → wavexis-2.3.5}/tests/integration/test_webauthn.py +0 -0
  99. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/__init__.py +0 -0
  100. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_abstract_backend_phase5.py +0 -0
  101. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_actions.py +0 -0
  102. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_actions_phase5.py +0 -0
  103. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_animation_action.py +0 -0
  104. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_assert_multi.py +0 -0
  105. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_auth.py +0 -0
  106. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_auto_wait.py +0 -0
  107. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_bidi_backend.py +0 -0
  108. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_bidi_phase5.py +0 -0
  109. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_bluetooth_action.py +0 -0
  110. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_cast_action.py +0 -0
  111. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_cleanup.py +0 -0
  112. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_cli_phase5.py +0 -0
  113. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_config.py +0 -0
  114. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_config_phase5.py +0 -0
  115. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_cookies_headers_actions.py +0 -0
  116. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_css_action.py +0 -0
  117. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_debug_action.py +0 -0
  118. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_dom_snapshot_action.py +0 -0
  119. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_download_screencast.py +0 -0
  120. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_emulation_action.py +0 -0
  121. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_error_handling.py +0 -0
  122. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_exceptions.py +0 -0
  123. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_format_record.py +0 -0
  124. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_global_options.py +0 -0
  125. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_iframe.py +0 -0
  126. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_init.py +0 -0
  127. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_input_action.py +0 -0
  128. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_interactions_crawl.py +0 -0
  129. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_media_action.py +0 -0
  130. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_multi.py +0 -0
  131. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_multi_improvements.py +0 -0
  132. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_new_features.py +0 -0
  133. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_output.py +0 -0
  134. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_overlay_action.py +0 -0
  135. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_perf_action.py +0 -0
  136. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_perf_console.py +0 -0
  137. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_plugins.py +0 -0
  138. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_raw.py +0 -0
  139. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_record.py +0 -0
  140. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_repl.py +0 -0
  141. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_serve.py +0 -0
  142. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_service_worker_action.py +0 -0
  143. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_shadow.py +0 -0
  144. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_storage_action.py +0 -0
  145. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_webaudio_action.py +0 -0
  146. {wavexis-2.3.3 → wavexis-2.3.5}/tests/unit/test_webauthn_action.py +0 -0
  147. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/__main__.py +0 -0
  148. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/__init__.py +0 -0
  149. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/accessibility.py +0 -0
  150. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/animation.py +0 -0
  151. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/base.py +0 -0
  152. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/bluetooth.py +0 -0
  153. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/browser.py +0 -0
  154. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/cast.py +0 -0
  155. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/console.py +0 -0
  156. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/cookies.py +0 -0
  157. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/crawl.py +0 -0
  158. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/css.py +0 -0
  159. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/debug.py +0 -0
  160. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/dialog.py +0 -0
  161. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/dom.py +0 -0
  162. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/dom_snapshot.py +0 -0
  163. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/download.py +0 -0
  164. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/emulation.py +0 -0
  165. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/eval.py +0 -0
  166. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/extract.py +0 -0
  167. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/form.py +0 -0
  168. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/har.py +0 -0
  169. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/headers.py +0 -0
  170. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/input.py +0 -0
  171. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/lighthouse.py +0 -0
  172. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/media.py +0 -0
  173. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/multi.py +0 -0
  174. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/navigate.py +0 -0
  175. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/network.py +0 -0
  176. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/overlay.py +0 -0
  177. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/pdf.py +0 -0
  178. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/performance.py +0 -0
  179. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/permissions.py +0 -0
  180. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/record.py +0 -0
  181. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/scrape.py +0 -0
  182. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/screencast.py +0 -0
  183. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/screenshot.py +0 -0
  184. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/security.py +0 -0
  185. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/service_worker.py +0 -0
  186. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/session.py +0 -0
  187. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/storage.py +0 -0
  188. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/tabs.py +0 -0
  189. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/wait.py +0 -0
  190. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/webaudio.py +0 -0
  191. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/webauthn.py +0 -0
  192. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/actions/websocket.py +0 -0
  193. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/auth.py +0 -0
  194. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/backend/__init__.py +0 -0
  195. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/backend/manager.py +0 -0
  196. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/cleanup.py +0 -0
  197. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/cli/__init__.py +0 -0
  198. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/cli/_advanced.py +0 -0
  199. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/cli/_config.py +0 -0
  200. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/cli/_debug.py +0 -0
  201. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/cli/_emulation.py +0 -0
  202. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/cli/_experimental.py +0 -0
  203. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/cli/_iframe.py +0 -0
  204. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/cli/_input.py +0 -0
  205. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/cli/_navigation.py +0 -0
  206. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/cli/_network.py +0 -0
  207. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/cli/_perf.py +0 -0
  208. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/cli/_serve.py +0 -0
  209. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/cli/_session.py +0 -0
  210. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/cli/_shadow.py +0 -0
  211. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/cli/_shared.py +0 -0
  212. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/cli/_workflow.py +0 -0
  213. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/config.py +0 -0
  214. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/exceptions.py +0 -0
  215. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/init.py +0 -0
  216. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/multi.py +0 -0
  217. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/output.py +0 -0
  218. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/plugins.py +0 -0
  219. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/record.py +0 -0
  220. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/repl.py +0 -0
  221. {wavexis-2.3.3 → wavexis-2.3.5}/wavexis/serve.py +0 -0
@@ -2,6 +2,28 @@
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
+
17
+ ## v2.3.4 — 2026-07-07
18
+
19
+ ### Added
20
+
21
+ - Generate locator: `suggest_locator` method in both CDP and BiDi backends
22
+ - `wavexis dom <url> --action suggest_locator -s <selector>` CLI command
23
+ - `--all` flag to return all suggestions ranked by specificity
24
+ - Priority order: `#id` > `[data-testid]` > `[aria-label]` > `[role]` > `:has-text()` > `tag.classes` > `parent > tag` > `:nth-of-type()` > `tag`
25
+ - Uses `CSS.escape()` for safe selector generation
26
+
5
27
  ## v2.3.3 — 2026-07-07
6
28
 
7
29
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavexis
3
- Version: 2.3.3
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.3"
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" }
@@ -132,6 +132,28 @@ class DummyBackend(AbstractBackend):
132
132
  """Dom scroll."""
133
133
  pass
134
134
 
135
+ async def suggest_locator(
136
+ self, selector: str, all: bool = False
137
+ ) -> list[str] | str:
138
+ """Suggest locator."""
139
+ return selector
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
+
135
157
  async def capture_har(self, params: object) -> dict:
136
158
  """Capture har."""
137
159
  return {}
@@ -66,6 +66,16 @@ class FakeBackend(AbstractBackend):
66
66
  """Dom focus."""
67
67
  async def dom_scroll(self, selector=None, x=0, y=0):
68
68
  """Dom scroll."""
69
+ async def suggest_locator(self, selector, all=False):
70
+ """Suggest locator."""
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."""
69
79
  async def capture_har(self, params):
70
80
  """Capture har."""
71
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
+ )
@@ -0,0 +1,110 @@
1
+ """Unit tests for suggest_locator in both backends."""
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 TestSuggestLocatorCDP:
44
+ """Tests for suggest_locator in CDP backend."""
45
+
46
+ def test_returns_best_selector(self) -> None:
47
+ """Test that suggest_locator returns the first (best) suggestion."""
48
+ backend = _make_cdp_backend()
49
+ suggestions = ["#login-btn", "button.primary", "button"]
50
+ backend._session.runtime.evaluate = AsyncMock(
51
+ return_value={"result": {"value": json.dumps(suggestions)}}
52
+ )
53
+ result = asyncio.run(backend.suggest_locator("#login-btn"))
54
+ assert result == "#login-btn"
55
+
56
+ def test_returns_all_suggestions(self) -> None:
57
+ """Test that suggest_locator with all=True returns all suggestions."""
58
+ backend = _make_cdp_backend()
59
+ suggestions = ["#my-id", "[data-testid='btn']", "button.clickable", "button"]
60
+ backend._session.runtime.evaluate = AsyncMock(
61
+ return_value={"result": {"value": json.dumps(suggestions)}}
62
+ )
63
+ result = asyncio.run(backend.suggest_locator("button", all=True))
64
+ assert isinstance(result, list)
65
+ assert len(result) == 4
66
+ assert result[0] == "#my-id"
67
+
68
+ def test_raises_on_element_not_found(self) -> None:
69
+ """Test that suggest_locator raises ElementNotFoundError when element missing."""
70
+ backend = _make_cdp_backend()
71
+ backend._session.runtime.evaluate = AsyncMock(
72
+ return_value={"result": {"value": None}}
73
+ )
74
+ with pytest.raises(ElementNotFoundError):
75
+ asyncio.run(backend.suggest_locator("#missing"))
76
+
77
+
78
+ @pytest.mark.unit
79
+ class TestSuggestLocatorBiDi:
80
+ """Tests for suggest_locator in BiDi backend."""
81
+
82
+ def test_returns_best_selector(self) -> None:
83
+ """Test that suggest_locator returns the first (best) suggestion."""
84
+ backend = _make_bidi_backend()
85
+ suggestions = ["#submit", "button[type='submit']", "button"]
86
+ result_mock = MagicMock()
87
+ result_mock.value = json.dumps(suggestions)
88
+ backend._client.script.evaluate = AsyncMock(return_value=result_mock)
89
+ result = asyncio.run(backend.suggest_locator("#submit"))
90
+ assert result == "#submit"
91
+
92
+ def test_returns_all_suggestions(self) -> None:
93
+ """Test that suggest_locator with all=True returns all suggestions."""
94
+ backend = _make_bidi_backend()
95
+ suggestions = ["#email", "[data-testid='email']", "input.email", "input"]
96
+ result_mock = MagicMock()
97
+ result_mock.value = json.dumps(suggestions)
98
+ backend._client.script.evaluate = AsyncMock(return_value=result_mock)
99
+ result = asyncio.run(backend.suggest_locator("input", all=True))
100
+ assert isinstance(result, list)
101
+ assert len(result) == 4
102
+
103
+ def test_raises_on_element_not_found(self) -> None:
104
+ """Test that suggest_locator raises ElementNotFoundError when element missing."""
105
+ backend = _make_bidi_backend()
106
+ result_mock = MagicMock()
107
+ result_mock.value = None
108
+ backend._client.script.evaluate = AsyncMock(return_value=result_mock)
109
+ with pytest.raises(ElementNotFoundError):
110
+ asyncio.run(backend.suggest_locator("#missing"))
@@ -1,3 +1,3 @@
1
1
  """wavexis — CLI de automatización de navegador."""
2
2
 
3
- __version__ = "2.3.3"
3
+ __version__ = "2.3.5"
@@ -149,6 +149,66 @@ class AbstractBackend(ABC):
149
149
  ) -> None:
150
150
  """Scroll to an element or by offset."""
151
151
 
152
+ @abstractmethod
153
+ async def suggest_locator(
154
+ self, selector: str, all: bool = False
155
+ ) -> list[str] | str:
156
+ """Suggest the best CSS selector for an element.
157
+
158
+ Analyzes the element and generates optimal selectors in priority order:
159
+ id > data-testid > aria-label > text > tag.classes > nth-of-type chain.
160
+
161
+ Args:
162
+ selector: CSS selector for the target element.
163
+ all: If True, return multiple suggestions; otherwise just the best one.
164
+
165
+ Returns:
166
+ List of selector strings when all=True, single best selector when all=False.
167
+ """
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
+
152
212
  # ── Network ────────────────────────────────────────────
153
213
 
154
214
  @abstractmethod
@@ -537,6 +537,172 @@ class BiDiBackend(AbstractBackend):
537
537
  js = f"window.scrollTo({x},{y})"
538
538
  await self._client.script.evaluate(self._context, js)
539
539
 
540
+ async def suggest_locator(
541
+ self, selector: str, all: bool = False
542
+ ) -> list[str] | str:
543
+ """Suggest the best CSS selector for an element.
544
+
545
+ Args:
546
+ selector: CSS selector for the target element.
547
+ all: If True, return multiple suggestions; otherwise just the best one.
548
+
549
+ Returns:
550
+ List of selector strings when all=True, single best selector when all=False.
551
+ """
552
+ client = self._require_client()
553
+ escaped = selector.replace("'", "\\'")
554
+ js = self._suggest_locator_js(escaped)
555
+ result = await client.script.evaluate(self._context, js)
556
+ raw = getattr(result, "value", None)
557
+ if not raw:
558
+ raise ElementNotFoundError(selector)
559
+ suggestions: list[str] = json.loads(raw)
560
+ if all:
561
+ return suggestions
562
+ return suggestions[0] if suggestions else selector
563
+
564
+ @staticmethod
565
+ def _suggest_locator_js(escaped: str) -> str:
566
+ """Build JS that generates CSS selector suggestions for an element."""
567
+ return (
568
+ f"(function(){{"
569
+ f"var el=document.querySelector('{escaped}');"
570
+ f"if(!el)return null;"
571
+ f"var s=[];var t=el.tagName.toLowerCase();"
572
+ f"var id=el.id;"
573
+ f"var tid=el.getAttribute('data-testid')"
574
+ f"||el.getAttribute('data-test-id')"
575
+ f"||el.getAttribute('data-cy');"
576
+ f"var aria=el.getAttribute('aria-label');"
577
+ f"var role=el.getAttribute('role');"
578
+ f"var txt=(el.textContent||'').trim().substring(0,50);"
579
+ f"var cls=Array.from(el.classList);"
580
+ f"if(id)s.push('#'+CSS.escape(id));"
581
+ f"if(tid)s.push('[data-testid=\"'+tid+'\"]');"
582
+ f"if(aria)s.push(t+'[aria-label=\"'+aria+'\"]');"
583
+ f"if(role)s.push(t+'[role=\"'+role+'\"]');"
584
+ f"if(txt&&txt.length<30)s.push(t+':has-text(\"'+txt+'\"]');"
585
+ f"if(cls.length>0)s.push(t+'.'+cls.join('.'));"
586
+ f"var p=el.parentElement;"
587
+ f"if(p&&p.id)s.push('#'+CSS.escape(p.id)+' > '+t);"
588
+ f"var sib=p?Array.from(p.children)"
589
+ f".filter(function(c){{return c.tagName===el.tagName}}):[];"
590
+ f"if(sib.length>1){{"
591
+ f"var i=sib.indexOf(el)+1;"
592
+ f"s.push(t+':nth-of-type('+i+')');}}"
593
+ f"s.push(t);"
594
+ f"return JSON.stringify(s);"
595
+ f"}})()"
596
+ )
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
+
540
706
  async def capture_har(self, params: HarParams) -> dict[str, Any]:
541
707
  """Capture HAR data via CDP Network domain.
542
708