wavexis 2.3.0__tar.gz → 2.3.2__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 (216) hide show
  1. {wavexis-2.3.0 → wavexis-2.3.2}/CHANGELOG.md +17 -0
  2. {wavexis-2.3.0 → wavexis-2.3.2}/PKG-INFO +1 -1
  3. {wavexis-2.3.0 → wavexis-2.3.2}/pyproject.toml +1 -1
  4. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_auto_wait.py +29 -0
  5. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_backend_manager.py +29 -3
  6. wavexis-2.3.2/tests/unit/test_iframe.py +203 -0
  7. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_manager.py +10 -3
  8. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/__init__.py +1 -1
  9. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/backend/base.py +48 -3
  10. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/backend/bidi.py +167 -11
  11. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/backend/cdp.py +135 -6
  12. wavexis-2.3.2/wavexis/cli/_iframe.py +132 -0
  13. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/app.py +1 -0
  14. {wavexis-2.3.0 → wavexis-2.3.2}/.dockerignore +0 -0
  15. {wavexis-2.3.0 → wavexis-2.3.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  16. {wavexis-2.3.0 → wavexis-2.3.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  17. {wavexis-2.3.0 → wavexis-2.3.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  18. {wavexis-2.3.0 → wavexis-2.3.2}/.github/workflows/ci.yml +0 -0
  19. {wavexis-2.3.0 → wavexis-2.3.2}/.github/workflows/docs.yml +0 -0
  20. {wavexis-2.3.0 → wavexis-2.3.2}/.github/workflows/release.yml +0 -0
  21. {wavexis-2.3.0 → wavexis-2.3.2}/.gitignore +0 -0
  22. {wavexis-2.3.0 → wavexis-2.3.2}/CODE_OF_CONDUCT.md +0 -0
  23. {wavexis-2.3.0 → wavexis-2.3.2}/CONTRIBUTING.md +0 -0
  24. {wavexis-2.3.0 → wavexis-2.3.2}/Dockerfile +0 -0
  25. {wavexis-2.3.0 → wavexis-2.3.2}/LICENSE +0 -0
  26. {wavexis-2.3.0 → wavexis-2.3.2}/README.md +0 -0
  27. {wavexis-2.3.0 → wavexis-2.3.2}/SECURITY.md +0 -0
  28. {wavexis-2.3.0 → wavexis-2.3.2}/docs/api/actions.md +0 -0
  29. {wavexis-2.3.0 → wavexis-2.3.2}/docs/api/backends.md +0 -0
  30. {wavexis-2.3.0 → wavexis-2.3.2}/docs/api/cli.md +0 -0
  31. {wavexis-2.3.0 → wavexis-2.3.2}/docs/api/config.md +0 -0
  32. {wavexis-2.3.0 → wavexis-2.3.2}/docs/api/exceptions.md +0 -0
  33. {wavexis-2.3.0 → wavexis-2.3.2}/docs/api/multi.md +0 -0
  34. {wavexis-2.3.0 → wavexis-2.3.2}/docs/api/plugins.md +0 -0
  35. {wavexis-2.3.0 → wavexis-2.3.2}/docs/api/serve.md +0 -0
  36. {wavexis-2.3.0 → wavexis-2.3.2}/docs/cookbook/auth.md +0 -0
  37. {wavexis-2.3.0 → wavexis-2.3.2}/docs/cookbook/ci-cd.md +0 -0
  38. {wavexis-2.3.0 → wavexis-2.3.2}/docs/cookbook/css.md +0 -0
  39. {wavexis-2.3.0 → wavexis-2.3.2}/docs/cookbook/debug.md +0 -0
  40. {wavexis-2.3.0 → wavexis-2.3.2}/docs/cookbook/record-replay.md +0 -0
  41. {wavexis-2.3.0 → wavexis-2.3.2}/docs/cookbook/scraping.md +0 -0
  42. {wavexis-2.3.0 → wavexis-2.3.2}/docs/cookbook/screenshots.md +0 -0
  43. {wavexis-2.3.0 → wavexis-2.3.2}/docs/cookbook/serve-mode.md +0 -0
  44. {wavexis-2.3.0 → wavexis-2.3.2}/docs/guide/assert.md +0 -0
  45. {wavexis-2.3.0 → wavexis-2.3.2}/docs/guide/backends.md +0 -0
  46. {wavexis-2.3.0 → wavexis-2.3.2}/docs/guide/commands.md +0 -0
  47. {wavexis-2.3.0 → wavexis-2.3.2}/docs/guide/init.md +0 -0
  48. {wavexis-2.3.0 → wavexis-2.3.2}/docs/guide/installation.md +0 -0
  49. {wavexis-2.3.0 → wavexis-2.3.2}/docs/guide/multi.md +0 -0
  50. {wavexis-2.3.0 → wavexis-2.3.2}/docs/guide/perf.md +0 -0
  51. {wavexis-2.3.0 → wavexis-2.3.2}/docs/guide/plugins.md +0 -0
  52. {wavexis-2.3.0 → wavexis-2.3.2}/docs/guide/quickstart.md +0 -0
  53. {wavexis-2.3.0 → wavexis-2.3.2}/docs/guide/raw.md +0 -0
  54. {wavexis-2.3.0 → wavexis-2.3.2}/docs/guide/repl.md +0 -0
  55. {wavexis-2.3.0 → wavexis-2.3.2}/docs/guide/troubleshooting.md +0 -0
  56. {wavexis-2.3.0 → wavexis-2.3.2}/docs/index.md +0 -0
  57. {wavexis-2.3.0 → wavexis-2.3.2}/mkdocs.yml +0 -0
  58. {wavexis-2.3.0 → wavexis-2.3.2}/stubs/bidiwave/__init__.pyi +0 -0
  59. {wavexis-2.3.0 → wavexis-2.3.2}/stubs/cdpwave/__init__.pyi +0 -0
  60. {wavexis-2.3.0 → wavexis-2.3.2}/tests/__init__.py +0 -0
  61. {wavexis-2.3.0 → wavexis-2.3.2}/tests/conftest.py +0 -0
  62. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/__init__.py +0 -0
  63. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_a11y.py +0 -0
  64. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_animation.py +0 -0
  65. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_backend_selection.py +0 -0
  66. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_browser.py +0 -0
  67. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_console.py +0 -0
  68. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_css.py +0 -0
  69. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_debug.py +0 -0
  70. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_dialog.py +0 -0
  71. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_dom.py +0 -0
  72. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_dom_snapshot.py +0 -0
  73. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_emulation.py +0 -0
  74. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_emulation_advanced.py +0 -0
  75. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_eval.py +0 -0
  76. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_har.py +0 -0
  77. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_input.py +0 -0
  78. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_media.py +0 -0
  79. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_multi.py +0 -0
  80. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_navigate.py +0 -0
  81. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_network.py +0 -0
  82. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_network_advanced.py +0 -0
  83. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_overlay.py +0 -0
  84. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_pdf.py +0 -0
  85. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_perf.py +0 -0
  86. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_permissions.py +0 -0
  87. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_raw.py +0 -0
  88. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_record.py +0 -0
  89. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_scrape.py +0 -0
  90. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_screenshot.py +0 -0
  91. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_security.py +0 -0
  92. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_serve.py +0 -0
  93. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_service_worker.py +0 -0
  94. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_storage.py +0 -0
  95. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_tabs.py +0 -0
  96. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_webaudio.py +0 -0
  97. {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_webauthn.py +0 -0
  98. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/__init__.py +0 -0
  99. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_abstract_backend_phase5.py +0 -0
  100. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_actions.py +0 -0
  101. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_actions_phase5.py +0 -0
  102. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_animation_action.py +0 -0
  103. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_assert_multi.py +0 -0
  104. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_auth.py +0 -0
  105. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_bidi_backend.py +0 -0
  106. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_bidi_phase5.py +0 -0
  107. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_bluetooth_action.py +0 -0
  108. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_cast_action.py +0 -0
  109. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_cleanup.py +0 -0
  110. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_cli_phase5.py +0 -0
  111. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_config.py +0 -0
  112. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_config_phase5.py +0 -0
  113. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_cookies_headers_actions.py +0 -0
  114. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_css_action.py +0 -0
  115. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_debug_action.py +0 -0
  116. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_dom_snapshot_action.py +0 -0
  117. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_download_screencast.py +0 -0
  118. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_emulation_action.py +0 -0
  119. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_error_handling.py +0 -0
  120. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_exceptions.py +0 -0
  121. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_format_record.py +0 -0
  122. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_global_options.py +0 -0
  123. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_init.py +0 -0
  124. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_input_action.py +0 -0
  125. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_interactions_crawl.py +0 -0
  126. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_media_action.py +0 -0
  127. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_multi.py +0 -0
  128. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_multi_improvements.py +0 -0
  129. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_new_features.py +0 -0
  130. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_output.py +0 -0
  131. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_overlay_action.py +0 -0
  132. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_perf_action.py +0 -0
  133. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_perf_console.py +0 -0
  134. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_plugins.py +0 -0
  135. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_raw.py +0 -0
  136. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_record.py +0 -0
  137. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_repl.py +0 -0
  138. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_serve.py +0 -0
  139. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_service_worker_action.py +0 -0
  140. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_storage_action.py +0 -0
  141. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_webaudio_action.py +0 -0
  142. {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_webauthn_action.py +0 -0
  143. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/__main__.py +0 -0
  144. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/__init__.py +0 -0
  145. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/accessibility.py +0 -0
  146. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/animation.py +0 -0
  147. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/base.py +0 -0
  148. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/bluetooth.py +0 -0
  149. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/browser.py +0 -0
  150. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/cast.py +0 -0
  151. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/console.py +0 -0
  152. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/cookies.py +0 -0
  153. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/crawl.py +0 -0
  154. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/css.py +0 -0
  155. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/debug.py +0 -0
  156. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/dialog.py +0 -0
  157. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/dom.py +0 -0
  158. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/dom_snapshot.py +0 -0
  159. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/download.py +0 -0
  160. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/emulation.py +0 -0
  161. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/eval.py +0 -0
  162. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/extract.py +0 -0
  163. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/form.py +0 -0
  164. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/har.py +0 -0
  165. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/headers.py +0 -0
  166. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/input.py +0 -0
  167. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/lighthouse.py +0 -0
  168. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/media.py +0 -0
  169. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/multi.py +0 -0
  170. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/navigate.py +0 -0
  171. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/network.py +0 -0
  172. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/overlay.py +0 -0
  173. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/pdf.py +0 -0
  174. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/performance.py +0 -0
  175. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/permissions.py +0 -0
  176. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/record.py +0 -0
  177. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/scrape.py +0 -0
  178. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/screencast.py +0 -0
  179. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/screenshot.py +0 -0
  180. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/security.py +0 -0
  181. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/service_worker.py +0 -0
  182. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/session.py +0 -0
  183. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/storage.py +0 -0
  184. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/tabs.py +0 -0
  185. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/wait.py +0 -0
  186. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/webaudio.py +0 -0
  187. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/webauthn.py +0 -0
  188. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/websocket.py +0 -0
  189. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/auth.py +0 -0
  190. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/backend/__init__.py +0 -0
  191. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/backend/manager.py +0 -0
  192. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cleanup.py +0 -0
  193. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/__init__.py +0 -0
  194. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/_advanced.py +0 -0
  195. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/_capture.py +0 -0
  196. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/_config.py +0 -0
  197. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/_debug.py +0 -0
  198. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/_emulation.py +0 -0
  199. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/_experimental.py +0 -0
  200. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/_input.py +0 -0
  201. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/_navigation.py +0 -0
  202. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/_network.py +0 -0
  203. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/_perf.py +0 -0
  204. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/_serve.py +0 -0
  205. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/_session.py +0 -0
  206. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/_shared.py +0 -0
  207. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/_workflow.py +0 -0
  208. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/config.py +0 -0
  209. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/exceptions.py +0 -0
  210. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/init.py +0 -0
  211. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/multi.py +0 -0
  212. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/output.py +0 -0
  213. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/plugins.py +0 -0
  214. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/record.py +0 -0
  215. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/repl.py +0 -0
  216. {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/serve.py +0 -0
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable changes to wavexis are documented in this file.
4
4
 
5
+ ## v2.3.2 — 2026-07-07
6
+
7
+ ### Added
8
+
9
+ - iframe support: `iframe_click`, `iframe_fill`, `iframe_eval` in both CDP and BiDi backends
10
+ - `wavexis iframe click/fill/eval` CLI command with `--iframe`, `--selector`, `--value`, `--expression` options
11
+ - Auto-waiting inside iframes via `_wait_for_element_in_iframe` with polling and configurable timeout
12
+ - `--no-wait` flag to skip auto-waiting for iframe interactions
13
+ - Same-origin iframe support via `contentDocument` access
14
+
15
+ ## v2.3.1 — 2026-07-07
16
+
17
+ ### Added
18
+
19
+ - `auto_wait: bool = True` parameter in `click`, `fill`, `hover` (both backends)
20
+ - Pass `auto_wait=False` to skip element visibility polling for instant execution
21
+
5
22
  ## v2.3.0 — 2026-07-07
6
23
 
7
24
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavexis
3
- Version: 2.3.0
3
+ Version: 2.3.2
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.0"
7
+ version = "2.3.2"
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" }
@@ -68,6 +68,35 @@ class TestAutoWait:
68
68
  asyncio.run(backend.click("#btn"))
69
69
  assert call_order == ["wait", "scroll", "box"]
70
70
 
71
+ def test_cdp_click_auto_wait_false_skips_wait(self) -> None:
72
+ """Test that click with auto_wait=False skips _wait_for_element."""
73
+ from wavexis.backend.cdp import CDPBackend
74
+
75
+ backend = CDPBackend()
76
+ backend._session = MagicMock()
77
+ backend._session.input = MagicMock()
78
+ backend._session.input.dispatch_mouse_event = AsyncMock()
79
+
80
+ call_order: list[str] = []
81
+
82
+ async def _mock_wait(selector: str, timeout_ms: int = 30000) -> None:
83
+ call_order.append("wait")
84
+
85
+ async def _mock_scroll(selector: str) -> None:
86
+ call_order.append("scroll")
87
+
88
+ async def _mock_box(selector: str) -> tuple[float, float]:
89
+ call_order.append("box")
90
+ return 50.0, 25.0
91
+
92
+ backend._wait_for_element = _mock_wait # type: ignore[assignment]
93
+ backend._scroll_into_view_if_needed = _mock_scroll # type: ignore[assignment]
94
+ backend._get_box_center = _mock_box # type: ignore[assignment]
95
+
96
+ asyncio.run(backend.click("#btn", auto_wait=False))
97
+ assert "wait" not in call_order
98
+ assert call_order == ["scroll", "box"]
99
+
71
100
  def test_bidi_wait_for_element_found(self) -> None:
72
101
  """Test BiDi _wait_for_element returns when element is visible."""
73
102
  from wavexis.backend.bidi import BiDiBackend
@@ -206,7 +206,13 @@ class DummyBackend(AbstractBackend):
206
206
  """Set dark mode."""
207
207
  pass
208
208
 
209
- async def click(self, selector: str, button: str = "left", click_count: int = 1) -> None:
209
+ async def click(
210
+ self,
211
+ selector: str,
212
+ button: str = "left",
213
+ click_count: int = 1,
214
+ auto_wait: bool = True,
215
+ ) -> None:
210
216
  """Click."""
211
217
  pass
212
218
 
@@ -214,7 +220,9 @@ class DummyBackend(AbstractBackend):
214
220
  """Type text."""
215
221
  pass
216
222
 
217
- async def fill(self, selector: str, value: str) -> None:
223
+ async def fill(
224
+ self, selector: str, value: str, auto_wait: bool = True
225
+ ) -> None:
218
226
  """Fill."""
219
227
  pass
220
228
 
@@ -222,7 +230,7 @@ class DummyBackend(AbstractBackend):
222
230
  """Select option."""
223
231
  pass
224
232
 
225
- async def hover(self, selector: str) -> None:
233
+ async def hover(self, selector: str, auto_wait: bool = True) -> None:
226
234
  """Hover."""
227
235
  pass
228
236
 
@@ -242,6 +250,24 @@ class DummyBackend(AbstractBackend):
242
250
  """Set files."""
243
251
  pass
244
252
 
253
+ async def iframe_eval(
254
+ self, iframe_selector: str, expression: str, await_promise: bool = False
255
+ ) -> object:
256
+ """Iframe eval."""
257
+ return None
258
+
259
+ async def iframe_click(
260
+ self, iframe_selector: str, selector: str, auto_wait: bool = True
261
+ ) -> None:
262
+ """Iframe click."""
263
+ pass
264
+
265
+ async def iframe_fill(
266
+ self, iframe_selector: str, selector: str, value: str, auto_wait: bool = True
267
+ ) -> None:
268
+ """Iframe fill."""
269
+ pass
270
+
245
271
  async def throttle_network(self, params: object) -> None:
246
272
  """Throttle network."""
247
273
  pass
@@ -0,0 +1,203 @@
1
+ """Unit tests for iframe support — iframe_eval, iframe_click, iframe_fill."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import Any
7
+ from unittest.mock import AsyncMock, MagicMock
8
+
9
+ import pytest
10
+
11
+ from wavexis.exceptions import ElementNotFoundError
12
+
13
+
14
+ def _make_cdp_backend() -> Any:
15
+ """Create a CDPBackend with mocked session."""
16
+ from wavexis.backend.cdp import CDPBackend
17
+
18
+ backend = CDPBackend()
19
+ backend._session = MagicMock()
20
+ backend._session.runtime = MagicMock()
21
+ backend._session.runtime.evaluate = AsyncMock(
22
+ return_value={"result": {"value": True}}
23
+ )
24
+ return backend
25
+
26
+
27
+ def _make_bidi_backend() -> Any:
28
+ """Create a BiDiBackend with mocked client."""
29
+ from wavexis.backend.bidi import BiDiBackend
30
+
31
+ backend = BiDiBackend()
32
+ backend._client = MagicMock()
33
+ backend._context = MagicMock()
34
+ backend._client.script = MagicMock()
35
+ result = MagicMock()
36
+ result.value = True
37
+ backend._client.script.evaluate = AsyncMock(return_value=result)
38
+ return backend
39
+
40
+
41
+ @pytest.mark.unit
42
+ class TestIframeEval:
43
+ """Tests for iframe_eval in both backends."""
44
+
45
+ def test_cdp_iframe_eval_returns_value(self) -> None:
46
+ """Test CDP iframe_eval returns the evaluated value."""
47
+ backend = _make_cdp_backend()
48
+ backend._session.runtime.evaluate = AsyncMock(
49
+ return_value={"result": {"value": 42}}
50
+ )
51
+ result = asyncio.run(
52
+ backend.iframe_eval("#myframe", "1 + 1", await_promise=False)
53
+ )
54
+ assert result == 42
55
+
56
+ def test_cdp_iframe_eval_null_iframe_not_found(self) -> None:
57
+ """Test CDP iframe_eval returns None when iframe is not found."""
58
+ backend = _make_cdp_backend()
59
+ backend._session.runtime.evaluate = AsyncMock(
60
+ return_value={"result": {"value": None}}
61
+ )
62
+ result = asyncio.run(
63
+ backend.iframe_eval("#missing", "document.title")
64
+ )
65
+ assert result is None
66
+
67
+ def test_bidi_iframe_eval_returns_value(self) -> None:
68
+ """Test BiDi iframe_eval returns the evaluated value."""
69
+ backend = _make_bidi_backend()
70
+ result_mock = MagicMock()
71
+ result_mock.value = "hello"
72
+ backend._client.script.evaluate = AsyncMock(return_value=result_mock)
73
+ result = asyncio.run(
74
+ backend.iframe_eval("#myframe", "document.title")
75
+ )
76
+ assert result == "hello"
77
+
78
+
79
+ @pytest.mark.unit
80
+ class TestIframeClick:
81
+ """Tests for iframe_click in both backends."""
82
+
83
+ def test_cdp_iframe_click_success(self) -> None:
84
+ """Test CDP iframe_click dispatches click inside iframe."""
85
+ backend = _make_cdp_backend()
86
+ backend._session.runtime.evaluate = AsyncMock(
87
+ return_value={"result": {"value": True}}
88
+ )
89
+ asyncio.run(backend.iframe_click("#myframe", "#btn", auto_wait=False))
90
+ backend._session.runtime.evaluate.assert_called()
91
+
92
+ def test_cdp_iframe_click_element_not_found(self) -> None:
93
+ """Test CDP iframe_click raises ElementNotFoundError when element missing."""
94
+ backend = _make_cdp_backend()
95
+ backend._session.runtime.evaluate = AsyncMock(
96
+ return_value={"result": {"value": False}}
97
+ )
98
+ with pytest.raises(ElementNotFoundError):
99
+ asyncio.run(backend.iframe_click("#myframe", "#missing", auto_wait=False))
100
+
101
+ def test_bidi_iframe_click_success(self) -> None:
102
+ """Test BiDi iframe_click dispatches click inside iframe."""
103
+ backend = _make_bidi_backend()
104
+ result_mock = MagicMock()
105
+ result_mock.value = True
106
+ backend._client.script.evaluate = AsyncMock(return_value=result_mock)
107
+ asyncio.run(backend.iframe_click("#myframe", "#btn", auto_wait=False))
108
+ backend._client.script.evaluate.assert_called()
109
+
110
+ def test_bidi_iframe_click_element_not_found(self) -> None:
111
+ """Test BiDi iframe_click raises ElementNotFoundError when element missing."""
112
+ backend = _make_bidi_backend()
113
+ result_mock = MagicMock()
114
+ result_mock.value = False
115
+ backend._client.script.evaluate = AsyncMock(return_value=result_mock)
116
+ with pytest.raises(ElementNotFoundError):
117
+ asyncio.run(backend.iframe_click("#myframe", "#missing", auto_wait=False))
118
+
119
+
120
+ @pytest.mark.unit
121
+ class TestIframeFill:
122
+ """Tests for iframe_fill in both backends."""
123
+
124
+ def test_cdp_iframe_fill_success(self) -> None:
125
+ """Test CDP iframe_fill sets value inside iframe."""
126
+ backend = _make_cdp_backend()
127
+ backend._session.runtime.evaluate = AsyncMock(
128
+ return_value={"result": {"value": True}}
129
+ )
130
+ asyncio.run(
131
+ backend.iframe_fill("#myframe", "#input", "test value", auto_wait=False)
132
+ )
133
+ backend._session.runtime.evaluate.assert_called()
134
+
135
+ def test_cdp_iframe_fill_element_not_found(self) -> None:
136
+ """Test CDP iframe_fill raises ElementNotFoundError when element missing."""
137
+ backend = _make_cdp_backend()
138
+ backend._session.runtime.evaluate = AsyncMock(
139
+ return_value={"result": {"value": False}}
140
+ )
141
+ with pytest.raises(ElementNotFoundError):
142
+ asyncio.run(
143
+ backend.iframe_fill("#myframe", "#missing", "val", auto_wait=False)
144
+ )
145
+
146
+ def test_bidi_iframe_fill_success(self) -> None:
147
+ """Test BiDi iframe_fill sets value inside iframe."""
148
+ backend = _make_bidi_backend()
149
+ result_mock = MagicMock()
150
+ result_mock.value = True
151
+ backend._client.script.evaluate = AsyncMock(return_value=result_mock)
152
+ asyncio.run(
153
+ backend.iframe_fill("#myframe", "#input", "test value", auto_wait=False)
154
+ )
155
+ backend._client.script.evaluate.assert_called()
156
+
157
+ def test_bidi_iframe_fill_element_not_found(self) -> None:
158
+ """Test BiDi iframe_fill raises ElementNotFoundError when element missing."""
159
+ backend = _make_bidi_backend()
160
+ result_mock = MagicMock()
161
+ result_mock.value = False
162
+ backend._client.script.evaluate = AsyncMock(return_value=result_mock)
163
+ with pytest.raises(ElementNotFoundError):
164
+ asyncio.run(
165
+ backend.iframe_fill("#myframe", "#missing", "val", auto_wait=False)
166
+ )
167
+
168
+
169
+ @pytest.mark.unit
170
+ class TestIframeAutoWait:
171
+ """Tests for auto_wait in iframe operations."""
172
+
173
+ def test_cdp_iframe_click_auto_wait_false_skips_wait(self) -> None:
174
+ """Test that iframe_click with auto_wait=False skips _wait_for_element_in_iframe."""
175
+ backend = _make_cdp_backend()
176
+ call_order: list[str] = []
177
+
178
+ async def _mock_wait(iframe_sel: str, sel: str, timeout_ms: int = 30000) -> None:
179
+ call_order.append("wait")
180
+
181
+ backend._wait_for_element_in_iframe = _mock_wait # type: ignore[assignment]
182
+ backend._session.runtime.evaluate = AsyncMock(
183
+ return_value={"result": {"value": True}}
184
+ )
185
+
186
+ asyncio.run(backend.iframe_click("#myframe", "#btn", auto_wait=False))
187
+ assert "wait" not in call_order
188
+
189
+ def test_cdp_iframe_click_auto_wait_true_calls_wait(self) -> None:
190
+ """Test that iframe_click with auto_wait=True calls _wait_for_element_in_iframe."""
191
+ backend = _make_cdp_backend()
192
+ call_order: list[str] = []
193
+
194
+ async def _mock_wait(iframe_sel: str, sel: str, timeout_ms: int = 30000) -> None:
195
+ call_order.append("wait")
196
+
197
+ backend._wait_for_element_in_iframe = _mock_wait # type: ignore[assignment]
198
+ backend._session.runtime.evaluate = AsyncMock(
199
+ return_value={"result": {"value": True}}
200
+ )
201
+
202
+ asyncio.run(backend.iframe_click("#myframe", "#btn", auto_wait=True))
203
+ assert "wait" in call_order
@@ -102,15 +102,15 @@ class FakeBackend(AbstractBackend):
102
102
  """Set timezone."""
103
103
  async def set_dark_mode(self, enabled):
104
104
  """Set dark mode."""
105
- async def click(self, selector, button="left", click_count=1):
105
+ async def click(self, selector, button="left", click_count=1, auto_wait=True):
106
106
  """Click."""
107
107
  async def type_text(self, selector, text, delay=0):
108
108
  """Type text."""
109
- async def fill(self, selector, value):
109
+ async def fill(self, selector, value, auto_wait=True):
110
110
  """Fill."""
111
111
  async def select_option(self, selector, value):
112
112
  """Select option."""
113
- async def hover(self, selector):
113
+ async def hover(self, selector, auto_wait=True):
114
114
  """Hover."""
115
115
  async def key_press(self, key):
116
116
  """Key press."""
@@ -262,6 +262,13 @@ class FakeBackend(AbstractBackend):
262
262
  """Bluetooth stop."""
263
263
  async def set_files(self, selector, files):
264
264
  """Set files."""
265
+ async def iframe_eval(self, iframe_selector, expression, await_promise=False):
266
+ """Iframe eval."""
267
+ return None
268
+ async def iframe_click(self, iframe_selector, selector, auto_wait=True):
269
+ """Iframe click."""
270
+ async def iframe_fill(self, iframe_selector, selector, value, auto_wait=True):
271
+ """Iframe fill."""
265
272
 
266
273
 
267
274
  class TestBackendManager:
@@ -1,3 +1,3 @@
1
1
  """wavexis — CLI de automatización de navegador."""
2
2
 
3
- __version__ = "2.3.0"
3
+ __version__ = "2.3.2"
@@ -237,7 +237,11 @@ class AbstractBackend(ABC):
237
237
 
238
238
  @abstractmethod
239
239
  async def click(
240
- self, selector: str, button: str = "left", click_count: int = 1
240
+ self,
241
+ selector: str,
242
+ button: str = "left",
243
+ click_count: int = 1,
244
+ auto_wait: bool = True,
241
245
  ) -> None:
242
246
  """Click an element matching a CSS selector."""
243
247
 
@@ -246,7 +250,9 @@ class AbstractBackend(ABC):
246
250
  """Type text into an element, optionally with delay between keystrokes."""
247
251
 
248
252
  @abstractmethod
249
- async def fill(self, selector: str, value: str) -> None:
253
+ async def fill(
254
+ self, selector: str, value: str, auto_wait: bool = True
255
+ ) -> None:
250
256
  """Fill an input element with a value (replaces existing content)."""
251
257
 
252
258
  @abstractmethod
@@ -254,7 +260,7 @@ class AbstractBackend(ABC):
254
260
  """Select an option in a <select> element by value."""
255
261
 
256
262
  @abstractmethod
257
- async def hover(self, selector: str) -> None:
263
+ async def hover(self, selector: str, auto_wait: bool = True) -> None:
258
264
  """Hover over an element matching a CSS selector."""
259
265
 
260
266
  @abstractmethod
@@ -278,6 +284,45 @@ class AbstractBackend(ABC):
278
284
  files: List of absolute file paths to upload.
279
285
  """
280
286
 
287
+ # ── iframe ─────────────────────────────────────────────
288
+
289
+ @abstractmethod
290
+ async def iframe_eval(
291
+ self, iframe_selector: str, expression: str, await_promise: bool = False
292
+ ) -> Any:
293
+ """Evaluate a JavaScript expression inside an iframe.
294
+
295
+ Args:
296
+ iframe_selector: CSS selector for the <iframe> element.
297
+ expression: JavaScript expression to evaluate in the iframe context.
298
+ await_promise: Whether to await a returned Promise.
299
+ """
300
+
301
+ @abstractmethod
302
+ async def iframe_click(
303
+ self, iframe_selector: str, selector: str, auto_wait: bool = True
304
+ ) -> None:
305
+ """Click an element inside an iframe.
306
+
307
+ Args:
308
+ iframe_selector: CSS selector for the <iframe> element.
309
+ selector: CSS selector inside the iframe for the target element.
310
+ auto_wait: If True, wait for element to be visible before clicking.
311
+ """
312
+
313
+ @abstractmethod
314
+ async def iframe_fill(
315
+ self, iframe_selector: str, selector: str, value: str, auto_wait: bool = True
316
+ ) -> None:
317
+ """Fill an input element inside an iframe.
318
+
319
+ Args:
320
+ iframe_selector: CSS selector for the <iframe> element.
321
+ selector: CSS selector inside the iframe for the target element.
322
+ value: Value to set in the input field.
323
+ auto_wait: If True, wait for element to be visible before filling.
324
+ """
325
+
281
326
  # ── Network advanced ───────────────────────────────────
282
327
 
283
328
  @abstractmethod
@@ -26,7 +26,11 @@ from wavexis.config import (
26
26
  ThrottleParams,
27
27
  WaitStrategy,
28
28
  )
29
- from wavexis.exceptions import SessionNotInitializedError, WaitTimeoutError
29
+ from wavexis.exceptions import (
30
+ ElementNotFoundError,
31
+ SessionNotInitializedError,
32
+ WaitTimeoutError,
33
+ )
30
34
 
31
35
  try:
32
36
  from bidiwave import BiDiClient
@@ -853,11 +857,23 @@ class BiDiBackend(AbstractBackend):
853
857
  await client.script.evaluate(self._context, js)
854
858
 
855
859
  async def click(
856
- self, selector: str, button: str = "left", click_count: int = 1
860
+ self,
861
+ selector: str,
862
+ button: str = "left",
863
+ click_count: int = 1,
864
+ auto_wait: bool = True,
857
865
  ) -> None:
858
- """Click an element via BiDi script.evaluate."""
866
+ """Click an element via BiDi script.evaluate.
867
+
868
+ Args:
869
+ selector: CSS selector for the target element.
870
+ button: Mouse button (unused in BiDi, for API compatibility).
871
+ click_count: Number of clicks to dispatch.
872
+ auto_wait: If True, wait for element to be visible before clicking.
873
+ """
859
874
  client = self._require_client()
860
- await self._wait_for_element(selector)
875
+ if auto_wait:
876
+ await self._wait_for_element(selector)
861
877
  await self._scroll_into_view_if_needed(selector)
862
878
  escaped = selector.replace("'", "\\'")
863
879
  js = (
@@ -887,10 +903,19 @@ class BiDiBackend(AbstractBackend):
887
903
  if delay > 0:
888
904
  await _asyncio.sleep(delay / 1000)
889
905
 
890
- async def fill(self, selector: str, value: str) -> None:
891
- """Fill an input element with a value via BiDi."""
906
+ async def fill(
907
+ self, selector: str, value: str, auto_wait: bool = True
908
+ ) -> None:
909
+ """Fill an input element with a value via BiDi.
910
+
911
+ Args:
912
+ selector: CSS selector for the target element.
913
+ value: Value to set in the input field.
914
+ auto_wait: If True, wait for element to be visible before filling.
915
+ """
892
916
  client = self._require_client()
893
- await self._wait_for_element(selector)
917
+ if auto_wait:
918
+ await self._wait_for_element(selector)
894
919
  await self._scroll_into_view_if_needed(selector)
895
920
  escaped = selector.replace("'", "\\'")
896
921
  escaped_val = value.replace("\\", "\\\\").replace("'", "\\'")
@@ -906,10 +931,16 @@ class BiDiBackend(AbstractBackend):
906
931
  js = f"document.querySelector('{escaped}').value = '{escaped_val}'"
907
932
  await self._client.script.evaluate(self._context, js)
908
933
 
909
- async def hover(self, selector: str) -> None:
910
- """Hover over an element via BiDi script.evaluate."""
934
+ async def hover(self, selector: str, auto_wait: bool = True) -> None:
935
+ """Hover over an element via BiDi script.evaluate.
936
+
937
+ Args:
938
+ selector: CSS selector for the target element.
939
+ auto_wait: If True, wait for element to be visible before hovering.
940
+ """
911
941
  client = self._require_client()
912
- await self._wait_for_element(selector)
942
+ if auto_wait:
943
+ await self._wait_for_element(selector)
913
944
  await self._scroll_into_view_if_needed(selector)
914
945
  escaped = selector.replace("'", "\\'")
915
946
  js = (
@@ -970,7 +1001,132 @@ class BiDiBackend(AbstractBackend):
970
1001
  )
971
1002
  await self._client.script.evaluate(self._context, js)
972
1003
 
973
- # ── Network advanced ───────────────────────────────────
1004
+ # ── iframe ─────────────────────────────────────────────
1005
+
1006
+ async def iframe_eval(
1007
+ self, iframe_selector: str, expression: str, await_promise: bool = False
1008
+ ) -> Any:
1009
+ """Evaluate a JavaScript expression inside an iframe.
1010
+
1011
+ Args:
1012
+ iframe_selector: CSS selector for the <iframe> element.
1013
+ expression: JavaScript expression to evaluate in the iframe context.
1014
+ await_promise: Whether to await a returned Promise.
1015
+
1016
+ Returns:
1017
+ The evaluation result value.
1018
+ """
1019
+ client = self._require_client()
1020
+ escaped_iframe = iframe_selector.replace("'", "\\'")
1021
+ escaped_expr = expression.replace("\\", "\\\\").replace("'", "\\'")
1022
+ js = (
1023
+ f"(function(){{var f=document.querySelector('{escaped_iframe}');"
1024
+ f"if(!f||!f.contentDocument)return null;"
1025
+ f"return (function(){{{escaped_expr}}}).call(f.contentDocument);}})()"
1026
+ )
1027
+ result = await client.script.evaluate(
1028
+ self._context, js, await_promise=await_promise
1029
+ )
1030
+ if hasattr(result, "value"):
1031
+ return result.value
1032
+ return result
1033
+
1034
+ async def _wait_for_element_in_iframe(
1035
+ self, iframe_selector: str, selector: str, timeout_ms: int = 30000
1036
+ ) -> None:
1037
+ """Wait for an element inside an iframe to exist and be visible.
1038
+
1039
+ Args:
1040
+ iframe_selector: CSS selector for the <iframe> element.
1041
+ selector: CSS selector inside the iframe.
1042
+ timeout_ms: Maximum wait time in milliseconds.
1043
+
1044
+ Raises:
1045
+ WaitTimeoutError: If the element is not found within the timeout.
1046
+ """
1047
+ import asyncio as _asyncio
1048
+ import time as _time
1049
+
1050
+ client = self._require_client()
1051
+ escaped_iframe = iframe_selector.replace("'", "\\'")
1052
+ escaped_sel = selector.replace("'", "\\'")
1053
+ js = (
1054
+ f"(function(){{var f=document.querySelector('{escaped_iframe}');"
1055
+ f"if(!f||!f.contentDocument)return false;"
1056
+ f"var el=f.contentDocument.querySelector('{escaped_sel}');"
1057
+ f"if(!el)return false;"
1058
+ f"var rect=el.getBoundingClientRect();"
1059
+ f"return rect.width>0&&rect.height>0;}})()"
1060
+ )
1061
+ deadline = _time.monotonic() + timeout_ms / 1000
1062
+ while _time.monotonic() < deadline:
1063
+ result = await client.script.evaluate(self._context, js)
1064
+ value = getattr(result, "value", None)
1065
+ if value is True or value == "true":
1066
+ return
1067
+ await _asyncio.sleep(0.1)
1068
+ raise WaitTimeoutError("selector", timeout_ms)
1069
+
1070
+ async def iframe_click(
1071
+ self, iframe_selector: str, selector: str, auto_wait: bool = True
1072
+ ) -> None:
1073
+ """Click an element inside an iframe.
1074
+
1075
+ Args:
1076
+ iframe_selector: CSS selector for the <iframe> element.
1077
+ selector: CSS selector inside the iframe for the target element.
1078
+ auto_wait: If True, wait for element to be visible before clicking.
1079
+ """
1080
+ client = self._require_client()
1081
+ if auto_wait:
1082
+ await self._wait_for_element_in_iframe(iframe_selector, selector)
1083
+ escaped_iframe = iframe_selector.replace("'", "\\'")
1084
+ escaped_sel = selector.replace("'", "\\'")
1085
+ js = (
1086
+ f"(function(){{var f=document.querySelector('{escaped_iframe}');"
1087
+ f"if(!f||!f.contentDocument)return false;"
1088
+ f"var el=f.contentDocument.querySelector('{escaped_sel}');"
1089
+ f"if(!el)return false;"
1090
+ f"el.scrollIntoView({{block:'center',behavior:'instant'}});"
1091
+ f"el.dispatchEvent(new MouseEvent('click',{{bubbles:true}}));"
1092
+ f"return true;}})()"
1093
+ )
1094
+ result = await client.script.evaluate(self._context, js)
1095
+ value = getattr(result, "value", None)
1096
+ if not value:
1097
+ raise ElementNotFoundError(selector)
1098
+
1099
+ async def iframe_fill(
1100
+ self, iframe_selector: str, selector: str, value: str, auto_wait: bool = True
1101
+ ) -> None:
1102
+ """Fill an input element inside an iframe.
1103
+
1104
+ Args:
1105
+ iframe_selector: CSS selector for the <iframe> element.
1106
+ selector: CSS selector inside the iframe for the target element.
1107
+ value: Value to set in the input field.
1108
+ auto_wait: If True, wait for element to be visible before filling.
1109
+ """
1110
+ client = self._require_client()
1111
+ if auto_wait:
1112
+ await self._wait_for_element_in_iframe(iframe_selector, selector)
1113
+ escaped_iframe = iframe_selector.replace("'", "\\'")
1114
+ escaped_sel = selector.replace("'", "\\'")
1115
+ escaped_val = value.replace("\\", "\\\\").replace("'", "\\'")
1116
+ js = (
1117
+ f"(function(){{var f=document.querySelector('{escaped_iframe}');"
1118
+ f"if(!f||!f.contentDocument)return false;"
1119
+ f"var el=f.contentDocument.querySelector('{escaped_sel}');"
1120
+ f"if(!el)return false;"
1121
+ f"el.focus();el.value='{escaped_val}';"
1122
+ f"el.dispatchEvent(new Event('input',{{bubbles:true}}));"
1123
+ f"el.dispatchEvent(new Event('change',{{bubbles:true}}));"
1124
+ f"return true;}})()"
1125
+ )
1126
+ result = await client.script.evaluate(self._context, js)
1127
+ result_value = getattr(result, "value", None)
1128
+ if not result_value:
1129
+ raise ElementNotFoundError(selector)
974
1130
 
975
1131
  async def block_requests(self, patterns: list[str]) -> None:
976
1132
  """Block requests matching URL patterns (partial BiDi support)."""