refactor browser config setup process

This commit is contained in:
Nick Sweeting
2025-03-26 19:46:47 -07:00
committed by Nick Sweeting
parent ff14e42b83
commit 69a960fa4b
22 changed files with 306 additions and 164 deletions
+108 -48
View File
@@ -7,18 +7,24 @@ import gc
import logging
import os
import subprocess
from dataclasses import dataclass, field
from typing import Literal
from typing import Any, Literal
import requests
from playwright._impl._api_structures import ProxySettings
from playwright.async_api import Browser as PlaywrightBrowser
from playwright.async_api import (
Playwright,
async_playwright,
)
from pydantic import BaseModel, ConfigDict, Field, field_validator
from typing_extensions import TypedDict
from browser_use.browser.chrome import CHROME_ARGS, CHROME_DISABLE_SECURITY_ARGS, CHROME_DOCKER_ARGS, CHROME_HEADLESS_ARGS
from browser_use.browser.chrome import (
CHROME_ARGS,
CHROME_DETERMINISTIC_RENDERING_ARGS,
CHROME_DISABLE_SECURITY_ARGS,
CHROME_DOCKER_ARGS,
CHROME_HEADLESS_ARGS,
)
from browser_use.browser.context import BrowserContext, BrowserContextConfig
from browser_use.browser.utils.screen_resolution import get_screen_resolution, get_window_adjustments
from browser_use.utils import time_execution_async
@@ -29,17 +35,23 @@ logger = logging.getLogger(__name__)
IN_DOCKER = os.environ.get('IN_DOCKER', 'false').lower() == 'true'
@dataclass
class BrowserConfig:
class ProxySettings(TypedDict, total=False):
server: str
bypass: str | None
username: str | None
password: str | None
class BrowserConfig(BaseModel):
r"""
Configuration for the Browser.
Default values:
headless: True
Whether to run browser in headless mode
headless: False
Whether to run browser in headless mode (not recommended)
disable_security: True
Disable browser security features
disable_security: False
Disable browser security features (required for cross-origin iframe support)
extra_browser_args: []
Extra arguments to pass to the browser
@@ -50,23 +62,53 @@ class BrowserConfig:
cdp_url: None
Connect to a browser instance via CDP
browser_instance_path: None
browser_binary_path: None
Path to a Browser instance to use to connect to your normal browser
e.g. '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome'
keep_alive: False
Keep the browser alive after the agent has finished running
deterministic_rendering: False
Enable deterministic rendering (makes GPU/font rendering consistent across different OS's and docker)
"""
headless: bool = False
disable_security: bool = True
extra_browser_args: list[str] = field(default_factory=list)
browser_instance_path: str | None = None
model_config = ConfigDict(arbitrary_types_allowed=True, extra='ignore')
wss_url: str | None = None
cdp_url: str | None = None
proxy: ProxySettings | None = field(default=None)
new_context_config: BrowserContextConfig = field(default_factory=BrowserContextConfig)
_force_keep_browser_alive: bool = False
browser_class: Literal['chromium', 'firefox', 'webkit'] = 'chromium'
browser_binary_path: str | None = None
browser_instance_path: str | None = Field(None, deprecated=True) # Old name for browser_binary_path
chrome_instance_path: str | None = Field(None, deprecated=True) # Old name for browser_binary_path
extra_browser_args: list[str] = Field(default_factory=list)
headless: bool = False
disable_security: bool = False
deterministic_rendering: bool = False
keep_alive: bool = False # used to be called _force_keep_browser_alive
proxy: ProxySettings | None = None
new_context_config: BrowserContextConfig = Field(default_factory=BrowserContextConfig)
@field_validator('browser_binary_path', mode='before')
def handle_browser_instance_path(cls, v: Any, info: Any) -> Any:
# If browser_binary_path is None but browser_instance_path is set, use that value
if v is None and ('browser_instance_path' in info.data or 'chrome_instance_path' in info.data):
return info.data.get('browser_instance_path', info.data.get('chrome_instance_path'))
assert info.data.get('browser_class') == 'chromium', (
'browser_binary_path only supports chromium-based browsers (make sure browser_class=chromium)'
)
return v
@field_validator('keep_alive', mode='before')
def handle_force_keep_browser_alive(cls, v: Any, info: Any) -> Any:
# If keep_alive is False but _force_keep_browser_alive is set, use that value
if v is False and '_force_keep_browser_alive' in info.data:
return info.data.get('_force_keep_browser_alive')
return v
# @singleton: TODO - think about id singleton makes sense here
@@ -110,9 +152,9 @@ class Browser:
return self.playwright_browser
async def _setup_cdp(self, playwright: Playwright) -> PlaywrightBrowser:
async def _setup_remote_cdp_browser(self, playwright: Playwright) -> PlaywrightBrowser:
"""Sets up and returns a Playwright Browser instance with anti-detection measures. Firefox has no longer CDP support."""
if 'firefox' in (self.config.browser_instance_path or '').lower():
if 'firefox' in (self.config.browser_binary_path or '').lower():
raise ValueError(
'CDP has been deprecated for firefox, check: https://fxdx.dev/deprecating-cdp-support-in-firefox-embracing-the-future-with-webdriver-bidi/'
)
@@ -123,7 +165,7 @@ class Browser:
browser = await browser_class.connect_over_cdp(self.config.cdp_url)
return browser
async def _setup_wss(self, playwright: Playwright) -> PlaywrightBrowser:
async def _setup_remote_wss_browser(self, playwright: Playwright) -> PlaywrightBrowser:
"""Sets up and returns a Playwright Browser instance with anti-detection measures."""
if not self.config.wss_url:
raise ValueError('WSS URL is required')
@@ -132,16 +174,20 @@ class Browser:
browser = await browser_class.connect(self.config.wss_url)
return browser
async def _setup_browser_with_instance(self, playwright: Playwright) -> PlaywrightBrowser:
async def _setup_user_provided_browser(self, playwright: Playwright) -> PlaywrightBrowser:
"""Sets up and returns a Playwright Browser instance with anti-detection measures."""
if not self.config.browser_instance_path:
raise ValueError('Chrome instance path is required')
if not self.config.browser_binary_path:
raise ValueError('A browser_binary_path is required')
assert self.config.browser_class == 'chromium', (
'browser_binary_path only supports chromium browsers (make sure browser_class=chromium)'
)
try:
# Check if browser is already running
response = requests.get('http://localhost:9222/json/version', timeout=2)
if response.status_code == 200:
logger.info('Reusing existing Chrome instance')
logger.info('Re-using existing browser found running on http://localhost:9222')
browser_class = getattr(playwright, self.config.browser_class)
browser = await browser_class.connect_over_cdp(
endpoint_url='http://localhost:9222',
@@ -153,12 +199,13 @@ class Browser:
# Start a new Chrome instance
chrome_launch_cmd = [
self.config.browser_instance_path,
self.config.browser_binary_path,
*{ # remove duplicates (usually preserves the order, but not guaranteed)
*CHROME_ARGS,
*(CHROME_DOCKER_ARGS if IN_DOCKER else []),
*(CHROME_HEADLESS_ARGS if self.config.headless else []),
*(CHROME_DISABLE_SECURITY_ARGS if self.config.disable_security else []),
*(CHROME_DETERMINISTIC_RENDERING_ARGS if self.config.deterministic_rendering else []),
*self.config.extra_browser_args,
},
]
@@ -192,10 +239,17 @@ class Browser:
' To start chrome in Debug mode, you need to close all existing Chrome instances and try again otherwise we can not connect to the instance.'
)
async def _setup_standard_browser(self, playwright: Playwright) -> PlaywrightBrowser:
async def _setup_builtin_browser(self, playwright: Playwright) -> PlaywrightBrowser:
"""Sets up and returns a Playwright Browser instance with anti-detection measures."""
screen_size = get_screen_resolution()
offset_x, offset_y = get_window_adjustments()
assert self.config.browser_binary_path is None, 'browser_binary_path should be None if trying to use the builtin browsers'
if self.config.headless:
screen_size = {'width': 1920, 'height': 1080}
offset_x, offset_y = 0, 0
else:
screen_size = get_screen_resolution()
offset_x, offset_y = get_window_adjustments()
browser_class = getattr(playwright, self.config.browser_class)
args = {
'chromium': list(
@@ -204,6 +258,7 @@ class Browser:
*(CHROME_DOCKER_ARGS if IN_DOCKER else []),
*(CHROME_HEADLESS_ARGS if self.config.headless else []),
*(CHROME_DISABLE_SECURITY_ARGS if self.config.disable_security else []),
*(CHROME_DETERMINISTIC_RENDERING_ARGS if self.config.deterministic_rendering else []),
f'--window-position={offset_x},{offset_y}',
f'--window-size={screen_size["width"]},{screen_size["height"]}',
*self.config.extra_browser_args,
@@ -222,48 +277,53 @@ class Browser:
}
],
}
browser_class = getattr(playwright, self.config.browser_class)
browser = await browser_class.launch(
headless=self.config.headless,
args=args[self.config.browser_class],
proxy=self.config.proxy,
)
# convert to Browser
return browser
async def _setup_browser(self, playwright: Playwright) -> PlaywrightBrowser:
"""Sets up and returns a Playwright Browser instance with anti-detection measures."""
try:
if self.config.cdp_url:
return await self._setup_cdp(playwright)
return await self._setup_remote_cdp_browser(playwright)
if self.config.wss_url:
return await self._setup_wss(playwright)
elif self.config.browser_instance_path:
return await self._setup_browser_with_instance(playwright)
return await self._setup_remote_wss_browser(playwright)
if self.config.headless:
logger.warning('⚠️ Headless mode is not recommended. Many sites will detect and block all headless browsers.')
if self.config.browser_binary_path:
return await self._setup_user_provided_browser(playwright)
else:
return await self._setup_standard_browser(playwright)
return await self._setup_builtin_browser(playwright)
except Exception as e:
logger.error(f'Failed to initialize Playwright browser: {str(e)}')
logger.error(f'Failed to initialize Playwright browser: {e}')
raise
async def close(self):
"""Close the browser instance"""
if self.config.keep_alive:
return
try:
if not self.config._force_keep_browser_alive:
if self.playwright_browser:
await self.playwright_browser.close()
del self.playwright_browser
if self.playwright:
await self.playwright.stop()
del self.playwright
# Then cleanup httpx clients
await self.cleanup_httpx_clients()
if self.playwright_browser:
await self.playwright_browser.close()
del self.playwright_browser
if self.playwright:
await self.playwright.stop()
del self.playwright
# Then cleanup httpx clients
await self.cleanup_httpx_clients()
except Exception as e:
logger.debug(f'Failed to close browser properly: {e}')
finally:
self.playwright_browser = None
self.playwright = None
gc.collect()
def __del__(self):
+40 -36
View File
@@ -61,49 +61,15 @@ CHROME_DISABLE_SECURITY_ARGS = [
'--allow-insecure-localhost',
]
CHROME_ARGS = [
# Profile data dir setup
# chrome://profile-internals
# f'--user-data-dir={CHROME_PROFILE_PATH}', # managed by playwright arg instead
# f'--profile-directory={CHROME_PROFILE_USER}',
'--password-store=basic', # use mock keychain instead of OS-provided keychain (we manage auth.json instead)
'--use-mock-keychain',
'--disable-cookie-encryption', # we need to be able to write unencrypted cookies to save/load auth.json
'--disable-sync', # don't try to use Google account sync features while automation is active
# Extensions
# chrome://inspect/#extensions
# f'--load-extension={CHROME_EXTENSIONS.map(({unpacked_path}) => unpacked_path).join(',')}', # not needed when using existing profile that already has extensions installed
f'--allowlisted-extension-id={",".join(CHROME_EXTENSIONS.keys())}',
'--allow-legacy-extension-manifests',
# flags to make chrome behave more deterministically across different OS's
CHROME_DETERMINISTIC_RENDERING_ARGS = [
'--deterministic-mode',
'--js-flags=--random-seed=1157259159', # make all JS random numbers deterministic by providing a seed
'--allow-pre-commit-input', # allow JS mutations before page rendering is complete
'--disable-blink-features=AutomationControlled', # hide the signatures that announce browser is being remote-controlled
# f'--proxy-server=https://43.159.28.126:2334:u7ce652b7568805c4-zone-custom-region-us-session-szGWq3FRU-sessTime-60:u7ce652b7568805c4', # send all network traffic through a proxy https://2captcha.com/proxy
# f'--proxy-bypass-list=127.0.0.1',
# Browser window and viewport setup
# chrome://version
# f'--user-agent="{DEFAULT_USER_AGENT}"',
# f'--window-size={DEFAULT_VIEWPORT.width},{DEFAULT_VIEWPORT.height}',
# '--window-position=0,0',
# '--start-maximized',
'--force-device-scale-factor=1',
'--hide-scrollbars', # hide scrollbars because otherwise they show up in screenshots
'--install-autogenerated-theme=0,0,0', # black border makes it easier to see which chrome window is browser-use's
#'--virtual-time-budget=60000', # fast-forward all animations & timers by 60s, dont use this it's unfortunately buggy and breaks screenshot and PDF capture sometimes
#'--autoplay-policy=no-user-gesture-required', # auto-start videos so they trigger network requests + show up in outputs
#'--disable-gesture-requirement-for-media-playback',
#'--lang=en-US,en;q=0.9',
# IO: stdin/stdout, debug port config
# chrome://inspect
'--log-level=2', # 1=DEBUG 2=WARNING 3=ERROR
'--enable-logging=stderr',
'--remote-debugging-address=0.0.0.0',
f'--remote-debugging-port={CHROME_DEBUG_PORT}',
# GPU, canvas, text, and pdf rendering config
# chrome://gpu
'--enable-webgl', # enable web-gl graphics support
'--enable-experimental-extension-apis', # add support for tab groups
'--font-render-hinting=none', # make rendering more deterministic by ignoring OS font hints, may also need css override, try: * {text-rendering: geometricprecision !important; -webkit-font-smoothing: antialiased;}
'--force-color-profile=srgb', # make rendering more deterministic by using consitent color profile, if browser looks weird, try: generic-rgb
'--disable-partial-raster', # make rendering more deterministic (TODO: verify if still needed)
@@ -126,6 +92,44 @@ CHROME_ARGS = [
'--disable-extensions-http-throttling', # dont throttle http traffic based on runtime heuristics
'--disable-field-trial-config', # disable shared field trial state between browser processes
'--disable-back-forward-cache', # disable browsing navigation cache
]
CHROME_ARGS = [
# Profile data dir setup
# chrome://profile-internals
# f'--user-data-dir={CHROME_PROFILE_PATH}', # managed by playwright arg instead
# f'--profile-directory={CHROME_PROFILE_USER}',
'--password-store=basic', # use mock keychain instead of OS-provided keychain (we manage auth.json instead)
'--use-mock-keychain',
'--disable-cookie-encryption', # we need to be able to write unencrypted cookies to save/load auth.json
'--disable-sync', # don't try to use Google account sync features while automation is active
# Extensions
# chrome://inspect/#extensions
# f'--load-extension={CHROME_EXTENSIONS.map(({unpacked_path}) => unpacked_path).join(',')}', # not needed when using existing profile that already has extensions installed
f'--allowlisted-extension-id={",".join(CHROME_EXTENSIONS.keys())}',
'--allow-legacy-extension-manifests',
'--allow-pre-commit-input', # allow JS mutations before page rendering is complete
'--disable-blink-features=AutomationControlled', # hide the signatures that announce browser is being remote-controlled
# f'--proxy-server=https://43.159.28.126:2334:u7ce652b7568805c4-zone-custom-region-us-session-szGWq3FRU-sessTime-60:u7ce652b7568805c4', # send all network traffic through a proxy https://2captcha.com/proxy
# f'--proxy-bypass-list=127.0.0.1',
# Browser window and viewport setup
# chrome://version
# f'--user-agent="{DEFAULT_USER_AGENT}"',
# f'--window-size={DEFAULT_VIEWPORT.width},{DEFAULT_VIEWPORT.height}',
# '--window-position=0,0',
# '--start-maximized',
'--install-autogenerated-theme=0,0,0', # black border makes it easier to see which chrome window is browser-use's
#'--virtual-time-budget=60000', # fast-forward all animations & timers by 60s, dont use this it's unfortunately buggy and breaks screenshot and PDF capture sometimes
#'--autoplay-policy=no-user-gesture-required', # auto-start videos so they trigger network requests + show up in outputs
#'--disable-gesture-requirement-for-media-playback',
#'--lang=en-US,en;q=0.9',
# IO: stdin/stdout, debug port config
# chrome://inspect
'--log-level=2', # 1=DEBUG 2=WARNING 3=ERROR
'--enable-logging=stderr',
'--remote-debugging-address=0.0.0.0',
f'--remote-debugging-port={CHROME_DEBUG_PORT}',
'--enable-experimental-extension-apis', # add support for tab groups
'--disable-focus-on-load', # prevent browser from hijacking focus
'--disable-window-activation',
# '--in-process-gpu', <- DONT USE THIS, makes headful startup time ~5-10s slower (tested v121 Google Chrome.app on macOS)
+30 -18
View File
@@ -11,8 +11,8 @@ import os
import re
import time
import uuid
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Optional, TypedDict
from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional
from playwright._impl._errors import TimeoutError
from playwright.async_api import Browser as PlaywrightBrowser
@@ -24,6 +24,8 @@ from playwright.async_api import (
FrameLocator,
Page,
)
from pydantic import BaseModel, ConfigDict, Field
from typing_extensions import TypedDict
from browser_use.browser.views import (
BrowserError,
@@ -46,8 +48,7 @@ class BrowserContextWindowSize(TypedDict):
height: int
@dataclass
class BrowserContextConfig:
class BrowserContextConfig(BaseModel):
"""
Configuration for the BrowserContext.
@@ -124,6 +125,8 @@ class BrowserContextConfig:
Changes the timezone of the browser. Example: 'Europe/Berlin'
"""
model_config = ConfigDict(arbitrary_types_allowed=True, extra='ignore')
cookies_file: str | None = None
minimum_wait_page_load_time: float = 0.25
wait_for_network_idle_page_load_time: float = 0.5
@@ -132,7 +135,7 @@ class BrowserContextConfig:
disable_security: bool = True
browser_window_size: BrowserContextWindowSize = field(default_factory=lambda: {'width': 1280, 'height': 1100})
browser_window_size: BrowserContextWindowSize | None = Field(default_factory=lambda: {'width': 1280, 'height': 1100})
no_viewport: Optional[bool] = None
save_recording_path: str | None = None
@@ -149,7 +152,7 @@ class BrowserContextConfig:
allowed_domains: list[str] | None = None
include_dynamic_attributes: bool = True
_force_keep_context_alive: bool = False
keep_alive: bool = False # used to be called _force_keep_context_alive
is_mobile: bool | None = None
has_touch: bool | None = None
geolocation: dict | None = None
@@ -176,13 +179,13 @@ class BrowserContext:
def __init__(
self,
browser: 'Browser',
config: BrowserContextConfig = BrowserContextConfig(),
config: BrowserContextConfig | None = None,
state: Optional[BrowserContextState] = None,
):
self.context_id = str(uuid.uuid4())
logger.debug(f'Initializing new browser context with id: {self.context_id}')
self.config = config
self.config = config or BrowserContextConfig(**browser.config)
self.browser = browser
self.state = state or BrowserContextState()
@@ -202,7 +205,6 @@ class BrowserContext:
@time_execution_async('--close')
async def close(self):
"""Close the browser instance"""
logger.debug('Closing browser context')
try:
if self.session is None:
@@ -226,7 +228,8 @@ class BrowserContext:
logger.debug(f'Failed to stop tracing: {e}')
# This is crucial - it closes the CDP connection
if not self.config._force_keep_context_alive:
if not self.config.keep_alive:
logger.debug('Closing browser context')
try:
await self.session.context.close()
except Exception as e:
@@ -239,7 +242,7 @@ class BrowserContext:
def __del__(self):
"""Cleanup when object is destroyed"""
if not self.config._force_keep_context_alive and self.session is not None:
if not self.config.keep_alive and self.session is not None:
logger.debug('BrowserContext was not properly closed before destruction')
try:
# Use sync Playwright method for force cleanup
@@ -327,7 +330,11 @@ class BrowserContext:
async def get_session(self) -> BrowserSession:
"""Lazy initialization of the browser and related components"""
if self.session is None:
return await self._initialize_session()
try:
return await self._initialize_session()
except Exception as e:
logger.error(f'❌ Failed to create new browser session: {e} (did the browser process quit?)')
raise e
return self.session
async def get_current_page(self) -> Page:
@@ -339,7 +346,7 @@ class BrowserContext:
"""Creates a new browser context with anti-detection measures and loads cookies if available."""
if self.browser.config.cdp_url and len(browser.contexts) > 0:
context = browser.contexts[0]
elif self.browser.config.browser_instance_path and len(browser.contexts) > 0:
elif self.browser.config.browser_binary_path and len(browser.contexts) > 0:
# Connect to existing Chrome instance instead of creating new one
context = browser.contexts[0]
else:
@@ -1172,7 +1179,7 @@ class BrowserContext:
"""
current_frame = await self.get_current_page()
try:
elements = await current_frame.query_selector_all(f"text={text}")
elements = await current_frame.query_selector_all(f'text={text}')
# considering only visible elements
elements = [el for el in elements if await el.is_visible()]
@@ -1195,8 +1202,6 @@ class BrowserContext:
logger.error(f"Failed to locate element by text '{text}': {str(e)}")
return None
@time_execution_async('--input_text_element_node')
async def _input_text_element_node(self, element_node: DOMElementNode, text: str):
"""
@@ -1387,8 +1392,15 @@ class BrowserContext:
if non_extension_pages:
return non_extension_pages[-1]
# Fallback to opening a new tab
return await session.context.new_page()
# Fallback to opening a new tab in the active window
try:
return await session.context.new_page()
except Exception:
# there is no browser window available (perhaps the user closed it?)
# reopen a new window in the browser and try again
logger.warning('⚠️ No browser window available, opening a new window')
await self._initialize_session()
return await session.context.new_page()
async def get_selector_map(self) -> SelectorMap:
session = await self.get_session()
+34 -30
View File
@@ -1,37 +1,41 @@
import sys
def get_screen_resolution():
if sys.platform == "darwin": # macOS
try:
from AppKit import NSScreen
screen = NSScreen.mainScreen().frame()
return {"width": int(screen.size.width), "height": int(screen.size.height)}
except ImportError:
print("AppKit is not available. Make sure you are running this on macOS.")
except Exception as e:
print(f"Error retrieving macOS screen resolution: {e}")
return {"width": 2560, "height": 1664}
if sys.platform == 'darwin': # macOS
try:
from AppKit import NSScreen
else: # Windows & Linux
try:
from screeninfo import get_monitors
monitors = get_monitors()
if not monitors:
raise Exception("No monitors detected.")
monitor = monitors[0]
return {"width": monitor.width, "height": monitor.height}
except ImportError:
print("screeninfo package not found. Install it using 'pip install screeninfo'.")
except Exception as e:
print(f"Error retrieving screen resolution: {e}")
screen = NSScreen.mainScreen().frame()
return {'width': int(screen.size.width), 'height': int(screen.size.height)}
except ImportError:
print('AppKit is not available. Make sure you are running this on macOS with pyobjc installed.')
except Exception as e:
print(f'Error retrieving macOS screen resolution: {e}')
return {'width': 2560, 'height': 1664}
else: # Windows & Linux
try:
from screeninfo import get_monitors
monitors = get_monitors()
if not monitors:
raise Exception('No monitors detected.')
monitor = monitors[0]
return {'width': monitor.width, 'height': monitor.height}
except ImportError:
print("screeninfo package not found. Install it using 'pip install screeninfo'.")
except Exception as e:
print(f'Error retrieving screen resolution: {e}')
return {'width': 1920, 'height': 1080}
return {"width": 1920, "height": 1080}
def get_window_adjustments():
"""Returns recommended x, y offsets for window positioning"""
if sys.platform == "darwin": # macOS
return -4, 24 # macOS has a small title bar, no border
elif sys.platform == "win32": # Windows
return -8, 0 # Windows has a border on the left
else: # Linux
return 0, 0
"""Returns recommended x, y offsets for window positioning"""
if sys.platform == 'darwin': # macOS
return -4, 24 # macOS has a small title bar, no border
elif sys.platform == 'win32': # Windows
return -8, 0 # Windows has a border on the left
else: # Linux
return 0, 0
+1 -1
View File
@@ -88,7 +88,7 @@ async def test_focus_vs_all_elements():
browser = Browser(
config=BrowserConfig(
# browser_instance_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
# browser_binary_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
)
)
context = BrowserContext(browser=browser, config=config) # noqa: F821
+2 -2
View File
@@ -98,11 +98,11 @@ Connect to your existing Chrome installation to access saved states and cookies.
```python
config = BrowserConfig(
browser_instance_path="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
browser_binary_path="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
)
```
- **browser_instance_path** (default: `None`)
- **browser_binary_path** (default: `None`)
Path to connect to an existing Browser installation. Particularly useful for workflows requiring existing login states or browser preferences.
<Note>This will overwrite other browser settings.</Note>
+1 -1
View File
@@ -24,7 +24,7 @@ import asyncio
browser = Browser(
config=BrowserConfig(
# Specify the path to your Chrome executable
browser_instance_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', # macOS path
browser_binary_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', # macOS path
# For Windows, typically: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
# For Linux, typically: '/usr/bin/google-chrome'
)
+1 -1
View File
@@ -16,7 +16,7 @@ from browser_use.browser.context import BrowserContext
browser = Browser(
config=BrowserConfig(
# NOTE: you need to close your chrome browser - so that this can open your browser in debug mode
browser_instance_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
browser_binary_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
)
)
+58
View File
@@ -0,0 +1,58 @@
import asyncio
import os
import sys
from langchain_openai import ChatOpenAI
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from browser_use import Agent, Browser, BrowserConfig, BrowserContextConfig
llm = ChatOpenAI(model='gpt-4o')
browser = Browser(
config=BrowserConfig(
headless=False,
disable_security=False,
keep_alive=True,
new_context_config=BrowserContextConfig(
keep_alive=True,
disable_security=False,
),
)
)
async def main():
agent = Agent(
task="""
Go to https://www.webflow.com/ and verify that the page is not blocked by a bot check.
""",
llm=llm,
browser=browser,
)
await agent.run()
input('Press Enter to continue to the next test...')
agent = Agent(
task="""
Go to https://www.okta.com/ and verify that the page is not blocked by a bot check.
""",
llm=llm,
browser=browser,
)
await agent.run()
agent = Agent(
task="""
Go to https://nowsecure.nl/ check the "I'm not a robot" checkbox.
""",
llm=llm,
browser=browser,
)
await agent.run()
input('Press Enter to close the browser...')
if __name__ == '__main__':
asyncio.run(main())
+1 -1
View File
@@ -20,7 +20,7 @@ logger = logging.getLogger(__name__)
browser = Browser(
config=BrowserConfig(
headless=False,
browser_instance_path=='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
browser_binary_path=='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
)
)
controller = Controller()
+1 -1
View File
@@ -24,7 +24,7 @@ if not os.getenv('OPENAI_API_KEY'):
browser = Browser(
config=BrowserConfig(
browser_instance_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
browser_binary_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
)
)
controller = Controller()
+1 -1
View File
@@ -49,7 +49,7 @@ llm = get_llm(args.provider)
browser = Browser(
config=BrowserConfig(
#browser_instance_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
#browser_binary_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
)
)
+1 -1
View File
@@ -15,7 +15,7 @@ llm = ChatOpenAI(
# Get your chrome path
browser = Browser(
config=BrowserConfig(
browser_instance_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
browser_binary_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
new_context_config=BrowserContextConfig(
_force_keep_context_alive=True,
),
+1 -1
View File
@@ -23,7 +23,7 @@ allowed_domains = ['google.com']
browser = Browser(
config=BrowserConfig(
browser_instance_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
browser_binary_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
new_context_config=BrowserContextConfig(
allowed_domains=allowed_domains,
),
+2 -4
View File
@@ -4,9 +4,7 @@ from pprint import pprint
from browser_use.browser.browser import Browser, BrowserConfig
from browser_use.browser.context import (
BrowserContext,
BrowserContextConfig,
BrowserContextWindowSize,
)
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@@ -16,7 +14,6 @@ from langchain_openai import ChatOpenAI
from browser_use import Agent
from browser_use.agent.views import AgentHistoryList
from browser_use.controller.service import Controller
llm = ChatOpenAI(model='gpt-4o')
browser = Browser(
@@ -33,7 +30,8 @@ async def main():
config=BrowserContextConfig(
trace_path='./tmp/result_processing',
no_viewport=False,
browser_window_size=BrowserContextWindowSize(width=1280, height=1000),
browser_window_width=1280,
browser_window_height=1000,
)
) as browser_context:
agent = Agent(
+1 -1
View File
@@ -42,7 +42,7 @@ llm = get_llm()
browser = Browser(
config=BrowserConfig(
# browser_instance_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
# browser_binary_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
)
)
+1 -1
View File
@@ -110,7 +110,7 @@ async def upload_cv(index: int, browser: BrowserContext):
browser = Browser(
config=BrowserConfig(
browser_instance_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
browser_binary_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
disable_security=True,
)
)
+1 -1
View File
@@ -15,7 +15,7 @@ from browser_use.browser.browser import Browser, BrowserConfig
browser = Browser(
config=BrowserConfig(
browser_instance_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
browser_binary_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
),
)
+1 -1
View File
@@ -71,7 +71,7 @@ def create_twitter_agent(config: TwitterConfig) -> Agent:
browser = Browser(
config=BrowserConfig(
headless=config.headless,
browser_instance_path=config.chrome_path,
browser_binary_path=config.chrome_path,
)
)
@@ -21,7 +21,7 @@ llm = ChatGoogleGenerativeAI(model='gemini-2.0-flash-exp', api_key=SecretStr(api
browser = Browser(
config=BrowserConfig(
# browser_instance_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
# browser_binary_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
)
)
file_path = os.path.join(os.path.dirname(__file__), 'twitter_cookies.txt')
+7 -1
View File
@@ -23,7 +23,13 @@ dependencies = [
"langchain-anthropic==0.3.3",
"langchain-ollama==0.2.2",
"google-api-core>=2.24.0",
"pyperclip>=1.9.0", # only used for examples
"pyperclip>=1.9.0",
# only used for examples that use copy/paste
"pyobjc>=11.0; platform_system == 'darwin'",
# only used to get screen resolution on macOS
"screeninfo>=0.8.1; platform_system != 'darwin'",
"typing-extensions>=4.12.2",
# only used to get screen resolution on Linux/Windows
]
urls = { "Repository" = "https://github.com/browser-use/browser-use" }
+12 -12
View File
@@ -10,7 +10,7 @@ from playwright._impl._api_structures import ProxySettings
async def test_standard_browser_launch(monkeypatch):
"""
Test that the standard browser is launched correctly:
When no remote (cdp or wss) or chrome instance is provided, the Browser class uses _setup_standard_browser.
When no remote (cdp or wss) or chrome instance is provided, the Browser class uses _setup_builtin_browser.
This test monkeypatches async_playwright to return dummy objects, and asserts that get_playwright_browser returns the expected DummyBrowser.
"""
class DummyBrowser:
@@ -30,7 +30,7 @@ async def test_standard_browser_launch(monkeypatch):
config = BrowserConfig(headless=True, disable_security=False, extra_browser_args=["--test"])
browser_obj = Browser(config=config)
result_browser = await browser_obj.get_playwright_browser()
assert isinstance(result_browser, DummyBrowser), "Expected DummyBrowser from _setup_standard_browser"
assert isinstance(result_browser, DummyBrowser), "Expected DummyBrowser from _setup_builtin_browser"
await browser_obj.close()
@pytest.mark.asyncio
async def test_cdp_browser_launch(monkeypatch):
@@ -88,7 +88,7 @@ async def test_wss_browser_launch(monkeypatch):
async def test_chrome_instance_browser_launch(monkeypatch):
"""
Test that when a chrome instance path is provided the Browser class uses
_setup_browser_with_instance branch and returns the expected DummyBrowser object
_setup_user_provided_browser branch and returns the expected DummyBrowser object
by reusing an existing Chrome instance.
"""
# Dummy response for requests.get when checking chrome debugging endpoint.
@@ -114,19 +114,19 @@ async def test_chrome_instance_browser_launch(monkeypatch):
async def start(self):
return DummyPlaywright()
monkeypatch.setattr("browser_use.browser.browser.async_playwright", lambda: DummyAsyncPlaywrightContext())
config = BrowserConfig(browser_instance_path="dummy/chrome", extra_browser_args=["--dummy-arg"])
config = BrowserConfig(browser_binary_path="dummy/chrome", extra_browser_args=["--dummy-arg"])
browser_obj = Browser(config=config)
result_browser = await browser_obj.get_playwright_browser()
assert isinstance(result_browser, DummyBrowser), "Expected DummyBrowser from _setup_browser_with_instance"
assert isinstance(result_browser, DummyBrowser), "Expected DummyBrowser from _setup_user_provided_browser"
await browser_obj.close()
@pytest.mark.asyncio
async def test_standard_browser_disable_security_args(monkeypatch):
"""
Test that the standard browser launch includes disable-security arguments when disable_security is True.
This verifies that _setup_standard_browser correctly appends the security disabling arguments along with
This verifies that _setup_builtin_browser correctly appends the security disabling arguments along with
the base arguments and any extra arguments provided.
"""
# These are the base arguments defined in _setup_standard_browser.
# These are the base arguments defined in _setup_builtin_browser.
base_args = [
'--no-sandbox',
'--disable-blink-features=AutomationControlled',
@@ -172,7 +172,7 @@ async def test_standard_browser_disable_security_args(monkeypatch):
config = BrowserConfig(headless=True, disable_security=True, extra_browser_args=extra_args)
browser_obj = Browser(config=config)
result_browser = await browser_obj.get_playwright_browser()
assert isinstance(result_browser, DummyBrowser), "Expected DummyBrowser from _setup_standard_browser with disable_security active"
assert isinstance(result_browser, DummyBrowser), "Expected DummyBrowser from _setup_builtin_browser with disable_security active"
await browser_obj.close()
@pytest.mark.asyncio
async def test_new_context_creation():
@@ -192,7 +192,7 @@ async def test_new_context_creation():
async def test_chrome_instance_browser_launch_failure(monkeypatch):
"""
Test that when a Chrome instance cannot be started or connected to,
the Browser._setup_browser_with_instance branch eventually raises a RuntimeError.
the Browser._setup_user_provided_browser branch eventually raises a RuntimeError.
We simulate failure by:
- Forcing requests.get to always raise a ConnectionError (so no existing instance is found).
- Monkeypatching subprocess.Popen to do nothing.
@@ -218,7 +218,7 @@ async def test_chrome_instance_browser_launch_failure(monkeypatch):
async def start(self):
return DummyPlaywright()
monkeypatch.setattr("browser_use.browser.browser.async_playwright", lambda: DummyAsyncPlaywrightContext())
config = BrowserConfig(browser_instance_path="dummy/chrome", extra_browser_args=["--dummy-arg"])
config = BrowserConfig(browser_binary_path="dummy/chrome", extra_browser_args=["--dummy-arg"])
browser_obj = Browser(config=config)
with pytest.raises(RuntimeError, match="To start chrome in Debug mode"):
await browser_obj.get_playwright_browser()
@@ -273,7 +273,7 @@ async def test_close_error_handling(monkeypatch):
@pytest.mark.asyncio
async def test_standard_browser_launch_with_proxy(monkeypatch):
"""
Test that when a proxy is provided in the BrowserConfig, the _setup_standard_browser method
Test that when a proxy is provided in the BrowserConfig, the _setup_builtin_browser method
correctly passes the proxy parameter to the playwright.chromium.launch method.
This test sets up a dummy async_playwright context and verifies that the dummy proxy is received.
"""
@@ -302,5 +302,5 @@ async def test_standard_browser_launch_with_proxy(monkeypatch):
browser_obj = Browser(config=config)
# Call get_playwright_browser and verify that the returned browser is as expected.
result_browser = await browser_obj.get_playwright_browser()
assert isinstance(result_browser, DummyBrowser), "Expected DummyBrowser from _setup_standard_browser with proxy provided"
assert isinstance(result_browser, DummyBrowser), "Expected DummyBrowser from _setup_builtin_browser with proxy provided"
await browser_obj.close()