feat(browser): implement modular browser management system

Adds a new browser management system with strategy pattern implementation:
- Introduces BrowserManager class with strategy pattern support
- Adds PlaywrightBrowserStrategy, CDPBrowserStrategy, and BuiltinBrowserStrategy
- Implements BrowserProfileManager for profile management
- Adds PagePoolConfig for browser page pooling
- Includes comprehensive test suite for all browser strategies

BREAKING CHANGE: Browser management has been moved to browser/ module. Direct usage of browser_manager.py and browser_profiler.py is deprecated.
This commit is contained in:
UncleCode
2025-03-21 22:50:00 +08:00
parent 6432ff1257
commit 4ab0893ffb
16 changed files with 2964 additions and 8 deletions

View File

@@ -0,0 +1,190 @@
"""Test examples for BrowserManager.
These examples demonstrate the functionality of BrowserManager
and serve as functional tests.
"""
import asyncio
import os
import sys
from typing import List
# Add the project root to Python path if running directly
if __name__ == "__main__":
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
from crawl4ai.browser import BrowserManager
from crawl4ai.async_configs import BrowserConfig, CrawlerRunConfig
from crawl4ai.async_logger import AsyncLogger
# Create a logger for clear terminal output
logger = AsyncLogger(verbose=True, log_file=None)
async def test_basic_browser_manager():
"""Test basic BrowserManager functionality with default configuration."""
logger.info("Starting test_basic_browser_manager", tag="TEST")
try:
# Create a browser manager with default config
manager = BrowserManager(logger=logger)
# Start the browser
await manager.start()
logger.info("Browser started successfully", tag="TEST")
# Get a page
crawler_config = CrawlerRunConfig(url="https://example.com")
page, context = await manager.get_page(crawler_config)
logger.info("Page created successfully", tag="TEST")
# Navigate to a website
await page.goto("https://example.com")
title = await page.title()
logger.info(f"Page title: {title}", tag="TEST")
# Clean up
await manager.close()
logger.success("test_basic_browser_manager completed successfully", tag="TEST")
return True
except Exception as e:
logger.error(f"test_basic_browser_manager failed: {str(e)}", tag="TEST")
return False
async def test_custom_browser_config():
"""Test BrowserManager with custom browser configuration."""
logger.info("Starting test_custom_browser_config", tag="TEST")
try:
# Create a custom browser config
browser_config = BrowserConfig(
browser_type="chromium",
headless=True,
viewport_width=1280,
viewport_height=800,
light_mode=True
)
# Create browser manager with the config
manager = BrowserManager(browser_config=browser_config, logger=logger)
# Start the browser
await manager.start()
logger.info("Browser started successfully with custom config", tag="TEST")
# Get a page
crawler_config = CrawlerRunConfig(url="https://example.com")
page, context = await manager.get_page(crawler_config)
# Navigate to a website
await page.goto("https://example.com")
title = await page.title()
logger.info(f"Page title: {title}", tag="TEST")
# Verify viewport size
viewport_size = await page.evaluate("() => ({ width: window.innerWidth, height: window.innerHeight })")
logger.info(f"Viewport size: {viewport_size}", tag="TEST")
# Clean up
await manager.close()
logger.success("test_custom_browser_config completed successfully", tag="TEST")
return True
except Exception as e:
logger.error(f"test_custom_browser_config failed: {str(e)}", tag="TEST")
return False
async def test_multiple_pages():
"""Test BrowserManager with multiple pages."""
logger.info("Starting test_multiple_pages", tag="TEST")
try:
# Create browser manager
manager = BrowserManager(logger=logger)
# Start the browser
await manager.start()
logger.info("Browser started successfully", tag="TEST")
# Create multiple pages
pages = []
urls = ["https://example.com", "https://example.org", "https://mozilla.org"]
for i, url in enumerate(urls):
crawler_config = CrawlerRunConfig(url=url)
page, context = await manager.get_page(crawler_config)
await page.goto(url)
pages.append((page, url))
logger.info(f"Created page {i+1} for {url}", tag="TEST")
# Verify all pages are loaded correctly
for i, (page, url) in enumerate(pages):
title = await page.title()
logger.info(f"Page {i+1} title: {title}", tag="TEST")
# Clean up
await manager.close()
logger.success("test_multiple_pages completed successfully", tag="TEST")
return True
except Exception as e:
logger.error(f"test_multiple_pages failed: {str(e)}", tag="TEST")
return False
async def test_session_management():
"""Test session management in BrowserManager."""
logger.info("Starting test_session_management", tag="TEST")
try:
# Create browser manager
manager = BrowserManager(logger=logger)
# Start the browser
await manager.start()
logger.info("Browser started successfully", tag="TEST")
# Create a session
session_id = "test_session_1"
crawler_config = CrawlerRunConfig(url="https://example.com", session_id=session_id)
page1, context1 = await manager.get_page(crawler_config)
await page1.goto("https://example.com")
logger.info(f"Created session with ID: {session_id}", tag="TEST")
# Get the same session again
page2, context2 = await manager.get_page(crawler_config)
# Verify it's the same page/context
is_same_page = page1 == page2
is_same_context = context1 == context2
logger.info(f"Same page: {is_same_page}, Same context: {is_same_context}", tag="TEST")
# Kill the session
await manager.kill_session(session_id)
logger.info(f"Killed session with ID: {session_id}", tag="TEST")
# Clean up
await manager.close()
logger.success("test_session_management completed successfully", tag="TEST")
return True
except Exception as e:
logger.error(f"test_session_management failed: {str(e)}", tag="TEST")
return False
async def run_tests():
"""Run all tests sequentially."""
results = []
# results.append(await test_basic_browser_manager())
# results.append(await test_custom_browser_config())
# results.append(await test_multiple_pages())
results.append(await test_session_management())
# Print summary
total = len(results)
passed = sum(results)
logger.info(f"Tests complete: {passed}/{total} passed", tag="SUMMARY")
if passed == total:
logger.success("All tests passed!", tag="SUMMARY")
else:
logger.error(f"{total - passed} tests failed", tag="SUMMARY")
if __name__ == "__main__":
asyncio.run(run_tests())

