[py] Install web extensions from the driver - #17970
Open
AutomatedTester wants to merge 1 commit into
Open
Conversation
Add install_web_extension and uninstall_web_extension to the driver, implementing the Python binding for ADR 17817. Install accepts an unpacked directory, a packed archive, or base64 bytes, and returns a WebExtension wrapping the id the browser assigned. Uninstall takes that object back rather than a raw id. A directory only resolves on the machine running the browser, so a remote session uploads it first and installs from the path the remote end hands back. The upload keeps the directory as the archive's single top-level entry, which is what the Grid resolves the returned path from. Firefox falls back to the classic moz/addon endpoints when BiDi is not enabled. Those endpoints are now registered on demand, because only a webdriver.Firefox session has them in its command registry; a Grid session driven through webdriver.Remote previously failed with a bare "AssertionError: Unrecognised command INSTALL_ADDON". Chromium without BiDi raises instead of silently doing less, and the Firefox-only install_addon and uninstall_addon are deprecated in favour of the new methods.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🔗 Related Issues
Implements the Python binding tracked by #17933 (deliberately not
Fixes, since Java, .NET and JavaScript are still outstanding).Decision record:
docs/decisions/17817-driver-extension-install.md(#17817).Ported alongside the Ruby implementation in #17879 so the two bindings behave the same.
💥 What does this PR do?
Adds
install_web_extensionanduninstall_web_extensionto the driver itself, so installing an extension no longer depends on the browser you happen to be driving.Against each decision in the ADR:
remote.webdriver.WebDriver, so every driver inherits thempath, file → base64, anything else passed through as base64permanent/allow_private_browsing→moz:permanent/moz:allowPrivateBrowsingWebExtensionwrapping the idselenium.webdriver.common.web_extensionTypeErrormoz/addon/install, mappingpermanentonto the classictemporaryflaginstall_addon/uninstall_addonnow emit aDeprecationWarningWebDriverException; Firefox-only options elsewhere raiseValueError🔧 Implementation Notes
The upload has to keep the directory as the archive's single root entry. A directory path only resolves on the machine running the browser, so a remote session uploads it first. The Grid answers with the path of the one top-level entry it unpacked, so the archive is built relative to the directory's parent. The classic
moz/addonendpoint wants the opposite — the extension's own contents at the archive root — so the two callers zip with different roots. There is a unit test pinning each.Registering the classic addon endpoints on demand.
INSTALL_ADDONandUNINSTALL_ADDONlive only inFirefoxRemoteConnection, which awebdriver.Remotesession against a Firefox node never constructs, so the classic fallback died onassert command_info is not Nonewith a bareAssertionError: Unrecognised command INSTALL_ADDON. It was also intermittent, becauseRemoteConnection.__init__assigns the shared module-levelremote_commandsdict rather than a copy, so building any local Firefox driver earlier in the process masked it. The driver now registers the two endpoints if the executor lacks them. Ruby avoids this by mixingFirefox::Featuresinto remote bridges; Python has no equivalent seam.Base64 rather than
archivePathfor packed extensions. The BiDi command also takes an archive path, but that is a remote-end path, so it would not survive a Grid hop. Sending the bytes inline works everywhere and matches Ruby.Vendor fields are written directly into the enhancement manifest. Ruby's generator emits a
Mozsubclass fromcommon/bidi/webextension-install-extensions.cddl; the Python generator emits theextensionsbag onInstallParametersbut no vendor variants, so the twomoz:keys are set inpy/private/bidi_enhancements_manifest.py. I verified the wire output is identical to Ruby's —Record.to_jsonsplats theextensionsmap withpayload.update(extras), so both send{"extensionData": …, "moz:permanent": true}. See the follow-up note below.🤖 AI assistance
💡 Additional Considerations
Tests. 31 unit tests in
py/test/unit/selenium/webdriver/common/web_extension_tests.pycovering both transports, the archive layouts, the deprecation warnings and the error paths; integration tests for the BiDi path on Firefox and Chromium, and for the Firefox classic fallback inff_installs_addons_tests.py(that file is outside theBIDI_TESTSglob, so it runs without--bidi).//py:unit,//py:ruff-check,//py:ruff-formatand//py:mypyare green locally; the browser tests need CI.Follow-up work:
common/bidi/webextension-install-extensions.cddlis a placeholder for Mozilla bug 2057588. When that lands upstream, Ruby picks the vendor fields up automatically and Python will not until the Python generator learns to emit vendor variants. Worth an issue.install_addonstill carries its own copy of the directory-zipping logic. Ruby's PR folded that into the shared helper; I left it alone to keep this diff reversible, but happy to do it here if preferred.Decisions worth a reviewer's opinion:
ValueError. Ruby raisesArgumentErrorstructurally, because its Chromium bridge method does not accept the keywords at all. Python cannot do that with a shared signature, so this is a deliberate choice of exception type.uninstall_web_extensionrejects a raw id withTypeError; Ruby duck-types it into aNoMethodError.🔄 Types of changes