wavexis 2.2.6__tar.gz → 2.3.0__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.0}/CHANGELOG.md +9 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/PKG-INFO +1 -1
- {wavexis-2.2.6 → wavexis-2.3.0}/pyproject.toml +1 -1
- wavexis-2.3.0/tests/unit/test_auto_wait.py +97 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_bidi_phase5.py +3 -1
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/__init__.py +1 -1
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/backend/bidi.py +36 -1
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/backend/cdp.py +31 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cleanup.py +3 -2
- {wavexis-2.2.6 → wavexis-2.3.0}/.dockerignore +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/.github/workflows/ci.yml +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/.github/workflows/docs.yml +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/.github/workflows/release.yml +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/.gitignore +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/CODE_OF_CONDUCT.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/CONTRIBUTING.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/Dockerfile +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/LICENSE +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/README.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/SECURITY.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/docs/api/actions.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/docs/api/backends.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/docs/api/cli.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/docs/api/config.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/docs/api/exceptions.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/docs/api/multi.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/docs/api/plugins.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/docs/api/serve.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/docs/cookbook/auth.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/docs/cookbook/ci-cd.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/docs/cookbook/css.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/docs/cookbook/debug.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/docs/cookbook/record-replay.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/docs/cookbook/scraping.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/docs/cookbook/screenshots.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/docs/cookbook/serve-mode.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/docs/guide/assert.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/docs/guide/backends.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/docs/guide/commands.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/docs/guide/init.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/docs/guide/installation.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/docs/guide/multi.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/docs/guide/perf.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/docs/guide/plugins.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/docs/guide/quickstart.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/docs/guide/raw.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/docs/guide/repl.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/docs/guide/troubleshooting.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/docs/index.md +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/mkdocs.yml +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/stubs/bidiwave/__init__.pyi +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/stubs/cdpwave/__init__.pyi +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/__init__.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/conftest.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/__init__.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_a11y.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_animation.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_backend_selection.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_browser.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_console.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_css.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_debug.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_dialog.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_dom.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_dom_snapshot.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_emulation.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_emulation_advanced.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_eval.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_har.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_input.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_media.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_multi.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_navigate.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_network.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_network_advanced.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_overlay.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_pdf.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_perf.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_permissions.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_raw.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_record.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_scrape.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_screenshot.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_security.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_serve.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_service_worker.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_storage.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_tabs.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_webaudio.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/integration/test_webauthn.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/__init__.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_abstract_backend_phase5.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_actions.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_actions_phase5.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_animation_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_assert_multi.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_auth.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_backend_manager.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_bidi_backend.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_bluetooth_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_cast_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_cleanup.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_cli_phase5.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_config.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_config_phase5.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_cookies_headers_actions.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_css_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_debug_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_dom_snapshot_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_download_screencast.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_emulation_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_error_handling.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_exceptions.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_format_record.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_global_options.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_init.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_input_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_interactions_crawl.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_manager.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_media_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_multi.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_multi_improvements.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_new_features.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_output.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_overlay_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_perf_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_perf_console.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_plugins.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_raw.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_record.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_repl.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_serve.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_service_worker_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_storage_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_webaudio_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/tests/unit/test_webauthn_action.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/__main__.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/__init__.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/accessibility.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/animation.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/base.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/bluetooth.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/browser.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/cast.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/console.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/cookies.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/crawl.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/css.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/debug.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/dialog.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/dom.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/dom_snapshot.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/download.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/emulation.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/eval.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/extract.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/form.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/har.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/headers.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/input.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/lighthouse.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/media.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/multi.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/navigate.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/network.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/overlay.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/pdf.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/performance.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/permissions.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/record.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/scrape.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/screencast.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/screenshot.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/security.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/service_worker.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/session.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/storage.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/tabs.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/wait.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/webaudio.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/webauthn.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/actions/websocket.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/auth.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/backend/__init__.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/backend/base.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/backend/manager.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/__init__.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/_advanced.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/_capture.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/_config.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/_debug.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/_emulation.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/_experimental.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/_input.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/_navigation.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/_network.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/_perf.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/_serve.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/_session.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/_shared.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/_workflow.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/cli/app.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/config.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/exceptions.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/init.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/multi.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/output.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/plugins.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/record.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/repl.py +0 -0
- {wavexis-2.2.6 → wavexis-2.3.0}/wavexis/serve.py +0 -0
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to wavexis are documented in this file.
|
|
4
4
|
|
|
5
|
+
## v2.3.0 — 2026-07-07
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Auto-waiting before `click`, `fill`, and `hover` in both CDP and BiDi backends
|
|
10
|
+
- `_wait_for_element` helper polls every 100ms until element exists and is visible (non-zero size)
|
|
11
|
+
- Configurable timeout (default 30s) via `WaitTimeoutError` with actionable hint
|
|
12
|
+
- Prevents `ElementNotFoundError` on dynamically loaded content
|
|
13
|
+
|
|
5
14
|
## v2.2.6 — 2026-07-07
|
|
6
15
|
|
|
7
16
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wavexis
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
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,97 @@
|
|
|
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_bidi_wait_for_element_found(self) -> None:
|
|
72
|
+
"""Test BiDi _wait_for_element returns when element is visible."""
|
|
73
|
+
from wavexis.backend.bidi import BiDiBackend
|
|
74
|
+
|
|
75
|
+
backend = BiDiBackend()
|
|
76
|
+
backend._client = MagicMock()
|
|
77
|
+
backend._context = MagicMock()
|
|
78
|
+
backend._client.script = MagicMock()
|
|
79
|
+
result = MagicMock()
|
|
80
|
+
result.value = True
|
|
81
|
+
backend._client.script.evaluate = AsyncMock(return_value=result)
|
|
82
|
+
asyncio.run(backend._wait_for_element("#btn", timeout_ms=100))
|
|
83
|
+
assert backend._client.script.evaluate.call_count >= 1
|
|
84
|
+
|
|
85
|
+
def test_bidi_wait_for_element_timeout(self) -> None:
|
|
86
|
+
"""Test BiDi _wait_for_element raises WaitTimeoutError on timeout."""
|
|
87
|
+
from wavexis.backend.bidi import BiDiBackend
|
|
88
|
+
|
|
89
|
+
backend = BiDiBackend()
|
|
90
|
+
backend._client = MagicMock()
|
|
91
|
+
backend._context = MagicMock()
|
|
92
|
+
backend._client.script = MagicMock()
|
|
93
|
+
result = MagicMock()
|
|
94
|
+
result.value = False
|
|
95
|
+
backend._client.script.evaluate = AsyncMock(return_value=result)
|
|
96
|
+
with pytest.raises(WaitTimeoutError):
|
|
97
|
+
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
|
|
@@ -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()
|
|
@@ -825,6 +857,7 @@ class BiDiBackend(AbstractBackend):
|
|
|
825
857
|
) -> None:
|
|
826
858
|
"""Click an element via BiDi script.evaluate."""
|
|
827
859
|
client = self._require_client()
|
|
860
|
+
await self._wait_for_element(selector)
|
|
828
861
|
await self._scroll_into_view_if_needed(selector)
|
|
829
862
|
escaped = selector.replace("'", "\\'")
|
|
830
863
|
js = (
|
|
@@ -857,6 +890,7 @@ class BiDiBackend(AbstractBackend):
|
|
|
857
890
|
async def fill(self, selector: str, value: str) -> None:
|
|
858
891
|
"""Fill an input element with a value via BiDi."""
|
|
859
892
|
client = self._require_client()
|
|
893
|
+
await self._wait_for_element(selector)
|
|
860
894
|
await self._scroll_into_view_if_needed(selector)
|
|
861
895
|
escaped = selector.replace("'", "\\'")
|
|
862
896
|
escaped_val = value.replace("\\", "\\\\").replace("'", "\\'")
|
|
@@ -875,6 +909,7 @@ class BiDiBackend(AbstractBackend):
|
|
|
875
909
|
async def hover(self, selector: str) -> None:
|
|
876
910
|
"""Hover over an element via BiDi script.evaluate."""
|
|
877
911
|
client = self._require_client()
|
|
912
|
+
await self._wait_for_element(selector)
|
|
878
913
|
await self._scroll_into_view_if_needed(selector)
|
|
879
914
|
escaped = selector.replace("'", "\\'")
|
|
880
915
|
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
|
|
|
@@ -1054,6 +1082,7 @@ class CDPBackend(AbstractBackend):
|
|
|
1054
1082
|
click_count: Number of clicks to dispatch.
|
|
1055
1083
|
"""
|
|
1056
1084
|
session = self._require_session()
|
|
1085
|
+
await self._wait_for_element(selector)
|
|
1057
1086
|
await self._scroll_into_view_if_needed(selector)
|
|
1058
1087
|
x, y = await self._get_box_center(selector)
|
|
1059
1088
|
btn_map = {"left": "left", "right": "right", "middle": "middle"}
|
|
@@ -1092,6 +1121,7 @@ class CDPBackend(AbstractBackend):
|
|
|
1092
1121
|
value: Value to set in the input field.
|
|
1093
1122
|
"""
|
|
1094
1123
|
session = self._require_session()
|
|
1124
|
+
await self._wait_for_element(selector)
|
|
1095
1125
|
await self._scroll_into_view_if_needed(selector)
|
|
1096
1126
|
escaped = selector.replace("'", "\\'")
|
|
1097
1127
|
js = (
|
|
@@ -1132,6 +1162,7 @@ class CDPBackend(AbstractBackend):
|
|
|
1132
1162
|
selector: CSS selector for the target element.
|
|
1133
1163
|
"""
|
|
1134
1164
|
session = self._require_session()
|
|
1165
|
+
await self._wait_for_element(selector)
|
|
1135
1166
|
await self._scroll_into_view_if_needed(selector)
|
|
1136
1167
|
x, y = await self._get_box_center(selector)
|
|
1137
1168
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|