View File

@@ -0,0 +1,160 @@
"""Test examples for BuiltinBrowserStrategy.
These examples demonstrate the functionality of BuiltinBrowserStrategy
and serve as functional tests.
"""
import asyncio
import os
import sys
# Add the project root to Python path if running directly
if __name__ == "__main__":
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
from crawl4ai.browser import BrowserManager
from crawl4ai.async_configs import BrowserConfig, CrawlerRunConfig
from crawl4ai.async_logger import AsyncLogger
# Create a logger for clear terminal output
logger = AsyncLogger(verbose=True, log_file=None)
async def test_builtin_browser():
"""Test using a builtin browser that persists between sessions."""
logger.info("Testing builtin browser", tag="TEST")
browser_config = BrowserConfig(
browser_mode="builtin",
headless=True
)
manager = BrowserManager(browser_config=browser_config, logger=logger)
try:
# Start should connect to existing builtin browser or create one
await manager.start()
logger.info("Connected to builtin browser", tag="TEST")
# Test page creation
crawler_config = CrawlerRunConfig()
page, context = await manager.get_page(crawler_config)
# Test navigation
await page.goto("https://example.com")
title = await page.title()
logger.info(f"Page title: {title}", tag="TEST")
# Close manager (should not close the builtin browser)
await manager.close()
logger.info("First session closed", tag="TEST")
# Create a second manager to verify browser persistence
logger.info("Creating second session to verify persistence", tag="TEST")
manager2 = BrowserManager(browser_config=browser_config, logger=logger)
await manager2.start()
logger.info("Connected to existing builtin browser", tag="TEST")
page2, context2 = await manager2.get_page(crawler_config)
await page2.goto("https://example.org")
title2 = await page2.title()
logger.info(f"Second session page title: {title2}", tag="TEST")
await manager2.close()
logger.info("Second session closed successfully", tag="TEST")
return True
except Exception as e:
logger.error(f"Test failed: {str(e)}", tag="TEST")
try:
await manager.close()
except:
pass
return False
async def test_builtin_browser_status():
"""Test getting status of the builtin browser."""
logger.info("Testing builtin browser status", tag="TEST")
from crawl4ai.browser.strategies import BuiltinBrowserStrategy
browser_config = BrowserConfig(
browser_mode="builtin",
headless=True
)
# Create strategy directly to access its status methods
strategy = BuiltinBrowserStrategy(browser_config, logger)
try:
# Get status before starting (should be not running)
status_before = await strategy.get_builtin_browser_status()
logger.info(f"Initial status: {status_before}", tag="TEST")
# Start the browser
await strategy.start()
logger.info("Browser started successfully", tag="TEST")
# Get status after starting
status_after = await strategy.get_builtin_browser_status()
logger.info(f"Status after start: {status_after}", tag="TEST")
# Create a page to verify functionality
crawler_config = CrawlerRunConfig()
page, context = await strategy.get_page(crawler_config)
await page.goto("https://example.com")
title = await page.title()
logger.info(f"Page title: {title}", tag="TEST")
# Close strategy (should not kill the builtin browser)
await strategy.close()
logger.info("Strategy closed successfully", tag="TEST")
# Create a new strategy object
strategy2 = BuiltinBrowserStrategy(browser_config, logger)
# Get status again (should still be running)
status_final = await strategy2.get_builtin_browser_status()
logger.info(f"Final status: {status_final}", tag="TEST")
# Verify that the status shows the browser is running
is_running = status_final.get('running', False)
logger.info(f"Builtin browser persistence confirmed: {is_running}", tag="TEST")
# Kill the builtin browser to clean up
logger.info("Killing builtin browser", tag="TEST")
success = await strategy2.kill_builtin_browser()
logger.info(f"Killed builtin browser successfully: {success}", tag="TEST")
return is_running and success
except Exception as e:
logger.error(f"Test failed: {str(e)}", tag="TEST")
try:
await strategy.close()
# Try to kill the builtin browser to clean up
strategy2 = BuiltinBrowserStrategy(browser_config, logger)
await strategy2.kill_builtin_browser()
except:
pass
return False
async def run_tests():
"""Run all tests sequentially."""
results = []
results.append(await test_builtin_browser())
results.append(await test_builtin_browser_status())
# Print summary
total = len(results)
passed = sum(results)
logger.info(f"Tests complete: {passed}/{total} passed", tag="SUMMARY")
if passed == total:
logger.success("All tests passed!", tag="SUMMARY")
else:
logger.error(f"{total - passed} tests failed", tag="SUMMARY")
if __name__ == "__main__":
asyncio.run(run_tests())

