wavexis 2.3.3__tar.gz → 2.3.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {wavexis-2.3.3 → wavexis-2.3.4}/CHANGELOG.md +10 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/PKG-INFO +1 -1
- {wavexis-2.3.3 → wavexis-2.3.4}/pyproject.toml +1 -1
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_backend_manager.py +6 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_manager.py +3 -0
- wavexis-2.3.4/tests/unit/test_suggest_locator.py +110 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/__init__.py +1 -1
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/backend/base.py +17 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/backend/bidi.py +58 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/backend/cdp.py +58 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_capture.py +18 -2
- {wavexis-2.3.3 → wavexis-2.3.4}/.dockerignore +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/.github/workflows/ci.yml +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/.github/workflows/docs.yml +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/.github/workflows/release.yml +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/.gitignore +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/CODE_OF_CONDUCT.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/CONTRIBUTING.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/Dockerfile +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/LICENSE +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/README.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/SECURITY.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/docs/api/actions.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/docs/api/backends.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/docs/api/cli.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/docs/api/config.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/docs/api/exceptions.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/docs/api/multi.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/docs/api/plugins.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/docs/api/serve.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/docs/cookbook/auth.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/docs/cookbook/ci-cd.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/docs/cookbook/css.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/docs/cookbook/debug.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/docs/cookbook/record-replay.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/docs/cookbook/scraping.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/docs/cookbook/screenshots.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/docs/cookbook/serve-mode.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/docs/guide/assert.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/docs/guide/backends.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/docs/guide/commands.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/docs/guide/init.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/docs/guide/installation.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/docs/guide/multi.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/docs/guide/perf.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/docs/guide/plugins.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/docs/guide/quickstart.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/docs/guide/raw.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/docs/guide/repl.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/docs/guide/troubleshooting.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/docs/index.md +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/mkdocs.yml +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/stubs/bidiwave/__init__.pyi +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/stubs/cdpwave/__init__.pyi +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/__init__.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/conftest.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/__init__.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_a11y.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_animation.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_backend_selection.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_browser.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_console.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_css.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_debug.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_dialog.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_dom.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_dom_snapshot.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_emulation.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_emulation_advanced.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_eval.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_har.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_input.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_media.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_multi.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_navigate.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_network.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_network_advanced.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_overlay.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_pdf.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_perf.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_permissions.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_raw.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_record.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_scrape.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_screenshot.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_security.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_serve.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_service_worker.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_storage.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_tabs.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_webaudio.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/integration/test_webauthn.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/__init__.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_abstract_backend_phase5.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_actions.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_actions_phase5.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_animation_action.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_assert_multi.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_auth.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_auto_wait.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_bidi_backend.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_bidi_phase5.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_bluetooth_action.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_cast_action.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_cleanup.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_cli_phase5.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_config.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_config_phase5.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_cookies_headers_actions.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_css_action.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_debug_action.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_dom_snapshot_action.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_download_screencast.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_emulation_action.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_error_handling.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_exceptions.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_format_record.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_global_options.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_iframe.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_init.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_input_action.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_interactions_crawl.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_media_action.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_multi.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_multi_improvements.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_new_features.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_output.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_overlay_action.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_perf_action.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_perf_console.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_plugins.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_raw.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_record.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_repl.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_serve.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_service_worker_action.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_shadow.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_storage_action.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_webaudio_action.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/tests/unit/test_webauthn_action.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/__main__.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/__init__.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/accessibility.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/animation.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/base.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/bluetooth.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/browser.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/cast.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/console.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/cookies.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/crawl.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/css.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/debug.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/dialog.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/dom.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/dom_snapshot.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/download.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/emulation.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/eval.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/extract.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/form.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/har.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/headers.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/input.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/lighthouse.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/media.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/multi.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/navigate.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/network.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/overlay.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/pdf.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/performance.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/permissions.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/record.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/scrape.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/screencast.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/screenshot.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/security.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/service_worker.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/session.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/storage.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/tabs.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/wait.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/webaudio.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/webauthn.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/actions/websocket.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/auth.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/backend/__init__.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/backend/manager.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cleanup.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/__init__.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_advanced.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_config.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_debug.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_emulation.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_experimental.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_iframe.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_input.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_navigation.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_network.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_perf.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_serve.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_session.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_shadow.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_shared.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/_workflow.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/cli/app.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/config.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/exceptions.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/init.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/multi.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/output.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/plugins.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/record.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/repl.py +0 -0
- {wavexis-2.3.3 → wavexis-2.3.4}/wavexis/serve.py +0 -0
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to wavexis are documented in this file.
|
|
4
4
|
|
|
5
|
+
## v2.3.4 — 2026-07-07
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Generate locator: `suggest_locator` method in both CDP and BiDi backends
|
|
10
|
+
- `wavexis dom <url> --action suggest_locator -s <selector>` CLI command
|
|
11
|
+
- `--all` flag to return all suggestions ranked by specificity
|
|
12
|
+
- Priority order: `#id` > `[data-testid]` > `[aria-label]` > `[role]` > `:has-text()` > `tag.classes` > `parent > tag` > `:nth-of-type()` > `tag`
|
|
13
|
+
- Uses `CSS.escape()` for safe selector generation
|
|
14
|
+
|
|
5
15
|
## v2.3.3 — 2026-07-07
|
|
6
16
|
|
|
7
17
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wavexis
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.4
|
|
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
|
|
@@ -132,6 +132,12 @@ class DummyBackend(AbstractBackend):
|
|
|
132
132
|
"""Dom scroll."""
|
|
133
133
|
pass
|
|
134
134
|
|
|
135
|
+
async def suggest_locator(
|
|
136
|
+
self, selector: str, all: bool = False
|
|
137
|
+
) -> list[str] | str:
|
|
138
|
+
"""Suggest locator."""
|
|
139
|
+
return selector
|
|
140
|
+
|
|
135
141
|
async def capture_har(self, params: object) -> dict:
|
|
136
142
|
"""Capture har."""
|
|
137
143
|
return {}
|
|
@@ -66,6 +66,9 @@ class FakeBackend(AbstractBackend):
|
|
|
66
66
|
"""Dom focus."""
|
|
67
67
|
async def dom_scroll(self, selector=None, x=0, y=0):
|
|
68
68
|
"""Dom scroll."""
|
|
69
|
+
async def suggest_locator(self, selector, all=False):
|
|
70
|
+
"""Suggest locator."""
|
|
71
|
+
return selector
|
|
69
72
|
async def capture_har(self, params):
|
|
70
73
|
"""Capture har."""
|
|
71
74
|
async def get_cookies(self):
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Unit tests for suggest_locator in both backends."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any
|
|
8
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from wavexis.exceptions import ElementNotFoundError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _make_cdp_backend() -> Any:
|
|
16
|
+
"""Create a CDPBackend with mocked session."""
|
|
17
|
+
from wavexis.backend.cdp import CDPBackend
|
|
18
|
+
|
|
19
|
+
backend = CDPBackend()
|
|
20
|
+
backend._session = MagicMock()
|
|
21
|
+
backend._session.runtime = MagicMock()
|
|
22
|
+
backend._session.runtime.evaluate = AsyncMock(
|
|
23
|
+
return_value={"result": {"value": None}}
|
|
24
|
+
)
|
|
25
|
+
return backend
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _make_bidi_backend() -> Any:
|
|
29
|
+
"""Create a BiDiBackend with mocked client."""
|
|
30
|
+
from wavexis.backend.bidi import BiDiBackend
|
|
31
|
+
|
|
32
|
+
backend = BiDiBackend()
|
|
33
|
+
backend._client = MagicMock()
|
|
34
|
+
backend._context = MagicMock()
|
|
35
|
+
backend._client.script = MagicMock()
|
|
36
|
+
result = MagicMock()
|
|
37
|
+
result.value = None
|
|
38
|
+
backend._client.script.evaluate = AsyncMock(return_value=result)
|
|
39
|
+
return backend
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.mark.unit
|
|
43
|
+
class TestSuggestLocatorCDP:
|
|
44
|
+
"""Tests for suggest_locator in CDP backend."""
|
|
45
|
+
|
|
46
|
+
def test_returns_best_selector(self) -> None:
|
|
47
|
+
"""Test that suggest_locator returns the first (best) suggestion."""
|
|
48
|
+
backend = _make_cdp_backend()
|
|
49
|
+
suggestions = ["#login-btn", "button.primary", "button"]
|
|
50
|
+
backend._session.runtime.evaluate = AsyncMock(
|
|
51
|
+
return_value={"result": {"value": json.dumps(suggestions)}}
|
|
52
|
+
)
|
|
53
|
+
result = asyncio.run(backend.suggest_locator("#login-btn"))
|
|
54
|
+
assert result == "#login-btn"
|
|
55
|
+
|
|
56
|
+
def test_returns_all_suggestions(self) -> None:
|
|
57
|
+
"""Test that suggest_locator with all=True returns all suggestions."""
|
|
58
|
+
backend = _make_cdp_backend()
|
|
59
|
+
suggestions = ["#my-id", "[data-testid='btn']", "button.clickable", "button"]
|
|
60
|
+
backend._session.runtime.evaluate = AsyncMock(
|
|
61
|
+
return_value={"result": {"value": json.dumps(suggestions)}}
|
|
62
|
+
)
|
|
63
|
+
result = asyncio.run(backend.suggest_locator("button", all=True))
|
|
64
|
+
assert isinstance(result, list)
|
|
65
|
+
assert len(result) == 4
|
|
66
|
+
assert result[0] == "#my-id"
|
|
67
|
+
|
|
68
|
+
def test_raises_on_element_not_found(self) -> None:
|
|
69
|
+
"""Test that suggest_locator raises ElementNotFoundError when element missing."""
|
|
70
|
+
backend = _make_cdp_backend()
|
|
71
|
+
backend._session.runtime.evaluate = AsyncMock(
|
|
72
|
+
return_value={"result": {"value": None}}
|
|
73
|
+
)
|
|
74
|
+
with pytest.raises(ElementNotFoundError):
|
|
75
|
+
asyncio.run(backend.suggest_locator("#missing"))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@pytest.mark.unit
|
|
79
|
+
class TestSuggestLocatorBiDi:
|
|
80
|
+
"""Tests for suggest_locator in BiDi backend."""
|
|
81
|
+
|
|
82
|
+
def test_returns_best_selector(self) -> None:
|
|
83
|
+
"""Test that suggest_locator returns the first (best) suggestion."""
|
|
84
|
+
backend = _make_bidi_backend()
|
|
85
|
+
suggestions = ["#submit", "button[type='submit']", "button"]
|
|
86
|
+
result_mock = MagicMock()
|
|
87
|
+
result_mock.value = json.dumps(suggestions)
|
|
88
|
+
backend._client.script.evaluate = AsyncMock(return_value=result_mock)
|
|
89
|
+
result = asyncio.run(backend.suggest_locator("#submit"))
|
|
90
|
+
assert result == "#submit"
|
|
91
|
+
|
|
92
|
+
def test_returns_all_suggestions(self) -> None:
|
|
93
|
+
"""Test that suggest_locator with all=True returns all suggestions."""
|
|
94
|
+
backend = _make_bidi_backend()
|
|
95
|
+
suggestions = ["#email", "[data-testid='email']", "input.email", "input"]
|
|
96
|
+
result_mock = MagicMock()
|
|
97
|
+
result_mock.value = json.dumps(suggestions)
|
|
98
|
+
backend._client.script.evaluate = AsyncMock(return_value=result_mock)
|
|
99
|
+
result = asyncio.run(backend.suggest_locator("input", all=True))
|
|
100
|
+
assert isinstance(result, list)
|
|
101
|
+
assert len(result) == 4
|
|
102
|
+
|
|
103
|
+
def test_raises_on_element_not_found(self) -> None:
|
|
104
|
+
"""Test that suggest_locator raises ElementNotFoundError when element missing."""
|
|
105
|
+
backend = _make_bidi_backend()
|
|
106
|
+
result_mock = MagicMock()
|
|
107
|
+
result_mock.value = None
|
|
108
|
+
backend._client.script.evaluate = AsyncMock(return_value=result_mock)
|
|
109
|
+
with pytest.raises(ElementNotFoundError):
|
|
110
|
+
asyncio.run(backend.suggest_locator("#missing"))
|
|
@@ -149,6 +149,23 @@ class AbstractBackend(ABC):
|
|
|
149
149
|
) -> None:
|
|
150
150
|
"""Scroll to an element or by offset."""
|
|
151
151
|
|
|
152
|
+
@abstractmethod
|
|
153
|
+
async def suggest_locator(
|
|
154
|
+
self, selector: str, all: bool = False
|
|
155
|
+
) -> list[str] | str:
|
|
156
|
+
"""Suggest the best CSS selector for an element.
|
|
157
|
+
|
|
158
|
+
Analyzes the element and generates optimal selectors in priority order:
|
|
159
|
+
id > data-testid > aria-label > text > tag.classes > nth-of-type chain.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
selector: CSS selector for the target element.
|
|
163
|
+
all: If True, return multiple suggestions; otherwise just the best one.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
List of selector strings when all=True, single best selector when all=False.
|
|
167
|
+
"""
|
|
168
|
+
|
|
152
169
|
# ── Network ────────────────────────────────────────────
|
|
153
170
|
|
|
154
171
|
@abstractmethod
|
|
@@ -537,6 +537,64 @@ class BiDiBackend(AbstractBackend):
|
|
|
537
537
|
js = f"window.scrollTo({x},{y})"
|
|
538
538
|
await self._client.script.evaluate(self._context, js)
|
|
539
539
|
|
|
540
|
+
async def suggest_locator(
|
|
541
|
+
self, selector: str, all: bool = False
|
|
542
|
+
) -> list[str] | str:
|
|
543
|
+
"""Suggest the best CSS selector for an element.
|
|
544
|
+
|
|
545
|
+
Args:
|
|
546
|
+
selector: CSS selector for the target element.
|
|
547
|
+
all: If True, return multiple suggestions; otherwise just the best one.
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
List of selector strings when all=True, single best selector when all=False.
|
|
551
|
+
"""
|
|
552
|
+
client = self._require_client()
|
|
553
|
+
escaped = selector.replace("'", "\\'")
|
|
554
|
+
js = self._suggest_locator_js(escaped)
|
|
555
|
+
result = await client.script.evaluate(self._context, js)
|
|
556
|
+
raw = getattr(result, "value", None)
|
|
557
|
+
if not raw:
|
|
558
|
+
raise ElementNotFoundError(selector)
|
|
559
|
+
suggestions: list[str] = json.loads(raw)
|
|
560
|
+
if all:
|
|
561
|
+
return suggestions
|
|
562
|
+
return suggestions[0] if suggestions else selector
|
|
563
|
+
|
|
564
|
+
@staticmethod
|
|
565
|
+
def _suggest_locator_js(escaped: str) -> str:
|
|
566
|
+
"""Build JS that generates CSS selector suggestions for an element."""
|
|
567
|
+
return (
|
|
568
|
+
f"(function(){{"
|
|
569
|
+
f"var el=document.querySelector('{escaped}');"
|
|
570
|
+
f"if(!el)return null;"
|
|
571
|
+
f"var s=[];var t=el.tagName.toLowerCase();"
|
|
572
|
+
f"var id=el.id;"
|
|
573
|
+
f"var tid=el.getAttribute('data-testid')"
|
|
574
|
+
f"||el.getAttribute('data-test-id')"
|
|
575
|
+
f"||el.getAttribute('data-cy');"
|
|
576
|
+
f"var aria=el.getAttribute('aria-label');"
|
|
577
|
+
f"var role=el.getAttribute('role');"
|
|
578
|
+
f"var txt=(el.textContent||'').trim().substring(0,50);"
|
|
579
|
+
f"var cls=Array.from(el.classList);"
|
|
580
|
+
f"if(id)s.push('#'+CSS.escape(id));"
|
|
581
|
+
f"if(tid)s.push('[data-testid=\"'+tid+'\"]');"
|
|
582
|
+
f"if(aria)s.push(t+'[aria-label=\"'+aria+'\"]');"
|
|
583
|
+
f"if(role)s.push(t+'[role=\"'+role+'\"]');"
|
|
584
|
+
f"if(txt&&txt.length<30)s.push(t+':has-text(\"'+txt+'\"]');"
|
|
585
|
+
f"if(cls.length>0)s.push(t+'.'+cls.join('.'));"
|
|
586
|
+
f"var p=el.parentElement;"
|
|
587
|
+
f"if(p&&p.id)s.push('#'+CSS.escape(p.id)+' > '+t);"
|
|
588
|
+
f"var sib=p?Array.from(p.children)"
|
|
589
|
+
f".filter(function(c){{return c.tagName===el.tagName}}):[];"
|
|
590
|
+
f"if(sib.length>1){{"
|
|
591
|
+
f"var i=sib.indexOf(el)+1;"
|
|
592
|
+
f"s.push(t+':nth-of-type('+i+')');}}"
|
|
593
|
+
f"s.push(t);"
|
|
594
|
+
f"return JSON.stringify(s);"
|
|
595
|
+
f"}})()"
|
|
596
|
+
)
|
|
597
|
+
|
|
540
598
|
async def capture_har(self, params: HarParams) -> dict[str, Any]:
|
|
541
599
|
"""Capture HAR data via CDP Network domain.
|
|
542
600
|
|
|
@@ -644,6 +644,64 @@ class CDPBackend(AbstractBackend):
|
|
|
644
644
|
js = f"window.scrollBy({x}, {y})"
|
|
645
645
|
await session.runtime.evaluate(js)
|
|
646
646
|
|
|
647
|
+
async def suggest_locator(
|
|
648
|
+
self, selector: str, all: bool = False
|
|
649
|
+
) -> list[str] | str:
|
|
650
|
+
"""Suggest the best CSS selector for an element.
|
|
651
|
+
|
|
652
|
+
Args:
|
|
653
|
+
selector: CSS selector for the target element.
|
|
654
|
+
all: If True, return multiple suggestions; otherwise just the best one.
|
|
655
|
+
|
|
656
|
+
Returns:
|
|
657
|
+
List of selector strings when all=True, single best selector when all=False.
|
|
658
|
+
"""
|
|
659
|
+
session = self._require_session()
|
|
660
|
+
escaped = selector.replace("'", "\\'")
|
|
661
|
+
js = self._suggest_locator_js(escaped)
|
|
662
|
+
result = await session.runtime.evaluate(js)
|
|
663
|
+
raw = result.get("result", {}).get("value")
|
|
664
|
+
if not raw:
|
|
665
|
+
raise ElementNotFoundError(selector)
|
|
666
|
+
suggestions: list[str] = json.loads(raw)
|
|
667
|
+
if all:
|
|
668
|
+
return suggestions
|
|
669
|
+
return suggestions[0] if suggestions else selector
|
|
670
|
+
|
|
671
|
+
@staticmethod
|
|
672
|
+
def _suggest_locator_js(escaped: str) -> str:
|
|
673
|
+
"""Build JS that generates CSS selector suggestions for an element."""
|
|
674
|
+
return (
|
|
675
|
+
f"(function(){{"
|
|
676
|
+
f"var el=document.querySelector('{escaped}');"
|
|
677
|
+
f"if(!el)return null;"
|
|
678
|
+
f"var s=[];var t=el.tagName.toLowerCase();"
|
|
679
|
+
f"var id=el.id;"
|
|
680
|
+
f"var tid=el.getAttribute('data-testid')"
|
|
681
|
+
f"||el.getAttribute('data-test-id')"
|
|
682
|
+
f"||el.getAttribute('data-cy');"
|
|
683
|
+
f"var aria=el.getAttribute('aria-label');"
|
|
684
|
+
f"var role=el.getAttribute('role');"
|
|
685
|
+
f"var txt=(el.textContent||'').trim().substring(0,50);"
|
|
686
|
+
f"var cls=Array.from(el.classList);"
|
|
687
|
+
f"if(id)s.push('#'+CSS.escape(id));"
|
|
688
|
+
f"if(tid)s.push('[data-testid=\"'+tid+'\"]');"
|
|
689
|
+
f"if(aria)s.push(t+'[aria-label=\"'+aria+'\"]');"
|
|
690
|
+
f"if(role)s.push(t+'[role=\"'+role+'\"]');"
|
|
691
|
+
f"if(txt&&txt.length<30)s.push(t+':has-text(\"'+txt+'\"]');"
|
|
692
|
+
f"if(cls.length>0)s.push(t+'.'+cls.join('.'));"
|
|
693
|
+
f"var p=el.parentElement;"
|
|
694
|
+
f"if(p&&p.id)s.push('#'+CSS.escape(p.id)+' > '+t);"
|
|
695
|
+
f"var sib=p?Array.from(p.children)"
|
|
696
|
+
f".filter(function(c){{return c.tagName===el.tagName}}):[];"
|
|
697
|
+
f"if(sib.length>1){{"
|
|
698
|
+
f"var i=sib.indexOf(el)+1;"
|
|
699
|
+
f"s.push(t+':nth-of-type('+i+')');}}"
|
|
700
|
+
f"s.push(t);"
|
|
701
|
+
f"return JSON.stringify(s);"
|
|
702
|
+
f"}})()"
|
|
703
|
+
)
|
|
704
|
+
|
|
647
705
|
# ── Network ────────────────────────────────────────────
|
|
648
706
|
|
|
649
707
|
async def capture_har(self, params: HarParams) -> dict[str, Any]:
|
|
@@ -274,14 +274,20 @@ def dom(
|
|
|
274
274
|
"get",
|
|
275
275
|
"--action",
|
|
276
276
|
"-a",
|
|
277
|
-
help=
|
|
277
|
+
help=(
|
|
278
|
+
"DOM action: get, query, attr, remove_attr, remove, focus, scroll, "
|
|
279
|
+
"suggest_locator"
|
|
280
|
+
),
|
|
278
281
|
),
|
|
279
282
|
selector: str = typer.Option("", "--selector", "-s", help="CSS selector"),
|
|
280
283
|
output: str | None = typer.Option(
|
|
281
284
|
None, "--output", "-o", help="Output file path"
|
|
282
285
|
),
|
|
283
286
|
outer: bool = typer.Option(True, "--outer/--inner", help="Outer or inner HTML"),
|
|
284
|
-
all: bool = typer.Option(
|
|
287
|
+
all: bool = typer.Option(
|
|
288
|
+
False, "--all",
|
|
289
|
+
help="Query all matching elements or all locator suggestions",
|
|
290
|
+
),
|
|
285
291
|
attribute: str | None = typer.Option(
|
|
286
292
|
None, "--attribute", help="Attribute name for get/set/remove"
|
|
287
293
|
),
|
|
@@ -302,6 +308,13 @@ def dom(
|
|
|
302
308
|
typer.echo(f"Output saved to {output}")
|
|
303
309
|
else:
|
|
304
310
|
typer.echo(result)
|
|
311
|
+
elif isinstance(result, list):
|
|
312
|
+
if output:
|
|
313
|
+
Output.write_json(result, output)
|
|
314
|
+
typer.echo(f"Output saved to {output}")
|
|
315
|
+
else:
|
|
316
|
+
for item in result:
|
|
317
|
+
typer.echo(item)
|
|
305
318
|
elif result is not None:
|
|
306
319
|
if output:
|
|
307
320
|
Output.write_json(result, output)
|
|
@@ -324,6 +337,9 @@ async def _dom(
|
|
|
324
337
|
backend = _get_backend()
|
|
325
338
|
try:
|
|
326
339
|
await backend.launch(_browser_options())
|
|
340
|
+
if action == "suggest_locator":
|
|
341
|
+
await backend.navigate(url, WaitStrategy(strategy="load"))
|
|
342
|
+
return await backend.suggest_locator(selector, all=all)
|
|
327
343
|
params = DOMParams(
|
|
328
344
|
url=url,
|
|
329
345
|
action=action,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|