Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions py/selenium/webdriver/common/bidi/webextension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

from typing import Dict, Union

from selenium.webdriver.common.bidi.common import command_builder


class WebExtension:
"""
BiDi implementation of the webExtension module.
"""

def __init__(self, conn):
self.conn = conn

def install(self, path=None, archive_path=None, base64_value=None) -> Dict:
"""Installs a web extension in the remote end.

You must provide exactly one of the parameters.

Parameters:
-----------
path: Path to an extension directory
archive_path: Path to an extension archive file
base64_value: Base64 encoded string of the extension archive

Returns:
-------
Dict: A dictionary containing the extension ID.
"""
if sum(x is not None for x in (path, archive_path, base64_value)) != 1:
raise ValueError("Exactly one of path, archive_path, or base64_value must be provided")

if path is not None:
extension_data = {"type": "path", "path": path}
elif archive_path is not None:
extension_data = {"type": "archivePath", "path": archive_path}
elif base64_value is not None:
extension_data = {"type": "base64", "value": base64_value}

params = {"extensionData": extension_data}
result = self.conn.execute(command_builder("webExtension.install", params))
return result

def uninstall(self, extension_id_or_result: Union[str, Dict]) -> None:
"""Uninstalls a web extension from the remote end.

Parameters:
-----------
extension_id_or_result: Either the extension ID as a string or the result dictionary
from a previous install() call containing the extension ID.
"""
if isinstance(extension_id_or_result, dict):
extension_id = extension_id_or_result.get("extension")
else:
extension_id = extension_id_or_result

params = {"extension": extension_id}
self.conn.execute(command_builder("webExtension.uninstall", params))
24 changes: 24 additions & 0 deletions py/selenium/webdriver/remote/webdriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from selenium.webdriver.common.bidi.script import Script
from selenium.webdriver.common.bidi.session import Session
from selenium.webdriver.common.bidi.storage import Storage
from selenium.webdriver.common.bidi.webextension import WebExtension
from selenium.webdriver.common.by import By
from selenium.webdriver.common.options import ArgOptions, BaseOptions
from selenium.webdriver.common.print_page_options import PrintOptions
Expand Down Expand Up @@ -263,6 +264,7 @@ def __init__(
self._bidi_session = None
self._browsing_context = None
self._storage = None
self._webextension = None

def __repr__(self):
return f'<{type(self).__module__}.{type(self).__name__} (session="{self.session_id}")>'
Expand Down Expand Up @@ -1337,6 +1339,28 @@ def storage(self):

return self._storage

@property
def webextension(self):
"""Returns a webextension module object for BiDi webextension commands.

Returns:
--------
WebExtension: an object containing access to BiDi webextension commands.

Examples:
---------
>>> extension_path = "/path/to/extension"
>>> extension_result = driver.webextension.install(path=extension_path)))
>>> driver.webextension.uninstall(extension_result)
"""
if not self._websocket_connection:
self._start_bidi()

if self._webextension is None:
self._webextension = WebExtension(self._websocket_connection)

return self._webextension

def _get_cdp_details(self):
import json

Expand Down
102 changes: 102 additions & 0 deletions py/test/selenium/webdriver/common/bidi_webextension_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

import base64
import os
import pytest

EXTENSION_ID = "[email protected]"
EXTENSION_PATH = "common/extensions/webextensions-selenium-example-signed"
EXTENSION_ARCHIVE_PATH = "common/extensions/webextensions-selenium-example.xpi"


# Function to find the project root directory
def find_project_root():
current_dir = os.path.dirname(os.path.abspath(__file__))

while current_dir != os.path.dirname(current_dir):
extensions_dir = os.path.join(current_dir, "common", "extensions")
if os.path.isdir(extensions_dir):
return current_dir
current_dir = os.path.dirname(current_dir)

return os.path.dirname(os.path.abspath(__file__))


@pytest.fixture
def locate_project_path():
"""Locate the project path for the extension files."""
project_root = find_project_root()
return project_root


def test_webextension_initialized(driver):
"""Test that the webextension module is initialized properly."""
assert driver.webextension is not None


@pytest.mark.xfail_chrome
@pytest.mark.xfail_edge
def test_install_extension_path(driver, locate_project_path):
"""Test installing an extension from a directory path."""
path = os.path.join(locate_project_path, EXTENSION_PATH)

ex_in = driver.webextension.install(path=path)
assert ex_in.get("extension") == EXTENSION_ID

driver.webextension.uninstall(ex_in)


@pytest.mark.xfail_chrome
@pytest.mark.xfail_edge
def test_install_archive_extension_path(driver, locate_project_path):
"""Test installing an extension from an archive path."""
path = os.path.join(locate_project_path, EXTENSION_ARCHIVE_PATH)

ex = driver.webextension.install(archive_path=path)
assert ex.get("extension") == EXTENSION_ID

driver.webextension.uninstall(ex)


@pytest.mark.xfail_chrome
@pytest.mark.xfail_edge
def test_install_base64_extension_path(driver, locate_project_path):
"""Test installing an extension from a base64 encoded string."""
path = os.path.join(locate_project_path, EXTENSION_ARCHIVE_PATH)

with open(path, "rb") as file:
base64_encoded = base64.b64encode(file.read()).decode("utf-8")

ex = driver.webextension.install(base64_value=base64_encoded)
assert ex.get("extension") == EXTENSION_ID

driver.webextension.uninstall(ex)


@pytest.mark.xfail_chrome
@pytest.mark.xfail_edge
def test_install_with_extension_id_uninstall(driver, locate_project_path):
"""Test uninstalling an extension using just the extension ID."""
path = os.path.join(locate_project_path, EXTENSION_PATH)

ex = driver.webextension.install(path=path)
extension_id = ex.get("extension")
assert extension_id == EXTENSION_ID

# Uninstall using the extension ID
driver.webextension.uninstall(extension_id)
Loading