diff --git a/crawl4ai/browser_manager.py b/crawl4ai/browser_manager.py index f4ab0aa32..6f7982271 100644 --- a/crawl4ai/browser_manager.py +++ b/crawl4ai/browser_manager.py @@ -157,8 +157,25 @@ def __init__( host (str): Host for debugging the browser. Default: "localhost". debugging_port (int): Port for debugging the browser. Default: 9222. cdp_url (str or None): CDP URL to connect to the browser. Default: None. - browser_config (BrowserConfig): Configuration object containing all browser settings. Default: None. + browser_config (BrowserConfig): Configuration object carrying the same settings. When + given it is used as-is and the individual arguments + above are ignored; when omitted one is built from + them. Default: None. """ + if browser_config is None: + # Every attribute below is read off `browser_config`, so without this the + # documented all-arguments form raised + # `AttributeError: 'NoneType' object has no attribute 'browser_type'`, and the + # arguments themselves reached nothing. + browser_config = BrowserConfig( + browser_type=browser_type, + headless=headless, + user_data_dir=user_data_dir, + host=host, + debugging_port=debugging_port, + cdp_url=cdp_url, + ) + self.browser_type = browser_config.browser_type self.user_data_dir = browser_config.user_data_dir self.headless = browser_config.headless diff --git a/tests/unit/test_managed_browser_arguments.py b/tests/unit/test_managed_browser_arguments.py new file mode 100644 index 000000000..5ca9f06cd --- /dev/null +++ b/tests/unit/test_managed_browser_arguments.py @@ -0,0 +1,66 @@ +"""`ManagedBrowser` reads every setting off `browser_config`. + +Without one, that raised `AttributeError: 'NoneType' object has no attribute +'browser_type'` for the documented all-arguments form, which is how +`BrowserProfiler.launch_builtin_browser` constructs it — so `crwl browser start` +and `crwl browser restart` could not start a browser at all. +""" + +import pytest + +from crawl4ai.async_configs import BrowserConfig +from crawl4ai.browser_manager import ManagedBrowser + + +def test_defaults_construct_without_a_config(): + browser = ManagedBrowser() + + assert browser.browser_type == "chromium" + assert browser.debugging_port == 9222 + assert browser.host == "localhost" + + +def test_launch_builtin_browser_argument_shape_constructs(): + """The exact call `BrowserProfiler.launch_builtin_browser` makes.""" + browser = ManagedBrowser( + browser_type="chromium", + user_data_dir="/tmp/crawl4ai-builtin", + headless=True, + logger=None, + debugging_port=9333, + ) + + assert browser.user_data_dir == "/tmp/crawl4ai-builtin" + assert browser.debugging_port == 9333 + + +@pytest.mark.parametrize( + ("kwargs", "attribute", "expected"), + [ + ({"browser_type": "firefox"}, "browser_type", "firefox"), + ({"user_data_dir": "/tmp/profile"}, "user_data_dir", "/tmp/profile"), + ({"headless": False}, "headless", False), + ({"host": "0.0.0.0"}, "host", "0.0.0.0"), + ({"debugging_port": 9333}, "debugging_port", 9333), + ({"cdp_url": "http://localhost:9444"}, "cdp_url", "http://localhost:9444"), + ], +) +def test_each_argument_reaches_the_instance(kwargs, attribute, expected): + assert getattr(ManagedBrowser(**kwargs), attribute) == expected + + +def test_a_supplied_config_still_wins(): + """Existing callers pass both; the config is the source of truth and stays so.""" + config = BrowserConfig(browser_type="chromium", debugging_port=9222, headless=True) + + browser = ManagedBrowser( + browser_type="firefox", + debugging_port=9333, + headless=False, + browser_config=config, + ) + + assert browser.browser_type == "chromium" + assert browser.debugging_port == 9222 + assert browser.headless is True + assert browser.browser_config is config