wavexis 2.2.6__tar.gz → 2.3.1__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.6 → wavexis-2.3.1}/CHANGELOG.md +16 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/PKG-INFO +1 -1
- {wavexis-2.2.6 → wavexis-2.3.1}/pyproject.toml +1 -1
- wavexis-2.3.1/tests/unit/test_auto_wait.py +126 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_bidi_phase5.py +3 -1
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/__init__.py +1 -1
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/backend/base.py +9 -3
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/backend/bidi.py +69 -7
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/backend/cdp.py +46 -3
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cleanup.py +3 -2
- {wavexis-2.2.6 → wavexis-2.3.1}/.dockerignore +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/.github/workflows/ci.yml +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/.github/workflows/docs.yml +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/.github/workflows/release.yml +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/.gitignore +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/CODE_OF_CONDUCT.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/CONTRIBUTING.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/Dockerfile +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/LICENSE +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/README.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/SECURITY.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/docs/api/actions.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/docs/api/backends.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/docs/api/cli.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/docs/api/config.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/docs/api/exceptions.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/docs/api/multi.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/docs/api/plugins.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/docs/api/serve.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/docs/cookbook/auth.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/docs/cookbook/ci-cd.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/docs/cookbook/css.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/docs/cookbook/debug.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/docs/cookbook/record-replay.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/docs/cookbook/scraping.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/docs/cookbook/screenshots.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/docs/cookbook/serve-mode.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/docs/guide/assert.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/docs/guide/backends.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/docs/guide/commands.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/docs/guide/init.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/docs/guide/installation.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/docs/guide/multi.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/docs/guide/perf.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/docs/guide/plugins.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/docs/guide/quickstart.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/docs/guide/raw.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/docs/guide/repl.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/docs/guide/troubleshooting.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/docs/index.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/mkdocs.yml +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/stubs/bidiwave/__init__.pyi +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/stubs/cdpwave/__init__.pyi +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/__init__.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/conftest.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/__init__.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_a11y.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_animation.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_backend_selection.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_browser.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_console.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_css.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_debug.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_dialog.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_dom.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_dom_snapshot.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_emulation.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_emulation_advanced.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_eval.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_har.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_input.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_media.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_multi.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_navigate.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_network.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_network_advanced.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_overlay.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_pdf.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_perf.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_permissions.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_raw.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_record.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_scrape.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_screenshot.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_security.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_serve.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_service_worker.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_storage.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_tabs.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_webaudio.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/integration/test_webauthn.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/__init__.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_abstract_backend_phase5.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_actions.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_actions_phase5.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_animation_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_assert_multi.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_auth.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_backend_manager.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_bidi_backend.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_bluetooth_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_cast_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_cleanup.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_cli_phase5.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_config.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_config_phase5.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_cookies_headers_actions.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_css_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_debug_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_dom_snapshot_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_download_screencast.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_emulation_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_error_handling.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_exceptions.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_format_record.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_global_options.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_init.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_input_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_interactions_crawl.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_manager.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_media_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_multi.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_multi_improvements.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_new_features.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_output.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_overlay_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_perf_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_perf_console.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_plugins.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_raw.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_record.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_repl.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_serve.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_service_worker_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_storage_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_webaudio_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/tests/unit/test_webauthn_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/__main__.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/__init__.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/accessibility.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/animation.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/base.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/bluetooth.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/browser.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/cast.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/console.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/cookies.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/crawl.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/css.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/debug.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/dialog.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/dom.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/dom_snapshot.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/download.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/emulation.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/eval.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/extract.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/form.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/har.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/headers.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/input.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/lighthouse.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/media.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/multi.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/navigate.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/network.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/overlay.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/pdf.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/performance.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/permissions.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/record.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/scrape.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/screencast.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/screenshot.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/security.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/service_worker.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/session.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/storage.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/tabs.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/wait.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/webaudio.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/webauthn.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/actions/websocket.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/auth.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/backend/__init__.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/backend/manager.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/__init__.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/_advanced.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/_capture.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/_config.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/_debug.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/_emulation.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/_experimental.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/_input.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/_navigation.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/_network.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/_perf.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/_serve.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/_session.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/_shared.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/_workflow.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/cli/app.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/config.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/exceptions.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/init.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/multi.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/output.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/plugins.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/record.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/wavexis/repl.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.1}/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.3.1 — 2026-07-07
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `auto_wait: bool = True` parameter in `click`, `fill`, `hover` (both backends)
|
|
10
|
+
- Pass `auto_wait=False` to skip element visibility polling for instant execution
|
|
11
|
+
|
|
12
|
+
## v2.3.0 — 2026-07-07
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Auto-waiting before `click`, `fill`, and `hover` in both CDP and BiDi backends
|
|
17
|
+
- `_wait_for_element` helper polls every 100ms until element exists and is visible (non-zero size)
|
|
18
|
+
- Configurable timeout (default 30s) via `WaitTimeoutError` with actionable hint
|
|
19
|
+
- Prevents `ElementNotFoundError` on dynamically loaded content
|
|
20
|
+
|
|
5
21
|
## v2.2.6 — 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.
|
|
3
|
+
Version: 2.3.1
|
|
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
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Unit tests for auto-waiting before click/fill/hover."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from wavexis.exceptions import WaitTimeoutError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.mark.unit
|
|
14
|
+
class TestAutoWait:
|
|
15
|
+
"""Tests for auto-waiting before input actions."""
|
|
16
|
+
|
|
17
|
+
def test_cdp_wait_for_element_found(self) -> None:
|
|
18
|
+
"""Test _wait_for_element returns when element is visible."""
|
|
19
|
+
from wavexis.backend.cdp import CDPBackend
|
|
20
|
+
|
|
21
|
+
backend = CDPBackend()
|
|
22
|
+
backend._session = MagicMock()
|
|
23
|
+
backend._session.runtime = MagicMock()
|
|
24
|
+
backend._session.runtime.evaluate = AsyncMock(
|
|
25
|
+
return_value={"result": {"value": True}}
|
|
26
|
+
)
|
|
27
|
+
asyncio.run(backend._wait_for_element("#btn", timeout_ms=100))
|
|
28
|
+
assert backend._session.runtime.evaluate.call_count >= 1
|
|
29
|
+
|
|
30
|
+
def test_cdp_wait_for_element_timeout(self) -> None:
|
|
31
|
+
"""Test _wait_for_element raises WaitTimeoutError on timeout."""
|
|
32
|
+
from wavexis.backend.cdp import CDPBackend
|
|
33
|
+
|
|
34
|
+
backend = CDPBackend()
|
|
35
|
+
backend._session = MagicMock()
|
|
36
|
+
backend._session.runtime = MagicMock()
|
|
37
|
+
backend._session.runtime.evaluate = AsyncMock(
|
|
38
|
+
return_value={"result": {"value": False}}
|
|
39
|
+
)
|
|
40
|
+
with pytest.raises(WaitTimeoutError):
|
|
41
|
+
asyncio.run(backend._wait_for_element("#missing", timeout_ms=50))
|
|
42
|
+
|
|
43
|
+
def test_cdp_click_calls_wait_for_element(self) -> None:
|
|
44
|
+
"""Test that click calls _wait_for_element before acting."""
|
|
45
|
+
from wavexis.backend.cdp import CDPBackend
|
|
46
|
+
|
|
47
|
+
backend = CDPBackend()
|
|
48
|
+
backend._session = MagicMock()
|
|
49
|
+
backend._session.input = MagicMock()
|
|
50
|
+
backend._session.input.dispatch_mouse_event = AsyncMock()
|
|
51
|
+
|
|
52
|
+
call_order: list[str] = []
|
|
53
|
+
|
|
54
|
+
async def _mock_wait(selector: str, timeout_ms: int = 30000) -> None:
|
|
55
|
+
call_order.append("wait")
|
|
56
|
+
|
|
57
|
+
async def _mock_scroll(selector: str) -> None:
|
|
58
|
+
call_order.append("scroll")
|
|
59
|
+
|
|
60
|
+
async def _mock_box(selector: str) -> tuple[float, float]:
|
|
61
|
+
call_order.append("box")
|
|
62
|
+
return 50.0, 25.0
|
|
63
|
+
|
|
64
|
+
backend._wait_for_element = _mock_wait # type: ignore[assignment]
|
|
65
|
+
backend._scroll_into_view_if_needed = _mock_scroll # type: ignore[assignment]
|
|
66
|
+
backend._get_box_center = _mock_box # type: ignore[assignment]
|
|
67
|
+
|
|
68
|
+
asyncio.run(backend.click("#btn"))
|
|
69
|
+
assert call_order == ["wait", "scroll", "box"]
|
|
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
|
+
|
|
100
|
+
def test_bidi_wait_for_element_found(self) -> None:
|
|
101
|
+
"""Test BiDi _wait_for_element returns when element is visible."""
|
|
102
|
+
from wavexis.backend.bidi import BiDiBackend
|
|
103
|
+
|
|
104
|
+
backend = BiDiBackend()
|
|
105
|
+
backend._client = MagicMock()
|
|
106
|
+
backend._context = MagicMock()
|
|
107
|
+
backend._client.script = MagicMock()
|
|
108
|
+
result = MagicMock()
|
|
109
|
+
result.value = True
|
|
110
|
+
backend._client.script.evaluate = AsyncMock(return_value=result)
|
|
111
|
+
asyncio.run(backend._wait_for_element("#btn", timeout_ms=100))
|
|
112
|
+
assert backend._client.script.evaluate.call_count >= 1
|
|
113
|
+
|
|
114
|
+
def test_bidi_wait_for_element_timeout(self) -> None:
|
|
115
|
+
"""Test BiDi _wait_for_element raises WaitTimeoutError on timeout."""
|
|
116
|
+
from wavexis.backend.bidi import BiDiBackend
|
|
117
|
+
|
|
118
|
+
backend = BiDiBackend()
|
|
119
|
+
backend._client = MagicMock()
|
|
120
|
+
backend._context = MagicMock()
|
|
121
|
+
backend._client.script = MagicMock()
|
|
122
|
+
result = MagicMock()
|
|
123
|
+
result.value = False
|
|
124
|
+
backend._client.script.evaluate = AsyncMock(return_value=result)
|
|
125
|
+
with pytest.raises(WaitTimeoutError):
|
|
126
|
+
asyncio.run(backend._wait_for_element("#missing", timeout_ms=50))
|
|
@@ -20,7 +20,9 @@ class TestBiDiBackendPhase5:
|
|
|
20
20
|
backend._client = MagicMock()
|
|
21
21
|
backend._context = MagicMock()
|
|
22
22
|
backend._client.script = MagicMock()
|
|
23
|
-
|
|
23
|
+
result = MagicMock()
|
|
24
|
+
result.value = True
|
|
25
|
+
backend._client.script.evaluate = AsyncMock(return_value=result)
|
|
24
26
|
backend._client._connection = MagicMock()
|
|
25
27
|
backend._client._connection.send_command = AsyncMock()
|
|
26
28
|
return backend
|
|
@@ -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
|
|
@@ -26,7 +26,7 @@ from wavexis.config import (
|
|
|
26
26
|
ThrottleParams,
|
|
27
27
|
WaitStrategy,
|
|
28
28
|
)
|
|
29
|
-
from wavexis.exceptions import SessionNotInitializedError
|
|
29
|
+
from wavexis.exceptions import SessionNotInitializedError, WaitTimeoutError
|
|
30
30
|
|
|
31
31
|
try:
|
|
32
32
|
from bidiwave import BiDiClient
|
|
@@ -807,6 +807,38 @@ class BiDiBackend(AbstractBackend):
|
|
|
807
807
|
|
|
808
808
|
# ── Input ──────────────────────────────────────────────
|
|
809
809
|
|
|
810
|
+
async def _wait_for_element(self, selector: str, timeout_ms: int = 30000) -> None:
|
|
811
|
+
"""Wait for an element to exist and be visible in the DOM.
|
|
812
|
+
|
|
813
|
+
Polls until the element matches, is attached, and has non-zero size.
|
|
814
|
+
|
|
815
|
+
Args:
|
|
816
|
+
selector: CSS selector for the target element.
|
|
817
|
+
timeout_ms: Maximum wait time in milliseconds.
|
|
818
|
+
|
|
819
|
+
Raises:
|
|
820
|
+
WaitTimeoutError: If the element is not found within the timeout.
|
|
821
|
+
"""
|
|
822
|
+
import asyncio as _asyncio
|
|
823
|
+
import time as _time
|
|
824
|
+
|
|
825
|
+
client = self._require_client()
|
|
826
|
+
escaped = selector.replace("'", "\\'")
|
|
827
|
+
js = (
|
|
828
|
+
f"(function(){{var el=document.querySelector('{escaped}');"
|
|
829
|
+
f"if(!el)return false;"
|
|
830
|
+
f"var rect=el.getBoundingClientRect();"
|
|
831
|
+
f"return rect.width>0&&rect.height>0;}})()"
|
|
832
|
+
)
|
|
833
|
+
deadline = _time.monotonic() + timeout_ms / 1000
|
|
834
|
+
while _time.monotonic() < deadline:
|
|
835
|
+
result = await client.script.evaluate(self._context, js)
|
|
836
|
+
value = getattr(result, "value", None)
|
|
837
|
+
if value is True or value == "true":
|
|
838
|
+
return
|
|
839
|
+
await _asyncio.sleep(0.1)
|
|
840
|
+
raise WaitTimeoutError("selector", timeout_ms)
|
|
841
|
+
|
|
810
842
|
async def _scroll_into_view_if_needed(self, selector: str) -> None:
|
|
811
843
|
"""Scroll element into view if it's not visible in the viewport."""
|
|
812
844
|
client = self._require_client()
|
|
@@ -821,10 +853,23 @@ class BiDiBackend(AbstractBackend):
|
|
|
821
853
|
await client.script.evaluate(self._context, js)
|
|
822
854
|
|
|
823
855
|
async def click(
|
|
824
|
-
self,
|
|
856
|
+
self,
|
|
857
|
+
selector: str,
|
|
858
|
+
button: str = "left",
|
|
859
|
+
click_count: int = 1,
|
|
860
|
+
auto_wait: bool = True,
|
|
825
861
|
) -> None:
|
|
826
|
-
"""Click an element via BiDi script.evaluate.
|
|
862
|
+
"""Click an element via BiDi script.evaluate.
|
|
863
|
+
|
|
864
|
+
Args:
|
|
865
|
+
selector: CSS selector for the target element.
|
|
866
|
+
button: Mouse button (unused in BiDi, for API compatibility).
|
|
867
|
+
click_count: Number of clicks to dispatch.
|
|
868
|
+
auto_wait: If True, wait for element to be visible before clicking.
|
|
869
|
+
"""
|
|
827
870
|
client = self._require_client()
|
|
871
|
+
if auto_wait:
|
|
872
|
+
await self._wait_for_element(selector)
|
|
828
873
|
await self._scroll_into_view_if_needed(selector)
|
|
829
874
|
escaped = selector.replace("'", "\\'")
|
|
830
875
|
js = (
|
|
@@ -854,9 +899,19 @@ class BiDiBackend(AbstractBackend):
|
|
|
854
899
|
if delay > 0:
|
|
855
900
|
await _asyncio.sleep(delay / 1000)
|
|
856
901
|
|
|
857
|
-
async def fill(
|
|
858
|
-
|
|
902
|
+
async def fill(
|
|
903
|
+
self, selector: str, value: str, auto_wait: bool = True
|
|
904
|
+
) -> None:
|
|
905
|
+
"""Fill an input element with a value via BiDi.
|
|
906
|
+
|
|
907
|
+
Args:
|
|
908
|
+
selector: CSS selector for the target element.
|
|
909
|
+
value: Value to set in the input field.
|
|
910
|
+
auto_wait: If True, wait for element to be visible before filling.
|
|
911
|
+
"""
|
|
859
912
|
client = self._require_client()
|
|
913
|
+
if auto_wait:
|
|
914
|
+
await self._wait_for_element(selector)
|
|
860
915
|
await self._scroll_into_view_if_needed(selector)
|
|
861
916
|
escaped = selector.replace("'", "\\'")
|
|
862
917
|
escaped_val = value.replace("\\", "\\\\").replace("'", "\\'")
|
|
@@ -872,9 +927,16 @@ class BiDiBackend(AbstractBackend):
|
|
|
872
927
|
js = f"document.querySelector('{escaped}').value = '{escaped_val}'"
|
|
873
928
|
await self._client.script.evaluate(self._context, js)
|
|
874
929
|
|
|
875
|
-
async def hover(self, selector: str) -> None:
|
|
876
|
-
"""Hover over an element via BiDi script.evaluate.
|
|
930
|
+
async def hover(self, selector: str, auto_wait: bool = True) -> None:
|
|
931
|
+
"""Hover over an element via BiDi script.evaluate.
|
|
932
|
+
|
|
933
|
+
Args:
|
|
934
|
+
selector: CSS selector for the target element.
|
|
935
|
+
auto_wait: If True, wait for element to be visible before hovering.
|
|
936
|
+
"""
|
|
877
937
|
client = self._require_client()
|
|
938
|
+
if auto_wait:
|
|
939
|
+
await self._wait_for_element(selector)
|
|
878
940
|
await self._scroll_into_view_if_needed(selector)
|
|
879
941
|
escaped = selector.replace("'", "\\'")
|
|
880
942
|
js = (
|
|
@@ -1026,6 +1026,34 @@ class CDPBackend(AbstractBackend):
|
|
|
1026
1026
|
cy = (min(ys) + max(ys)) / 2
|
|
1027
1027
|
return cx, cy
|
|
1028
1028
|
|
|
1029
|
+
async def _wait_for_element(self, selector: str, timeout_ms: int = 30000) -> None:
|
|
1030
|
+
"""Wait for an element to exist and be visible in the DOM.
|
|
1031
|
+
|
|
1032
|
+
Polls until the element matches, is attached, and has non-zero size.
|
|
1033
|
+
|
|
1034
|
+
Args:
|
|
1035
|
+
selector: CSS selector for the target element.
|
|
1036
|
+
timeout_ms: Maximum wait time in milliseconds.
|
|
1037
|
+
|
|
1038
|
+
Raises:
|
|
1039
|
+
WaitTimeoutError: If the element is not found within the timeout.
|
|
1040
|
+
"""
|
|
1041
|
+
session = self._require_session()
|
|
1042
|
+
escaped = selector.replace("'", "\\'")
|
|
1043
|
+
js = (
|
|
1044
|
+
f"(function(){{var el=document.querySelector('{escaped}');"
|
|
1045
|
+
f"if(!el)return false;"
|
|
1046
|
+
f"var rect=el.getBoundingClientRect();"
|
|
1047
|
+
f"return rect.width>0&&rect.height>0;}})()"
|
|
1048
|
+
)
|
|
1049
|
+
deadline = time.monotonic() + timeout_ms / 1000
|
|
1050
|
+
while time.monotonic() < deadline:
|
|
1051
|
+
result = await session.runtime.evaluate(js)
|
|
1052
|
+
if result.get("result", {}).get("value") is True:
|
|
1053
|
+
return
|
|
1054
|
+
await asyncio.sleep(0.1)
|
|
1055
|
+
raise WaitTimeoutError("selector", timeout_ms)
|
|
1056
|
+
|
|
1029
1057
|
async def _scroll_into_view_if_needed(self, selector: str) -> None:
|
|
1030
1058
|
"""Scroll element into view if it's not visible in the viewport.
|
|
1031
1059
|
|
|
@@ -1044,7 +1072,11 @@ class CDPBackend(AbstractBackend):
|
|
|
1044
1072
|
await session.runtime.evaluate(js)
|
|
1045
1073
|
|
|
1046
1074
|
async def click(
|
|
1047
|
-
self,
|
|
1075
|
+
self,
|
|
1076
|
+
selector: str,
|
|
1077
|
+
button: str = "left",
|
|
1078
|
+
click_count: int = 1,
|
|
1079
|
+
auto_wait: bool = True,
|
|
1048
1080
|
) -> None:
|
|
1049
1081
|
"""Click an element matching a CSS selector.
|
|
1050
1082
|
|
|
@@ -1052,8 +1084,11 @@ class CDPBackend(AbstractBackend):
|
|
|
1052
1084
|
selector: CSS selector for the target element.
|
|
1053
1085
|
button: Mouse button — "left", "right", or "middle".
|
|
1054
1086
|
click_count: Number of clicks to dispatch.
|
|
1087
|
+
auto_wait: If True, wait for element to be visible before clicking.
|
|
1055
1088
|
"""
|
|
1056
1089
|
session = self._require_session()
|
|
1090
|
+
if auto_wait:
|
|
1091
|
+
await self._wait_for_element(selector)
|
|
1057
1092
|
await self._scroll_into_view_if_needed(selector)
|
|
1058
1093
|
x, y = await self._get_box_center(selector)
|
|
1059
1094
|
btn_map = {"left": "left", "right": "right", "middle": "middle"}
|
|
@@ -1084,14 +1119,19 @@ class CDPBackend(AbstractBackend):
|
|
|
1084
1119
|
if delay > 0:
|
|
1085
1120
|
await asyncio.sleep(delay / 1000)
|
|
1086
1121
|
|
|
1087
|
-
async def fill(
|
|
1122
|
+
async def fill(
|
|
1123
|
+
self, selector: str, value: str, auto_wait: bool = True
|
|
1124
|
+
) -> None:
|
|
1088
1125
|
"""Fill an input element with a value (replaces existing content).
|
|
1089
1126
|
|
|
1090
1127
|
Args:
|
|
1091
1128
|
selector: CSS selector for the target element.
|
|
1092
1129
|
value: Value to set in the input field.
|
|
1130
|
+
auto_wait: If True, wait for element to be visible before filling.
|
|
1093
1131
|
"""
|
|
1094
1132
|
session = self._require_session()
|
|
1133
|
+
if auto_wait:
|
|
1134
|
+
await self._wait_for_element(selector)
|
|
1095
1135
|
await self._scroll_into_view_if_needed(selector)
|
|
1096
1136
|
escaped = selector.replace("'", "\\'")
|
|
1097
1137
|
js = (
|
|
@@ -1125,13 +1165,16 @@ class CDPBackend(AbstractBackend):
|
|
|
1125
1165
|
if not result.get("result", {}).get("value"):
|
|
1126
1166
|
raise ElementNotFoundError(selector)
|
|
1127
1167
|
|
|
1128
|
-
async def hover(self, selector: str) -> None:
|
|
1168
|
+
async def hover(self, selector: str, auto_wait: bool = True) -> None:
|
|
1129
1169
|
"""Hover over an element matching a CSS selector.
|
|
1130
1170
|
|
|
1131
1171
|
Args:
|
|
1132
1172
|
selector: CSS selector for the target element.
|
|
1173
|
+
auto_wait: If True, wait for element to be visible before hovering.
|
|
1133
1174
|
"""
|
|
1134
1175
|
session = self._require_session()
|
|
1176
|
+
if auto_wait:
|
|
1177
|
+
await self._wait_for_element(selector)
|
|
1135
1178
|
await self._scroll_into_view_if_needed(selector)
|
|
1136
1179
|
x, y = await self._get_box_center(selector)
|
|
1137
1180
|
await session.input.dispatch_mouse_event(
|
|
@@ -23,8 +23,9 @@ _SIGNAL_NAMES: dict[int, str] = {
|
|
|
23
23
|
signal.SIGTERM: "SIGTERM",
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
_sigbreak = getattr(signal, "SIGBREAK", None)
|
|
27
|
+
if _sigbreak is not None:
|
|
28
|
+
_SIGNAL_NAMES[_sigbreak] = "SIGBREAK"
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
def register_backend(backend: Any) -> None:
|
|
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
|