ai-dev-browser 0.8.2__tar.gz → 0.8.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.
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/PKG-INFO +1 -1
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/_cli.py +87 -26
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/_tab.py +4 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/_transport.py +30 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/ax.py +8 -2
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/elements.py +63 -9
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/page.py +133 -13
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser.egg-info/PKG-INFO +1 -1
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/.github/workflows/ci.yml +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/.github/workflows/publish.yml +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/.gitignore +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/.gitmodules +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/.pre-commit-config.yaml +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/.secrets.baseline +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/LICENSE +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/README.md +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/__init__.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/_version.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/__init__.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/accessibility.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/animation.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/audits.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/autofill.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/background_service.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/bluetooth_emulation.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/browser.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/cache_storage.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/cast.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/console.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/crash_report_context.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/css.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/debugger.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/device_access.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/device_orientation.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/dom.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/dom_debugger.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/dom_snapshot.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/dom_storage.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/emulation.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/event_breakpoints.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/extensions.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/fed_cm.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/fetch.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/file_system.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/headless_experimental.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/heap_profiler.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/indexed_db.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/input_.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/inspector.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/io.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/layer_tree.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/log.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/media.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/memory.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/network.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/overlay.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/page.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/performance.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/performance_timeline.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/preload.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/profiler.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/pwa.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/py.typed +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/runtime.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/schema.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/security.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/service_worker.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/smart_card_emulation.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/storage.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/system_info.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/target.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/tethering.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/tracing.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/util.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/web_audio.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/web_authn.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/web_mcp.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/__init__.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/_case.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/_element.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/browser.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/cdp.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/chrome.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/config.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/connection.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/cookies.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/dialog.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/download.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/human.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/login.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/mouse.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/navigation.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/overlays.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/port.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/process.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/snapshot.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/storage.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/tabs.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/text_match.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/window.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/pool/__init__.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/pool/job.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/pool/persistence.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/pool/pool.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/pool/worker.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/profile.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/py.typed +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/__init__.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/_generate.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/browser_list.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/browser_start.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/browser_stop.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/cdp_send.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/click_by_html_id.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/click_by_ref.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/click_by_text.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/click_by_xpath.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/cookies_list.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/cookies_load.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/cookies_save.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/dialog_respond.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/download.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/drag_by_ref.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/find_by_html_id.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/find_by_text.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/find_by_xpath.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/focus_by_ref.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/highlight_by_ref.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/hover_by_ref.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/html_by_ref.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/js_evaluate.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/login_interactive.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/mouse_click.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/mouse_drag.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/mouse_move.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/page_discover.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/page_emulate_focus.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/page_goto.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/page_html.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/page_info.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/page_reload.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/page_screenshot.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/page_scroll.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/page_wait_element.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/page_wait_ready.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/page_wait_url.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/screenshot_by_ref.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/select_by_ref.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/storage_get.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/storage_set.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/tab_close.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/tab_list.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/tab_new.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/tab_switch.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/type_by_ref.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/type_by_text.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/upload_by_ref.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/window_set.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser.egg-info/SOURCES.txt +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser.egg-info/dependency_links.txt +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser.egg-info/requires.txt +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser.egg-info/top_level.txt +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/pyproject.toml +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/scripts/sync_cdp.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/setup.cfg +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/__init__.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/conftest.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/integration/__init__.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/integration/scenarios_workspace.json +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/integration/test_dialog_respond.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/integration/test_find_and_interact_workflows.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/integration/test_locator_workflows.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/integration/test_multi_profile_pool.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/integration/test_page_reload.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/integration/test_startup_timeout.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/integration/test_timeout_and_retry.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/integration/test_workspace_workflows.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/unit/test_case.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/unit/test_cli_param_type.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/unit/test_no_acronym_rot.py +0 -0
- {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/uv.lock +0 -0
|
@@ -88,6 +88,48 @@ def _parse_docstring_args(docstring: str) -> dict[str, str]:
|
|
|
88
88
|
return args_section
|
|
89
89
|
|
|
90
90
|
|
|
91
|
+
def _parse_docstring_failure(docstring: str) -> str | None:
|
|
92
|
+
"""Extract the body of the Failure: section from a Google-style docstring.
|
|
93
|
+
|
|
94
|
+
Tool authors write failure-path steering once in the docstring's
|
|
95
|
+
`Failure:` section (parallel to `Args:` / `Returns:`). This parser
|
|
96
|
+
extracts it at wrap time; `wrap_core` then auto-injects it as the
|
|
97
|
+
`hint` field on any failure return — SSOT with auto-split across
|
|
98
|
+
`--help` (full docstring, reference surface) and failure `hint`
|
|
99
|
+
(just this section, runtime steering surface).
|
|
100
|
+
|
|
101
|
+
Rationale: guidance about "what to do if this tool fails" placed
|
|
102
|
+
only in the docstring reaches the LLM at most via `--help`, which
|
|
103
|
+
is an on-demand call the LLM rarely makes at invocation-failure
|
|
104
|
+
time. Routing the same authored text into the failure return is
|
|
105
|
+
the only channel with 100% reach at the moment the LLM needs to
|
|
106
|
+
recover.
|
|
107
|
+
|
|
108
|
+
Returns the flat concatenated hint text, or None if no Failure:
|
|
109
|
+
section is present.
|
|
110
|
+
"""
|
|
111
|
+
if not docstring:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
in_failure = False
|
|
115
|
+
lines: list[str] = []
|
|
116
|
+
for raw in docstring.split("\n"):
|
|
117
|
+
stripped = raw.strip()
|
|
118
|
+
# Google-style section headers are single-word lines ending with ':'
|
|
119
|
+
is_section_header = stripped.endswith(":") and len(stripped.split()) == 1
|
|
120
|
+
if is_section_header:
|
|
121
|
+
if stripped == "Failure:":
|
|
122
|
+
in_failure = True
|
|
123
|
+
continue
|
|
124
|
+
if in_failure:
|
|
125
|
+
break # next section ends the Failure block
|
|
126
|
+
elif in_failure:
|
|
127
|
+
lines.append(stripped)
|
|
128
|
+
|
|
129
|
+
text = " ".join(line for line in lines if line).strip()
|
|
130
|
+
return text or None
|
|
131
|
+
|
|
132
|
+
|
|
91
133
|
def _get_param_type(hint) -> type | Callable[[str], bool]:
|
|
92
134
|
"""Convert type hint to argparse type."""
|
|
93
135
|
import types
|
|
@@ -368,25 +410,38 @@ def wrap_core(core_func: Callable, result_key: str = "success") -> Callable:
|
|
|
368
410
|
element_click = as_cli()(wrap_core(click, "clicked"))
|
|
369
411
|
"""
|
|
370
412
|
|
|
413
|
+
failure_hint = _parse_docstring_failure(core_func.__doc__ or "")
|
|
414
|
+
|
|
371
415
|
@functools.wraps(core_func)
|
|
372
416
|
async def wrapper(*args, **kwargs):
|
|
373
417
|
try:
|
|
374
418
|
result = await core_func(*args, **kwargs)
|
|
375
|
-
if isinstance(result, bool):
|
|
376
|
-
if result:
|
|
377
|
-
return {result_key: True}
|
|
378
|
-
else:
|
|
379
|
-
return {"error": "Operation failed"}
|
|
380
|
-
elif isinstance(result, dict):
|
|
381
|
-
# Filter out non-serializable values for CLI output
|
|
382
|
-
return _filter_dict_for_json(result)
|
|
383
|
-
else:
|
|
384
|
-
return {result_key: result}
|
|
385
419
|
except Exception as e:
|
|
386
420
|
# Verbatim message — Python `repr(e)` and CLI stdout stay in
|
|
387
|
-
# lockstep (cli-args-ssot rule
|
|
421
|
+
# lockstep (cli-args-ssot rule 7: never re-render error text
|
|
388
422
|
# in tool files).
|
|
389
|
-
|
|
423
|
+
out: dict = {"error": str(e)}
|
|
424
|
+
if failure_hint:
|
|
425
|
+
out["hint"] = failure_hint
|
|
426
|
+
return out
|
|
427
|
+
|
|
428
|
+
if isinstance(result, bool):
|
|
429
|
+
if result:
|
|
430
|
+
return {result_key: True}
|
|
431
|
+
out = {"error": "Operation failed"}
|
|
432
|
+
if failure_hint:
|
|
433
|
+
out["hint"] = failure_hint
|
|
434
|
+
return out
|
|
435
|
+
if isinstance(result, dict):
|
|
436
|
+
filtered = _filter_dict_for_json(result)
|
|
437
|
+
# Auto-inject failure hint when the tool reports failure via
|
|
438
|
+
# result_key=False. Pairs with cli-args-ssot Rule 5a: failure
|
|
439
|
+
# steering goes through the return channel (100% reach at
|
|
440
|
+
# invocation time), not docstring (only reaches on --help).
|
|
441
|
+
if filtered.get(result_key) is False and failure_hint:
|
|
442
|
+
filtered.setdefault("hint", failure_hint)
|
|
443
|
+
return filtered
|
|
444
|
+
return {result_key: result}
|
|
390
445
|
|
|
391
446
|
return wrapper
|
|
392
447
|
|
|
@@ -411,24 +466,30 @@ def wrap_core_sync(core_func: Callable, result_key: str = "success") -> Callable
|
|
|
411
466
|
browser_start = as_cli(requires_tab=False)(wrap_core_sync(browser_start, "port"))
|
|
412
467
|
"""
|
|
413
468
|
|
|
469
|
+
failure_hint = _parse_docstring_failure(core_func.__doc__ or "")
|
|
470
|
+
|
|
414
471
|
@functools.wraps(core_func)
|
|
415
472
|
def wrapper(*args, **kwargs):
|
|
416
473
|
try:
|
|
417
474
|
result = core_func(*args, **kwargs)
|
|
418
|
-
if isinstance(result, bool):
|
|
419
|
-
if result:
|
|
420
|
-
return {result_key: True}
|
|
421
|
-
else:
|
|
422
|
-
return {"error": "Operation failed"}
|
|
423
|
-
elif isinstance(result, dict):
|
|
424
|
-
# Filter out non-serializable values for CLI output
|
|
425
|
-
return _filter_dict_for_json(result)
|
|
426
|
-
else:
|
|
427
|
-
return {result_key: result}
|
|
428
475
|
except Exception as e:
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
return
|
|
476
|
+
out: dict = {"error": str(e)}
|
|
477
|
+
if failure_hint:
|
|
478
|
+
out["hint"] = failure_hint
|
|
479
|
+
return out
|
|
480
|
+
|
|
481
|
+
if isinstance(result, bool):
|
|
482
|
+
if result:
|
|
483
|
+
return {result_key: True}
|
|
484
|
+
out = {"error": "Operation failed"}
|
|
485
|
+
if failure_hint:
|
|
486
|
+
out["hint"] = failure_hint
|
|
487
|
+
return out
|
|
488
|
+
if isinstance(result, dict):
|
|
489
|
+
filtered = _filter_dict_for_json(result)
|
|
490
|
+
if filtered.get(result_key) is False and failure_hint:
|
|
491
|
+
filtered.setdefault("hint", failure_hint)
|
|
492
|
+
return filtered
|
|
493
|
+
return {result_key: result}
|
|
433
494
|
|
|
434
495
|
return wrapper
|
|
@@ -195,6 +195,10 @@ class Tab:
|
|
|
195
195
|
"""Register a CDP event handler."""
|
|
196
196
|
self._connection.add_handler(event_type, handler)
|
|
197
197
|
|
|
198
|
+
def remove_handler(self, event_type, handler) -> bool:
|
|
199
|
+
"""Unregister a CDP event handler (pair with add_handler)."""
|
|
200
|
+
return self._connection.remove_handler(event_type, handler)
|
|
201
|
+
|
|
198
202
|
# =========================================================================
|
|
199
203
|
# JavaScript evaluation
|
|
200
204
|
# =========================================================================
|
|
@@ -242,6 +242,36 @@ class CDPConnection:
|
|
|
242
242
|
else:
|
|
243
243
|
self.handlers[evt].append(handler)
|
|
244
244
|
|
|
245
|
+
def remove_handler(
|
|
246
|
+
self,
|
|
247
|
+
event_type: type,
|
|
248
|
+
handler: Callable,
|
|
249
|
+
) -> bool:
|
|
250
|
+
"""Unregister a previously-added event handler.
|
|
251
|
+
|
|
252
|
+
Counterpart to add_handler for short-lived captures (e.g.
|
|
253
|
+
`js_evaluate` subscribing to `Runtime.consoleAPICalled` for the
|
|
254
|
+
duration of a single eval then detaching). Without this, each
|
|
255
|
+
call leaks the handler closure and its captured state.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
event_type: CDP event class the handler was registered on.
|
|
259
|
+
Only single-class removal is supported; to undo a
|
|
260
|
+
module-level add_handler, unregister per-class.
|
|
261
|
+
handler: The exact callable passed to add_handler.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
True if removed, False if not found.
|
|
265
|
+
"""
|
|
266
|
+
bucket = self.handlers.get(event_type)
|
|
267
|
+
if not bucket:
|
|
268
|
+
return False
|
|
269
|
+
try:
|
|
270
|
+
bucket.remove(handler)
|
|
271
|
+
return True
|
|
272
|
+
except ValueError:
|
|
273
|
+
return False
|
|
274
|
+
|
|
245
275
|
# Domains that should never be removed by _register_handlers cleanup.
|
|
246
276
|
# These are either always-on (target, storage, input_) or essential
|
|
247
277
|
# for core operations (page, dom) — enabled by Tab._ensure_connected().
|
|
@@ -309,8 +309,14 @@ async def click_by_ref(
|
|
|
309
309
|
dict with clicked status, element info, and navigation feedback:
|
|
310
310
|
`{clicked, ref, role, name, url_before, url_after, title_after, navigated}`.
|
|
311
311
|
`navigated=True` means the top-level URL changed after the click
|
|
312
|
-
(SPA route change or full page load).
|
|
313
|
-
|
|
312
|
+
(SPA route change or full page load).
|
|
313
|
+
|
|
314
|
+
Failure:
|
|
315
|
+
Ref is stale (page navigated or element was removed between
|
|
316
|
+
the `page_discover` / `find_by_text` call that returned it
|
|
317
|
+
and this click). Re-run `page_discover` or `find_by_text` to
|
|
318
|
+
get a fresh ref, or use a stable locator — `click_by_html_id`
|
|
319
|
+
/ `click_by_xpath` / `click_by_text`.
|
|
314
320
|
|
|
315
321
|
Example:
|
|
316
322
|
# First page_discover elements
|
|
@@ -432,6 +432,15 @@ async def page_wait_element(
|
|
|
432
432
|
|
|
433
433
|
Returns:
|
|
434
434
|
dict with found, elapsed, message
|
|
435
|
+
|
|
436
|
+
Failure:
|
|
437
|
+
Element didn't appear within `timeout` seconds. Try a longer
|
|
438
|
+
timeout if the page is slow; a broader locator (partial text
|
|
439
|
+
instead of exact, less-specific CSS selector); or confirm the
|
|
440
|
+
element is expected on this page via `page_discover`. For
|
|
441
|
+
iframe-embedded targets with a text locator, note that
|
|
442
|
+
text-based wait scans the top frame only — use
|
|
443
|
+
`find_by_text` + `click_by_ref` pattern instead of waiting.
|
|
435
444
|
"""
|
|
436
445
|
result = await _wait_for_element(tab, text=text, selector=selector, timeout=timeout)
|
|
437
446
|
|
|
@@ -464,13 +473,6 @@ async def click_by_text(
|
|
|
464
473
|
(form-validation error rendering, captcha pixels for OCR, final
|
|
465
474
|
result view for the user).
|
|
466
475
|
|
|
467
|
-
**Top-frame only.** Unlike `click_by_html_id` / `click_by_xpath`
|
|
468
|
-
(which recurse through `window.frames`) and `find_by_text` (which
|
|
469
|
-
scans the full AX tree), this goes through CDP `DOM.performSearch`
|
|
470
|
-
and won't find text inside an iframe (nav menus in `<iframe>`,
|
|
471
|
-
embedded widgets). For those, use `find_by_text` → `click_by_ref`
|
|
472
|
-
or `click_by_xpath` / `click_by_html_id`.
|
|
473
|
-
|
|
474
476
|
Prefer when text is unique / unambiguous and top-frame. For elements
|
|
475
477
|
you already identified via `page_discover`, use `click_by_ref`.
|
|
476
478
|
|
|
@@ -483,8 +485,17 @@ async def click_by_text(
|
|
|
483
485
|
Returns:
|
|
484
486
|
dict with clicked, text, url_before, url_after, title_after, navigated.
|
|
485
487
|
`navigated=True` means the top-level URL changed after the click
|
|
486
|
-
(SPA route change or full page load).
|
|
487
|
-
|
|
488
|
+
(SPA route change or full page load).
|
|
489
|
+
|
|
490
|
+
Failure:
|
|
491
|
+
Not found in the top frame. This tool uses CDP
|
|
492
|
+
`DOM.performSearch` on the main document only — it won't find
|
|
493
|
+
text inside iframes (nav menus in `<iframe>`, embedded
|
|
494
|
+
widgets). Try `find_by_text` → `click_by_ref` (scans
|
|
495
|
+
same-origin iframes and falls back to non-interactable AX
|
|
496
|
+
nodes like `<div onclick>`), or `click_by_xpath` /
|
|
497
|
+
`click_by_html_id` which recurse through `window.frames`
|
|
498
|
+
natively.
|
|
488
499
|
|
|
489
500
|
Example:
|
|
490
501
|
click_by_text("登录")
|
|
@@ -551,6 +562,15 @@ async def find_by_text(
|
|
|
551
562
|
Returns:
|
|
552
563
|
dict: `{found: True, ref, role, name, x, y, ...}` on hit,
|
|
553
564
|
`{found: False, text}` otherwise.
|
|
565
|
+
|
|
566
|
+
Failure:
|
|
567
|
+
Text not found in main frame or any same-origin iframe, in
|
|
568
|
+
either the interactable or fallback tier. Check spelling /
|
|
569
|
+
case (match is case-insensitive substring); try a shorter
|
|
570
|
+
substring; or switch locator — `find_by_html_id` /
|
|
571
|
+
`find_by_xpath` if a DOM-level locator is known. For a broad
|
|
572
|
+
survey of what's on the page, run `page_discover` without a
|
|
573
|
+
text filter. Cross-origin iframes are not scanned.
|
|
554
574
|
"""
|
|
555
575
|
from .snapshot import page_discover
|
|
556
576
|
|
|
@@ -606,6 +626,13 @@ async def type_by_text(
|
|
|
606
626
|
Returns:
|
|
607
627
|
dict with typed status
|
|
608
628
|
|
|
629
|
+
Failure:
|
|
630
|
+
Input with this accessible name not found in the top frame.
|
|
631
|
+
For iframe-embedded inputs, use `find_by_text` (AX-tree scan,
|
|
632
|
+
iframe-aware) to get a ref, then `type_by_ref`. If the input
|
|
633
|
+
lacks an accessible name, locate by html id with
|
|
634
|
+
`find_by_html_id` → `type_by_ref`.
|
|
635
|
+
|
|
609
636
|
Example:
|
|
610
637
|
type_by_text(name="用户名", text="myusername")
|
|
611
638
|
type_by_text(name="Search", text="query", clear=True)
|
|
@@ -822,6 +849,12 @@ async def find_by_html_id(tab: Tab, html_id: str) -> dict:
|
|
|
822
849
|
dict: `{found: true, tag, text, visible, attrs}` on hit,
|
|
823
850
|
`{found: false}` otherwise.
|
|
824
851
|
|
|
852
|
+
Failure:
|
|
853
|
+
No element with this id in any same-origin frame. Use
|
|
854
|
+
`page_discover` to see the ids actually present, or switch
|
|
855
|
+
locator — `find_by_text` if you know the visible label,
|
|
856
|
+
`find_by_xpath` for attribute-predicate / positional lookups.
|
|
857
|
+
|
|
825
858
|
Example:
|
|
826
859
|
result = await find_by_html_id(tab, "submit-btn")
|
|
827
860
|
if result["found"] and result["visible"]:
|
|
@@ -869,6 +902,13 @@ async def click_by_html_id(tab: Tab, html_id: str) -> dict:
|
|
|
869
902
|
Returns:
|
|
870
903
|
dict: `{clicked, html_id, url_before, url_after, title_after, navigated, error?}`.
|
|
871
904
|
`navigated=True` means the top-level URL changed after the click.
|
|
905
|
+
|
|
906
|
+
Failure:
|
|
907
|
+
No element with this id found in any same-origin frame. Run
|
|
908
|
+
`find_by_html_id` first to verify existence and get attrs, or
|
|
909
|
+
switch locator — `click_by_text` (top frame only, use
|
|
910
|
+
`find_by_text` → `click_by_ref` for iframe targets) or
|
|
911
|
+
`click_by_xpath` for attribute / positional predicates.
|
|
872
912
|
"""
|
|
873
913
|
url_before_state = await _capture_page_state(tab)
|
|
874
914
|
url_before = url_before_state.get("url", "")
|
|
@@ -927,6 +967,14 @@ async def find_by_xpath(tab: Tab, xpath: str) -> dict:
|
|
|
927
967
|
Returns:
|
|
928
968
|
dict: `{found: true, tag, text, visible, attrs}` on hit,
|
|
929
969
|
`{found: false}` otherwise.
|
|
970
|
+
|
|
971
|
+
Failure:
|
|
972
|
+
XPath returned no match in any same-origin frame. Shorten the
|
|
973
|
+
expression (e.g. `//button` instead of
|
|
974
|
+
`//button[@data-role='x']`) to verify the broader target
|
|
975
|
+
exists, or switch locator — `find_by_text` by visible label,
|
|
976
|
+
`find_by_html_id` if an id is known, or `page_discover` for a
|
|
977
|
+
structural survey.
|
|
930
978
|
"""
|
|
931
979
|
expr = """
|
|
932
980
|
(function(xpath) {
|
|
@@ -975,6 +1023,12 @@ async def click_by_xpath(tab: Tab, xpath: str) -> dict:
|
|
|
975
1023
|
|
|
976
1024
|
Returns:
|
|
977
1025
|
dict: `{clicked, xpath, url_before, url_after, title_after, navigated, error?}`.
|
|
1026
|
+
|
|
1027
|
+
Failure:
|
|
1028
|
+
XPath returned no match in any same-origin frame. Verify with
|
|
1029
|
+
`find_by_xpath` first (returns the matched element's attrs
|
|
1030
|
+
without clicking), or switch locator — `click_by_text` /
|
|
1031
|
+
`click_by_html_id`, or `page_discover` → `click_by_ref`.
|
|
978
1032
|
"""
|
|
979
1033
|
url_before_state = await _capture_page_state(tab)
|
|
980
1034
|
url_before = url_before_state.get("url", "")
|
|
@@ -62,12 +62,14 @@ def read_screenshot_metadata(path: str) -> dict:
|
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
async def js_evaluate(tab: Tab, expression: str) -> dict:
|
|
65
|
-
"""Use when: NO specific tool fits —
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
"""Use when: NO specific tool fits — the last-resort raw JS escape
|
|
66
|
+
hatch. Works equally for **read** expressions (`document.title`,
|
|
67
|
+
`innerText`) and **side-effect** expressions (`.click()`, `.submit()`,
|
|
68
|
+
DOM mutations) — the return dict captures *everything* observable
|
|
69
|
+
during the eval, not just the expression value.
|
|
68
70
|
|
|
69
|
-
Before picking this: the locate+act combinations below cover almost
|
|
70
|
-
intents atomically
|
|
71
|
+
Before picking this: the locate+act combinations below cover almost
|
|
72
|
+
all intents atomically and are more specific:
|
|
71
73
|
|
|
72
74
|
- Locate + act by html id: `click_by_html_id` / `find_by_html_id`
|
|
73
75
|
- Locate + act by XPath: `click_by_xpath` / `find_by_xpath`
|
|
@@ -85,18 +87,136 @@ async def js_evaluate(tab: Tab, expression: str) -> dict:
|
|
|
85
87
|
|
|
86
88
|
Args:
|
|
87
89
|
tab: Tab instance
|
|
88
|
-
expression: JavaScript code to execute. Result of last expression
|
|
90
|
+
expression: JavaScript code to execute. Result of last expression
|
|
91
|
+
is returned. `console.log` / `warn` / `error` / `info` output
|
|
92
|
+
during the eval is captured separately.
|
|
89
93
|
|
|
90
94
|
Returns:
|
|
91
|
-
dict with
|
|
95
|
+
dict with:
|
|
96
|
+
|
|
97
|
+
- `result`: the expression's evaluated value (may be `None` if
|
|
98
|
+
the expression is a void side-effect like `.click()`)
|
|
99
|
+
- `console`: list of `{level, text}` entries emitted during
|
|
100
|
+
the eval, if any (only present when non-empty)
|
|
101
|
+
- `url_before` / `url_after` / `title_after` / `navigated`:
|
|
102
|
+
top-level navigation observables, same shape as
|
|
103
|
+
`click_by_*`. `navigated=True` iff URL changed.
|
|
104
|
+
- `error`: `{message, description, stack}` if the expression
|
|
105
|
+
threw; absent on success.
|
|
106
|
+
|
|
107
|
+
Example:
|
|
108
|
+
# Read — result field carries the answer
|
|
109
|
+
await js_evaluate(tab, "document.title")
|
|
110
|
+
# → {"result": "...", "url_before": ..., "navigated": False}
|
|
111
|
+
|
|
112
|
+
# Side effect — navigated/url_after + console confirm what happened
|
|
113
|
+
await js_evaluate(tab, "document.querySelector('#login').click()")
|
|
114
|
+
# → {"result": None, "url_before": "/login", "url_after": "/home",
|
|
115
|
+
# "navigated": True, "title_after": "Home"}
|
|
116
|
+
|
|
117
|
+
# Debugging — console lines captured
|
|
118
|
+
await js_evaluate(tab, "console.log('x=', x); x + 1")
|
|
119
|
+
# → {"result": 2, "console": [{"level": "log", "text": "x= 1"}], ...}
|
|
92
120
|
"""
|
|
93
|
-
|
|
94
|
-
|
|
121
|
+
from ai_dev_browser.cdp import runtime
|
|
122
|
+
|
|
123
|
+
from .elements import _POST_CLICK_NAV_DELAY, _capture_page_state
|
|
124
|
+
|
|
125
|
+
before = await _capture_page_state(tab)
|
|
126
|
+
|
|
127
|
+
console_msgs: list[dict] = []
|
|
128
|
+
|
|
129
|
+
def _stringify_arg(arg) -> str:
|
|
130
|
+
# Runtime.RemoteObject: value (primitive) / description / unserializable_value
|
|
131
|
+
val = getattr(arg, "value", None)
|
|
132
|
+
if val is not None:
|
|
133
|
+
try:
|
|
134
|
+
return json.dumps(val, ensure_ascii=False)
|
|
135
|
+
except (TypeError, ValueError):
|
|
136
|
+
return str(val)
|
|
137
|
+
desc = getattr(arg, "description", None)
|
|
138
|
+
if desc:
|
|
139
|
+
return str(desc)
|
|
140
|
+
unser = getattr(arg, "unserializable_value", None)
|
|
141
|
+
if unser:
|
|
142
|
+
return str(unser)
|
|
143
|
+
return ""
|
|
144
|
+
|
|
145
|
+
def on_console(event):
|
|
146
|
+
level = getattr(event, "type_", "log")
|
|
147
|
+
text = " ".join(_stringify_arg(a) for a in event.args)
|
|
148
|
+
console_msgs.append({"level": level, "text": text})
|
|
149
|
+
|
|
150
|
+
# Runtime.enable() is idempotent and required for consoleAPICalled events.
|
|
151
|
+
await tab.send(runtime.enable())
|
|
152
|
+
tab.add_handler(runtime.ConsoleAPICalled, on_console)
|
|
153
|
+
|
|
154
|
+
result_value: object = None
|
|
155
|
+
error_info: dict | None = None
|
|
95
156
|
try:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
157
|
+
raw = await tab.evaluate(expression)
|
|
158
|
+
except Exception as e:
|
|
159
|
+
error_info = {"message": str(e)}
|
|
160
|
+
else:
|
|
161
|
+
# tab.evaluate returns ExceptionDetails on eval-time exceptions
|
|
162
|
+
# (not a Python raise), see _tab.py::evaluate.
|
|
163
|
+
if isinstance(raw, runtime.ExceptionDetails):
|
|
164
|
+
exc = getattr(raw, "exception", None)
|
|
165
|
+
stack = getattr(raw, "stack_trace", None)
|
|
166
|
+
error_info = {
|
|
167
|
+
"message": getattr(raw, "text", "") or "",
|
|
168
|
+
"description": getattr(exc, "description", None) if exc else None,
|
|
169
|
+
"stack": [
|
|
170
|
+
{
|
|
171
|
+
"function": f.function_name,
|
|
172
|
+
"url": f.url,
|
|
173
|
+
"line": f.line_number,
|
|
174
|
+
"column": f.column_number,
|
|
175
|
+
}
|
|
176
|
+
for f in (stack.call_frames if stack else [])
|
|
177
|
+
]
|
|
178
|
+
if stack
|
|
179
|
+
else None,
|
|
180
|
+
}
|
|
181
|
+
else:
|
|
182
|
+
try:
|
|
183
|
+
json.dumps(raw)
|
|
184
|
+
result_value = raw
|
|
185
|
+
except (TypeError, ValueError):
|
|
186
|
+
result_value = str(raw)
|
|
187
|
+
|
|
188
|
+
# Give any navigation triggered by the eval a moment to commit,
|
|
189
|
+
# mirroring click_by_*'s _POST_CLICK_NAV_DELAY so URL snapshots
|
|
190
|
+
# reflect the post-action state.
|
|
191
|
+
import asyncio as _asyncio
|
|
192
|
+
|
|
193
|
+
await _asyncio.sleep(_POST_CLICK_NAV_DELAY)
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
after = await _capture_page_state(tab)
|
|
197
|
+
except Exception:
|
|
198
|
+
# Context destroyed mid-read (full-page nav) — we know URL changed
|
|
199
|
+
after = {"url": None, "title": None}
|
|
200
|
+
|
|
201
|
+
tab.remove_handler(runtime.ConsoleAPICalled, on_console)
|
|
202
|
+
|
|
203
|
+
url_before = before.get("url", "") if isinstance(before, dict) else ""
|
|
204
|
+
url_after = after.get("url") if isinstance(after, dict) else None
|
|
205
|
+
title_after = after.get("title", "") if isinstance(after, dict) else ""
|
|
206
|
+
navigated = bool(url_before) and url_after is not None and url_after != url_before
|
|
207
|
+
|
|
208
|
+
out: dict = {
|
|
209
|
+
"result": result_value,
|
|
210
|
+
"url_before": url_before,
|
|
211
|
+
"url_after": url_after,
|
|
212
|
+
"title_after": title_after,
|
|
213
|
+
"navigated": navigated,
|
|
214
|
+
}
|
|
215
|
+
if console_msgs:
|
|
216
|
+
out["console"] = console_msgs
|
|
217
|
+
if error_info is not None:
|
|
218
|
+
out["error"] = error_info
|
|
219
|
+
return out
|
|
100
220
|
|
|
101
221
|
|
|
102
222
|
async def page_screenshot(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|