Skip to content

Add support to BiDi protocol (firefox)#396

Open
thalissonvs wants to merge 57 commits into
mainfrom
feat/firefox
Open

Add support to BiDi protocol (firefox)#396
thalissonvs wants to merge 57 commits into
mainfrom
feat/firefox

Conversation

@thalissonvs
Copy link
Copy Markdown
Member

No description provided.

thalissonvs and others added 28 commits March 24, 2026 14:14
…c providers

Add LLM-based extraction support alongside the existing CSS selector strategy.
Fields with only a description (no selector) can now be extracted via LLM providers.

- Add ExtractionStrategy enum (CSS, LLM, AUTO) and LLMProvider protocol
- Add OpenAI and Anthropic built-in providers using aiohttp (no SDK dependency)
- Add clean_html utility for HTML-to-clean-HTML conversion (boilerplate removal)
- Add html_to_markdown converter with Readability-inspired heuristics
- Add tab.llm_provider and tab.extraction_strategy convenience properties
- Add validate_llm_provider for fail-fast when LLM is needed but not configured
- Add LLMExtractionFailed and LLMProviderNotConfigured exceptions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… protocol base

Move all CDP protocol definitions from protocol/ to protocol/cdp/ to
make room for the new BiDi protocol subpackage. Add initial BiDi base
types and session/browser module definitions for Firefox support.
…nection handler

Split ConnectionHandler into BaseConnectionHandler (ABC) with shared
WebSocket mechanics and CDPConnectionHandler / BiDiConnectionHandler
implementations. Extract CDP-specific event tracking from EventsManager
into CDPEventTracker and BiDiEventTracker classes.
…hromium

Rename interfaces.py to protocols.py with OptionsProtocol and
BrowserOptionsManagerProtocol using typing.Protocol instead of ABC.
Move options.py and tab.py into chromium/ subpackage. Merge
ChromiumOptionsManager into chromium/options.py. Use OptionsProtocol
in managers for dependency inversion.
Add FirefoxBrowser base class and Firefox implementation using
BiDi connection handler. Same public API as Chromium Browser with
typed command execution. Unsupported CDP-specific methods raise
UnsupportedOperation.
…ic methods

Add pydoll/protocol/types.py with protocol-agnostic types (Header,
Cookie, CookieParam, WindowBounds, BrowserVersion, DownloadBehavior,
RequestMethod). Privatize CDP-specific methods in Browser and Tab:
enable/disable domain events, get_targets, get_tab_by_target,
get_window_id_for_target, get_window_id. Update get_version to return
BrowserVersion, set_window_bounds to accept WindowBounds, remove
events_enabled from set_download_behavior. Add plan.md for unified API
migration.
Update FirefoxBrowser to use BrowserVersion, Cookie, CookieParam,
DownloadBehavior, WindowBounds from pydoll.protocol.types. Convert
between generic and BiDi-specific formats internally. Remove
CDP-specific stubs, keep get_opened_tabs as NotImplementedError
until BiDi Tab is built.
…requests

Add pydoll/protocol/events.py with Event enum and CDP/BiDi mapping.
Add InterceptedRequest for protocol-agnostic request interception.
Implement intercept_requests()/remove_intercept() in both Chrome and
Firefox browsers. Update on() to accept Event enum with auto-enable
for CDP domains. Fix BiDi Command type aliases to wrap with
CommandResponse for correct typing.
…s, and cookies

Add BiDiTab class using shared BiDi connection with context ID.
Implements go_to, refresh, close, bring_to_front, title, current_url,
page_source, take_screenshot, print_to_pdf, execute_script, cookies,
dialog handling, on/remove_callback, and network_logs. Update
FirefoxBrowser with start returning tab, get_opened_tabs, and new_tab.
Convert BiDi remote values (number, string, boolean, null, undefined,
array, object, map, set, date, regexp, bigint) to native Python types
instead of returning raw protocol dicts.
… implementation

Extract CDP-specific element finding logic into CDPFindElementsMixin.
FindElementsMixin now contains only protocol-agnostic code (find, query,
retry/timeout, selector parsing). Add WebElementProtocol in
elements/protocols.py. Move web_element.py and shadow_root.py into
elements/cdp/ subpackage. Privatize find_or_wait_element.
Implement BiDi element finding using browsingContext.locateNodes with
typed Locator variants (CssLocator, XPathLocator). BiDiWebElement uses
sharedId for element references and script.callFunction for all
interactions (click, text, visibility, etc). BiDiTab now inherits
BidiFindElementsMixin for find()/query() support.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 8, 2026

Important

Review skipped

Too many files!

This PR contains 271 files, which is 121 over the limit of 150.

To get a review, narrow the scope:
• coderabbit review --type committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dccdfb4b-d158-45bf-ba0f-31e8dfc648e8

📥 Commits

Reviewing files that changed from the base of the PR and between 59330ab and b44b82b.

