wavexis 2.3.1__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.1 → wavexis-2.3.2}/CHANGELOG.md +10 -0
  2. {wavexis-2.3.1 → wavexis-2.3.2}/PKG-INFO +1 -1
  3. {wavexis-2.3.1 → wavexis-2.3.2}/pyproject.toml +1 -1
  4. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_backend_manager.py +29 -3
  5. wavexis-2.3.2/tests/unit/test_iframe.py +203 -0
  6. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_manager.py +10 -3
  7. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/__init__.py +1 -1
  8. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/backend/base.py +39 -0
  9. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/backend/bidi.py +131 -2
  10. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/backend/cdp.py +117 -0
  11. wavexis-2.3.2/wavexis/cli/_iframe.py +132 -0
  12. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/cli/app.py +1 -0
  13. {wavexis-2.3.1 → wavexis-2.3.2}/.dockerignore +0 -0
  14. {wavexis-2.3.1 → wavexis-2.3.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  15. {wavexis-2.3.1 → wavexis-2.3.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  16. {wavexis-2.3.1 → wavexis-2.3.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  17. {wavexis-2.3.1 → wavexis-2.3.2}/.github/workflows/ci.yml +0 -0
  18. {wavexis-2.3.1 → wavexis-2.3.2}/.github/workflows/docs.yml +0 -0
  19. {wavexis-2.3.1 → wavexis-2.3.2}/.github/workflows/release.yml +0 -0
  20. {wavexis-2.3.1 → wavexis-2.3.2}/.gitignore +0 -0
  21. {wavexis-2.3.1 → wavexis-2.3.2}/CODE_OF_CONDUCT.md +0 -0
  22. {wavexis-2.3.1 → wavexis-2.3.2}/CONTRIBUTING.md +0 -0
  23. {wavexis-2.3.1 → wavexis-2.3.2}/Dockerfile +0 -0
  24. {wavexis-2.3.1 → wavexis-2.3.2}/LICENSE +0 -0
  25. {wavexis-2.3.1 → wavexis-2.3.2}/README.md +0 -0
  26. {wavexis-2.3.1 → wavexis-2.3.2}/SECURITY.md +0 -0
  27. {wavexis-2.3.1 → wavexis-2.3.2}/docs/api/actions.md +0 -0
  28. {wavexis-2.3.1 → wavexis-2.3.2}/docs/api/backends.md +0 -0
  29. {wavexis-2.3.1 → wavexis-2.3.2}/docs/api/cli.md +0 -0
  30. {wavexis-2.3.1 → wavexis-2.3.2}/docs/api/config.md +0 -0
  31. {wavexis-2.3.1 → wavexis-2.3.2}/docs/api/exceptions.md +0 -0
  32. {wavexis-2.3.1 → wavexis-2.3.2}/docs/api/multi.md +0 -0
  33. {wavexis-2.3.1 → wavexis-2.3.2}/docs/api/plugins.md +0 -0
  34. {wavexis-2.3.1 → wavexis-2.3.2}/docs/api/serve.md +0 -0
  35. {wavexis-2.3.1 → wavexis-2.3.2}/docs/cookbook/auth.md +0 -0
  36. {wavexis-2.3.1 → wavexis-2.3.2}/docs/cookbook/ci-cd.md +0 -0
  37. {wavexis-2.3.1 → wavexis-2.3.2}/docs/cookbook/css.md +0 -0
  38. {wavexis-2.3.1 → wavexis-2.3.2}/docs/cookbook/debug.md +0 -0
  39. {wavexis-2.3.1 → wavexis-2.3.2}/docs/cookbook/record-replay.md +0 -0
  40. {wavexis-2.3.1 → wavexis-2.3.2}/docs/cookbook/scraping.md +0 -0
  41. {wavexis-2.3.1 → wavexis-2.3.2}/docs/cookbook/screenshots.md +0 -0
  42. {wavexis-2.3.1 → wavexis-2.3.2}/docs/cookbook/serve-mode.md +0 -0
  43. {wavexis-2.3.1 → wavexis-2.3.2}/docs/guide/assert.md +0 -0
  44. {wavexis-2.3.1 → wavexis-2.3.2}/docs/guide/backends.md +0 -0
  45. {wavexis-2.3.1 → wavexis-2.3.2}/docs/guide/commands.md +0 -0
  46. {wavexis-2.3.1 → wavexis-2.3.2}/docs/guide/init.md +0 -0
  47. {wavexis-2.3.1 → wavexis-2.3.2}/docs/guide/installation.md +0 -0
  48. {wavexis-2.3.1 → wavexis-2.3.2}/docs/guide/multi.md +0 -0
  49. {wavexis-2.3.1 → wavexis-2.3.2}/docs/guide/perf.md +0 -0
  50. {wavexis-2.3.1 → wavexis-2.3.2}/docs/guide/plugins.md +0 -0
  51. {wavexis-2.3.1 → wavexis-2.3.2}/docs/guide/quickstart.md +0 -0
  52. {wavexis-2.3.1 → wavexis-2.3.2}/docs/guide/raw.md +0 -0
  53. {wavexis-2.3.1 → wavexis-2.3.2}/docs/guide/repl.md +0 -0
  54. {wavexis-2.3.1 → wavexis-2.3.2}/docs/guide/troubleshooting.md +0 -0
  55. {wavexis-2.3.1 → wavexis-2.3.2}/docs/index.md +0 -0
  56. {wavexis-2.3.1 → wavexis-2.3.2}/mkdocs.yml +0 -0
  57. {wavexis-2.3.1 → wavexis-2.3.2}/stubs/bidiwave/__init__.pyi +0 -0
  58. {wavexis-2.3.1 → wavexis-2.3.2}/stubs/cdpwave/__init__.pyi +0 -0
  59. {wavexis-2.3.1 → wavexis-2.3.2}/tests/__init__.py +0 -0
  60. {wavexis-2.3.1 → wavexis-2.3.2}/tests/conftest.py +0 -0
  61. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/__init__.py +0 -0
  62. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_a11y.py +0 -0
  63. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_animation.py +0 -0
  64. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_backend_selection.py +0 -0
  65. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_browser.py +0 -0
  66. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_console.py +0 -0
  67. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_css.py +0 -0
  68. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_debug.py +0 -0
  69. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_dialog.py +0 -0
  70. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_dom.py +0 -0
  71. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_dom_snapshot.py +0 -0
  72. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_emulation.py +0 -0
  73. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_emulation_advanced.py +0 -0
  74. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_eval.py +0 -0
  75. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_har.py +0 -0
  76. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_input.py +0 -0
  77. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_media.py +0 -0
  78. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_multi.py +0 -0
  79. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_navigate.py +0 -0
  80. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_network.py +0 -0
  81. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_network_advanced.py +0 -0
  82. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_overlay.py +0 -0
  83. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_pdf.py +0 -0
  84. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_perf.py +0 -0
  85. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_permissions.py +0 -0
  86. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_raw.py +0 -0
  87. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_record.py +0 -0
  88. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_scrape.py +0 -0
  89. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_screenshot.py +0 -0
  90. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_security.py +0 -0
  91. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_serve.py +0 -0
  92. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_service_worker.py +0 -0
  93. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_storage.py +0 -0
  94. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_tabs.py +0 -0
  95. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_webaudio.py +0 -0
  96. {wavexis-2.3.1 → wavexis-2.3.2}/tests/integration/test_webauthn.py +0 -0
  97. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/__init__.py +0 -0
  98. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_abstract_backend_phase5.py +0 -0
  99. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_actions.py +0 -0
  100. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_actions_phase5.py +0 -0
  101. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_animation_action.py +0 -0
  102. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_assert_multi.py +0 -0
  103. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_auth.py +0 -0
  104. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_auto_wait.py +0 -0
  105. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_bidi_backend.py +0 -0
  106. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_bidi_phase5.py +0 -0
  107. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_bluetooth_action.py +0 -0
  108. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_cast_action.py +0 -0
  109. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_cleanup.py +0 -0
  110. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_cli_phase5.py +0 -0
  111. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_config.py +0 -0
  112. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_config_phase5.py +0 -0
  113. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_cookies_headers_actions.py +0 -0
  114. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_css_action.py +0 -0
  115. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_debug_action.py +0 -0
  116. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_dom_snapshot_action.py +0 -0
  117. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_download_screencast.py +0 -0
  118. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_emulation_action.py +0 -0
  119. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_error_handling.py +0 -0
  120. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_exceptions.py +0 -0
  121. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_format_record.py +0 -0
  122. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_global_options.py +0 -0
  123. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_init.py +0 -0
  124. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_input_action.py +0 -0
  125. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_interactions_crawl.py +0 -0
  126. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_media_action.py +0 -0
  127. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_multi.py +0 -0
  128. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_multi_improvements.py +0 -0
  129. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_new_features.py +0 -0
  130. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_output.py +0 -0
  131. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_overlay_action.py +0 -0
  132. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_perf_action.py +0 -0
  133. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_perf_console.py +0 -0
  134. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_plugins.py +0 -0
  135. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_raw.py +0 -0
  136. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_record.py +0 -0
  137. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_repl.py +0 -0
  138. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_serve.py +0 -0
  139. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_service_worker_action.py +0 -0
  140. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_storage_action.py +0 -0
  141. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_webaudio_action.py +0 -0
  142. {wavexis-2.3.1 → wavexis-2.3.2}/tests/unit/test_webauthn_action.py +0 -0
  143. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/__main__.py +0 -0
  144. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/__init__.py +0 -0
  145. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/accessibility.py +0 -0
  146. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/animation.py +0 -0
  147. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/base.py +0 -0
  148. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/bluetooth.py +0 -0
  149. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/browser.py +0 -0
  150. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/cast.py +0 -0
  151. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/console.py +0 -0
  152. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/cookies.py +0 -0
  153. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/crawl.py +0 -0
  154. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/css.py +0 -0
  155. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/debug.py +0 -0
  156. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/dialog.py +0 -0
  157. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/dom.py +0 -0
  158. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/dom_snapshot.py +0 -0
  159. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/download.py +0 -0
  160. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/emulation.py +0 -0
  161. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/eval.py +0 -0
  162. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/extract.py +0 -0
  163. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/form.py +0 -0
  164. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/har.py +0 -0
  165. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/headers.py +0 -0
  166. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/input.py +0 -0
  167. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/lighthouse.py +0 -0
  168. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/media.py +0 -0
  169. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/multi.py +0 -0
  170. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/navigate.py +0 -0
  171. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/network.py +0 -0
  172. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/overlay.py +0 -0
  173. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/pdf.py +0 -0
  174. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/performance.py +0 -0
  175. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/permissions.py +0 -0
  176. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/record.py +0 -0
  177. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/scrape.py +0 -0
  178. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/screencast.py +0 -0
  179. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/screenshot.py +0 -0
  180. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/security.py +0 -0
  181. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/service_worker.py +0 -0
  182. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/session.py +0 -0
  183. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/storage.py +0 -0
  184. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/tabs.py +0 -0
  185. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/wait.py +0 -0
  186. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/webaudio.py +0 -0
  187. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/webauthn.py +0 -0
  188. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/actions/websocket.py +0 -0
  189. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/auth.py +0 -0
  190. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/backend/__init__.py +0 -0
  191. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/backend/manager.py +0 -0
  192. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/cleanup.py +0 -0
  193. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/cli/__init__.py +0 -0
  194. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/cli/_advanced.py +0 -0
  195. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/cli/_capture.py +0 -0
  196. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/cli/_config.py +0 -0
  197. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/cli/_debug.py +0 -0
  198. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/cli/_emulation.py +0 -0
  199. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/cli/_experimental.py +0 -0
  200. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/cli/_input.py +0 -0
  201. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/cli/_navigation.py +0 -0
  202. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/cli/_network.py +0 -0
  203. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/cli/_perf.py +0 -0
  204. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/cli/_serve.py +0 -0
  205. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/cli/_session.py +0 -0
  206. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/cli/_shared.py +0 -0
  207. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/cli/_workflow.py +0 -0
  208. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/config.py +0 -0
  209. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/exceptions.py +0 -0
  210. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/init.py +0 -0
  211. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/multi.py +0 -0
  212. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/output.py +0 -0
  213. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/plugins.py +0 -0
  214. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/record.py +0 -0
  215. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/repl.py +0 -0
  216. {wavexis-2.3.1 → wavexis-2.3.2}/wavexis/serve.py +0 -0
@@ -2,6 +2,16 @@
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
+
5
15
  ## v2.3.1 — 2026-07-07
6
16
 
7
17
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavexis
3
- Version: 2.3.1
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.1"
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" }
@@ -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.1"
3
+ __version__ = "2.3.2"
@@ -284,6 +284,45 @@ class AbstractBackend(ABC):
284
284
  files: List of absolute file paths to upload.
285
285
  """
286
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
+
287
326
  # ── Network advanced ───────────────────────────────────
288
327
 
289
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
@@ -997,7 +1001,132 @@ class BiDiBackend(AbstractBackend):
997
1001
  )
998
1002
  await self._client.script.evaluate(self._context, js)
999
1003
 
1000
- # ── 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)
1001
1130
 
1002
1131
  async def block_requests(self, patterns: list[str]) -> None:
1003
1132
  """Block requests matching URL patterns (partial BiDi support)."""
@@ -1252,6 +1252,123 @@ class CDPBackend(AbstractBackend):
1252
1252
  {"files": files, "nodeId": node_id},
1253
1253
  )
1254
1254
 
1255
+ # ── iframe ─────────────────────────────────────────────
1256
+
1257
+ async def iframe_eval(
1258
+ self, iframe_selector: str, expression: str, await_promise: bool = False
1259
+ ) -> Any:
1260
+ """Evaluate a JavaScript expression inside an iframe.
1261
+
1262
+ Args:
1263
+ iframe_selector: CSS selector for the <iframe> element.
1264
+ expression: JavaScript expression to evaluate in the iframe context.
1265
+ await_promise: Whether to await a returned Promise.
1266
+
1267
+ Returns:
1268
+ The evaluation result value.
1269
+ """
1270
+ session = self._require_session()
1271
+ escaped_iframe = iframe_selector.replace("'", "\\'")
1272
+ escaped_expr = expression.replace("\\", "\\\\").replace("'", "\\'")
1273
+ js = (
1274
+ f"(function(){{var f=document.querySelector('{escaped_iframe}');"
1275
+ f"if(!f||!f.contentDocument)return null;"
1276
+ f"return (function(){{{escaped_expr}}}).call(f.contentDocument);}})()"
1277
+ )
1278
+ result = await session.runtime.evaluate(js, await_promise=await_promise)
1279
+ return result.get("result", {}).get("value")
1280
+
1281
+ async def _wait_for_element_in_iframe(
1282
+ self, iframe_selector: str, selector: str, timeout_ms: int = 30000
1283
+ ) -> None:
1284
+ """Wait for an element inside an iframe to exist and be visible.
1285
+
1286
+ Args:
1287
+ iframe_selector: CSS selector for the <iframe> element.
1288
+ selector: CSS selector inside the iframe.
1289
+ timeout_ms: Maximum wait time in milliseconds.
1290
+
1291
+ Raises:
1292
+ WaitTimeoutError: If the element is not found within the timeout.
1293
+ """
1294
+ session = self._require_session()
1295
+ escaped_iframe = iframe_selector.replace("'", "\\'")
1296
+ escaped_sel = selector.replace("'", "\\'")
1297
+ js = (
1298
+ f"(function(){{var f=document.querySelector('{escaped_iframe}');"
1299
+ f"if(!f||!f.contentDocument)return false;"
1300
+ f"var el=f.contentDocument.querySelector('{escaped_sel}');"
1301
+ f"if(!el)return false;"
1302
+ f"var rect=el.getBoundingClientRect();"
1303
+ f"return rect.width>0&&rect.height>0;}})()"
1304
+ )
1305
+ deadline = time.monotonic() + timeout_ms / 1000
1306
+ while time.monotonic() < deadline:
1307
+ result = await session.runtime.evaluate(js)
1308
+ if result.get("result", {}).get("value") is True:
1309
+ return
1310
+ await asyncio.sleep(0.1)
1311
+ raise WaitTimeoutError("selector", timeout_ms)
1312
+
1313
+ async def iframe_click(
1314
+ self, iframe_selector: str, selector: str, auto_wait: bool = True
1315
+ ) -> None:
1316
+ """Click an element inside an iframe.
1317
+
1318
+ Args:
1319
+ iframe_selector: CSS selector for the <iframe> element.
1320
+ selector: CSS selector inside the iframe for the target element.
1321
+ auto_wait: If True, wait for element to be visible before clicking.
1322
+ """
1323
+ session = self._require_session()
1324
+ if auto_wait:
1325
+ await self._wait_for_element_in_iframe(iframe_selector, selector)
1326
+ escaped_iframe = iframe_selector.replace("'", "\\'")
1327
+ escaped_sel = selector.replace("'", "\\'")
1328
+ js = (
1329
+ f"(function(){{var f=document.querySelector('{escaped_iframe}');"
1330
+ f"if(!f||!f.contentDocument)return false;"
1331
+ f"var el=f.contentDocument.querySelector('{escaped_sel}');"
1332
+ f"if(!el)return false;"
1333
+ f"el.scrollIntoView({{block:'center',behavior:'instant'}});"
1334
+ f"el.dispatchEvent(new MouseEvent('click',{{bubbles:true}}));"
1335
+ f"return true;}})()"
1336
+ )
1337
+ result = await session.runtime.evaluate(js)
1338
+ if not result.get("result", {}).get("value"):
1339
+ raise ElementNotFoundError(selector)
1340
+
1341
+ async def iframe_fill(
1342
+ self, iframe_selector: str, selector: str, value: str, auto_wait: bool = True
1343
+ ) -> None:
1344
+ """Fill an input element inside an iframe.
1345
+
1346
+ Args:
1347
+ iframe_selector: CSS selector for the <iframe> element.
1348
+ selector: CSS selector inside the iframe for the target element.
1349
+ value: Value to set in the input field.
1350
+ auto_wait: If True, wait for element to be visible before filling.
1351
+ """
1352
+ session = self._require_session()
1353
+ if auto_wait:
1354
+ await self._wait_for_element_in_iframe(iframe_selector, selector)
1355
+ escaped_iframe = iframe_selector.replace("'", "\\'")
1356
+ escaped_sel = selector.replace("'", "\\'")
1357
+ escaped_val = value.replace("\\", "\\\\").replace("'", "\\'")
1358
+ js = (
1359
+ f"(function(){{var f=document.querySelector('{escaped_iframe}');"
1360
+ f"if(!f||!f.contentDocument)return false;"
1361
+ f"var el=f.contentDocument.querySelector('{escaped_sel}');"
1362
+ f"if(!el)return false;"
1363
+ f"el.focus();el.value='{escaped_val}';"
1364
+ f"el.dispatchEvent(new Event('input',{{bubbles:true}}));"
1365
+ f"el.dispatchEvent(new Event('change',{{bubbles:true}}));"
1366
+ f"return true;}})()"
1367
+ )
1368
+ result = await session.runtime.evaluate(js)
1369
+ if not result.get("result", {}).get("value"):
1370
+ raise ElementNotFoundError(selector)
1371
+
1255
1372
  # ── Network advanced ───────────────────────────────────
1256
1373
 
1257
1374
  async def block_requests(self, patterns: list[str]) -> None: