mirror of
https://github.com/browser-use/browser-use.git
synced 2026-09-14 19:59:47 +08:00
refactor browser config setup process
This commit is contained in:
committed by
Nick Sweeting
parent
ff14e42b83
commit
69a960fa4b
+108
-48
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -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())
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
Reference in New Issue
Block a user