📒 Files selected for processing (271)
  • .gitignore
  • pydoll/browser/__init__.py
  • pydoll/browser/_conformance.py
  • pydoll/browser/chromium/_cookies.py
  • pydoll/browser/chromium/base.py
  • pydoll/browser/chromium/chrome.py
  • pydoll/browser/chromium/edge.py
  • pydoll/browser/chromium/options.py
  • pydoll/browser/chromium/tab.py
  • pydoll/browser/firefox/__init__.py
  • pydoll/browser/firefox/base.py
  • pydoll/browser/firefox/firefox.py
  • pydoll/browser/firefox/options.py
  • pydoll/browser/firefox/tab.py
  • pydoll/browser/intercepted_request.py
  • pydoll/browser/interfaces.py
  • pydoll/browser/managers/__init__.py
  • pydoll/browser/managers/browser_options_manager.py
  • pydoll/browser/managers/proxy_manager.py
  • pydoll/browser/protocols.py
  • pydoll/browser/requests/__init__.py
  • pydoll/browser/requests/base.py
  • pydoll/browser/requests/bidi/__init__.py
  • pydoll/browser/requests/bidi/har_recorder.py
  • pydoll/browser/requests/bidi/request.py
  • pydoll/browser/requests/cdp/__init__.py
  • pydoll/browser/requests/cdp/har_recorder.py
  • pydoll/browser/requests/cdp/request.py
  • pydoll/browser/requests/har.py
  • pydoll/browser/requests/har_recorder.py
  • pydoll/browser/requests/request.py
  • pydoll/browser/requests/response.py
  • pydoll/commands/__init__.py
  • pydoll/commands/bidi/__init__.py
  • pydoll/commands/bidi/browser_commands.py
  • pydoll/commands/bidi/browsing_context_commands.py
  • pydoll/commands/bidi/emulation_commands.py
  • pydoll/commands/bidi/input_commands.py
  • pydoll/commands/bidi/network_commands.py
  • pydoll/commands/bidi/permissions_commands.py
  • pydoll/commands/bidi/script_commands.py
  • pydoll/commands/bidi/session_commands.py
  • pydoll/commands/bidi/storage_commands.py
  • pydoll/commands/bidi/web_extension_commands.py
  • pydoll/commands/cdp/__init__.py
  • pydoll/commands/cdp/accessibility_commands.py
  • pydoll/commands/cdp/browser_commands.py
  • pydoll/commands/cdp/dom_commands.py
  • pydoll/commands/cdp/emulation_commands.py
  • pydoll/commands/cdp/fetch_commands.py
  • pydoll/commands/cdp/input_commands.py
  • pydoll/commands/cdp/network_commands.py
  • pydoll/commands/cdp/page_commands.py
  • pydoll/commands/cdp/runtime_commands.py
  • pydoll/commands/cdp/storage_commands.py
  • pydoll/commands/cdp/target_commands.py
  • pydoll/connection/base_connection_handler.py
  • pydoll/connection/bidi_connection_handler.py
  • pydoll/connection/connection_handler.py
  • pydoll/connection/managers/commands_manager.py
  • pydoll/connection/managers/event_trackers.py
  • pydoll/connection/managers/events_manager.py
  • pydoll/constants.py
  • pydoll/elements/bidi/__init__.py
  • pydoll/elements/bidi/shadow_root.py
  • pydoll/elements/bidi/web_element.py
  • pydoll/elements/cdp/__init__.py
  • pydoll/elements/cdp/shadow_root.py
  • pydoll/elements/cdp/web_element.py
  • pydoll/elements/mixins/bidi_find_elements_mixin.py
  • pydoll/elements/mixins/cdp_find_elements_mixin.py
  • pydoll/elements/mixins/find_elements_mixin.py
  • pydoll/elements/protocols.py
  • pydoll/exceptions.py
  • pydoll/extractor/engine.py
  • pydoll/interactions/iframe.py
  • pydoll/interactions/keyboard.py
  • pydoll/interactions/mouse.py
  • pydoll/interactions/scroll.py
  • pydoll/protocol/base.py
  • pydoll/protocol/bidi/__init__.py
  • pydoll/protocol/bidi/base.py
  • pydoll/protocol/bidi/browser/__init__.py
  • pydoll/protocol/bidi/browser/methods.py
  • pydoll/protocol/bidi/browser/types.py
  • pydoll/protocol/bidi/browsing_context/__init__.py
  • pydoll/protocol/bidi/browsing_context/events.py
  • pydoll/protocol/bidi/browsing_context/methods.py
  • pydoll/protocol/bidi/browsing_context/types.py
  • pydoll/protocol/bidi/emulation/__init__.py
  • pydoll/protocol/bidi/emulation/methods.py
  • pydoll/protocol/bidi/emulation/types.py
  • pydoll/protocol/bidi/input/__init__.py
  • pydoll/protocol/bidi/input/events.py
  • pydoll/protocol/bidi/input/methods.py
  • pydoll/protocol/bidi/input/types.py
  • pydoll/protocol/bidi/log/__init__.py
  • pydoll/protocol/bidi/log/events.py
  • pydoll/protocol/bidi/log/types.py
  • pydoll/protocol/bidi/network/__init__.py
  • pydoll/protocol/bidi/network/events.py
  • pydoll/protocol/bidi/network/methods.py
  • pydoll/protocol/bidi/network/types.py
  • pydoll/protocol/bidi/permissions/__init__.py
  • pydoll/protocol/bidi/permissions/methods.py
  • pydoll/protocol/bidi/permissions/types.py
  • pydoll/protocol/bidi/script/__init__.py
  • pydoll/protocol/bidi/script/events.py
  • pydoll/protocol/bidi/script/methods.py
  • pydoll/protocol/bidi/script/types.py
  • pydoll/protocol/bidi/session/__init__.py
  • pydoll/protocol/bidi/session/methods.py
  • pydoll/protocol/bidi/session/types.py
  • pydoll/protocol/bidi/storage/__init__.py
  • pydoll/protocol/bidi/storage/methods.py
  • pydoll/protocol/bidi/storage/types.py
  • pydoll/protocol/bidi/web_extension/__init__.py
  • pydoll/protocol/bidi/web_extension/methods.py
  • pydoll/protocol/bidi/web_extension/types.py
  • pydoll/protocol/cdp/__init__.py
  • pydoll/protocol/cdp/accessibility/__init__.py
  • pydoll/protocol/cdp/accessibility/events.py
  • pydoll/protocol/cdp/accessibility/methods.py
  • pydoll/protocol/cdp/accessibility/types.py
  • pydoll/protocol/cdp/base.py
  • pydoll/protocol/cdp/browser/__init__.py
  • pydoll/protocol/cdp/browser/events.py
  • pydoll/protocol/cdp/browser/methods.py
  • pydoll/protocol/cdp/browser/types.py
  • pydoll/protocol/cdp/debugger/types.py
  • pydoll/protocol/cdp/dom/__init__.py
  • pydoll/protocol/cdp/dom/events.py
  • pydoll/protocol/cdp/dom/methods.py
  • pydoll/protocol/cdp/dom/types.py
  • pydoll/protocol/cdp/emulation/__init__.py
  • pydoll/protocol/cdp/emulation/methods.py
  • pydoll/protocol/cdp/emulation/types.py
  • pydoll/protocol/cdp/fetch/__init__.py
  • pydoll/protocol/cdp/fetch/events.py
  • pydoll/protocol/cdp/fetch/methods.py
  • pydoll/protocol/cdp/fetch/types.py
  • pydoll/protocol/cdp/input/__init__.py
  • pydoll/protocol/cdp/input/events.py
  • pydoll/protocol/cdp/input/methods.py
  • pydoll/protocol/cdp/input/types.py
  • pydoll/protocol/cdp/io/types.py
  • pydoll/protocol/cdp/network/__init__.py
  • pydoll/protocol/cdp/network/events.py
  • pydoll/protocol/cdp/network/methods.py
  • pydoll/protocol/cdp/network/types.py
  • pydoll/protocol/cdp/page/__init__.py
  • pydoll/protocol/cdp/page/events.py
  • pydoll/protocol/cdp/page/methods.py
  • pydoll/protocol/cdp/page/types.py
  • pydoll/protocol/cdp/runtime/__init__.py
  • pydoll/protocol/cdp/runtime/events.py
  • pydoll/protocol/cdp/runtime/methods.py
  • pydoll/protocol/cdp/runtime/types.py
  • pydoll/protocol/cdp/security/types.py
  • pydoll/protocol/cdp/storage/__init__.py
  • pydoll/protocol/cdp/storage/events.py
  • pydoll/protocol/cdp/storage/methods.py
  • pydoll/protocol/cdp/storage/types.py
  • pydoll/protocol/cdp/target/__init__.py
  • pydoll/protocol/cdp/target/events.py
  • pydoll/protocol/cdp/target/methods.py
  • pydoll/protocol/cdp/target/types.py
  • pydoll/protocol/events.py
  • pydoll/protocol/har_types.py
  • pydoll/protocol/types.py
  • pydoll/utils/bundle.py
  • pydoll/utils/socks5_proxy_forwarder.py
  • pydoll/utils/user_agent_parser.py
  • pyproject.toml
  • tests/_waits.py
  • tests/bidi/integration/conftest.py
  • tests/bidi/integration/test_bidi_browser_integration.py
  • tests/bidi/integration/test_bidi_download_integration.py
  • tests/bidi/integration/test_bidi_extractor_integration.py
  • tests/bidi/integration/test_bidi_har_integration.py
  • tests/bidi/integration/test_bidi_iframe_integration.py
  • tests/bidi/integration/test_bidi_interactions_integration.py
  • tests/bidi/integration/test_bidi_network_introspection_integration.py
  • tests/bidi/integration/test_bidi_relations_integration.py
  • tests/bidi/integration/test_bidi_request_integration.py
  • tests/bidi/integration/test_bidi_shadow_dom_integration.py
  • tests/bidi/integration/test_bidi_upload_integration.py
  • tests/bidi/integration/test_bidi_web_element_integration.py
  • tests/bidi/unit/conftest.py
  • tests/bidi/unit/test_bidi_browser_commands.py
  • tests/bidi/unit/test_bidi_har_recorder.py
  • tests/bidi/unit/test_bidi_keyboard.py
  • tests/bidi/unit/test_bidi_request.py
  • tests/bidi/unit/test_bidi_shadow_root.py
  • tests/bidi/unit/test_bidi_tab_commands.py
  • tests/bidi/unit/test_bidi_tab_features.py
  • tests/bidi/unit/test_bidi_web_element.py
  • tests/bidi/unit/test_firefox_options.py
  • tests/cdp/integration/conftest.py
  • tests/cdp/integration/test_browser_integration.py
  • tests/cdp/integration/test_click_nested_integration.py
  • tests/cdp/integration/test_connection_handler.py
  • tests/cdp/integration/test_core_integration.py
  • tests/cdp/integration/test_extractor_integration.py
  • tests/cdp/integration/test_har_recording_integration.py
  • tests/cdp/integration/test_iframe_integration.py
  • tests/cdp/integration/test_interactions_integration.py
  • tests/cdp/integration/test_nested_oopif_integration.py
  • tests/cdp/integration/test_shadow_root_integration.py
  • tests/cdp/integration/test_tab.py
  • tests/cdp/integration/test_tab_features_integration.py
  • tests/cdp/integration/test_tab_io_integration.py
  • tests/cdp/integration/test_tab_request_integration.py
  • tests/cdp/integration/test_web_element_integration.py
  • tests/cdp/integration/test_worker_user_agent_integration.py
  • tests/cdp/unit/conftest.py
  • tests/cdp/unit/test_browser_commands.py
  • tests/cdp/unit/test_browser_options.py
  • tests/cdp/unit/test_browser_wiring.py
  • tests/cdp/unit/test_bundle.py
  • tests/cdp/unit/test_extractor_helpers.py
  • tests/cdp/unit/test_find_elements_mixin.py
  • tests/cdp/unit/test_har_cookie_parsing.py
  • tests/cdp/unit/test_har_recorder.py
  • tests/cdp/unit/test_interactions.py
  • tests/cdp/unit/test_request_cookie_extraction.py
  • tests/cdp/unit/test_shadow_root.py
  • tests/cdp/unit/test_tab_commands.py
  • tests/cdp/unit/test_web_element.py
  • tests/common/integration/test_socks5_forwarder.py
  • tests/common/unit/test_commands_manager.py
  • tests/common/unit/test_cubic_bezier.py
  • tests/common/unit/test_decorators.py
  • tests/common/unit/test_events_manager.py
  • tests/common/unit/test_interactions_utils.py
  • tests/common/unit/test_keyboard_humanized.py
  • tests/common/unit/test_mouse_humanized.py
  • tests/common/unit/test_proxy_manager.py
  • tests/common/unit/test_scroll_humanized.py
  • tests/common/unit/test_selector_parser.py
  • tests/common/unit/test_socks5_forwarder_unit.py
  • tests/common/unit/test_temp_dir_manager.py
  • tests/common/unit/test_user_agent_parser.py
  • tests/common/unit/test_utils.py
  • tests/conftest.py
  • tests/pages/iframe_features.html
  • tests/pages/oopif/oopif_content.html
  • tests/pages/oopif/oopif_main.html
  • tests/pages/oopif/oopif_nested.html
  • tests/pages/oopif/oopif_shadow_iframe.html
  • tests/pages/plain_text.html
  • tests/pages/shadow_dom_test.html
  • tests/pages/test_children.html
  • tests/pages/test_click_nested.html
  • tests/pages/test_click_nested_iframe_content.html
  • tests/pages/test_core_simple.html
  • tests/pages/test_extractor.html
  • tests/pages/test_frame_content.html
  • tests/pages/test_frameset.html
  • tests/pages/test_har_recording.html
  • tests/pages/test_iframe_content.html
  • tests/pages/test_iframe_nested.html
  • tests/pages/test_iframe_nested_level.html
  • tests/pages/test_iframe_parent_level.html
  • tests/pages/test_iframe_simple.html
  • tests/pages/test_multiple_iframes.html
  • tests/pages/web_element.html
  • tests/pages/worker_ua/dedicated_worker.js
  • tests/pages/worker_ua/main.html
  • tests/pages/worker_ua/service_worker.js
  • tests/unit/test_browser_commands.py

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/firefox

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Brings main's test-suite rewrite (unit/integration split), connection-layer
resilience, User-Agent propagation to worker contexts, and the 2.23.0 bump.