View File

@@ -0,0 +1,227 @@
"""Test examples for CDPBrowserStrategy.
These examples demonstrate the functionality of CDPBrowserStrategy
and serve as functional tests.
"""
import asyncio
import os
import sys
# Add the project root to Python path if running directly
if __name__ == "__main__":
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
from crawl4ai.browser import BrowserManager
from crawl4ai.async_configs import BrowserConfig, CrawlerRunConfig
from crawl4ai.async_logger import AsyncLogger
# Create a logger for clear terminal output
logger = AsyncLogger(verbose=True, log_file=None)
async def test_cdp_launch_connect():
"""Test launching a browser and connecting via CDP."""
logger.info("Testing launch and connect via CDP", tag="TEST")
browser_config = BrowserConfig(
use_managed_browser=True,
headless=True
)
manager = BrowserManager(browser_config=browser_config, logger=logger)
try:
await manager.start()
logger.info("Browser launched and connected via CDP", tag="TEST")
# Test with multiple pages
pages = []
for i in range(3):
crawler_config = CrawlerRunConfig()
page, context = await manager.get_page(crawler_config)
await page.goto(f"https://example.com?test={i}")
pages.append(page)
logger.info(f"Created page {i+1}", tag="TEST")
# Verify all pages are working
for i, page in enumerate(pages):
title = await page.title()
logger.info(f"Page {i+1} title: {title}", tag="TEST")
await manager.close()
logger.info("Browser closed successfully", tag="TEST")
return True
except Exception as e:
logger.error(f"Test failed: {str(e)}", tag="TEST")
try:
await manager.close()
except:
pass
return False
async def test_cdp_with_user_data_dir():
"""Test CDP browser with a user data directory."""
logger.info("Testing CDP browser with user data directory", tag="TEST")
# Create a temporary user data directory
import tempfile
user_data_dir = tempfile.mkdtemp(prefix="crawl4ai-test-")
logger.info(f"Created temporary user data directory: {user_data_dir}", tag="TEST")
browser_config = BrowserConfig(
use_managed_browser=True,
headless=True,
user_data_dir=user_data_dir
)
manager = BrowserManager(browser_config=browser_config, logger=logger)
try:
await manager.start()
logger.info("Browser launched with user data directory", tag="TEST")
# Navigate to a page and store some data
crawler_config = CrawlerRunConfig()
page, context = await manager.get_page(crawler_config)
# Set a cookie
await context.add_cookies([{
"name": "test_cookie",
"value": "test_value",
"url": "https://example.com"
}])
# Visit the site
await page.goto("https://example.com")
# Verify cookie was set
cookies = await context.cookies(["https://example.com"])
has_test_cookie = any(cookie["name"] == "test_cookie" for cookie in cookies)
logger.info(f"Cookie set successfully: {has_test_cookie}", tag="TEST")
# Close the browser
await manager.close()
logger.info("First browser session closed", tag="TEST")
# Start a new browser with the same user data directory
logger.info("Starting second browser session with same user data directory", tag="TEST")
manager2 = BrowserManager(browser_config=browser_config, logger=logger)
await manager2.start()
# Get a new page and check if the cookie persists
page2, context2 = await manager2.get_page(crawler_config)
await page2.goto("https://example.com")
# Verify cookie persisted
cookies2 = await context2.cookies(["https://example.com"])
has_test_cookie2 = any(cookie["name"] == "test_cookie" for cookie in cookies2)
logger.info(f"Cookie persisted across sessions: {has_test_cookie2}", tag="TEST")
# Clean up
await manager2.close()
# Remove temporary directory
import shutil
shutil.rmtree(user_data_dir, ignore_errors=True)
logger.info(f"Removed temporary user data directory", tag="TEST")
return has_test_cookie and has_test_cookie2
except Exception as e:
logger.error(f"Test failed: {str(e)}", tag="TEST")
try:
await manager.close()
except:
pass
# Clean up temporary directory
try:
import shutil
shutil.rmtree(user_data_dir, ignore_errors=True)
except:
pass
return False
async def test_cdp_session_management():
"""Test session management with CDP browser."""
logger.info("Testing session management with CDP browser", tag="TEST")
browser_config = BrowserConfig(
use_managed_browser=True,
headless=True
)
manager = BrowserManager(browser_config=browser_config, logger=logger)
try:
await manager.start()
logger.info("Browser launched successfully", tag="TEST")
# Create two sessions
session1_id = "test_session_1"
session2_id = "test_session_2"
# Set up first session
crawler_config1 = CrawlerRunConfig(session_id=session1_id)
page1, context1 = await manager.get_page(crawler_config1)
await page1.goto("https://example.com")
await page1.evaluate("localStorage.setItem('session1_data', 'test_value')")
logger.info(f"Set up session 1 with ID: {session1_id}", tag="TEST")
# Set up second session
crawler_config2 = CrawlerRunConfig(session_id=session2_id)
page2, context2 = await manager.get_page(crawler_config2)
await page2.goto("https://example.org")
await page2.evaluate("localStorage.setItem('session2_data', 'test_value2')")
logger.info(f"Set up session 2 with ID: {session2_id}", tag="TEST")
# Get first session again
page1_again, _ = await manager.get_page(crawler_config1)
# Verify it's the same page and data persists
is_same_page = page1 == page1_again
data1 = await page1_again.evaluate("localStorage.getItem('session1_data')")
logger.info(f"Session 1 reuse successful: {is_same_page}, data: {data1}", tag="TEST")
# Kill first session
await manager.kill_session(session1_id)
logger.info(f"Killed session 1", tag="TEST")
# Verify second session still works
data2 = await page2.evaluate("localStorage.getItem('session2_data')")
logger.info(f"Session 2 still functional after killing session 1, data: {data2}", tag="TEST")
# Clean up
await manager.close()
logger.info("Browser closed successfully", tag="TEST")
return is_same_page and data1 == "test_value" and data2 == "test_value2"
except Exception as e:
logger.error(f"Test failed: {str(e)}", tag="TEST")
try:
await manager.close()
except:
pass
return False
async def run_tests():
"""Run all tests sequentially."""
results = []
results.append(await test_cdp_launch_connect())
results.append(await test_cdp_with_user_data_dir())
results.append(await test_cdp_session_management())
# Print summary
total = len(results)
passed = sum(results)
logger.info(f"Tests complete: {passed}/{total} passed", tag="SUMMARY")
if passed == total:
logger.success("All tests passed!", tag="SUMMARY")
else:
logger.error(f"{total - passed} tests failed", tag="SUMMARY")
if __name__ == "__main__":
asyncio.run(run_tests())

