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.
- {wavexis-2.3.0 → wavexis-2.3.2}/CHANGELOG.md +17 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/PKG-INFO +1 -1
- {wavexis-2.3.0 → wavexis-2.3.2}/pyproject.toml +1 -1
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_auto_wait.py +29 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_backend_manager.py +29 -3
- wavexis-2.3.2/tests/unit/test_iframe.py +203 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_manager.py +10 -3
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/__init__.py +1 -1
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/backend/base.py +48 -3
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/backend/bidi.py +167 -11
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/backend/cdp.py +135 -6
- wavexis-2.3.2/wavexis/cli/_iframe.py +132 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/app.py +1 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/.dockerignore +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/.github/workflows/ci.yml +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/.github/workflows/docs.yml +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/.github/workflows/release.yml +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/.gitignore +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/CODE_OF_CONDUCT.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/CONTRIBUTING.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/Dockerfile +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/LICENSE +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/README.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/SECURITY.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/docs/api/actions.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/docs/api/backends.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/docs/api/cli.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/docs/api/config.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/docs/api/exceptions.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/docs/api/multi.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/docs/api/plugins.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/docs/api/serve.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/docs/cookbook/auth.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/docs/cookbook/ci-cd.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/docs/cookbook/css.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/docs/cookbook/debug.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/docs/cookbook/record-replay.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/docs/cookbook/scraping.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/docs/cookbook/screenshots.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/docs/cookbook/serve-mode.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/docs/guide/assert.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/docs/guide/backends.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/docs/guide/commands.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/docs/guide/init.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/docs/guide/installation.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/docs/guide/multi.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/docs/guide/perf.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/docs/guide/plugins.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/docs/guide/quickstart.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/docs/guide/raw.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/docs/guide/repl.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/docs/guide/troubleshooting.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/docs/index.md +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/mkdocs.yml +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/stubs/bidiwave/__init__.pyi +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/stubs/cdpwave/__init__.pyi +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/__init__.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/conftest.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/__init__.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_a11y.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_animation.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_backend_selection.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_browser.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_console.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_css.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_debug.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_dialog.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_dom.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_dom_snapshot.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_emulation.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_emulation_advanced.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_eval.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_har.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_input.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_media.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_multi.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_navigate.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_network.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_network_advanced.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_overlay.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_pdf.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_perf.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_permissions.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_raw.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_record.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_scrape.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_screenshot.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_security.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_serve.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_service_worker.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_storage.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_tabs.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_webaudio.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/integration/test_webauthn.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/__init__.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_abstract_backend_phase5.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_actions.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_actions_phase5.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_animation_action.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_assert_multi.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_auth.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_bidi_backend.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_bidi_phase5.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_bluetooth_action.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_cast_action.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_cleanup.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_cli_phase5.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_config.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_config_phase5.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_cookies_headers_actions.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_css_action.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_debug_action.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_dom_snapshot_action.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_download_screencast.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_emulation_action.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_error_handling.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_exceptions.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_format_record.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_global_options.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_init.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_input_action.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_interactions_crawl.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_media_action.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_multi.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_multi_improvements.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_new_features.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_output.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_overlay_action.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_perf_action.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_perf_console.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_plugins.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_raw.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_record.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_repl.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_serve.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_service_worker_action.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_storage_action.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_webaudio_action.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/tests/unit/test_webauthn_action.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/__main__.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/__init__.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/accessibility.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/animation.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/base.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/bluetooth.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/browser.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/cast.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/console.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/cookies.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/crawl.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/css.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/debug.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/dialog.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/dom.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/dom_snapshot.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/download.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/emulation.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/eval.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/extract.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/form.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/har.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/headers.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/input.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/lighthouse.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/media.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/multi.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/navigate.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/network.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/overlay.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/pdf.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/performance.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/permissions.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/record.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/scrape.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/screencast.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/screenshot.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/security.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/service_worker.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/session.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/storage.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/tabs.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/wait.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/webaudio.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/webauthn.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/actions/websocket.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/auth.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/backend/__init__.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/backend/manager.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cleanup.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/__init__.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/_advanced.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/_capture.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/_config.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/_debug.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/_emulation.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/_experimental.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/_input.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/_navigation.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/_network.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/_perf.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/_serve.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/_session.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/_shared.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/cli/_workflow.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/config.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/exceptions.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/init.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/multi.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/output.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/plugins.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/record.py +0 -0
- {wavexis-2.3.0 → wavexis-2.3.2}/wavexis/repl.py +0 -0
- {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.
|
|
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
|
|
@@ -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(
|
|
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(
|
|
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:
|
|
@@ -237,7 +237,11 @@ class AbstractBackend(ABC):
|
|
|
237
237
|
|
|
238
238
|
@abstractmethod
|
|
239
239
|
async def click(
|
|
240
|
-
self,
|
|
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(
|
|
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
|
|
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,
|
|
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
|
-
|
|
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(
|
|
891
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
# ──
|
|
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)."""
|