Conflict resolution:
- tests/, config, docs: took main (the new suite supersedes this branch's
  edits to the old mock-heavy tests).
- Source files manually integrated to preserve the BiDi refactor:
  - BaseConnectionHandler: folded in main's resilience (connection lock +
    health check, _teardown_connection, fail_all_pending, malformed-message-
    safe receive loop, queue-based EventsManager start/stop/enqueue) while
    keeping the protocol-agnostic hooks; CDPConnectionHandler stays thin.
  - EventsManager: kept protocol-agnostic (callbacks + queue); network_logs
    and dialog stay in the event trackers so BiDi keeps its own tracking.
    EventTracker.track is now sync for the non-blocking read loop.
  - chromium/base.py: reconciled imports to cdp.* paths and generic
    protocol/types; kept main's worker User-Agent propagation.
Reverts the LLM feature (originally added in 10d10cd) that slipped into
this branch but is out of scope for the Firefox/BiDi work: the LLM/AUTO
extraction strategies, OpenAI/Anthropic providers, the html_to_markdown
helper, and the llm_provider/extraction_strategy hooks on Tab. The CSS
selector-based extractor is kept intact.

Removed: extractor/llm.py, extractor/providers/, utils/html_to_markdown.py.
Restored to CSS-only: extractor/engine.py, __init__.py, exceptions.py, and
Tab.extract()/extract_all() (dropped the llm_provider/strategy params and
the llm_provider/extraction_strategy properties).
…on, escape hatch