View File

@@ -0,0 +1,77 @@
"""Combined test runner for all browser module tests.
This script runs all the browser module tests in sequence and
provides a comprehensive summary.
"""
import asyncio
import os
import sys
import time
# Add the project root to Python path if running directly
if __name__ == "__main__":
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
from crawl4ai.async_logger import AsyncLogger
# Create a logger for clear terminal output
logger = AsyncLogger(verbose=True, log_file=None)
async def run_test_module(module_name, header):
"""Run all tests in a module and return results."""
logger.info(f"\n{'-'*30}", tag="TEST")
logger.info(f"RUNNING: {header}", tag="TEST")
logger.info(f"{'-'*30}", tag="TEST")
# Import the module dynamically
module = __import__(f"tests.browser.{module_name}", fromlist=["run_tests"])
# Track time for performance measurement
start_time = time.time()
# Run the tests
await module.run_tests()
# Calculate time taken
time_taken = time.time() - start_time
logger.info(f"Time taken: {time_taken:.2f} seconds", tag="TIMING")
return time_taken
async def main():
"""Run all test modules."""
logger.info("STARTING COMPREHENSIVE BROWSER MODULE TESTS", tag="MAIN")
# List of test modules to run
test_modules = [
("test_browser_manager", "Browser Manager Tests"),
("test_playwright_strategy", "Playwright Strategy Tests"),
("test_cdp_strategy", "CDP Strategy Tests"),
("test_builtin_strategy", "Builtin Browser Strategy Tests"),
("test_profiles", "Profile Management Tests")
]
# Run each test module
timings = {}
for module_name, header in test_modules:
try:
time_taken = await run_test_module(module_name, header)
timings[module_name] = time_taken
except Exception as e:
logger.error(f"Error running {module_name}: {str(e)}", tag="ERROR")
# Print summary
logger.info("\n\nTEST SUMMARY:", tag="SUMMARY")
logger.info(f"{'-'*50}", tag="SUMMARY")
for module_name, header in test_modules:
if module_name in timings:
logger.info(f"{header}: {timings[module_name]:.2f} seconds", tag="SUMMARY")
else:
logger.error(f"{header}: FAILED TO RUN", tag="SUMMARY")
logger.info(f"{'-'*50}", tag="SUMMARY")
total_time = sum(timings.values())
logger.info(f"Total time: {total_time:.2f} seconds", tag="SUMMARY")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,275 @@
"""Test examples for PlaywrightBrowserStrategy.
These examples demonstrate the functionality of PlaywrightBrowserStrategy
and serve as functional tests.
"""
import asyncio
import os
import sys
# Add the project root to Python path if running directly
if __name__ == "__main__":
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
from crawl4ai.browser import BrowserManager
from crawl4ai.async_configs import BrowserConfig, CrawlerRunConfig
from crawl4ai.async_logger import AsyncLogger
# Create a logger for clear terminal output
logger = AsyncLogger(verbose=True, log_file=None)
async def test_playwright_basic():
"""Test basic Playwright browser functionality."""
logger.info("Testing standard Playwright browser", tag="TEST")
# Create browser config for standard Playwright
browser_config = BrowserConfig(
headless=True,
viewport_width=1280,
viewport_height=800
)
# Create browser manager with the config
manager = BrowserManager(browser_config=browser_config, logger=logger)
try:
# Start the browser
await manager.start()
logger.info("Browser started successfully", tag="TEST")
# Create crawler config
crawler_config = CrawlerRunConfig(url="https://example.com")
# Get a page
page, context = await manager.get_page(crawler_config)
logger.info("Got page successfully", tag="TEST")
# Navigate to a website
await page.goto("https://example.com")
logger.info("Navigated to example.com", tag="TEST")
# Get page title
title = await page.title()
logger.info(f"Page title: {title}", tag="TEST")
# Clean up
await manager.close()
logger.info("Browser closed successfully", tag="TEST")
return True
except Exception as e:
logger.error(f"Test failed: {str(e)}", tag="TEST")
# Ensure cleanup
try:
await manager.close()
except:
pass
return False
async def test_playwright_text_mode():
"""Test Playwright browser in text-only mode."""
logger.info("Testing Playwright text mode", tag="TEST")
# Create browser config with text mode enabled
browser_config = BrowserConfig(
headless=True,
text_mode=True # Enable text-only mode
)
# Create browser manager with the config
manager = BrowserManager(browser_config=browser_config, logger=logger)
try:
# Start the browser
await manager.start()
logger.info("Browser started successfully in text mode", tag="TEST")
# Get a page
crawler_config = CrawlerRunConfig(url="https://example.com")
page, context = await manager.get_page(crawler_config)
# Navigate to a website
await page.goto("https://example.com")
logger.info("Navigated to example.com", tag="TEST")
# Get page title
title = await page.title()
logger.info(f"Page title: {title}", tag="TEST")
# Check if images are blocked in text mode
# We'll check if any image requests were made
has_images = False
async with page.expect_request("**/*.{png,jpg,jpeg,gif,webp,svg}", timeout=1000) as request_info:
try:
# Try to load a page with images
await page.goto("https://picsum.photos/", wait_until="domcontentloaded")
request = await request_info.value
has_images = True
except:
# Timeout without image requests means text mode is working
has_images = False
logger.info(f"Text mode image blocking working: {not has_images}", tag="TEST")
# Clean up
await manager.close()
logger.info("Browser closed successfully", tag="TEST")
return True
except Exception as e:
logger.error(f"Test failed: {str(e)}", tag="TEST")
# Ensure cleanup
try:
await manager.close()
except:
pass
return False
async def test_playwright_context_reuse():
"""Test context caching and reuse with identical configurations."""
logger.info("Testing context reuse with identical configurations", tag="TEST")
# Create browser config
browser_config = BrowserConfig(headless=True)
# Create browser manager
manager = BrowserManager(browser_config=browser_config, logger=logger)
try:
# Start the browser
await manager.start()
logger.info("Browser started successfully", tag="TEST")
# Create identical crawler configs
crawler_config1 = CrawlerRunConfig(
url="https://example.com",
viewport_width=1280,
viewport_height=800
)
crawler_config2 = CrawlerRunConfig(
url="https://example.org", # Different URL but same browser parameters
viewport_width=1280,
viewport_height=800
)
# Get pages with these configs
page1, context1 = await manager.get_page(crawler_config1)
page2, context2 = await manager.get_page(crawler_config2)
# Check if contexts are reused
is_same_context = context1 == context2
logger.info(f"Contexts reused: {is_same_context}", tag="TEST")
# Now try with a different config
crawler_config3 = CrawlerRunConfig(
url="https://example.net",
viewport_width=800, # Different viewport size
viewport_height=600
)
page3, context3 = await manager.get_page(crawler_config3)
# This should be a different context
is_different_context = context1 != context3
logger.info(f"Different contexts for different configs: {is_different_context}", tag="TEST")
# Clean up
await manager.close()
logger.info("Browser closed successfully", tag="TEST")
# Both tests should pass for success
return is_same_context and is_different_context
except Exception as e:
logger.error(f"Test failed: {str(e)}", tag="TEST")
# Ensure cleanup
try:
await manager.close()
except:
pass
return False
async def test_playwright_session_management():
"""Test session management with Playwright browser."""
logger.info("Testing session management with Playwright browser", tag="TEST")
browser_config = BrowserConfig(
headless=True
)
manager = BrowserManager(browser_config=browser_config, logger=logger)
try:
await manager.start()
logger.info("Browser launched successfully", tag="TEST")
# Create two sessions
session1_id = "playwright_session_1"
session2_id = "playwright_session_2"
# Set up first session
crawler_config1 = CrawlerRunConfig(session_id=session1_id, url="https://example.com")
page1, context1 = await manager.get_page(crawler_config1)
await page1.goto("https://example.com")
await page1.evaluate("localStorage.setItem('playwright_session1_data', 'test_value1')")
logger.info(f"Set up session 1 with ID: {session1_id}", tag="TEST")
# Set up second session
crawler_config2 = CrawlerRunConfig(session_id=session2_id, url="https://example.org")
page2, context2 = await manager.get_page(crawler_config2)
await page2.goto("https://example.org")
await page2.evaluate("localStorage.setItem('playwright_session2_data', 'test_value2')")
logger.info(f"Set up session 2 with ID: {session2_id}", tag="TEST")
# Get first session again
page1_again, context1_again = await manager.get_page(crawler_config1)
# Verify it's the same page and data persists
is_same_page = page1 == page1_again
is_same_context = context1 == context1_again
data1 = await page1_again.evaluate("localStorage.getItem('playwright_session1_data')")
logger.info(f"Session 1 reuse successful: {is_same_page}, data: {data1}", tag="TEST")
# Kill first session
await manager.kill_session(session1_id)
logger.info(f"Killed session 1", tag="TEST")
# Verify second session still works
data2 = await page2.evaluate("localStorage.getItem('playwright_session2_data')")
logger.info(f"Session 2 still functional after killing session 1, data: {data2}", tag="TEST")
# Clean up
await manager.close()
logger.info("Browser closed successfully", tag="TEST")
return is_same_page and is_same_context and data1 == "test_value1" and data2 == "test_value2"
except Exception as e:
logger.error(f"Test failed: {str(e)}", tag="TEST")
try:
await manager.close()
except:
pass
return False
async def run_tests():
"""Run all tests sequentially."""
results = []
results.append(await test_playwright_basic())
results.append(await test_playwright_text_mode())
results.append(await test_playwright_context_reuse())
results.append(await test_playwright_session_management())
# Print summary
total = len(results)
passed = sum(results)
logger.info(f"Tests complete: {passed}/{total} passed", tag="SUMMARY")
if passed == total:
logger.success("All tests passed!", tag="SUMMARY")
else:
logger.error(f"{total - passed} tests failed", tag="SUMMARY")
if __name__ == "__main__":
asyncio.run(run_tests())

