wavexis 2.2.0__tar.gz → 2.2.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.2.0 → wavexis-2.2.2}/CHANGELOG.md +16 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/PKG-INFO +1 -1
- {wavexis-2.2.0 → wavexis-2.2.2}/pyproject.toml +1 -1
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_global_options.py +16 -1
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/__init__.py +1 -1
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/backend/bidi.py +22 -9
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/backend/cdp.py +33 -5
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/_shared.py +9 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/config.py +2 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/.dockerignore +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/.github/workflows/ci.yml +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/.github/workflows/docs.yml +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/.github/workflows/release.yml +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/.gitignore +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/CODE_OF_CONDUCT.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/CONTRIBUTING.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/Dockerfile +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/LICENSE +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/README.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/SECURITY.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/docs/api/actions.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/docs/api/backends.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/docs/api/cli.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/docs/api/config.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/docs/api/exceptions.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/docs/api/multi.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/docs/api/plugins.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/docs/api/serve.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/docs/cookbook/auth.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/docs/cookbook/ci-cd.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/docs/cookbook/css.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/docs/cookbook/debug.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/docs/cookbook/record-replay.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/docs/cookbook/scraping.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/docs/cookbook/screenshots.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/docs/cookbook/serve-mode.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/docs/guide/assert.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/docs/guide/backends.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/docs/guide/commands.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/docs/guide/init.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/docs/guide/installation.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/docs/guide/multi.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/docs/guide/perf.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/docs/guide/plugins.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/docs/guide/quickstart.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/docs/guide/raw.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/docs/guide/repl.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/docs/guide/troubleshooting.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/docs/index.md +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/mkdocs.yml +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/stubs/bidiwave/__init__.pyi +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/stubs/cdpwave/__init__.pyi +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/__init__.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/conftest.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/__init__.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_a11y.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_animation.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_backend_selection.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_browser.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_console.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_css.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_debug.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_dialog.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_dom.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_dom_snapshot.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_emulation.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_emulation_advanced.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_eval.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_har.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_input.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_media.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_multi.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_navigate.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_network.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_network_advanced.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_overlay.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_pdf.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_perf.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_permissions.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_raw.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_record.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_scrape.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_screenshot.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_security.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_serve.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_service_worker.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_storage.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_tabs.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_webaudio.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/integration/test_webauthn.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/__init__.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_abstract_backend_phase5.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_actions.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_actions_phase5.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_animation_action.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_assert_multi.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_auth.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_backend_manager.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_bidi_backend.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_bidi_phase5.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_bluetooth_action.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_cast_action.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_cli_phase5.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_config.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_config_phase5.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_cookies_headers_actions.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_css_action.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_debug_action.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_dom_snapshot_action.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_download_screencast.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_emulation_action.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_error_handling.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_exceptions.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_format_record.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_init.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_input_action.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_interactions_crawl.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_manager.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_media_action.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_multi.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_multi_improvements.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_new_features.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_output.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_overlay_action.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_perf_action.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_perf_console.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_plugins.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_raw.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_record.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_repl.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_serve.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_service_worker_action.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_storage_action.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_webaudio_action.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/tests/unit/test_webauthn_action.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/__main__.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/__init__.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/accessibility.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/animation.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/base.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/bluetooth.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/browser.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/cast.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/console.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/cookies.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/crawl.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/css.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/debug.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/dialog.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/dom.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/dom_snapshot.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/download.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/emulation.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/eval.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/extract.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/form.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/har.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/headers.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/input.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/lighthouse.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/media.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/multi.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/navigate.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/network.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/overlay.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/pdf.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/performance.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/permissions.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/record.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/scrape.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/screencast.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/screenshot.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/security.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/service_worker.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/session.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/storage.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/tabs.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/wait.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/webaudio.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/webauthn.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/actions/websocket.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/auth.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/backend/__init__.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/backend/base.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/backend/manager.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/__init__.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/_advanced.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/_capture.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/_config.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/_debug.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/_emulation.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/_experimental.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/_input.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/_navigation.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/_network.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/_perf.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/_serve.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/_session.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/_workflow.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/cli/app.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/exceptions.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/init.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/multi.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/output.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/plugins.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/record.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/repl.py +0 -0
- {wavexis-2.2.0 → wavexis-2.2.2}/wavexis/serve.py +0 -0
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to wavexis are documented in this file.
|
|
4
4
|
|
|
5
|
+
## v2.2.2 — 2026-07-07
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Auto scroll-into-view before `click`, `fill`, and `hover` in both CDP and BiDi backends
|
|
10
|
+
- `_scroll_into_view_if_needed` helper checks if element is in viewport and scrolls if needed
|
|
11
|
+
|
|
12
|
+
## v2.2.1 — 2026-07-07
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `--browser-url` global flag to connect to an existing browser (e.g. `--browser-url ws://localhost:9222`)
|
|
17
|
+
- `browser_url` field in `BrowserOptions` dataclass
|
|
18
|
+
- `browser_url` support in `config.yml` (`browser_url: ws://localhost:9222`)
|
|
19
|
+
- `CDPBackend.launch()` now uses `CDPClient.connect()` when `browser_url` is set instead of launching a new browser
|
|
20
|
+
|
|
5
21
|
## v2.2.0 — 2026-07-07
|
|
6
22
|
|
|
7
23
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wavexis
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.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
|
|
@@ -31,6 +31,11 @@ class TestBrowserOptions:
|
|
|
31
31
|
assert opts.proxy is None
|
|
32
32
|
assert opts.timeout == 30000
|
|
33
33
|
assert opts.user_data_dir is None
|
|
34
|
+
assert opts.browser_url is None
|
|
35
|
+
|
|
36
|
+
def test_with_browser_url(self) -> None:
|
|
37
|
+
opts = BrowserOptions(browser_url="ws://localhost:9222")
|
|
38
|
+
assert opts.browser_url == "ws://localhost:9222"
|
|
34
39
|
|
|
35
40
|
def test_with_user_data_dir(self) -> None:
|
|
36
41
|
opts = BrowserOptions(user_data_dir="/tmp/wavexis-profile")
|
|
@@ -63,6 +68,13 @@ class TestBrowserOptionsHelper:
|
|
|
63
68
|
assert opts.timeout == 30000
|
|
64
69
|
assert opts.proxy is None
|
|
65
70
|
assert opts.user_data_dir is None
|
|
71
|
+
assert opts.browser_url is None
|
|
72
|
+
|
|
73
|
+
def test_browser_url_options(self) -> None:
|
|
74
|
+
ctx = _fresh_ctx()
|
|
75
|
+
ctx.browser_url = "ws://localhost:9222"
|
|
76
|
+
opts = _cli._browser_options()
|
|
77
|
+
assert opts.browser_url == "ws://localhost:9222"
|
|
66
78
|
|
|
67
79
|
def test_user_data_dir_options(self) -> None:
|
|
68
80
|
ctx = _fresh_ctx()
|
|
@@ -101,6 +113,7 @@ class TestLoadGlobalConfig:
|
|
|
101
113
|
assert ctx.timeout == 30000
|
|
102
114
|
assert ctx.proxy is None
|
|
103
115
|
assert ctx.user_data_dir is None
|
|
116
|
+
assert ctx.browser_url is None
|
|
104
117
|
|
|
105
118
|
def test_loads_config(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
106
119
|
config_dir = tmp_path / ".wavexis"
|
|
@@ -111,7 +124,8 @@ class TestLoadGlobalConfig:
|
|
|
111
124
|
"headless: false\n"
|
|
112
125
|
"timeout: 60000\n"
|
|
113
126
|
"proxy: http://proxy:9090\n"
|
|
114
|
-
"user_data_dir: /tmp/profile\n"
|
|
127
|
+
"user_data_dir: /tmp/profile\n"
|
|
128
|
+
"browser_url: ws://localhost:9222\n",
|
|
115
129
|
encoding="utf-8",
|
|
116
130
|
)
|
|
117
131
|
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
|
@@ -124,6 +138,7 @@ class TestLoadGlobalConfig:
|
|
|
124
138
|
assert ctx.timeout == 60000
|
|
125
139
|
assert ctx.proxy == "http://proxy:9090"
|
|
126
140
|
assert ctx.user_data_dir == "/tmp/profile"
|
|
141
|
+
assert ctx.browser_url == "ws://localhost:9222"
|
|
127
142
|
|
|
128
143
|
def test_cli_overrides_config(
|
|
129
144
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
@@ -807,19 +807,32 @@ class BiDiBackend(AbstractBackend):
|
|
|
807
807
|
|
|
808
808
|
# ── Input ──────────────────────────────────────────────
|
|
809
809
|
|
|
810
|
+
async def _scroll_into_view_if_needed(self, selector: str) -> None:
|
|
811
|
+
"""Scroll element into view if it's not visible in the viewport."""
|
|
812
|
+
client = self._require_client()
|
|
813
|
+
escaped = selector.replace("'", "\\'")
|
|
814
|
+
js = (
|
|
815
|
+
f"(function(){{var el=document.querySelector('{escaped}');"
|
|
816
|
+
f"if(!el)return;var rect=el.getBoundingClientRect();"
|
|
817
|
+
f"if(rect.top<0||rect.bottom>window.innerHeight||"
|
|
818
|
+
f"rect.left<0||rect.right>window.innerWidth)"
|
|
819
|
+
f"el.scrollIntoView({{block:'center',behavior:'instant'}});}})()"
|
|
820
|
+
)
|
|
821
|
+
await client.script.evaluate(self._context, js)
|
|
822
|
+
|
|
810
823
|
async def click(
|
|
811
824
|
self, selector: str, button: str = "left", click_count: int = 1
|
|
812
825
|
) -> None:
|
|
813
826
|
"""Click an element via BiDi script.evaluate."""
|
|
814
|
-
|
|
815
|
-
|
|
827
|
+
client = self._require_client()
|
|
828
|
+
await self._scroll_into_view_if_needed(selector)
|
|
816
829
|
escaped = selector.replace("'", "\\'")
|
|
817
830
|
js = (
|
|
818
831
|
f"document.querySelector('{escaped}')"
|
|
819
832
|
f".dispatchEvent(new MouseEvent('click',{{bubbles:true}}))"
|
|
820
833
|
)
|
|
821
834
|
for _ in range(click_count):
|
|
822
|
-
await
|
|
835
|
+
await client.script.evaluate(self._context, js)
|
|
823
836
|
|
|
824
837
|
async def type_text(self, selector: str, text: str, delay: int = 0) -> None:
|
|
825
838
|
"""Type text into an element via BiDi."""
|
|
@@ -843,12 +856,12 @@ class BiDiBackend(AbstractBackend):
|
|
|
843
856
|
|
|
844
857
|
async def fill(self, selector: str, value: str) -> None:
|
|
845
858
|
"""Fill an input element with a value via BiDi."""
|
|
846
|
-
|
|
847
|
-
|
|
859
|
+
client = self._require_client()
|
|
860
|
+
await self._scroll_into_view_if_needed(selector)
|
|
848
861
|
escaped = selector.replace("'", "\\'")
|
|
849
862
|
escaped_val = value.replace("\\", "\\\\").replace("'", "\\'")
|
|
850
863
|
js = f"document.querySelector('{escaped}').value = '{escaped_val}'"
|
|
851
|
-
await
|
|
864
|
+
await client.script.evaluate(self._context, js)
|
|
852
865
|
|
|
853
866
|
async def select_option(self, selector: str, value: str) -> None:
|
|
854
867
|
"""Select an option in a <select> element by value via BiDi."""
|
|
@@ -861,15 +874,15 @@ class BiDiBackend(AbstractBackend):
|
|
|
861
874
|
|
|
862
875
|
async def hover(self, selector: str) -> None:
|
|
863
876
|
"""Hover over an element via BiDi script.evaluate."""
|
|
864
|
-
|
|
865
|
-
|
|
877
|
+
client = self._require_client()
|
|
878
|
+
await self._scroll_into_view_if_needed(selector)
|
|
866
879
|
escaped = selector.replace("'", "\\'")
|
|
867
880
|
js = (
|
|
868
881
|
f"var el=document.querySelector('{escaped}');"
|
|
869
882
|
f"el.dispatchEvent(new MouseEvent('mouseover',{{bubbles:true}}));"
|
|
870
883
|
f"el.dispatchEvent(new MouseEvent('mousemove',{{bubbles:true}}))"
|
|
871
884
|
)
|
|
872
|
-
await
|
|
885
|
+
await client.script.evaluate(self._context, js)
|
|
873
886
|
|
|
874
887
|
async def key_press(self, key: str) -> None:
|
|
875
888
|
"""Press a keyboard key via BiDi script.evaluate."""
|
|
@@ -83,11 +83,19 @@ class CDPBackend(AbstractBackend):
|
|
|
83
83
|
if options.proxy:
|
|
84
84
|
extra_args.append(f"--proxy-server={options.proxy}")
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
86
|
+
if options.browser_url:
|
|
87
|
+
from urllib.parse import urlparse
|
|
88
|
+
|
|
89
|
+
parsed = urlparse(options.browser_url)
|
|
90
|
+
host = parsed.hostname or "localhost"
|
|
91
|
+
port = parsed.port or 9222
|
|
92
|
+
self._client = await CDPClient.connect(host=host, port=port) # type: ignore[attr-defined]
|
|
93
|
+
else:
|
|
94
|
+
self._client = await CDPClient.launch(
|
|
95
|
+
headless=options.headless,
|
|
96
|
+
user_data_dir=options.user_data_dir, # type: ignore[call-arg]
|
|
97
|
+
extra_args=extra_args if extra_args else None,
|
|
98
|
+
)
|
|
91
99
|
self._session = await self._client.new_page()
|
|
92
100
|
|
|
93
101
|
if options.user_agent:
|
|
@@ -1018,6 +1026,23 @@ class CDPBackend(AbstractBackend):
|
|
|
1018
1026
|
cy = (min(ys) + max(ys)) / 2
|
|
1019
1027
|
return cx, cy
|
|
1020
1028
|
|
|
1029
|
+
async def _scroll_into_view_if_needed(self, selector: str) -> None:
|
|
1030
|
+
"""Scroll element into view if it's not visible in the viewport.
|
|
1031
|
+
|
|
1032
|
+
Args:
|
|
1033
|
+
selector: CSS selector for the target element.
|
|
1034
|
+
"""
|
|
1035
|
+
session = self._require_session()
|
|
1036
|
+
escaped = selector.replace("'", "\\'")
|
|
1037
|
+
js = (
|
|
1038
|
+
f"(function(){{var el=document.querySelector('{escaped}');"
|
|
1039
|
+
f"if(!el)return;var rect=el.getBoundingClientRect();"
|
|
1040
|
+
f"if(rect.top<0||rect.bottom>window.innerHeight||"
|
|
1041
|
+
f"rect.left<0||rect.right>window.innerWidth)"
|
|
1042
|
+
f"el.scrollIntoView({{block:'center',behavior:'instant'}});}})()"
|
|
1043
|
+
)
|
|
1044
|
+
await session.runtime.evaluate(js)
|
|
1045
|
+
|
|
1021
1046
|
async def click(
|
|
1022
1047
|
self, selector: str, button: str = "left", click_count: int = 1
|
|
1023
1048
|
) -> None:
|
|
@@ -1029,6 +1054,7 @@ class CDPBackend(AbstractBackend):
|
|
|
1029
1054
|
click_count: Number of clicks to dispatch.
|
|
1030
1055
|
"""
|
|
1031
1056
|
session = self._require_session()
|
|
1057
|
+
await self._scroll_into_view_if_needed(selector)
|
|
1032
1058
|
x, y = await self._get_box_center(selector)
|
|
1033
1059
|
btn_map = {"left": "left", "right": "right", "middle": "middle"}
|
|
1034
1060
|
btn = btn_map.get(button, "left")
|
|
@@ -1066,6 +1092,7 @@ class CDPBackend(AbstractBackend):
|
|
|
1066
1092
|
value: Value to set in the input field.
|
|
1067
1093
|
"""
|
|
1068
1094
|
session = self._require_session()
|
|
1095
|
+
await self._scroll_into_view_if_needed(selector)
|
|
1069
1096
|
escaped = selector.replace("'", "\\'")
|
|
1070
1097
|
js = (
|
|
1071
1098
|
f"(function(){{var el=document.querySelector('{escaped}');"
|
|
@@ -1105,6 +1132,7 @@ class CDPBackend(AbstractBackend):
|
|
|
1105
1132
|
selector: CSS selector for the target element.
|
|
1106
1133
|
"""
|
|
1107
1134
|
session = self._require_session()
|
|
1135
|
+
await self._scroll_into_view_if_needed(selector)
|
|
1108
1136
|
x, y = await self._get_box_center(selector)
|
|
1109
1137
|
await session.input.dispatch_mouse_event(
|
|
1110
1138
|
type_="mouseMoved", x=x, y=y
|
|
@@ -76,6 +76,7 @@ class CLIContext:
|
|
|
76
76
|
timeout: int = 30000
|
|
77
77
|
proxy: str | None = None
|
|
78
78
|
user_data_dir: str | None = None
|
|
79
|
+
browser_url: str | None = None
|
|
79
80
|
|
|
80
81
|
|
|
81
82
|
_ctx: contextvars.ContextVar[CLIContext | None] = contextvars.ContextVar(
|
|
@@ -116,6 +117,8 @@ def _load_global_config() -> None:
|
|
|
116
117
|
ctx.proxy = str(raw["proxy"])
|
|
117
118
|
if "user_data_dir" in raw:
|
|
118
119
|
ctx.user_data_dir = str(raw["user_data_dir"])
|
|
120
|
+
if "browser_url" in raw:
|
|
121
|
+
ctx.browser_url = str(raw["browser_url"])
|
|
119
122
|
except (OSError, ValueError, TypeError, yaml.YAMLError) as exc:
|
|
120
123
|
if ctx.verbose:
|
|
121
124
|
_echo(f"Warning: failed to load config from {config_path}: {exc}")
|
|
@@ -144,6 +147,9 @@ def main_callback(
|
|
|
144
147
|
user_data_dir: str | None = typer.Option(
|
|
145
148
|
None, "--user-data-dir", help="Path to persistent browser profile directory"
|
|
146
149
|
),
|
|
150
|
+
browser_url: str | None = typer.Option(
|
|
151
|
+
None, "--browser-url", help="Connect to existing browser (e.g. ws://localhost:9222)"
|
|
152
|
+
),
|
|
147
153
|
version: bool = typer.Option(
|
|
148
154
|
False, "--version", help="Print wavexis version and exit"
|
|
149
155
|
),
|
|
@@ -162,6 +168,8 @@ def main_callback(
|
|
|
162
168
|
ctx.proxy = proxy
|
|
163
169
|
if user_data_dir:
|
|
164
170
|
ctx.user_data_dir = user_data_dir
|
|
171
|
+
if browser_url:
|
|
172
|
+
ctx.browser_url = browser_url
|
|
165
173
|
if version:
|
|
166
174
|
from wavexis import __version__
|
|
167
175
|
|
|
@@ -236,6 +244,7 @@ def _browser_options() -> BrowserOptions:
|
|
|
236
244
|
timeout=ctx.timeout,
|
|
237
245
|
proxy=ctx.proxy,
|
|
238
246
|
user_data_dir=ctx.user_data_dir,
|
|
247
|
+
browser_url=ctx.browser_url,
|
|
239
248
|
)
|
|
240
249
|
|
|
241
250
|
|
|
@@ -147,6 +147,7 @@ class BrowserOptions:
|
|
|
147
147
|
proxy: Proxy server URL (e.g. http://proxy:8080 or socks5://proxy:1080).
|
|
148
148
|
timeout: Default navigation timeout in milliseconds.
|
|
149
149
|
user_data_dir: Path to a persistent user data directory for browser profiles.
|
|
150
|
+
browser_url: URL of an existing browser to connect to (e.g. ws://localhost:9222).
|
|
150
151
|
"""
|
|
151
152
|
|
|
152
153
|
headless: bool = True
|
|
@@ -157,6 +158,7 @@ class BrowserOptions:
|
|
|
157
158
|
proxy: str | None = None
|
|
158
159
|
timeout: int = 30000
|
|
159
160
|
user_data_dir: str | None = None
|
|
161
|
+
browser_url: str | None = None
|
|
160
162
|
|
|
161
163
|
|
|
162
164
|
@dataclass
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|