wavexis 2.3.2__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.2 → wavexis-2.3.4}/CHANGELOG.md +21 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/PKG-INFO +1 -1
- {wavexis-2.3.2 → wavexis-2.3.4}/pyproject.toml +1 -1
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_backend_manager.py +24 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_manager.py +10 -0
- wavexis-2.3.4/tests/unit/test_shadow.py +232 -0
- wavexis-2.3.4/tests/unit/test_suggest_locator.py +110 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/__init__.py +1 -1
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/backend/base.py +56 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/backend/bidi.py +187 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/backend/cdp.py +185 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_capture.py +18 -2
- wavexis-2.3.4/wavexis/cli/_shadow.py +135 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/app.py +1 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/.dockerignore +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/.github/workflows/ci.yml +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/.github/workflows/docs.yml +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/.github/workflows/release.yml +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/.gitignore +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/CODE_OF_CONDUCT.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/CONTRIBUTING.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/Dockerfile +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/LICENSE +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/README.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/SECURITY.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/docs/api/actions.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/docs/api/backends.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/docs/api/cli.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/docs/api/config.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/docs/api/exceptions.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/docs/api/multi.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/docs/api/plugins.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/docs/api/serve.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/docs/cookbook/auth.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/docs/cookbook/ci-cd.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/docs/cookbook/css.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/docs/cookbook/debug.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/docs/cookbook/record-replay.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/docs/cookbook/scraping.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/docs/cookbook/screenshots.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/docs/cookbook/serve-mode.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/docs/guide/assert.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/docs/guide/backends.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/docs/guide/commands.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/docs/guide/init.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/docs/guide/installation.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/docs/guide/multi.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/docs/guide/perf.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/docs/guide/plugins.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/docs/guide/quickstart.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/docs/guide/raw.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/docs/guide/repl.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/docs/guide/troubleshooting.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/docs/index.md +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/mkdocs.yml +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/stubs/bidiwave/__init__.pyi +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/stubs/cdpwave/__init__.pyi +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/__init__.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/conftest.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/__init__.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_a11y.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_animation.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_backend_selection.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_browser.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_console.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_css.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_debug.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_dialog.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_dom.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_dom_snapshot.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_emulation.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_emulation_advanced.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_eval.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_har.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_input.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_media.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_multi.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_navigate.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_network.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_network_advanced.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_overlay.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_pdf.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_perf.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_permissions.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_raw.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_record.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_scrape.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_screenshot.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_security.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_serve.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_service_worker.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_storage.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_tabs.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_webaudio.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/integration/test_webauthn.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/__init__.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_abstract_backend_phase5.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_actions.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_actions_phase5.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_animation_action.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_assert_multi.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_auth.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_auto_wait.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_bidi_backend.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_bidi_phase5.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_bluetooth_action.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_cast_action.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_cleanup.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_cli_phase5.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_config.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_config_phase5.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_cookies_headers_actions.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_css_action.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_debug_action.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_dom_snapshot_action.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_download_screencast.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_emulation_action.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_error_handling.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_exceptions.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_format_record.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_global_options.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_iframe.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_init.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_input_action.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_interactions_crawl.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_media_action.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_multi.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_multi_improvements.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_new_features.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_output.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_overlay_action.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_perf_action.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_perf_console.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_plugins.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_raw.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_record.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_repl.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_serve.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_service_worker_action.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_storage_action.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_webaudio_action.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/tests/unit/test_webauthn_action.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/__main__.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/__init__.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/accessibility.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/animation.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/base.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/bluetooth.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/browser.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/cast.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/console.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/cookies.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/crawl.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/css.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/debug.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/dialog.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/dom.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/dom_snapshot.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/download.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/emulation.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/eval.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/extract.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/form.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/har.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/headers.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/input.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/lighthouse.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/media.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/multi.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/navigate.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/network.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/overlay.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/pdf.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/performance.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/permissions.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/record.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/scrape.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/screencast.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/screenshot.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/security.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/service_worker.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/session.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/storage.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/tabs.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/wait.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/webaudio.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/webauthn.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/actions/websocket.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/auth.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/backend/__init__.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/backend/manager.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cleanup.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/__init__.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_advanced.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_config.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_debug.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_emulation.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_experimental.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_iframe.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_input.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_navigation.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_network.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_perf.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_serve.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_session.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_shared.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/cli/_workflow.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/config.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/exceptions.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/init.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/multi.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/output.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/plugins.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/record.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/repl.py +0 -0
- {wavexis-2.3.2 → wavexis-2.3.4}/wavexis/serve.py +0 -0
|
@@ -2,6 +2,27 @@
|
|
|
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
|
+
|
|
15
|
+
## v2.3.3 — 2026-07-07
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Shadow DOM support: `shadow_click`, `shadow_fill`, `shadow_eval` in both CDP and BiDi backends
|
|
20
|
+
- `wavexis shadow click/fill/eval` CLI command with `--selectors` (comma-separated piercing chain)
|
|
21
|
+
- `_build_shadow_pierce_js` helper generates JS that traverses `shadowRoot` boundaries
|
|
22
|
+
- Auto-waiting inside shadow DOM via `_wait_for_element_in_shadow` with polling and configurable timeout
|
|
23
|
+
- `--no-wait` flag to skip auto-waiting for shadow interactions
|
|
24
|
+
- Events dispatched with `composed: true` to cross shadow boundaries
|
|
25
|
+
|
|
5
26
|
## v2.3.2 — 2026-07-07
|
|
6
27
|
|
|
7
28
|
### 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 {}
|
|
@@ -268,6 +274,24 @@ class DummyBackend(AbstractBackend):
|
|
|
268
274
|
"""Iframe fill."""
|
|
269
275
|
pass
|
|
270
276
|
|
|
277
|
+
async def shadow_eval(
|
|
278
|
+
self, selectors: list[str], expression: str, await_promise: bool = False
|
|
279
|
+
) -> object:
|
|
280
|
+
"""Shadow eval."""
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
async def shadow_click(
|
|
284
|
+
self, selectors: list[str], auto_wait: bool = True
|
|
285
|
+
) -> None:
|
|
286
|
+
"""Shadow click."""
|
|
287
|
+
pass
|
|
288
|
+
|
|
289
|
+
async def shadow_fill(
|
|
290
|
+
self, selectors: list[str], value: str, auto_wait: bool = True
|
|
291
|
+
) -> None:
|
|
292
|
+
"""Shadow fill."""
|
|
293
|
+
pass
|
|
294
|
+
|
|
271
295
|
async def throttle_network(self, params: object) -> None:
|
|
272
296
|
"""Throttle network."""
|
|
273
297
|
pass
|
|
@@ -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):
|
|
@@ -269,6 +272,13 @@ class FakeBackend(AbstractBackend):
|
|
|
269
272
|
"""Iframe click."""
|
|
270
273
|
async def iframe_fill(self, iframe_selector, selector, value, auto_wait=True):
|
|
271
274
|
"""Iframe fill."""
|
|
275
|
+
async def shadow_eval(self, selectors, expression, await_promise=False):
|
|
276
|
+
"""Shadow eval."""
|
|
277
|
+
return None
|
|
278
|
+
async def shadow_click(self, selectors, auto_wait=True):
|
|
279
|
+
"""Shadow click."""
|
|
280
|
+
async def shadow_fill(self, selectors, value, auto_wait=True):
|
|
281
|
+
"""Shadow fill."""
|
|
272
282
|
|
|
273
283
|
|
|
274
284
|
class TestBackendManager:
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""Unit tests for Shadow DOM support — shadow_eval, shadow_click, shadow_fill."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import Any
|
|
7
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from wavexis.exceptions import ElementNotFoundError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _make_cdp_backend() -> Any:
|
|
15
|
+
"""Create a CDPBackend with mocked session."""
|
|
16
|
+
from wavexis.backend.cdp import CDPBackend
|
|
17
|
+
|
|
18
|
+
backend = CDPBackend()
|
|
19
|
+
backend._session = MagicMock()
|
|
20
|
+
backend._session.runtime = MagicMock()
|
|
21
|
+
backend._session.runtime.evaluate = AsyncMock(
|
|
22
|
+
return_value={"result": {"value": True}}
|
|
23
|
+
)
|
|
24
|
+
return backend
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _make_bidi_backend() -> Any:
|
|
28
|
+
"""Create a BiDiBackend with mocked client."""
|
|
29
|
+
from wavexis.backend.bidi import BiDiBackend
|
|
30
|
+
|
|
31
|
+
backend = BiDiBackend()
|
|
32
|
+
backend._client = MagicMock()
|
|
33
|
+
backend._context = MagicMock()
|
|
34
|
+
backend._client.script = MagicMock()
|
|
35
|
+
result = MagicMock()
|
|
36
|
+
result.value = True
|
|
37
|
+
backend._client.script.evaluate = AsyncMock(return_value=result)
|
|
38
|
+
return backend
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.mark.unit
|
|
42
|
+
class TestShadowEval:
|
|
43
|
+
"""Tests for shadow_eval in both backends."""
|
|
44
|
+
|
|
45
|
+
def test_cdp_shadow_eval_returns_value(self) -> None:
|
|
46
|
+
"""Test CDP shadow_eval returns the evaluated value."""
|
|
47
|
+
backend = _make_cdp_backend()
|
|
48
|
+
backend._session.runtime.evaluate = AsyncMock(
|
|
49
|
+
return_value={"result": {"value": 42}}
|
|
50
|
+
)
|
|
51
|
+
result = asyncio.run(
|
|
52
|
+
backend.shadow_eval(["my-component", "button"], "1 + 1")
|
|
53
|
+
)
|
|
54
|
+
assert result == 42
|
|
55
|
+
|
|
56
|
+
def test_cdp_shadow_eval_null_element_not_found(self) -> None:
|
|
57
|
+
"""Test CDP shadow_eval returns None when element is not found."""
|
|
58
|
+
backend = _make_cdp_backend()
|
|
59
|
+
backend._session.runtime.evaluate = AsyncMock(
|
|
60
|
+
return_value={"result": {"value": None}}
|
|
61
|
+
)
|
|
62
|
+
result = asyncio.run(
|
|
63
|
+
backend.shadow_eval(["missing", "button"], "this.textContent")
|
|
64
|
+
)
|
|
65
|
+
assert result is None
|
|
66
|
+
|
|
67
|
+
def test_bidi_shadow_eval_returns_value(self) -> None:
|
|
68
|
+
"""Test BiDi shadow_eval returns the evaluated value."""
|
|
69
|
+
backend = _make_bidi_backend()
|
|
70
|
+
result_mock = MagicMock()
|
|
71
|
+
result_mock.value = "hello"
|
|
72
|
+
backend._client.script.evaluate = AsyncMock(return_value=result_mock)
|
|
73
|
+
result = asyncio.run(
|
|
74
|
+
backend.shadow_eval(["my-component", "button"], "this.textContent")
|
|
75
|
+
)
|
|
76
|
+
assert result == "hello"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@pytest.mark.unit
|
|
80
|
+
class TestShadowClick:
|
|
81
|
+
"""Tests for shadow_click in both backends."""
|
|
82
|
+
|
|
83
|
+
def test_cdp_shadow_click_success(self) -> None:
|
|
84
|
+
"""Test CDP shadow_click dispatches click inside shadow DOM."""
|
|
85
|
+
backend = _make_cdp_backend()
|
|
86
|
+
backend._session.runtime.evaluate = AsyncMock(
|
|
87
|
+
return_value={"result": {"value": True}}
|
|
88
|
+
)
|
|
89
|
+
asyncio.run(backend.shadow_click(["my-component", "button"], auto_wait=False))
|
|
90
|
+
backend._session.runtime.evaluate.assert_called()
|
|
91
|
+
|
|
92
|
+
def test_cdp_shadow_click_element_not_found(self) -> None:
|
|
93
|
+
"""Test CDP shadow_click raises ElementNotFoundError when element missing."""
|
|
94
|
+
backend = _make_cdp_backend()
|
|
95
|
+
backend._session.runtime.evaluate = AsyncMock(
|
|
96
|
+
return_value={"result": {"value": False}}
|
|
97
|
+
)
|
|
98
|
+
with pytest.raises(ElementNotFoundError):
|
|
99
|
+
asyncio.run(backend.shadow_click(["missing", "btn"], auto_wait=False))
|
|
100
|
+
|
|
101
|
+
def test_bidi_shadow_click_success(self) -> None:
|
|
102
|
+
"""Test BiDi shadow_click dispatches click inside shadow DOM."""
|
|
103
|
+
backend = _make_bidi_backend()
|
|
104
|
+
result_mock = MagicMock()
|
|
105
|
+
result_mock.value = True
|
|
106
|
+
backend._client.script.evaluate = AsyncMock(return_value=result_mock)
|
|
107
|
+
asyncio.run(backend.shadow_click(["my-component", "button"], auto_wait=False))
|
|
108
|
+
backend._client.script.evaluate.assert_called()
|
|
109
|
+
|
|
110
|
+
def test_bidi_shadow_click_element_not_found(self) -> None:
|
|
111
|
+
"""Test BiDi shadow_click raises ElementNotFoundError when element missing."""
|
|
112
|
+
backend = _make_bidi_backend()
|
|
113
|
+
result_mock = MagicMock()
|
|
114
|
+
result_mock.value = False
|
|
115
|
+
backend._client.script.evaluate = AsyncMock(return_value=result_mock)
|
|
116
|
+
with pytest.raises(ElementNotFoundError):
|
|
117
|
+
asyncio.run(backend.shadow_click(["missing", "btn"], auto_wait=False))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@pytest.mark.unit
|
|
121
|
+
class TestShadowFill:
|
|
122
|
+
"""Tests for shadow_fill in both backends."""
|
|
123
|
+
|
|
124
|
+
def test_cdp_shadow_fill_success(self) -> None:
|
|
125
|
+
"""Test CDP shadow_fill sets value inside shadow DOM."""
|
|
126
|
+
backend = _make_cdp_backend()
|
|
127
|
+
backend._session.runtime.evaluate = AsyncMock(
|
|
128
|
+
return_value={"result": {"value": True}}
|
|
129
|
+
)
|
|
130
|
+
asyncio.run(
|
|
131
|
+
backend.shadow_fill(["my-component", "input"], "test value", auto_wait=False)
|
|
132
|
+
)
|
|
133
|
+
backend._session.runtime.evaluate.assert_called()
|
|
134
|
+
|
|
135
|
+
def test_cdp_shadow_fill_element_not_found(self) -> None:
|
|
136
|
+
"""Test CDP shadow_fill raises ElementNotFoundError when element missing."""
|
|
137
|
+
backend = _make_cdp_backend()
|
|
138
|
+
backend._session.runtime.evaluate = AsyncMock(
|
|
139
|
+
return_value={"result": {"value": False}}
|
|
140
|
+
)
|
|
141
|
+
with pytest.raises(ElementNotFoundError):
|
|
142
|
+
asyncio.run(
|
|
143
|
+
backend.shadow_fill(["missing", "input"], "val", auto_wait=False)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def test_bidi_shadow_fill_success(self) -> None:
|
|
147
|
+
"""Test BiDi shadow_fill sets value inside shadow DOM."""
|
|
148
|
+
backend = _make_bidi_backend()
|
|
149
|
+
result_mock = MagicMock()
|
|
150
|
+
result_mock.value = True
|
|
151
|
+
backend._client.script.evaluate = AsyncMock(return_value=result_mock)
|
|
152
|
+
asyncio.run(
|
|
153
|
+
backend.shadow_fill(["my-component", "input"], "test value", auto_wait=False)
|
|
154
|
+
)
|
|
155
|
+
backend._client.script.evaluate.assert_called()
|
|
156
|
+
|
|
157
|
+
def test_bidi_shadow_fill_element_not_found(self) -> None:
|
|
158
|
+
"""Test BiDi shadow_fill raises ElementNotFoundError when element missing."""
|
|
159
|
+
backend = _make_bidi_backend()
|
|
160
|
+
result_mock = MagicMock()
|
|
161
|
+
result_mock.value = False
|
|
162
|
+
backend._client.script.evaluate = AsyncMock(return_value=result_mock)
|
|
163
|
+
with pytest.raises(ElementNotFoundError):
|
|
164
|
+
asyncio.run(
|
|
165
|
+
backend.shadow_fill(["missing", "input"], "val", auto_wait=False)
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@pytest.mark.unit
|
|
170
|
+
class TestShadowAutoWait:
|
|
171
|
+
"""Tests for auto_wait in shadow operations."""
|
|
172
|
+
|
|
173
|
+
def test_cdp_shadow_click_auto_wait_false_skips_wait(self) -> None:
|
|
174
|
+
"""Test that shadow_click with auto_wait=False skips _wait_for_element_in_shadow."""
|
|
175
|
+
backend = _make_cdp_backend()
|
|
176
|
+
call_order: list[str] = []
|
|
177
|
+
|
|
178
|
+
async def _mock_wait(selectors: list[str], timeout_ms: int = 30000) -> None:
|
|
179
|
+
call_order.append("wait")
|
|
180
|
+
|
|
181
|
+
backend._wait_for_element_in_shadow = _mock_wait # type: ignore[assignment]
|
|
182
|
+
backend._session.runtime.evaluate = AsyncMock(
|
|
183
|
+
return_value={"result": {"value": True}}
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
asyncio.run(backend.shadow_click(["my-component", "btn"], auto_wait=False))
|
|
187
|
+
assert "wait" not in call_order
|
|
188
|
+
|
|
189
|
+
def test_cdp_shadow_click_auto_wait_true_calls_wait(self) -> None:
|
|
190
|
+
"""Test that shadow_click with auto_wait=True calls _wait_for_element_in_shadow."""
|
|
191
|
+
backend = _make_cdp_backend()
|
|
192
|
+
call_order: list[str] = []
|
|
193
|
+
|
|
194
|
+
async def _mock_wait(selectors: list[str], timeout_ms: int = 30000) -> None:
|
|
195
|
+
call_order.append("wait")
|
|
196
|
+
|
|
197
|
+
backend._wait_for_element_in_shadow = _mock_wait # type: ignore[assignment]
|
|
198
|
+
backend._session.runtime.evaluate = AsyncMock(
|
|
199
|
+
return_value={"result": {"value": True}}
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
asyncio.run(backend.shadow_click(["my-component", "btn"], auto_wait=True))
|
|
203
|
+
assert "wait" in call_order
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@pytest.mark.unit
|
|
207
|
+
class TestBuildShadowPierceJs:
|
|
208
|
+
"""Tests for _build_shadow_pierce_js helper."""
|
|
209
|
+
|
|
210
|
+
def test_single_selector(self) -> None:
|
|
211
|
+
"""Test JS generation for a single selector (no shadow piercing)."""
|
|
212
|
+
from wavexis.backend.cdp import CDPBackend
|
|
213
|
+
|
|
214
|
+
js = CDPBackend._build_shadow_pierce_js(["#my-element"])
|
|
215
|
+
assert "document.querySelector" in js
|
|
216
|
+
assert "shadowRoot" not in js
|
|
217
|
+
|
|
218
|
+
def test_nested_selectors(self) -> None:
|
|
219
|
+
"""Test JS generation for nested shadow piercing."""
|
|
220
|
+
from wavexis.backend.cdp import CDPBackend
|
|
221
|
+
|
|
222
|
+
js = CDPBackend._build_shadow_pierce_js(["my-component", "inner", "button"])
|
|
223
|
+
assert js.count("shadowRoot") == 4
|
|
224
|
+
assert "inner" in js
|
|
225
|
+
assert "button" in js
|
|
226
|
+
|
|
227
|
+
def test_selector_escaping(self) -> None:
|
|
228
|
+
"""Test that single quotes in selectors are escaped."""
|
|
229
|
+
from wavexis.backend.cdp import CDPBackend
|
|
230
|
+
|
|
231
|
+
js = CDPBackend._build_shadow_pierce_js(["[data-test='foo']"])
|
|
232
|
+
assert "\\'" in js
|
|
@@ -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
|
|
@@ -323,6 +340,45 @@ class AbstractBackend(ABC):
|
|
|
323
340
|
auto_wait: If True, wait for element to be visible before filling.
|
|
324
341
|
"""
|
|
325
342
|
|
|
343
|
+
# ── Shadow DOM ──────────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
@abstractmethod
|
|
346
|
+
async def shadow_eval(
|
|
347
|
+
self, selectors: list[str], expression: str, await_promise: bool = False
|
|
348
|
+
) -> Any:
|
|
349
|
+
"""Evaluate a JavaScript expression inside a shadow DOM tree.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
selectors: List of CSS selectors piercing shadow boundaries.
|
|
353
|
+
selectors[0] is in the main document, selectors[1] in
|
|
354
|
+
selectors[0].shadowRoot, and so on.
|
|
355
|
+
expression: JavaScript expression to evaluate in the shadow context.
|
|
356
|
+
await_promise: Whether to await a returned Promise.
|
|
357
|
+
"""
|
|
358
|
+
|
|
359
|
+
@abstractmethod
|
|
360
|
+
async def shadow_click(
|
|
361
|
+
self, selectors: list[str], auto_wait: bool = True
|
|
362
|
+
) -> None:
|
|
363
|
+
"""Click an element inside a shadow DOM tree.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
selectors: List of CSS selectors piercing shadow boundaries.
|
|
367
|
+
auto_wait: If True, wait for element to be visible before clicking.
|
|
368
|
+
"""
|
|
369
|
+
|
|
370
|
+
@abstractmethod
|
|
371
|
+
async def shadow_fill(
|
|
372
|
+
self, selectors: list[str], value: str, auto_wait: bool = True
|
|
373
|
+
) -> None:
|
|
374
|
+
"""Fill an input element inside a shadow DOM tree.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
selectors: List of CSS selectors piercing shadow boundaries.
|
|
378
|
+
value: Value to set in the input field.
|
|
379
|
+
auto_wait: If True, wait for element to be visible before filling.
|
|
380
|
+
"""
|
|
381
|
+
|
|
326
382
|
# ── Network advanced ───────────────────────────────────
|
|
327
383
|
|
|
328
384
|
@abstractmethod
|