- Add BrowserProtocol: portable browser-level contract, generic over the tab type.
- Add generic Permission enum (superset of CDP permissions) and align grant_permissions
  to Sequence[Permission] on both browsers. Firefox/BiDi maps the supported subset to W3C
  names and warns on the rest; add BiDi permissions module + reset_permissions.
- Add execute_protocol_command(command) escape hatch on Browser and Tab (CDP and BiDi),
  outside the common contract and typed per protocol; CDPFindElementsMixin._execute_command
  now takes a timeout.
- Consistency: drop continue/fail/fulfill_request from the public Browser surface
  (continue_request kept internal for proxy auth), remove get_window_id_for_tab from both
  browsers, reduce generic DownloadBehavior to ALLOW/DENY.
- Fix request-interception lifecycle: CDP disables the Fetch domain when the last intercept
  is removed (requests no longer hang); BiDi removes the registered callback on remove_intercept.
Add BiDiMouse reusing the CDP Mouse humanization (Bezier path, Fitts's Law timing, minimum-jerk, tremor, overshoot) and swapping only the dispatch backend to input.performActions, so pointer events report isTrusted=true. Expose it as a shared BiDiTab.mouse and propagate it to elements (mirroring the CDP ownership model) so cursor position persists across interactions.

BiDiWebElement.click now performs a real click: humanize=True travels a Bezier curve to the element center via the shared mouse; humanize=False jumps to the element with an element-origin pointer move. Replaces the synthetic JS click (isTrusted=false).
Register a preload script on start/connect that redefines navigator.webdriver to return false with a native-looking toString, in every realm before page scripts run. Vanilla Firefox forces the flag to true while the Remote Agent is active; this no-ops when the browser already reports false (e.g. Camoufox).
Add BiDiKeyboard reusing the Keyboard humanization (typo simulation, variable delays, timing) and swapping the dispatch backend to input.performActions, so key events report isTrusted=true. Map the Key enum to WebDriver key values (Private-Use-Area code points for special keys; literal chars for text/symbols). Modifiers are pressed as real keys since BiDi has no modifier bitmask.

Expose BiDiTab.keyboard and route BiDiWebElement.type_text through it (clicks to focus, then types), replacing the previous JS value assignment. Native edit commands such as Ctrl+A select-all are not triggered by Firefox BiDi (events are delivered trusted, but the browser's editing action is not), matching Selenium; application-level hotkeys and special-key navigation work.
Add TabProtocol (portable tab contract) + a mypy conformance harness (browser/_conformance.py) asserting both Tab (CDP) and BiDiTab (BiDi) satisfy it.

BREAKING (CDP): Tab.execute_script now returns the script's deserialized Python value (was the raw CDP response) and takes (script, *args); the CDP power kwargs move to the escape hatch (execute_protocol_command(RuntimeCommands.evaluate(...))). Both protocols now behave identically: same call -> same Python value -> same ScriptExecutionError on throw. BiDiTab.execute_script gains *args support.

Also: Tab.get_cookies/set_cookies use the generic Cookie/CookieParam types; BiDiTab.take_screenshot/print_to_pdf gain quality/beyond_viewport and display_header_footer/print_background for CDP parity; new ScriptExecutionError raised by both backends. Remove the now-dead _get_evaluate_command and _validate_argument_error.
Add BiDiScroll reusing the Scroll humanization (Bezier easing, momentum, jitter, micro-pauses, overshoot) and swapping only the wheel dispatch to input.performActions. Expose it as BiDiTab.scroll.

Refactor the base Scroll JS helpers to use the protocol-agnostic Tab.execute_script (instead of raw RuntimeCommands.evaluate) so they work on both backends; GET_VIEWPORT_CENTER now returns an array directly (execute_script deserializes it).
The post-BiDi refactor dropped the events_enabled parameter from the public Browser.set_download_behavior, losing the CDP download-events capability (the command builder still supports it). Restore it as a CDP-only extra (absent from the portable BrowserProtocol; default False).
…rgence

Update imports to the post-refactor module paths (chromium/, elements/cdp/, protocol/cdp/, chromium/options), rename privatized event-domain methods (enable_X -> _enable_X) in calls and parametrize data, and adjust result access to the converged execute_script (Python value) and the generic BrowserVersion. Point the events_manager tests at CDPEventTracker, where network-log/dialog tracking now lives.

Remove tests for the three intentionally-removed Browser methods (get_window_id_for_tab, fail_request, fulfill_request). Asserts and expected values are unchanged. Unit suite green (409); core integration green on real Chrome (14).
@thalissonvs thalissonvs self-assigned this May 23, 2026
@thalissonvs thalissonvs changed the title Feat/firefox Add support to BiDi protocol (firefox) May 23, 2026
…h, drop get_frame

Add scroll to TabProtocol (both Tab and BiDiTab implement it; conformance harness stays green). Privatize Tab.continue_request/continue_with_auth -> _continue_request/_continue_with_auth (used internally by the intercept flow; base.py callers updated) and remove the public Tab.fail_request/fulfill_request (the escape hatch covers them). Remove the deprecated Tab.get_frame (OOPIF model; no BiDi equivalent) and the now-dead IFrame alias.

Update tests: rename the privatized calls and drop the tests for the removed methods.
Drop unused imports, sort import blocks, and wrap over-length type-alias lines in protocol/bidi. noqa the accepted lint: PLR6301 on the abstract connection-handler hooks (override methods that can't be static), PLR0911/PLR0912 on the BiDi RemoteValue deserializer and locator builder, and PLC0415 on the intentional circular-import-avoidance imports.
…otocol

Add BiDiTab.expect_file_chooser: subscribe to input.fileDialogOpened and set the given files on the originating <input> via input.setFiles when a dialog opens (reactive, trusted path; mirrors the CDP expect_file_chooser). The proactive WebElement.set_input_files already worked.

Add expect_file_chooser to TabProtocol (both Tab and BiDiTab satisfy it; typed as AbstractAsyncContextManager[None] so the @asynccontextmanager methods conform).

Add the first BiDi/Firefox integration test (test_bidi_upload_integration.py): real headless Firefox exercising both upload paths and asserting the file reflected on the input. Drop the now-unused exception imports left by the earlier get_frame removal.
An iframe is searched like any element: tab.find locates the iframe, then .find() on it descends into the iframe's child browsing context. Add a _resolve_locate_target() hook to BidiFindElementsMixin (default: own context + startNodes), overridden by BiDiWebElement to resolve an iframe's child context via contentWindow (a BiDi window RemoteValue carries the context id) and search there. _create_element_from_node now threads the located context so inner elements operate in the right document; nested iframes work recursively, and the shared cross-iframe segment walker now works on BiDi too. Works for same- and cross-origin frames.

Add a real-Firefox integration test (test_bidi_iframe_integration.py): direct iframe.find, find_all inside the frame, and a cross-iframe xpath selector.
Add BiDiWebElement.get_shadow_root() -> BiDiShadowRoot, a finder scoped to the shadow tree by passing the shadow root's sharedId as the locateNodes start node, which reaches closed roots too (the privileged sharedId, like CDP's pierce — no includeShadowTree needed for the scoped search). _create_element_from_node now captures the shadowRoot reference (id + mode) from the NodeRemoteValue. BiDiShadowRoot exposes query()/find_all, mode, host_element and inner_html, and is _css_only (XPath does not cross shadow boundaries), mirroring the CDP ShadowRoot.

Add a real-Firefox integration test (test_bidi_shadow_dom_integration.py): query inside open and closed roots, find_all, and ShadowRootNotFound for a plain element.
Promote shadow-root traversal to the protocol-agnostic surface:
- new ShadowRootProtocol (mode, inner_html, find/query)
- get_shadow_root on WebElementProtocol
- find_shadow_roots on TabProtocol

Back it with BiDiTab.find_shadow_roots (collects open and closed roots
via locateNodes + includeShadowTree='all'), and make CDP
Tab.find_shadow_roots' deep flag keyword-only so both tabs satisfy the
shared signature.
…g the API

Unify the CDP/BiDi command model and tighten types end-to-end so the whole
package type-checks, while improving (not regressing) the public typing.

Core
- Unify Command into one generic TypedDict (protocol/base.py) with a phantom
  `response: NotRequired[R]` field, so the response type is structurally
  recoverable (mypy can't infer a TypedDict type arg that appears in no field);
  re-exported from cdp/base and bidi/base. execute_command and
  create_command_future are now generic (Command[P, R] -> R), dropping a
  `# type: ignore`.
- FindElementsMixin is generic over the element type: Tab.find()/query() return
  the concrete WebElement (CDP) / BiDiWebElement (BiDi) for full IDE
  autocomplete, while the shared protocols use Sequence[WebElementProtocol] so
  the concrete returns conform covariantly.

BiDi typing
- Discriminated unions via Literal `type` tags (input source actions,
  EvaluateResult); typed source-action construction for mouse/keyboard/scroll
  and element click.
- Real types for locateNodes params/result, ElementClipRectangle, and
  cookie/proxy/intercept/window/download construction (no more dict
  placeholders). Dynamic deserialization isolated to
  _deserialize_remote_value(Mapping) -> object.

WebElement conformance
- WebElement and BiDiWebElement now satisfy WebElementProtocol; implemented
  BiDiWebElement.get_children_elements / get_siblings_elements.

CDP↔generic convergence
- get_cookies/set_cookies convert between the portable Cookie types and CDP
  (chromium/_cookies.py); CDP-only cookie fields stay reachable via the escape
  hatch. set_window_bounds maps WindowBounds→Bounds; expect_download keeps the
  CDP DEFAULT reset via the escape hatch.

Misc: edge.py Options→ChromiumOptions; guard the browser-WS port; CDP event
tracker stores raw dicts (drops two `# type: ignore`s).

Tests: realistic CDP cookie fakes in the cookie round-trips; new BiDi relations
integration test. Unit 407 green; BiDi + CDP cookie integration green.
Restore one behavior regression and reconcile integration tests left stale by
the earlier execute_script convergence, Tab method privatization, and
get_version/get_targets changes. Behavior is unchanged; asserts only change
where an API shape genuinely did.

Fix (code)
- The CDP cookie converter dropped CookieParam's `url`, breaking url-based
  set_cookies. Add `url` to the portable CookieParam and pass it through.

Reconcile (tests only)
- execute_script: helpers used the pre-convergence raw shape
  (return_by_value=True + result['result']['result']['value']). Update the
  `_live` helpers (which span Tab — converged — and WebElement — still raw) and
  call sites; route the two iframe context_id scripts through the escape hatch
  (execute_protocol_command(RuntimeCommands.evaluate(...))).
- Privatized APIs: enable_*_events / *_events_enabled /
  intercept_file_chooser_dialog_enabled -> their private names.
- get_version converged to BrowserVersion (product/protocolVersion ->
  browserName/browserVersion); get_targets -> _get_targets.

Full suite: 687 passed, 2 xfailed. mypy + ruff clean.
… common/cdp/bidi

Reorganize the suite into protocol modules and add a Firefox/WebDriver-BiDi
suite parallel to the CDP one. Failures surfaced two real BiDi bugs, fixed in
the code (the tests assert observable behaviour and are protocol-shaped, not
implementation-coupled).

Structure
- tests/{common,cdp,bidi}/{unit,integration}; existing CDP tests moved under
  tests/cdp (git mv preserves history), protocol-agnostic ones under
  tests/common. Shared tests/pages (reused by both protocols), shared
  tests/_waits.py, and a root conftest with ci_chrome_options +
  ci_firefox_options + a page_url helper. pythonpath = [".", "tests"].

BiDi unit suite
- FakeBiDiConnection mirroring the BiDi peer; covers BiDiTab command translation
  (navigate, locateNodes, evaluate/callFunction, cookies) and BiDiWebElement
  (cached attributes, trusted click/type via input.performActions).

BiDi integration suite (real headless Firefox)
- Browser lifecycle (start, get_version, navigation, new_tab, contexts) and the
  element API end to end: find/query/find_all, click (trusted + humanized),
  type, option select, visibility/interactability, traversal, scroll-into-view,
  screenshots, trusted mouse/scroll/keyboard, plus the existing
  iframe/relations/shadow/upload tests. Scoped to what BiDi supports today
  (CDP-only HAR/OOPIF/fetch-interception/bundle/request are out of scope).

Code fixes (found by the tests)
- FirefoxBrowser.get_version re-called session.new, which errors on an already
  established session; cache the capabilities from start()/connect() instead.
- BiDiWebElement.click could not select an <option> (no box for a trusted
  pointer); delegate option selection to JavaScript, mirroring the CDP element.

Full suite: 726 passed, 2 xfailed. mypy + ruff clean.
…de, harden conformance

Five follow-ups closing consistency gaps in the Firefox/BiDi integration.

1. BiDiWebElement.execute_script now returns the deserialized Python value (was
   raw RemoteValue), matching Tab.execute_script; raises ScriptExecutionError.
2. _conformance.py now also asserts Browser, WebElement and ShadowRoot conform to
   their portable protocols (CDP + BiDi), not just Tab — locks the backends from
   drifting.
3. Fix Firefox docstring (get_opened_contexts -> get_opened_tabs).
4. remove_intercept now unsubscribes from network.beforeRequestSent when the last
   intercept is removed, and intercept_requests subscribes only on the first —
   balanced subscribe/unsubscribe (no leaked subscription).
5. User-Agent:
   - options.user_agent property on ChromiumOptions and FirefoxOptions (and
     OptionsProtocol).
   - Tab.useragent_override(user_agent) on both tabs (and TabProtocol): CDP via
     Emulation.setUserAgentOverride, BiDi via emulation.setUserAgentOverride
     scoped to the context. Verified e2e on real Firefox (navigator.userAgent).

Tests: BiDi unit (useragent_override, execute_script deserialization), CDP unit
(useragent_override), BiDi integration (useragent_override changes
navigator.userAgent). Full BiDi suite 56 passed; mypy + ruff clean.
Completes the user_agent property by actually applying it when the browser
starts, on both backends:

- Chromium: the user_agent setter now syncs the canonical --user-agent= argument
  (same pattern as headless / webrtc_leak_protection), so the existing start-time
  override machinery (Emulation.setUserAgentOverride + navigator/client-hints +
  worker propagation) picks it up automatically.
- Firefox/BiDi: start() and connect() apply emulation.setUserAgentOverride scoped
  to the default user context, so the initial tab and tabs opened later in that
  context use it.

Verified e2e: setting options.user_agent makes navigator.userAgent report it on
both real Chrome and real Firefox. Unit tests cover the Chromium arg sync
(add/replace/clear).
… API)

Brings Firefox/BiDi to parity with CDP's Tab.expect_download and makes the
download API portable across both backends.

- BiDiTab.expect_download(keep_file_at, timeout): async context manager that
  routes downloads to a managed directory (temp by default, cleaned up on exit)
  and yields a handle. BiDi reports the saved path on
  browsingContext.downloadEnd, so completion needs no progress polling — it
  filters events to this browsing context and resolves on status 'complete'.
- _BiDiDownloadHandle mirrors CDP's _DownloadHandle: file_path, wait_started,
  wait_finished, read_bytes, read_base64.
- New DownloadHandleProtocol + expect_download on TabProtocol, so callers get
  the same surface regardless of protocol; _conformance.py asserts both concrete
  handles satisfy it.

Verified e2e on real Firefox (temp-dir read, base64, keep_file_at persist);
CDP expect_download still green. mypy/ruff clean.
Replicates the CDP requests module on Firefox/BiDi and makes it a portable API.
Same mechanism on both backends: run the page's fetch() for status/body, and
read the request/response headers + Set-Cookie that JS cannot see from the
protocol's network events.

Structure (mirrors elements/cdp + elements/bidi):
- requests/base.py: BaseRequest — protocol-agnostic public API (request/get/
  post/put/patch/delete/head/options), URL/option building, cookie parsing and
  Response assembly, with hooks for the protocol-specific bits.
- requests/cdp/: Request + har_recorder (moved here, behavior unchanged).
- requests/bidi/: BiDiRequest — fetch via script.evaluate (JSON-stringified to
  dodge RemoteValue serialization limits), metadata from network.beforeRequestSent
  and network.responseCompleted. BiDi exposes Set-Cookie directly in
  response.headers (no extra-info event needed); header values are unwrapped from
  their BytesValue form, and events are matched to the request URL.

Portability: Response now uses portable Header/ResponseCookie. New RequestProtocol
+ tab.request on TabProtocol; _conformance asserts both Request classes conform.
BiDiTab gains a lazy tab.request property.

Verified e2e on real Firefox: GET/POST, response headers, Set-Cookie (incl.
HttpOnly), and session-cookie reuse. CDP request + HAR integration still green.
HAR recording for BiDi is intentionally deferred. mypy + ruff clean.
Brings HAR 1.2 capture to Firefox/BiDi and factors the recorder like the request
client (shared base + cdp/bidi subclasses).

- requests/har.py: BaseHarRecorder owns the protocol-agnostic part — correlating
  per-request pending dicts into HarEntry/HarRequest/HarResponse, cookie/query/
  header parsing, the stop() lifecycle — plus the HarCapture export object.
  Subclasses provide event subscription, response-body retrieval, and timings.
- requests/cdp/har_recorder.py: HarRecorder now extends BaseHarRecorder (CDP
  events + getResponseBody + ResourceTiming); behavior unchanged.
- requests/bidi/har_recorder.py: BiDiHarRecorder — beforeRequestSent/
  responseCompleted/fetchError, response bodies via a data collector
  (network.addDataCollector + network.getData, since BiDi has no getResponseBody),
  and FetchTimingInfo (absolute epoch ms) mapped to HAR phase durations. Header
  BytesValues are unwrapped; duplicate Set-Cookie is newline-joined for the shared
  parser.
- BiDiRequest.record() context manager.
- har_types moved to pydoll/protocol/har_types.py (it is HAR-spec, not CDP).

Verified e2e on real Firefox: navigation + fetch entries, response bodies
(decoded from the collector), Set-Cookie, and a valid saved HAR 1.2 file. CDP HAR
unit + integration still green. mypy + ruff clean.
Closes two of the remaining Tab gaps for Firefox/BiDi.

Extractor (extract / extract_all):
- ExtractionEngine is now protocol-agnostic — it only ever used query +
  get_attribute + text. Swapped the CDP WebElement/FindElementsMixin coupling for
  WebElementProtocol (runtime_checkable, so isinstance works on both backends)
  and a small structural _SupportsQuery protocol for the scope. CDP behavior
  unchanged.
- BiDiTab.extract / extract_all reuse that engine.

Network introspection:
- BiDiTab.get_network_logs(filter) returns this context's beforeRequestSent
  events, lazily enabling capture on first use (subscribe + response data
  collector).
- BiDiTab.get_network_response_body(request_id) reads the body back from the
  collector (decoded to text), gated on prior enablement (NetworkEventsNotEnabled
  otherwise). The introspection subscription includes responseCompleted, which
  Firefox requires for the collector to finalize bodies (without it getData
  blocks).

Verified e2e on real Firefox: extract/extract_all (fields, attribute, list,
repeated containers) and network logs + response body retrieval. CDP extractor
unit + integration still green. mypy + ruff clean.
…test

Integration tests were opening (and closing) a fresh headless Firefox for every
test. Now a module-scoped firefox_browser fixture is shared and each test gets a
fresh browser context (isolated cookies/storage) via the tab/served_tab fixtures,
running on a module-scoped event loop. Lifecycle tests (start/version/contexts)
keep opening their own browser, as they must.

Full BiDi suite: 86 passed, ~66s -> ~39s. Behavior unchanged.
Two parts:

1. Fix the coverage omit globs to match the documented policy. The comment says
   protocol event TypedDicts and command factories are excluded as tautological
   declarative layers, but the globs ('protocol/*/events.py', 'commands/*.py')
   matched nothing — those modules live one level deeper (protocol/cdp/page/
   events.py, commands/cdp/page_commands.py). Made them recursive so the coverage
   target reflects logic, not declarations.

2. Add ~170 unit tests covering previously-untested logic, asserting behavior and
   state (not call sequences), mirroring the existing fake-connection style:
   - FirefoxBrowser: a full browser-commands suite (downloads, cookies, user
     contexts, windows, permissions, interception incl. firing the handler,
     connect) via a new fake_bidi_browser fixture.
   - BiDiTab: screenshots, PDF, dialogs, shadow-root collection, network
     introspection, find locator/raise branches.
   - BiDiWebElement / BiDiShadowRoot / BiDiKeyboard / FirefoxOptions /
     BiDiHarRecorder handlers.
   - CDP Browser: fetch interception + intercepted-request continue/fail/respond,
     proxy-auth callbacks, worker User-Agent auto-attach, Edge binary resolution,
     temp-dir preferences.
   - CubicBezier easing math and SOCKS5 handshake parsing/error paths.

Full suite: 898 passed, 2 xfailed; coverage 95%.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant