""" 浏览器配置模块 提供跨浏览器测试的配置和工具函数 """ from typing import Dict, List, Optional, Tuple from dataclasses import dataclass from enum import Enum from playwright.sync_api import Browser, BrowserType, BrowserContext, Page from playwright.sync_api import Error as PlaywrightError from playwright.sync_api import sync_playwright from config.settings import get_settings class BrowserTypeEnum(Enum): """支持的浏览器类型""" CHROMIUM = "chromium" FIREFOX = "firefox" WEBKIT = "webkit" @dataclass class BrowserCapabilities: """浏览器能力描述""" name: str display_name: str channel: Optional[str] is_headless_supported: bool default_viewport: tuple user_agent: str description: str class BrowserConfigManager: """浏览器配置管理器""" # 浏览器能力定义 BROWSER_CAPABILITIES: Dict[str, BrowserCapabilities] = { "chromium": BrowserCapabilities( name="chromium", display_name="Chrome/Chromium", channel="chrome", is_headless_supported=True, default_viewport=(1920, 1080), user_agent=( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" ), description="Google Chrome / Chromium浏览器" ), "firefox": BrowserCapabilities( name="firefox", display_name="Firefox", channel=None, is_headless_supported=True, default_viewport=(1920, 1080), user_agent=( "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) " "Gecko/20100101 Firefox/121.0" ), description="Mozilla Firefox浏览器" ), "webkit": BrowserCapabilities( name="webkit", display_name="WebKit (Safari)", channel=None, is_headless_supported=True, default_viewport=(1920, 1080), user_agent=( "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2) " "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15" ), description="Apple WebKit (Safari)浏览器" ) } def __init__(self): self.settings = get_settings() self._playwright: Optional[sync_playwright] = None self._browser: Optional[Browser] = None self._context: Optional[BrowserContext] = None def _ensure_playwright(self) -> sync_playwright: """确保Playwright实例已启动""" if self._playwright is None: self._playwright = sync_playwright().start() return self._playwright def get_available_browsers(self) -> List[str]: """获取可用的浏览器列表""" available = [] p = self._ensure_playwright() browser_map = { "chromium": p.chromium, "firefox": p.firefox, "webkit": p.webkit } for name in browser_map: try: available.append(name) except Exception: continue return available def _get_browser_type(self, browser_name: str): """获取浏览器类型""" p = self._ensure_playwright() browser_map = { "chromium": p.chromium, "firefox": p.firefox, "webkit": p.webkit } return browser_map.get(browser_name) def launch_browser( self, browser_name: str = "chromium", headless: bool = False, viewport: Optional[Tuple[int, int]] = None, **kwargs ) -> Browser: """启动浏览器""" capabilities = self.BROWSER_CAPABILITIES.get(browser_name) if not capabilities: raise ValueError(f"不支持的浏览器类型: {browser_name}") viewport = viewport or (self.settings.viewport_width, self.settings.viewport_height) launch_args = self._get_launch_arguments(browser_name, headless) browser_type = self._get_browser_type(browser_name) if not browser_type: raise ValueError(f"不支持的浏览器类型: {browser_name}") self._browser = browser_type.launch( headless=headless, args=launch_args, **kwargs ) return self._browser def _get_launch_arguments(self, browser_name: str, headless: bool) -> List[str]: """获取浏览器启动参数""" args = [] if browser_name == "chromium": args.extend([ "--disable-extensions", "--disable-background-networking", "--disable-sync", "--disable-translate", "--metrics-recording-only", "--mute-audio", "--no-first-run", "--safebrowsing-disable-auto-update", "--ignore-certificate-errors", "--ignore-ssl-errors", "--disable-dev-shm-usage", ]) if headless: args.extend([ "--headless=new", "--disable-gpu", "--no-sandbox", ]) elif browser_name == "firefox": if headless: args.extend(["-headless"]) args.extend([ "-profile", "/tmp/firefox-profile", ]) elif browser_name == "webkit": if headless: args.append("--headless") args.extend([ "--no-sandbox", "--disable-setuid-sandbox", ]) return args def create_context( self, browser: Browser, viewport: Optional[Tuple[int, int]] = None, **context_kwargs ) -> BrowserContext: """创建浏览器上下文""" viewport = viewport or (self.settings.viewport_width, self.settings.viewport_height) capabilities = self.BROWSER_CAPABILITIES.get( browser.browser_type.name, self.BROWSER_CAPABILITIES["chromium"] ) context_options = { "viewport": { "width": viewport[0], "height": viewport[1] }, "user_agent": capabilities.user_agent, "locale": "zh-CN", "timezone_id": "Asia/Shanghai", **context_kwargs } self._context = browser.new_context(**context_options) return self._context def create_browser_session( self, browser_name: str = "chromium", headless: bool = False, viewport: Optional[Tuple[int, int]] = None ) -> Tuple[Browser, BrowserContext, Page]: """创建完整的浏览器会话""" browser = self.launch_browser(browser_name, headless, viewport) context = self.create_context(browser, viewport) page = context.new_page() return browser, context, page def close_browser(self) -> None: """关闭浏览器和上下文""" if self._context: try: self._context.close() except Exception: pass self._context = None if self._browser: try: self._browser.close() except Exception: pass self._browser = None if self._playwright: try: self._playwright.stop() except Exception: pass self._playwright = None def __enter__(self) -> 'BrowserConfigManager': """上下文管理器入口""" return self def __exit__(self, exc_type, exc_val, exc_tb) -> None: """上下文管理器退出""" self.close_browser() def get_browser_factory() -> BrowserConfigManager: """获取浏览器工厂实例""" return BrowserConfigManager()