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