base_test.py 9.83 KB
"""
基础测试类 - 提供通用的浏览器自动化测试功能
Python 3.7.8 兼容
"""

import os
import time
from datetime import datetime
from typing import Optional, Dict, Any
from playwright.sync_api import sync_playwright, Page, Browser, BrowserContext


class BaseBrowserTest:
    """浏览器自动化测试基类"""
    
    def __init__(self, headless: bool = False, slow_mo: int = 100):
        """
        初始化浏览器测试
        
        Args:
            headless: 是否无头模式运行
            slow_mo: 操作间隔时间(毫秒)
        """
        self.headless = headless
        self.slow_mo = slow_mo
        self.playwright = None
        self.browser: Optional[Browser] = None
        self.context: Optional[BrowserContext] = None
        self.page: Optional[Page] = None
        
        # 测试配置
        self.base_url = os.environ.get('FRONTEND_URL', 'http://localhost:3001')
        self.screenshots_dir = os.path.join(os.path.dirname(__file__), 'screenshots')
        self.reports_dir = os.path.join(os.path.dirname(__file__), 'reports')
        
        # 创建必要的目录
        os.makedirs(self.screenshots_dir, exist_ok=True)
        os.makedirs(self.reports_dir, exist_ok=True)
    
    def setup_browser(self) -> None:
        """启动浏览器"""
        self.playwright = sync_playwright().start()
        
        # 启动浏览器(使用Chromium)
        self.browser = self.playwright.chromium.launch(
            headless=self.headless,
            slow_mo=self.slow_mo,
            args=['--no-sandbox', '--disable-dev-shm-usage']
        )
        
        # 创建浏览器上下文
        self.context = self.browser.new_context(
            viewport={'width': 1920, 'height': 1080},
            user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
        )
        
        # 创建新页面
        self.page = self.context.new_page()
        
        # 设置默认超时时间
        self.page.set_default_timeout(30000)  # 30秒
    
    def teardown_browser(self) -> None:
        """关闭浏览器"""
        if self.page:
            self.page.close()
        if self.context:
            self.context.close()
        if self.browser:
            self.browser.close()
        if self.playwright:
            self.playwright.stop()
    
    def take_screenshot(self, name: str = None) -> str:
        """
        截图
        
        Args:
            name: 截图文件名(不包含扩展名)
            
        Returns:
            截图文件路径
        """
        if not name:
            timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
            name = f'screenshot_{timestamp}'
        
        screenshot_path = os.path.join(self.screenshots_dir, f'{name}.png')
        self.page.screenshot(path=screenshot_path, full_page=True)
        print(f"截图已保存: {screenshot_path}")
        return screenshot_path
    
    def wait_for_element(self, selector: str, timeout: int = 10000) -> None:
        """
        等待元素出现
        
        Args:
            selector: CSS选择器
            timeout: 超时时间(毫秒)
        """
        self.page.wait_for_selector(selector, timeout=timeout)
    
    def wait_for_text(self, text: str, timeout: int = 10000) -> None:
        """
        等待文本出现
        
        Args:
            text: 要等待的文本
            timeout: 超时时间(毫秒)
        """
        self.page.wait_for_selector(f"text={text}", timeout=timeout)
    
    def fill_input(self, selector: str, value: str) -> None:
        """
        填充输入框
        
        Args:
            selector: CSS选择器
            value: 要输入的值
        """
        self.page.fill(selector, value)
    
    def click_element(self, selector: str) -> None:
        """
        点击元素
        
        Args:
            selector: CSS选择器
        """
        self.page.click(selector)
    
    def select_option(self, selector: str, value: str) -> None:
        """
        选择下拉框选项
        
        Args:
            selector: CSS选择器
            value: 选项值
        """
        self.page.select_option(selector, value)
    
    def get_text(self, selector: str) -> str:
        """
        获取元素文本
        
        Args:
            selector: CSS选择器
            
        Returns:
            元素文本内容
        """
        return self.page.text_content(selector)
    
    def get_element_count(self, selector: str) -> int:
        """
        获取元素数量
        
        Args:
            selector: CSS选择器
            
        Returns:
            元素数量
        """
        return self.page.locator(selector).count()
    
    def is_element_visible(self, selector: str) -> bool:
        """
        检查元素是否可见
        
        Args:
            selector: CSS选择器
            
        Returns:
            元素是否可见
        """
        return self.page.is_visible(selector)
    
    def navigate_to(self, url: str) -> None:
        """
        导航到指定URL
        
        Args:
            url: 目标URL
        """
        self.page.goto(url)
        self.page.wait_for_load_state('networkidle')
    
    def login(self, username: str = 'admin', password: str = 'password') -> bool:
        """
        登录系统
        
        Args:
            username: 用户名
            password: 密码
            
        Returns:
            登录是否成功
        """
        try:
            # 导航到登录页面
            self.navigate_to(self.base_url)
            
            # 等待登录表单加载
            self.wait_for_element('input[placeholder="请输入用户名"]')
            
            # 填写用户名
            self.fill_input('input[placeholder="请输入用户名"]', username)
            
            # 填写密码
            self.fill_input('input[placeholder="请输入密码"]', password)
            
            # 点击登录按钮
            self.click_element('button:has-text("登录")')
            
            # 等待登录完成(检查是否跳转到首页或出现错误)
            try:
                # 等待页面跳转或出现成功/失败提示
                self.page.wait_for_load_state('networkidle', timeout=10000)
                
                # 检查是否成功登录(通过URL或页面元素判断)
                current_url = self.page.url
                
                # 等待页面完全加载
                self.page.wait_for_timeout(2000)
                
                # 检查是否有错误提示
                error_indicators = [
                    'text=用户名或密码错误',
                    'text=登录失败',
                    'text=Invalid',
                    'text=Error',
                    '.error',
                    '.alert-danger'
                ]
                
                has_error = False
                for indicator in error_indicators:
                    if self.is_element_visible(indicator):
                        has_error = True
                        print(f"发现错误提示: {indicator}")
                        break
                
                # 检查是否跳转到其他页面(表示登录成功)
                if has_error:
                    print(f"登录失败,发现错误提示,当前URL: {current_url}")
                    return False
                elif 'dashboard' in current_url or 'main' in current_url:
                    print(f"登录成功,当前URL: {current_url}")
                    return True
                elif current_url == self.base_url or current_url == f'{self.base_url}/':
                    print(f"登录失败,仍在登录页面,当前URL: {current_url}")
                    return False
                else:
                    print(f"登录状态不明确,当前URL: {current_url}")
                    return False
                    
            except Exception as e:
                print(f"登录等待超时: {e}")
                return False
                
        except Exception as e:
            print(f"登录过程出错: {e}")
            self.take_screenshot('login_error')
            return False
    
    def run_test(self, test_name: str, test_func) -> Dict[str, Any]:
        """
        运行测试并生成报告
        
        Args:
            test_name: 测试名称
            test_func: 测试函数
            
        Returns:
            测试结果字典
        """
        start_time = datetime.now()
        result = {
            'test_name': test_name,
            'start_time': start_time.isoformat(),
            'success': False,
            'error': None,
            'screenshots': [],
            'duration': 0
        }
        
        try:
            print(f"\n{'='*50}")
            print(f"开始测试: {test_name}")
            print(f"{'='*50}")
            
            # 设置浏览器
            self.setup_browser()
            
            # 执行测试
            test_func()
            
            result['success'] = True
            print(f"✅ 测试通过: {test_name}")
            
        except Exception as e:
            result['error'] = str(e)
            print(f"❌ 测试失败: {test_name}")
            print(f"错误信息: {e}")
            
            # 失败时截图
            screenshot_path = self.take_screenshot(f'{test_name}_error')
            result['screenshots'].append(screenshot_path)
            
        finally:
            # 清理资源
            self.teardown_browser()
            
            # 计算耗时
            end_time = datetime.now()
            result['end_time'] = end_time.isoformat()
            result['duration'] = (end_time - start_time).total_seconds()
            
            print(f"测试耗时: {result['duration']:.2f}秒")
        
        return result