From 5fc0626ecf4b41c3edae79caae262979d24ebedd Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Thu, 6 Nov 2025 19:14:36 +0530 Subject: [PATCH 1/4] add `set_download_behavior` command --- py/selenium/webdriver/common/bidi/browser.py | 43 ++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/py/selenium/webdriver/common/bidi/browser.py b/py/selenium/webdriver/common/bidi/browser.py index 3355c07eff012..5fc353b479875 100644 --- a/py/selenium/webdriver/common/bidi/browser.py +++ b/py/selenium/webdriver/common/bidi/browser.py @@ -236,3 +236,46 @@ def get_client_windows(self) -> list[ClientWindowInfo]: """ result = self.conn.execute(command_builder("browser.getClientWindows", {})) return [ClientWindowInfo.from_dict(window) for window in result["clientWindows"]] + + def set_download_behavior( + self, + *, + allowed: Optional[bool] = None, + destination_folder: Optional[str] = None, + user_contexts: Optional[list[str]] = None, + ) -> None: + """Set the download behavior for the browser or specific user contexts. + + Args: + allowed: True to allow downloads, False to deny downloads, or None to + clear download behavior (revert to default). + destination_folder: Required when allowed is True. Specifies the folder + to store downloads in. + user_contexts: Optional list of user context IDs to apply this + behavior to. If omitted, updates the default behavior. + + Raises: + ValueError: If allowed=True and destination_folder is missing, or if + allowed=False and destination_folder is provided. + """ + params: dict[str, Any] = {} + + if allowed is None: + params["downloadBehavior"] = None + else: + if allowed: + if not destination_folder: + raise ValueError("destination_folder is required when allowed=True.") + params["downloadBehavior"] = { + "type": "allowed", + "destinationFolder": destination_folder, + } + else: + if destination_folder: + raise ValueError("destination_folder should not be provided when allowed=False.") + params["downloadBehavior"] = {"type": "denied"} + + if user_contexts is not None: + params["userContexts"] = user_contexts + + self.conn.execute(command_builder("browser.setDownloadBehavior", params)) From 79a445c9e538a2a84b1f9309f207f5e885fe3217 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Thu, 6 Nov 2025 19:18:08 +0530 Subject: [PATCH 2/4] add tests --- .../webdriver/common/bidi_browser_tests.py | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/py/test/selenium/webdriver/common/bidi_browser_tests.py b/py/test/selenium/webdriver/common/bidi_browser_tests.py index 74b406b54c22e..65c62a124c289 100644 --- a/py/test/selenium/webdriver/common/bidi_browser_tests.py +++ b/py/test/selenium/webdriver/common/bidi_browser_tests.py @@ -16,17 +16,22 @@ # under the License. import http.server +import os import socketserver +import tempfile import threading +import time import pytest from selenium.webdriver.common.bidi.browser import ClientWindowInfo, ClientWindowState +from selenium.webdriver.common.bidi.browsing_context import ReadinessState from selenium.webdriver.common.bidi.session import UserPromptHandler, UserPromptHandlerType from selenium.webdriver.common.by import By from selenium.webdriver.common.proxy import Proxy, ProxyType from selenium.webdriver.common.utils import free_port from selenium.webdriver.common.window import WindowTypes +from selenium.webdriver.support.ui import WebDriverWait class FakeProxyHandler(http.server.SimpleHTTPRequestHandler): @@ -262,3 +267,90 @@ def test_create_user_context_with_unhandled_prompt_behavior(driver, pages): # Clean up driver.browser.remove_user_context(user_context) + + +@pytest.mark.xfail_firefox +def test_set_download_behavior_allowed(driver, pages): + with tempfile.TemporaryDirectory() as tmp_dir: + driver.browser.set_download_behavior(allowed=True, destination_folder=tmp_dir) + + context_id = driver.current_window_handle + url = pages.url("downloads/download.html") + driver.browsing_context.navigate(context=context_id, url=url, wait=ReadinessState.COMPLETE) + + driver.find_element(By.ID, "file-1").click() + + WebDriverWait(driver, 5).until(lambda d: "file_1.txt" in os.listdir(tmp_dir)) + + files = os.listdir(tmp_dir) + assert "file_1.txt" in files, f"Expected file_1.txt in {tmp_dir}, but found: {files}" + + driver.browser.set_download_behavior(allowed=None) + + +@pytest.mark.xfail_firefox +def test_set_download_behavior_denied(driver, pages): + with tempfile.TemporaryDirectory() as tmp_dir: + driver.browser.set_download_behavior(allowed=False) + + context_id = driver.current_window_handle + url = pages.url("downloads/download.html") + driver.browsing_context.navigate(context=context_id, url=url, wait=ReadinessState.COMPLETE) + + driver.find_element(By.ID, "file-1").click() + + time.sleep(1) + + files = os.listdir(tmp_dir) + assert len(files) == 0, f"No files should be downloaded when denied, but found: {files}" + + driver.browser.set_download_behavior(allowed=None) + + +@pytest.mark.xfail_firefox +def test_set_download_behavior_user_context(driver, pages): + user_context = driver.browser.create_user_context() + + try: + bc = driver.browsing_context.create(type=WindowTypes.WINDOW, user_context=user_context) + driver.switch_to.window(bc) + + with tempfile.TemporaryDirectory() as tmp_dir: + driver.browser.set_download_behavior(allowed=True, destination_folder=tmp_dir, user_contexts=[user_context]) + + url = pages.url("downloads/download.html") + driver.browsing_context.navigate(context=bc, url=url, wait=ReadinessState.COMPLETE) + + driver.find_element(By.ID, "file-1").click() + + WebDriverWait(driver, 5).until(lambda d: "file_1.txt" in os.listdir(tmp_dir)) + + files = os.listdir(tmp_dir) + assert "file_1.txt" in files, f"Expected file_1.txt in {tmp_dir}, but found: {files}" + + initial_file_count = len(files) + + driver.browser.set_download_behavior(allowed=False, user_contexts=[user_context]) + + driver.find_element(By.ID, "file-2").click() + + time.sleep(1) + + files_after = os.listdir(tmp_dir) + assert len(files_after) == initial_file_count, ( + f"No new files should be downloaded when denied, but found: {files_after}" + ) + + driver.browser.set_download_behavior(allowed=None, user_contexts=[user_context]) + + finally: + driver.browser.remove_user_context(user_context) + + +@pytest.mark.xfail_firefox +def test_set_download_behavior_validation(driver): + with pytest.raises(ValueError, match="destination_folder is required when allowed=True"): + driver.browser.set_download_behavior(allowed=True) + + with pytest.raises(ValueError, match="destination_folder should not be provided when allowed=False"): + driver.browser.set_download_behavior(allowed=False, destination_folder="/tmp") From a899fef30d38bc1ff1d92bbb26e635c9a35782b9 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Thu, 6 Nov 2025 22:38:27 +0530 Subject: [PATCH 3/4] support `Path` and address review comments --- py/selenium/webdriver/common/bidi/browser.py | 7 +- .../webdriver/common/bidi_browser_tests.py | 64 ++++++++++--------- 2 files changed, 37 insertions(+), 34 deletions(-) diff --git a/py/selenium/webdriver/common/bidi/browser.py b/py/selenium/webdriver/common/bidi/browser.py index 5fc353b479875..f597332563a14 100644 --- a/py/selenium/webdriver/common/bidi/browser.py +++ b/py/selenium/webdriver/common/bidi/browser.py @@ -14,8 +14,7 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. - - +import os from typing import Any, Optional from selenium.webdriver.common.bidi.common import command_builder @@ -241,7 +240,7 @@ def set_download_behavior( self, *, allowed: Optional[bool] = None, - destination_folder: Optional[str] = None, + destination_folder: Optional[str | os.PathLike] = None, user_contexts: Optional[list[str]] = None, ) -> None: """Set the download behavior for the browser or specific user contexts. @@ -268,7 +267,7 @@ def set_download_behavior( raise ValueError("destination_folder is required when allowed=True.") params["downloadBehavior"] = { "type": "allowed", - "destinationFolder": destination_folder, + "destinationFolder": os.fspath(destination_folder), } else: if destination_folder: diff --git a/py/test/selenium/webdriver/common/bidi_browser_tests.py b/py/test/selenium/webdriver/common/bidi_browser_tests.py index 65c62a124c289..bf85b00857f33 100644 --- a/py/test/selenium/webdriver/common/bidi_browser_tests.py +++ b/py/test/selenium/webdriver/common/bidi_browser_tests.py @@ -18,12 +18,11 @@ import http.server import os import socketserver -import tempfile import threading -import time import pytest +from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.bidi.browser import ClientWindowInfo, ClientWindowState from selenium.webdriver.common.bidi.browsing_context import ReadinessState from selenium.webdriver.common.bidi.session import UserPromptHandler, UserPromptHandlerType @@ -270,9 +269,9 @@ def test_create_user_context_with_unhandled_prompt_behavior(driver, pages): @pytest.mark.xfail_firefox -def test_set_download_behavior_allowed(driver, pages): - with tempfile.TemporaryDirectory() as tmp_dir: - driver.browser.set_download_behavior(allowed=True, destination_folder=tmp_dir) +def test_set_download_behavior_allowed(driver, pages, tmp_path): + try: + driver.browser.set_download_behavior(allowed=True, destination_folder=tmp_path) context_id = driver.current_window_handle url = pages.url("downloads/download.html") @@ -280,17 +279,17 @@ def test_set_download_behavior_allowed(driver, pages): driver.find_element(By.ID, "file-1").click() - WebDriverWait(driver, 5).until(lambda d: "file_1.txt" in os.listdir(tmp_dir)) - - files = os.listdir(tmp_dir) - assert "file_1.txt" in files, f"Expected file_1.txt in {tmp_dir}, but found: {files}" + WebDriverWait(driver, 5).until(lambda d: "file_1.txt" in os.listdir(tmp_path)) + files = os.listdir(tmp_path) + assert "file_1.txt" in files, f"Expected file_1.txt in {tmp_path}, but found: {files}" + finally: driver.browser.set_download_behavior(allowed=None) @pytest.mark.xfail_firefox -def test_set_download_behavior_denied(driver, pages): - with tempfile.TemporaryDirectory() as tmp_dir: +def test_set_download_behavior_denied(driver, pages, tmp_path): + try: driver.browser.set_download_behavior(allowed=False) context_id = driver.current_window_handle @@ -299,34 +298,38 @@ def test_set_download_behavior_denied(driver, pages): driver.find_element(By.ID, "file-1").click() - time.sleep(1) - - files = os.listdir(tmp_dir) - assert len(files) == 0, f"No files should be downloaded when denied, but found: {files}" - + try: + WebDriverWait(driver, 3, poll_frequency=0.2).until(lambda _: len(os.listdir(tmp_path)) > 0) + files = os.listdir(tmp_path) + pytest.fail(f"A file was downloaded unexpectedly: {files}") + except TimeoutException: + pass # Expected, no file downloaded + finally: driver.browser.set_download_behavior(allowed=None) @pytest.mark.xfail_firefox -def test_set_download_behavior_user_context(driver, pages): +def test_set_download_behavior_user_context(driver, pages, tmp_path): user_context = driver.browser.create_user_context() try: bc = driver.browsing_context.create(type=WindowTypes.WINDOW, user_context=user_context) driver.switch_to.window(bc) - with tempfile.TemporaryDirectory() as tmp_dir: - driver.browser.set_download_behavior(allowed=True, destination_folder=tmp_dir, user_contexts=[user_context]) + try: + driver.browser.set_download_behavior( + allowed=True, destination_folder=tmp_path, user_contexts=[user_context] + ) url = pages.url("downloads/download.html") driver.browsing_context.navigate(context=bc, url=url, wait=ReadinessState.COMPLETE) driver.find_element(By.ID, "file-1").click() - WebDriverWait(driver, 5).until(lambda d: "file_1.txt" in os.listdir(tmp_dir)) + WebDriverWait(driver, 5).until(lambda d: "file_1.txt" in os.listdir(tmp_path)) - files = os.listdir(tmp_dir) - assert "file_1.txt" in files, f"Expected file_1.txt in {tmp_dir}, but found: {files}" + files = os.listdir(tmp_path) + assert "file_1.txt" in files, f"Expected file_1.txt in {tmp_path}, but found: {files}" initial_file_count = len(files) @@ -334,15 +337,16 @@ def test_set_download_behavior_user_context(driver, pages): driver.find_element(By.ID, "file-2").click() - time.sleep(1) - - files_after = os.listdir(tmp_dir) - assert len(files_after) == initial_file_count, ( - f"No new files should be downloaded when denied, but found: {files_after}" - ) - + try: + WebDriverWait(driver, 3, poll_frequency=0.2).until( + lambda _: len(os.listdir(tmp_path)) > initial_file_count + ) + files_after = os.listdir(tmp_path) + pytest.fail(f"A file was downloaded unexpectedly: {files_after}") + except TimeoutException: + pass # Expected, no file downloaded + finally: driver.browser.set_download_behavior(allowed=None, user_contexts=[user_context]) - finally: driver.browser.remove_user_context(user_context) From 0e88b8c7891e26c33eac3fc2cc202a3825877821 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Fri, 7 Nov 2025 11:57:09 +0530 Subject: [PATCH 4/4] debug: get browser/driver info --- py/test/selenium/webdriver/common/bidi_browser_tests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/py/test/selenium/webdriver/common/bidi_browser_tests.py b/py/test/selenium/webdriver/common/bidi_browser_tests.py index bf85b00857f33..e89ccd3f0ff59 100644 --- a/py/test/selenium/webdriver/common/bidi_browser_tests.py +++ b/py/test/selenium/webdriver/common/bidi_browser_tests.py @@ -270,6 +270,7 @@ def test_create_user_context_with_unhandled_prompt_behavior(driver, pages): @pytest.mark.xfail_firefox def test_set_download_behavior_allowed(driver, pages, tmp_path): + print(f"Driver info: {driver.capabilities}") try: driver.browser.set_download_behavior(allowed=True, destination_folder=tmp_path)