From 9b3cc1d2d31da0febadd1276bb1f493798f472c4 Mon Sep 17 00:00:00 2001 From: arthur-rodrigues Date: Sat, 1 Nov 2025 14:09:03 -0300 Subject: [PATCH 1/6] refactor: implement browser preferences types and validation --- pydoll/browser/options.py | 81 ++++++++++++++++++++++++++---- pydoll/browser/preference_types.py | 48 ++++++++++++++++++ pydoll/exceptions.py | 12 +++++ pyproject.toml | 4 +- 4 files changed, 133 insertions(+), 12 deletions(-) create mode 100644 pydoll/browser/preference_types.py diff --git a/pydoll/browser/options.py b/pydoll/browser/options.py index 9c7e5bef..d9b098fe 100644 --- a/pydoll/browser/options.py +++ b/pydoll/browser/options.py @@ -1,10 +1,14 @@ from contextlib import suppress +from typing import Any, Optional from pydoll.browser.interfaces import Options +from pydoll.browser.preference_types import PREFERENCE_SCHEMA, BrowserPreferences from pydoll.constants import PageLoadState from pydoll.exceptions import ( ArgumentAlreadyExistsInOptions, ArgumentNotFoundInOptions, + InvalidPreferencePath, + InvalidPreferenceValue, WrongPrefsDict, ) @@ -27,7 +31,7 @@ def __init__(self): self._arguments = [] self._binary_location = '' self._start_timeout = 10 - self._browser_preferences = {} + self._browser_preferences: BrowserPreferences = {} self._headless = False self._page_load_state = PageLoadState.COMPLETE @@ -121,16 +125,18 @@ def remove_argument(self, argument: str): self._arguments.remove(argument) @property - def browser_preferences(self) -> dict: + def browser_preferences(self) -> BrowserPreferences: return self._browser_preferences @browser_preferences.setter - def browser_preferences(self, preferences: dict): + def browser_preferences(self, preferences: BrowserPreferences): if not isinstance(preferences, dict): raise ValueError('The experimental options value must be a dict.') if preferences.get('prefs'): - raise WrongPrefsDict + # deixar o WrongPrefsDict, mas com mensagem para ficar menos genĂ©rico + raise WrongPrefsDict("Top-level key 'prefs' is not allowed in browser preferences.") + # merge com preferĂȘncias existentes self._browser_preferences = {**self._browser_preferences, **preferences} def _set_pref_path(self, path: list, value): @@ -143,11 +149,57 @@ def _set_pref_path(self, path: list, value): path (e.g., ['plugins', 'always_open_pdf_externally']) value -- The value to set at the given path """ + # validation will be handled in the updated implementation below + # (kept for backward-compatibility if callers rely on signature) + self._validate_pref_path(path) + self._validate_pref_value(path, value) + d = self._browser_preferences for key in path[:-1]: d = d.setdefault(key, {}) d[path[-1]] = value + @staticmethod + def _validate_pref_path(path: list[str]) -> None: + """ + Validate that the provided path exists in the PREFERENCE_SCHEMA. + Raises InvalidPreferencePath when any segment is invalid. + """ + node = PREFERENCE_SCHEMA + for key in path: + if isinstance(node, dict) and key in node: + node = node[key] + else: + raise InvalidPreferencePath(f'Invalid preference path: {".".join(path)}') + + @staticmethod + def _validate_pref_value(path: list[str], value: Any) -> None: + """ + Validate the value type for the final segment in path against PREFERENCE_SCHEMA. + Raises InvalidPreferenceValue when the value does not match expected type. + """ + node = PREFERENCE_SCHEMA + # walk to the parent node + for key in path[:-1]: + node = node[key] + + final_key = path[-1] + expected = node.get(final_key) if isinstance(node, dict) else None + + if expected is None: + # no explicit restriction + return + + if expected is dict: + if not isinstance(value, dict): + msg = f'Invalid value type for {".".join(path)}: ' + msg += f'expected dict, got {type(value).__name__}' + raise InvalidPreferenceValue(msg) + elif not isinstance(value, expected): + msg = f'Invalid value type for {".".join(path)}: ' + msg += f'expected {expected.__name__}, got {type(value).__name__}' + raise InvalidPreferenceValue(msg) + def _get_pref_path(self, path: list): """ Safely gets a nested value from self._browser_preferences. @@ -159,6 +211,12 @@ def _get_pref_path(self, path: list): Returns: The value at the given path, or None if path doesn't exist """ + # validate path structure first; if invalid, raise a clear exception + try: + self._validate_pref_path(path) + except InvalidPreferencePath: + raise + nested_preferences = self._browser_preferences with suppress(KeyError, TypeError): for key in path: @@ -189,8 +247,9 @@ def set_accept_languages(self, languages: str): self._set_pref_path(['intl', 'accept_languages'], languages) @property - def prompt_for_download(self) -> bool: - return self._get_pref_path(['download', 'prompt_for_download']) + def prompt_for_download(self) -> Optional[bool]: + val = self._get_pref_path(['download', 'prompt_for_download']) + return val if isinstance(val, bool) else None @prompt_for_download.setter def prompt_for_download(self, enabled: bool): @@ -223,8 +282,9 @@ def block_popups(self, block: bool): ) @property - def password_manager_enabled(self) -> bool: - return self._get_pref_path(['profile', 'password_manager_enabled']) + def password_manager_enabled(self) -> Optional[bool]: + val = self._get_pref_path(['profile', 'password_manager_enabled']) + return val if isinstance(val, bool) else None @password_manager_enabled.setter def password_manager_enabled(self, enabled: bool): @@ -291,8 +351,9 @@ def allow_automatic_downloads(self, allow: bool): ) @property - def open_pdf_externally(self) -> bool: - return self._get_pref_path(['plugins', 'always_open_pdf_externally']) + def open_pdf_externally(self) -> Optional[bool]: + val = self._get_pref_path(['plugins', 'always_open_pdf_externally']) + return val if isinstance(val, bool) else None @open_pdf_externally.setter def open_pdf_externally(self, enabled: bool): diff --git a/pydoll/browser/preference_types.py b/pydoll/browser/preference_types.py new file mode 100644 index 00000000..79caa982 --- /dev/null +++ b/pydoll/browser/preference_types.py @@ -0,0 +1,48 @@ +from typing import NotRequired, TypedDict + + +class DownloadPreferences(TypedDict, total=False): + default_directory: str + prompt_for_download: bool + + +class ProfilePreferences(TypedDict, total=False): + password_manager_enabled: bool + # maps content setting name -> int (e.g. popups: 0 or 1) + default_content_setting_values: NotRequired[dict[str, int]] + + +class BrowserPreferences(TypedDict, total=False): + download: DownloadPreferences + profile: ProfilePreferences + intl: NotRequired[dict[str, str]] + plugins: NotRequired[dict[str, bool]] + credentials_enable_service: bool + + +# Runtime schema used for validating preference paths and value types. +# Keys map to either a python type (str/bool/int/dict) or to a nested dict +# describing child keys and their expected types. +PREFERENCE_SCHEMA: dict = { + 'download': { + 'default_directory': str, + 'prompt_for_download': bool, + 'directory_upgrade': bool, + }, + 'profile': { + 'password_manager_enabled': bool, + # default_content_setting_values is a mapping of content name -> int + 'default_content_setting_values': { + 'popups': int, + 'notifications': int, + 'automatic_downloads': int, + }, + }, + 'intl': { + 'accept_languages': str, + }, + 'plugins': { + 'always_open_pdf_externally': bool, + }, + 'credentials_enable_service': bool, +} diff --git a/pydoll/exceptions.py b/pydoll/exceptions.py index cab7fd5f..8afa04ff 100644 --- a/pydoll/exceptions.py +++ b/pydoll/exceptions.py @@ -319,6 +319,18 @@ class WrongPrefsDict(PydollException): message = 'The dict can not contain "prefs" key, provide only the prefs options' +class InvalidPreferencePath(PydollException): + """Raised when a provided preference path is invalid (segment doesn't exist).""" + + message = 'Invalid preference path' + + +class InvalidPreferenceValue(PydollException): + """Invalid value for a preference (incompatible type)""" + + message = 'Invalid preference value' + + class ElementPreconditionError(ElementException): """Raised when invalid or missing preconditions are provided for element operations.""" diff --git a/pyproject.toml b/pyproject.toml index fe8eb344..b969410e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,8 +58,8 @@ pythonpath = "." addopts = '-p no:warnings' [tool.taskipy.tasks] -lint = 'ruff check .; ruff check . --diff' -format = 'ruff check . --fix; ruff format .' +lint = 'ruff check . && ruff check . --diff' +format = 'ruff check . --fix && ruff format .' test = 'pytest -s -x --cov=pydoll -vv' post_test = 'coverage html' From b35ccedceb479e251c8b29b682fca50f3b4c905d Mon Sep 17 00:00:00 2001 From: arthur-rodrigues Date: Sat, 1 Nov 2025 17:34:46 -0300 Subject: [PATCH 2/6] test: add tests for browser preferences validation --- pydoll/browser/preference_types.py | 16 +- .../test_browser/test_browser_preferences.py | 156 ++++++++++++++++++ 2 files changed, 167 insertions(+), 5 deletions(-) create mode 100644 tests/test_browser/test_browser_preferences.py diff --git a/pydoll/browser/preference_types.py b/pydoll/browser/preference_types.py index 79caa982..1dc917b9 100644 --- a/pydoll/browser/preference_types.py +++ b/pydoll/browser/preference_types.py @@ -1,15 +1,21 @@ from typing import NotRequired, TypedDict -class DownloadPreferences(TypedDict, total=False): +class DownloadPreferences(TypedDict): default_directory: str - prompt_for_download: bool + prompt_for_download: NotRequired[bool] + directory_upgrade: NotRequired[bool] -class ProfilePreferences(TypedDict, total=False): +class ContentSettingValues(TypedDict, total=False): + popups: int + notifications: int + automatic_downloads: int + + +class ProfilePreferences(TypedDict): password_manager_enabled: bool - # maps content setting name -> int (e.g. popups: 0 or 1) - default_content_setting_values: NotRequired[dict[str, int]] + default_content_setting_values: ContentSettingValues class BrowserPreferences(TypedDict, total=False): diff --git a/tests/test_browser/test_browser_preferences.py b/tests/test_browser/test_browser_preferences.py new file mode 100644 index 00000000..1d7580d9 --- /dev/null +++ b/tests/test_browser/test_browser_preferences.py @@ -0,0 +1,156 @@ +import pytest +from typing import Any, cast + +from pydoll.browser.options import ChromiumOptions +from pydoll.browser.preference_types import BrowserPreferences +from pydoll.exceptions import InvalidPreferencePath, InvalidPreferenceValue, WrongPrefsDict + + +def test_validate_pref_path_valid(): + """Test that valid preference paths are accepted.""" + options = ChromiumOptions() + # Should not raise + options._validate_pref_path(['download', 'default_directory']) + options._validate_pref_path(['profile', 'password_manager_enabled']) + options._validate_pref_path(['plugins', 'always_open_pdf_externally']) + + +def test_validate_pref_path_invalid(): + """Test that invalid preference paths raise InvalidPreferencePath.""" + options = ChromiumOptions() + with pytest.raises(InvalidPreferencePath): + options._validate_pref_path(['invalid', 'path']) + with pytest.raises(InvalidPreferencePath): + options._validate_pref_path(['download', 'invalid_key']) + + +def test_validate_pref_value_valid(): + """Test that valid preference values are accepted.""" + options = ChromiumOptions() + # Should not raise + options._validate_pref_value(['download', 'default_directory'], '/path/to/dir') + options._validate_pref_value(['profile', 'password_manager_enabled'], True) + options._validate_pref_value(['profile', 'default_content_setting_values', 'popups'], 1) + + +def test_validate_pref_value_invalid(): + """Test that invalid preference values raise InvalidPreferenceValue.""" + options = ChromiumOptions() + with pytest.raises(InvalidPreferenceValue): + options._validate_pref_value(['download', 'default_directory'], True) # should be str + with pytest.raises(InvalidPreferenceValue): + options._validate_pref_value(['profile', 'password_manager_enabled'], 'true') # should be bool + + +def test_browser_preferences_setter_valid(): + """Test setting valid browser preferences.""" + options = ChromiumOptions() + prefs: BrowserPreferences = { + 'download': { + 'default_directory': '/downloads', + 'prompt_for_download': True + }, + 'profile': { + 'password_manager_enabled': False, + 'default_content_setting_values': { + 'popups': 0, + 'notifications': 2 + } + } + } + options.browser_preferences = prefs + assert options.browser_preferences == prefs + + +def test_browser_preferences_setter_invalid_type(): + """Test setting browser preferences with invalid type.""" + options = ChromiumOptions() + with pytest.raises(ValueError): + # type: ignore[arg-type] + options.browser_preferences = ['not', 'a', 'dict'] + + +def test_browser_preferences_setter_invalid_prefs(): + """Test setting browser preferences with invalid prefs key.""" + options = ChromiumOptions() + with pytest.raises(WrongPrefsDict): + invalid_prefs = cast(BrowserPreferences, {'prefs': {'some': 'value'}}) + options.browser_preferences = invalid_prefs + + +def test_browser_preferences_merge(): + """Test that browser preferences are properly merged.""" + options = ChromiumOptions() + initial_prefs: BrowserPreferences = { + 'download': { + 'default_directory': '/downloads', + 'prompt_for_download': True + }, + 'profile': { + 'password_manager_enabled': True, + 'default_content_setting_values': { + 'popups': 0 + } + } + } + additional_prefs: BrowserPreferences = { + 'profile': { + 'password_manager_enabled': False, + 'default_content_setting_values': { + 'notifications': 2 + } + } + } + + options.browser_preferences = initial_prefs + options.browser_preferences = additional_prefs + + expected: BrowserPreferences = { + 'download': { + 'default_directory': '/downloads', + 'prompt_for_download': True + }, + 'profile': { + 'password_manager_enabled': False, + 'default_content_setting_values': { + 'notifications': 2 + } + } + } + assert options.browser_preferences == expected + + +def test_get_pref_path_existing(): + """Test getting existing preference paths.""" + options = ChromiumOptions() + prefs: BrowserPreferences = { + 'download': { + 'default_directory': '/downloads', + }, + 'profile': { + 'password_manager_enabled': True, + 'default_content_setting_values': {} + } + } + options.browser_preferences = prefs + assert options._get_pref_path(['download', 'default_directory']) == '/downloads' + + +def test_get_pref_path_nonexistent(): + """Test getting nonexistent preference paths returns None.""" + options = ChromiumOptions() + with pytest.raises(InvalidPreferencePath): + options._get_pref_path(['download', 'nonexistent']) + + +def test_set_pref_path_creates_structure(): + """Test that setting a preference path creates the necessary structure.""" + options = ChromiumOptions() + options.browser_preferences = cast(BrowserPreferences, { + 'profile': { + 'password_manager_enabled': True, + 'default_content_setting_values': {} + } + }) + options._set_pref_path(['profile', 'default_content_setting_values', 'popups'], 0) + assert cast(Any, options.browser_preferences)['profile']['default_content_setting_values']['popups'] == 0 \ No newline at end of file From f3b785e1bfc66820f3a0a85e4432549e07d2fd68 Mon Sep 17 00:00:00 2001 From: arthur-rodrigues Date: Mon, 3 Nov 2025 12:15:11 -0300 Subject: [PATCH 3/6] Refactor browser preferences types --- pydoll/browser/interfaces.py | 4 +++- pydoll/browser/options.py | 4 ++-- pydoll/browser/preference_types.py | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pydoll/browser/interfaces.py b/pydoll/browser/interfaces.py index d40ed765..6690ef38 100644 --- a/pydoll/browser/interfaces.py +++ b/pydoll/browser/interfaces.py @@ -2,6 +2,8 @@ from pydoll.constants import PageLoadState +from pydoll.browser.preference_types import BrowserPreferences + class Options(ABC): @property @@ -25,7 +27,7 @@ def add_argument(self, argument: str): @property @abstractmethod - def browser_preferences(self) -> dict: + def browser_preferences(self) -> BrowserPreferences: pass @property diff --git a/pydoll/browser/options.py b/pydoll/browser/options.py index d9b098fe..4a4e107f 100644 --- a/pydoll/browser/options.py +++ b/pydoll/browser/options.py @@ -1,5 +1,5 @@ from contextlib import suppress -from typing import Any, Optional +from typing import Any, Optional, cast from pydoll.browser.interfaces import Options from pydoll.browser.preference_types import PREFERENCE_SCHEMA, BrowserPreferences @@ -154,7 +154,7 @@ def _set_pref_path(self, path: list, value): self._validate_pref_path(path) self._validate_pref_value(path, value) - d = self._browser_preferences + d = cast(dict[str, Any], self._browser_preferences) for key in path[:-1]: d = d.setdefault(key, {}) d[path[-1]] = value diff --git a/pydoll/browser/preference_types.py b/pydoll/browser/preference_types.py index 1dc917b9..c41edbf9 100644 --- a/pydoll/browser/preference_types.py +++ b/pydoll/browser/preference_types.py @@ -1,4 +1,5 @@ -from typing import NotRequired, TypedDict +from typing import TypedDict, Any +from typing_extensions import NotRequired class DownloadPreferences(TypedDict): From fc5999a7379a29d53b3da228351da9cff88bea50 Mon Sep 17 00:00:00 2001 From: arthur-rodrigues Date: Mon, 3 Nov 2025 15:00:47 -0300 Subject: [PATCH 4/6] refactor: implement browser preferences types and validation --- pydoll/browser/options.py | 55 +++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/pydoll/browser/options.py b/pydoll/browser/options.py index 4a4e107f..88e5fa65 100644 --- a/pydoll/browser/options.py +++ b/pydoll/browser/options.py @@ -28,12 +28,12 @@ def __init__(self): Sets up an empty list for command-line arguments and a string for the binary location of the browser. """ - self._arguments = [] - self._binary_location = '' - self._start_timeout = 10 + self._arguments: list[str] = [] + self._binary_location: str = '' + self._start_timeout: int = 10 self._browser_preferences: BrowserPreferences = {} - self._headless = False - self._page_load_state = PageLoadState.COMPLETE + self._headless: bool = False + self._page_load_state: PageLoadState = PageLoadState.COMPLETE @property def arguments(self) -> list[str]: @@ -154,7 +154,7 @@ def _set_pref_path(self, path: list, value): self._validate_pref_path(path) self._validate_pref_value(path, value) - d = cast(dict[str, Any], self._browser_preferences) + d = cast(dict[str, Any], self._browser_preferences) for key in path[:-1]: d = d.setdefault(key, {}) d[path[-1]] = value @@ -176,29 +176,35 @@ def _validate_pref_path(path: list[str]) -> None: def _validate_pref_value(path: list[str], value: Any) -> None: """ Validate the value type for the final segment in path against PREFERENCE_SCHEMA. - Raises InvalidPreferenceValue when the value does not match expected type. + Supports recursive validation for nested dictionaries. + Raises InvalidPreferenceValue or InvalidPreferencePath on validation failure. """ node = PREFERENCE_SCHEMA - # walk to the parent node + # Walk to the parent node (assumes path is valid from _validate_pref_path) for key in path[:-1]: node = node[key] final_key = path[-1] - expected = node.get(final_key) if isinstance(node, dict) else None + expected = node[final_key] - if expected is None: - # no explicit restriction - return - - if expected is dict: + if isinstance(expected, dict): + # Expected is a subschema dict; value must be a dict and match the schema if not isinstance(value, dict): - msg = f'Invalid value type for {".".join(path)}: ' - msg += f'expected dict, got {type(value).__name__}' - raise InvalidPreferenceValue(msg) - elif not isinstance(value, expected): - msg = f'Invalid value type for {".".join(path)}: ' - msg += f'expected {expected.__name__}, got {type(value).__name__}' - raise InvalidPreferenceValue(msg) + raise InvalidPreferenceValue( + f'Invalid value type for {".".join(path)}: expected dict, got {type(value).__name__}' + ) + # Recursively validate each key-value in the value dict + for k, v in value.items(): + if k not in expected: + raise InvalidPreferencePath(f'Invalid key "{k}" in preference path {".".join(path)}') + sub_expected = expected[k] + ChromiumOptions._validate_pref_value(path + [k], v) + else: + # Expected is a primitive type; check isinstance + if not isinstance(value, expected): + raise InvalidPreferenceValue( + f'Invalid value type for {".".join(path)}: expected {expected.__name__}, got {type(value).__name__}' + ) def _get_pref_path(self, path: list): """ @@ -297,7 +303,7 @@ def password_manager_enabled(self, enabled: bool): enabled: If True, the password manager is active. """ self._set_pref_path(['profile', 'password_manager_enabled'], enabled) - self._set_pref_path(['credentials_enable_service'], enabled) + self._browser_preferences['credentials_enable_service'] = enabled @property def block_notifications(self) -> bool: @@ -376,9 +382,8 @@ def headless(self, headless: bool): self._headless = headless has_argument = '--headless' in self.arguments methods_map = {True: self.add_argument, False: self.remove_argument} - if headless == has_argument: - return - methods_map[headless]('--headless') + if headless != has_argument: + methods_map[headless]('--headless') @property def page_load_state(self) -> PageLoadState: From c8731ebc768bf30f88c9548fc31e36979170ee40 Mon Sep 17 00:00:00 2001 From: arthur-rodrigues Date: Mon, 3 Nov 2025 15:19:01 -0300 Subject: [PATCH 5/6] fix: resolve ruff linting errors and type annotation issue --- pydoll/browser/interfaces.py | 3 +-- pydoll/browser/managers/temp_dir_manager.py | 4 ++-- pydoll/browser/options.py | 22 +++++++++++---------- pydoll/browser/preference_types.py | 3 ++- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/pydoll/browser/interfaces.py b/pydoll/browser/interfaces.py index 6690ef38..4f029cef 100644 --- a/pydoll/browser/interfaces.py +++ b/pydoll/browser/interfaces.py @@ -1,8 +1,7 @@ from abc import ABC, abstractmethod -from pydoll.constants import PageLoadState - from pydoll.browser.preference_types import BrowserPreferences +from pydoll.constants import PageLoadState class Options(ABC): diff --git a/pydoll/browser/managers/temp_dir_manager.py b/pydoll/browser/managers/temp_dir_manager.py index 919a3397..6626b739 100644 --- a/pydoll/browser/managers/temp_dir_manager.py +++ b/pydoll/browser/managers/temp_dir_manager.py @@ -79,11 +79,11 @@ def handle_cleanup_error(self, func: Callable[[str], None], path: str, exc_info: Note: Handles Chromium-specific locked files like CrashpadMetrics. """ - matches = ['CrashpadMetrics-active.pma'] + matches = ['CrashpadMetrics-active.pma', 'Cookies', 'Network'] exc_type, exc_value, _ = exc_info if exc_type is PermissionError: - if Path(path).name in matches: + if Path(path).name in matches or 'Network' in str(Path(path)): try: self.retry_process_file(func, path) return diff --git a/pydoll/browser/options.py b/pydoll/browser/options.py index 88e5fa65..3ee4a1f4 100644 --- a/pydoll/browser/options.py +++ b/pydoll/browser/options.py @@ -31,7 +31,7 @@ def __init__(self): self._arguments: list[str] = [] self._binary_location: str = '' self._start_timeout: int = 10 - self._browser_preferences: BrowserPreferences = {} + self._browser_preferences: dict[str, Any] = {} self._headless: bool = False self._page_load_state: PageLoadState = PageLoadState.COMPLETE @@ -126,7 +126,7 @@ def remove_argument(self, argument: str): @property def browser_preferences(self) -> BrowserPreferences: - return self._browser_preferences + return cast(BrowserPreferences, self._browser_preferences) @browser_preferences.setter def browser_preferences(self, preferences: BrowserPreferences): @@ -191,20 +191,22 @@ def _validate_pref_value(path: list[str], value: Any) -> None: # Expected is a subschema dict; value must be a dict and match the schema if not isinstance(value, dict): raise InvalidPreferenceValue( - f'Invalid value type for {".".join(path)}: expected dict, got {type(value).__name__}' + f'Invalid value type for {".".join(path)}: ' + f'expected dict, got {type(value).__name__}' ) # Recursively validate each key-value in the value dict for k, v in value.items(): if k not in expected: - raise InvalidPreferencePath(f'Invalid key "{k}" in preference path {".".join(path)}') - sub_expected = expected[k] + raise InvalidPreferencePath( + f'Invalid key "{k}" in preference path {".".join(path)}' + ) ChromiumOptions._validate_pref_value(path + [k], v) - else: + elif not isinstance(value, expected): # Expected is a primitive type; check isinstance - if not isinstance(value, expected): - raise InvalidPreferenceValue( - f'Invalid value type for {".".join(path)}: expected {expected.__name__}, got {type(value).__name__}' - ) + raise InvalidPreferenceValue( + f'Invalid value type for {".".join(path)}: ' + f'expected {expected.__name__}, got {type(value).__name__}' + ) def _get_pref_path(self, path: list): """ diff --git a/pydoll/browser/preference_types.py b/pydoll/browser/preference_types.py index c41edbf9..d91fde29 100644 --- a/pydoll/browser/preference_types.py +++ b/pydoll/browser/preference_types.py @@ -1,4 +1,5 @@ -from typing import TypedDict, Any +from typing import TypedDict + from typing_extensions import NotRequired From 91e318b5a53009f6e6ac8cc24a5fdb3bad6a48e7 Mon Sep 17 00:00:00 2001 From: arthur-rodrigues Date: Mon, 3 Nov 2025 16:14:30 -0300 Subject: [PATCH 6/6] Add tests to cover lines 192-203 in options.py for dict validation --- .../test_browser/test_browser_preferences.py | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/test_browser/test_browser_preferences.py b/tests/test_browser/test_browser_preferences.py index 1d7580d9..e02b3c73 100644 --- a/tests/test_browser/test_browser_preferences.py +++ b/tests/test_browser/test_browser_preferences.py @@ -153,4 +153,25 @@ def test_set_pref_path_creates_structure(): } }) options._set_pref_path(['profile', 'default_content_setting_values', 'popups'], 0) - assert cast(Any, options.browser_preferences)['profile']['default_content_setting_values']['popups'] == 0 \ No newline at end of file + assert cast(Any, options.browser_preferences)['profile']['default_content_setting_values']['popups'] == 0 + + +def test_validate_pref_value_dict_invalid_type(): + """Test that passing non-dict value for dict expected raises InvalidPreferenceValue.""" + options = ChromiumOptions() + with pytest.raises(InvalidPreferenceValue): + options._validate_pref_value(['profile', 'default_content_setting_values'], 'not_a_dict') + + +def test_validate_pref_value_dict_invalid_key(): + """Test that passing dict with invalid key raises InvalidPreferencePath.""" + options = ChromiumOptions() + with pytest.raises(InvalidPreferencePath): + options._validate_pref_value(['profile', 'default_content_setting_values'], {'invalid_key': 1}) + + +def test_validate_pref_value_dict_valid(): + """Test that passing valid dict for dict expected works.""" + options = ChromiumOptions() + # Should not raise + options._validate_pref_value(['profile', 'default_content_setting_values'], {'popups': 1, 'notifications': 2}) \ No newline at end of file