diff --git a/py/selenium/webdriver/common/bidi/permissions.py b/py/selenium/webdriver/common/bidi/permissions.py new file mode 100644 index 0000000000000..bb897f78dd6c7 --- /dev/null +++ b/py/selenium/webdriver/common/bidi/permissions.py @@ -0,0 +1,88 @@ +# 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 Optional, Union + +from selenium.webdriver.common.bidi.common import command_builder + + +class PermissionState: + """Represents the possible permission states.""" + + GRANTED = "granted" + DENIED = "denied" + PROMPT = "prompt" + + +class PermissionDescriptor: + """Represents a permission descriptor.""" + + def __init__(self, name: str): + self.name = name + + def to_dict(self) -> dict: + return {"name": self.name} + + +class Permissions: + """ + BiDi implementation of the permissions module. + """ + + def __init__(self, conn): + self.conn = conn + + def set_permission( + self, + descriptor: Union[str, PermissionDescriptor], + state: str, + origin: str, + user_context: Optional[str] = None, + ) -> None: + """Sets a permission state for a given permission descriptor. + + Parameters: + ----------- + descriptor: The permission name (str) or PermissionDescriptor object. + Examples: "geolocation", "camera", "microphone" + state: The permission state (granted, denied, prompt). + origin: The origin for which the permission is set. + user_context: The user context id (optional). + + Raises: + ------ + ValueError: If the permission state is invalid. + """ + if state not in [PermissionState.GRANTED, PermissionState.DENIED, PermissionState.PROMPT]: + valid_states = f"{PermissionState.GRANTED}, {PermissionState.DENIED}, {PermissionState.PROMPT}" + raise ValueError(f"Invalid permission state. Must be one of: {valid_states}") + + if isinstance(descriptor, str): + permission_descriptor = PermissionDescriptor(descriptor) + else: + permission_descriptor = descriptor + + params = { + "descriptor": permission_descriptor.to_dict(), + "state": state, + "origin": origin, + } + + if user_context is not None: + params["userContext"] = user_context + + self.conn.execute(command_builder("permissions.setPermission", params)) diff --git a/py/selenium/webdriver/remote/webdriver.py b/py/selenium/webdriver/remote/webdriver.py index 149f12d8fe1a0..07627a488a10e 100644 --- a/py/selenium/webdriver/remote/webdriver.py +++ b/py/selenium/webdriver/remote/webdriver.py @@ -42,6 +42,7 @@ from selenium.webdriver.common.bidi.browser import Browser from selenium.webdriver.common.bidi.browsing_context import BrowsingContext from selenium.webdriver.common.bidi.network import Network +from selenium.webdriver.common.bidi.permissions import Permissions from selenium.webdriver.common.bidi.script import Script from selenium.webdriver.common.bidi.session import Session from selenium.webdriver.common.bidi.storage import Storage @@ -265,6 +266,7 @@ def __init__( self._browsing_context = None self._storage = None self._webextension = None + self._permissions = None def __repr__(self): return f'<{type(self).__module__}.{type(self).__name__} (session="{self.session_id}")>' @@ -1339,6 +1341,28 @@ def storage(self): return self._storage + @property + def permissions(self): + """Returns a permissions module object for BiDi permissions commands. + + Returns: + -------- + Permissions: an object containing access to BiDi permissions commands. + + Examples: + --------- + >>> from selenium.webdriver.common.bidi.permissions import PermissionDescriptor, PermissionState + >>> descriptor = PermissionDescriptor("geolocation") + >>> driver.permissions.set_permission(descriptor, PermissionState.GRANTED, "https://example.com") + """ + if not self._websocket_connection: + self._start_bidi() + + if self._permissions is None: + self._permissions = Permissions(self._websocket_connection) + + return self._permissions + @property def webextension(self): """Returns a webextension module object for BiDi webextension commands. diff --git a/py/test/selenium/webdriver/common/bidi_permissions_tests.py b/py/test/selenium/webdriver/common/bidi_permissions_tests.py new file mode 100644 index 0000000000000..eefae96773048 --- /dev/null +++ b/py/test/selenium/webdriver/common/bidi_permissions_tests.py @@ -0,0 +1,142 @@ +# 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 pytest + +from selenium.webdriver.common.bidi.permissions import PermissionDescriptor, PermissionState +from selenium.webdriver.common.window import WindowTypes + + +def get_origin(driver): + """Get the current window origin.""" + return driver.execute_script("return window.location.origin;") + + +def get_geolocation_permission(driver): + """Get the geolocation permission state.""" + script = """ + const callback = arguments[arguments.length - 1]; + navigator.permissions.query({ name: 'geolocation' }).then(permission => { + callback(permission.state); + }).catch(error => { + callback(null); + }); + """ + return driver.execute_async_script(script) + + +def test_permissions_initialized(driver): + """Test that the permissions module is initialized properly.""" + assert driver.permissions is not None + + +def test_can_set_permission_to_granted(driver, pages): + """Test setting permission to granted state.""" + pages.load("blank.html") + + origin = get_origin(driver) + + # Set geolocation permission to granted + driver.permissions.set_permission("geolocation", PermissionState.GRANTED, origin) + + result = get_geolocation_permission(driver) + assert result == PermissionState.GRANTED + + +def test_can_set_permission_to_denied(driver, pages): + """Test setting permission to denied state.""" + pages.load("blank.html") + + origin = get_origin(driver) + + # Set geolocation permission to denied + driver.permissions.set_permission("geolocation", PermissionState.DENIED, origin) + + result = get_geolocation_permission(driver) + assert result == PermissionState.DENIED + + +def test_can_set_permission_to_prompt(driver, pages): + """Test setting permission to prompt state.""" + pages.load("blank.html") + + origin = get_origin(driver) + + # First set to denied, then to prompt since most of the time the default state is prompt + driver.permissions.set_permission("geolocation", PermissionState.DENIED, origin) + driver.permissions.set_permission("geolocation", PermissionState.PROMPT, origin) + + result = get_geolocation_permission(driver) + assert result == PermissionState.PROMPT + + +def test_can_set_permission_for_user_context(driver, pages): + """Test setting permission for a specific user context.""" + # Create a user context + user_context = driver.browser.create_user_context() + + context_id = driver.browsing_context.create(type=WindowTypes.TAB, user_context=user_context) + + # Navigate both contexts to the same page + pages.load("blank.html") + original_window = driver.current_window_handle + driver.switch_to.window(context_id) + pages.load("blank.html") + + origin = get_origin(driver) + + # Get original permission states + driver.switch_to.window(original_window) + original_permission = get_geolocation_permission(driver) + + driver.switch_to.window(context_id) + + # Set permission only for the user context using PermissionDescriptor + descriptor = PermissionDescriptor("geolocation") + driver.permissions.set_permission(descriptor, PermissionState.GRANTED, origin, user_context) + + # Check that the original window's permission hasn't changed + driver.switch_to.window(original_window) + updated_original_permission = get_geolocation_permission(driver) + assert updated_original_permission == original_permission + + # Check that the new context's permission was updated + driver.switch_to.window(context_id) + updated_new_context_permission = get_geolocation_permission(driver) + assert updated_new_context_permission == PermissionState.GRANTED + + driver.browsing_context.close(context_id) + driver.browser.remove_user_context(user_context) + + +def test_invalid_permission_state_raises_error(driver, pages): + """Test that invalid permission state raises ValueError.""" + pages.load("blank.html") + origin = get_origin(driver) + + # set permission using PermissionDescriptor + descriptor = PermissionDescriptor("geolocation") + + with pytest.raises(ValueError, match="Invalid permission state"): + driver.permissions.set_permission(descriptor, "invalid_state", origin) + + +def test_permission_states_constants(): + """Test that permission state constants are correctly defined.""" + assert PermissionState.GRANTED == "granted" + assert PermissionState.DENIED == "denied" + assert PermissionState.PROMPT == "prompt"