View File

@@ -0,0 +1,176 @@
"""Test examples for BrowserProfileManager.
These examples demonstrate the functionality of BrowserProfileManager
and serve as functional tests.
"""
import asyncio
import os
import sys
import uuid
import shutil
# Add the project root to Python path if running directly
if __name__ == "__main__":
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
from crawl4ai.browser import BrowserManager, BrowserProfileManager
from crawl4ai.async_configs import BrowserConfig, CrawlerRunConfig
from crawl4ai.async_logger import AsyncLogger
# Create a logger for clear terminal output
logger = AsyncLogger(verbose=True, log_file=None)
async def test_profile_creation():
"""Test creating and managing browser profiles."""
logger.info("Testing profile creation and management", tag="TEST")
profile_manager = BrowserProfileManager(logger=logger)
try:
# List existing profiles
profiles = profile_manager.list_profiles()
logger.info(f"Found {len(profiles)} existing profiles", tag="TEST")
# Generate a unique profile name for testing
test_profile_name = f"test-profile-{uuid.uuid4().hex[:8]}"
# Create a test profile directory
profile_path = os.path.join(profile_manager.profiles_dir, test_profile_name)
os.makedirs(os.path.join(profile_path, "Default"), exist_ok=True)
# Create a dummy Preferences file to simulate a Chrome profile
with open(os.path.join(profile_path, "Default", "Preferences"), "w") as f:
f.write("{\"test\": true}")
logger.info(f"Created test profile at: {profile_path}", tag="TEST")
# Verify the profile is now in the list
profiles = profile_manager.list_profiles()
profile_found = any(p["name"] == test_profile_name for p in profiles)
logger.info(f"Profile found in list: {profile_found}", tag="TEST")
# Try to get the profile path
retrieved_path = profile_manager.get_profile_path(test_profile_name)
path_match = retrieved_path == profile_path
logger.info(f"Retrieved correct profile path: {path_match}", tag="TEST")
# Delete the profile
success = profile_manager.delete_profile(test_profile_name)
logger.info(f"Profile deletion successful: {success}", tag="TEST")
# Verify it's gone
profiles_after = profile_manager.list_profiles()
profile_removed = not any(p["name"] == test_profile_name for p in profiles_after)
logger.info(f"Profile removed from list: {profile_removed}", tag="TEST")
# Clean up just in case
if os.path.exists(profile_path):
shutil.rmtree(profile_path, ignore_errors=True)
return profile_found and path_match and success and profile_removed
except Exception as e:
logger.error(f"Test failed: {str(e)}", tag="TEST")
# Clean up test directory
try:
if os.path.exists(profile_path):
shutil.rmtree(profile_path, ignore_errors=True)
except:
pass
return False
async def test_profile_with_browser():
"""Test using a profile with a browser."""
logger.info("Testing using a profile with a browser", tag="TEST")
profile_manager = BrowserProfileManager(logger=logger)
test_profile_name = f"test-browser-profile-{uuid.uuid4().hex[:8]}"
profile_path = None
try:
# Create a test profile directory
profile_path = os.path.join(profile_manager.profiles_dir, test_profile_name)
os.makedirs(os.path.join(profile_path, "Default"), exist_ok=True)
# Create a dummy Preferences file to simulate a Chrome profile
with open(os.path.join(profile_path, "Default", "Preferences"), "w") as f:
f.write("{\"test\": true}")
logger.info(f"Created test profile at: {profile_path}", tag="TEST")
# Now use this profile with a browser
browser_config = BrowserConfig(
user_data_dir=profile_path,
headless=True
)
manager = BrowserManager(browser_config=browser_config, logger=logger)
# Start the browser with the profile
await manager.start()
logger.info("Browser started with profile", tag="TEST")
# Create a page
crawler_config = CrawlerRunConfig()
page, context = await manager.get_page(crawler_config)
# Navigate and set some data to verify profile works
await page.goto("https://example.com")
await page.evaluate("localStorage.setItem('test_data', 'profile_value')")
# Close browser
await manager.close()
logger.info("First browser session closed", tag="TEST")
# Create a new browser with the same profile
manager2 = BrowserManager(browser_config=browser_config, logger=logger)
await manager2.start()
logger.info("Second browser session started with same profile", tag="TEST")
# Get a page and check if the data persists
page2, context2 = await manager2.get_page(crawler_config)
await page2.goto("https://example.com")
data = await page2.evaluate("localStorage.getItem('test_data')")
# Verify data persisted
data_persisted = data == "profile_value"
logger.info(f"Data persisted across sessions: {data_persisted}", tag="TEST")
# Clean up
await manager2.close()
logger.info("Second browser session closed", tag="TEST")
# Delete the test profile
success = profile_manager.delete_profile(test_profile_name)
logger.info(f"Test profile deleted: {success}", tag="TEST")
return data_persisted and success
except Exception as e:
logger.error(f"Test failed: {str(e)}", tag="TEST")
# Clean up
try:
if profile_path and os.path.exists(profile_path):
shutil.rmtree(profile_path, ignore_errors=True)
except:
pass
return False
async def run_tests():
"""Run all tests sequentially."""
results = []
results.append(await test_profile_creation())
results.append(await test_profile_with_browser())
# Print summary
total = len(results)
passed = sum(results)
logger.info(f"Tests complete: {passed}/{total} passed", tag="SUMMARY")
if passed == total:
logger.success("All tests passed!", tag="SUMMARY")
else:
logger.error(f"{total - passed} tests failed", tag="SUMMARY")
if __name__ == "__main__":
asyncio.run(run_tests())