Showing
47 changed files
with
726 additions
and
0 deletions
tests/browser/README.md
0 → 100644
| 1 | +# 浏览器自动化测试 | ||
| 2 | + | ||
| 3 | +基于 Python 3.7.8 和 Playwright 的 Apple ERP 系统浏览器自动化测试框架。 | ||
| 4 | + | ||
| 5 | +## 环境要求 | ||
| 6 | + | ||
| 7 | +- Python 3.7.8+ | ||
| 8 | +- Playwright 浏览器自动化框架 | ||
| 9 | + | ||
| 10 | +## 安装依赖 | ||
| 11 | + | ||
| 12 | +```bash | ||
| 13 | +# 安装 Python 依赖 | ||
| 14 | +pip install -r requirements.txt | ||
| 15 | + | ||
| 16 | +# 安装 Playwright 浏览器 | ||
| 17 | +playwright install chromium | ||
| 18 | +``` | ||
| 19 | + | ||
| 20 | +## 项目结构 | ||
| 21 | + | ||
| 22 | +``` | ||
| 23 | +tests/browser/ | ||
| 24 | +├── __init__.py # 包初始化 | ||
| 25 | +├── base_test.py # 基础测试类 | ||
| 26 | +├── test_login.py # 登录功能测试 | ||
| 27 | +├── test_order_management.py # 订单管理功能测试 | ||
| 28 | +├── test_runner.py # 测试运行器 | ||
| 29 | +├── requirements.txt # 依赖文件 | ||
| 30 | +├── README.md # 说明文档 | ||
| 31 | +├── screenshots/ # 测试截图目录 | ||
| 32 | +└── reports/ # 测试报告目录 | ||
| 33 | +``` | ||
| 34 | + | ||
| 35 | +## 使用方法 | ||
| 36 | + | ||
| 37 | +### 1. 单独运行测试 | ||
| 38 | + | ||
| 39 | +```bash | ||
| 40 | +# 运行登录测试 | ||
| 41 | +python test_login.py | ||
| 42 | + | ||
| 43 | +# 运行订单管理测试 | ||
| 44 | +python test_order_management.py | ||
| 45 | +``` | ||
| 46 | + | ||
| 47 | +### 2. 运行所有测试 | ||
| 48 | + | ||
| 49 | +```bash | ||
| 50 | +# 运行所有测试并生成报告 | ||
| 51 | +python test_runner.py | ||
| 52 | +``` | ||
| 53 | + | ||
| 54 | +### 3. 环境变量配置 | ||
| 55 | + | ||
| 56 | +```bash | ||
| 57 | +# 设置前端应用地址(默认: http://localhost:3000) | ||
| 58 | +export FRONTEND_URL=http://localhost:3000 | ||
| 59 | + | ||
| 60 | +# 设置浏览器模式(默认: 非无头模式) | ||
| 61 | +export HEADLESS=false | ||
| 62 | +``` | ||
| 63 | + | ||
| 64 | +## 测试功能 | ||
| 65 | + | ||
| 66 | +### 登录测试 (test_login.py) | ||
| 67 | + | ||
| 68 | +- ✅ 成功登录测试 | ||
| 69 | +- ✅ 无效用户名测试 | ||
| 70 | +- ✅ 无效密码测试 | ||
| 71 | +- ✅ 空凭据测试 | ||
| 72 | +- ✅ 记住我功能测试 | ||
| 73 | + | ||
| 74 | +### 订单管理测试 (test_order_management.py) | ||
| 75 | + | ||
| 76 | +- ✅ 订单列表显示测试 | ||
| 77 | +- ✅ 按订单编号搜索测试 | ||
| 78 | +- ✅ 按经销商搜索测试 | ||
| 79 | +- ✅ 订单状态筛选测试 | ||
| 80 | +- ✅ 重置搜索测试 | ||
| 81 | +- ✅ 分页功能测试 | ||
| 82 | +- ✅ 表格操作测试 | ||
| 83 | +- ✅ 操作按钮测试 | ||
| 84 | + | ||
| 85 | +## 测试报告 | ||
| 86 | + | ||
| 87 | +测试完成后会自动生成两种格式的报告: | ||
| 88 | + | ||
| 89 | +### HTML 报告 | ||
| 90 | +- 美观的可视化界面 | ||
| 91 | +- 测试统计信息 | ||
| 92 | +- 详细的测试结果 | ||
| 93 | +- 截图链接 | ||
| 94 | + | ||
| 95 | +### JSON 报告 | ||
| 96 | +- 机器可读的格式 | ||
| 97 | +- 完整的测试数据 | ||
| 98 | +- 便于集成到CI/CD | ||
| 99 | + | ||
| 100 | +## 截图功能 | ||
| 101 | + | ||
| 102 | +- 每个测试步骤都会自动截图 | ||
| 103 | +- 失败测试会额外截图错误状态 | ||
| 104 | +- 截图保存在 `screenshots/` 目录 | ||
| 105 | +- 支持全页面截图 | ||
| 106 | + | ||
| 107 | +## 配置选项 | ||
| 108 | + | ||
| 109 | +### BaseBrowserTest 配置 | ||
| 110 | + | ||
| 111 | +```python | ||
| 112 | +# 创建测试实例 | ||
| 113 | +test = BaseBrowserTest( | ||
| 114 | + headless=False, # 是否无头模式 | ||
| 115 | + slow_mo=500 # 操作间隔时间(毫秒) | ||
| 116 | +) | ||
| 117 | +``` | ||
| 118 | + | ||
| 119 | +### 元素定位策略 | ||
| 120 | + | ||
| 121 | +基于前端代码分析,使用以下定位策略: | ||
| 122 | + | ||
| 123 | +**登录页面元素:** | ||
| 124 | +- 用户名输入框: `input[placeholder="请输入用户名"]` | ||
| 125 | +- 密码输入框: `input[placeholder="请输入密码"]` | ||
| 126 | +- 登录按钮: `button:has-text("登录")` | ||
| 127 | +- 记住我复选框: `input[type="checkbox"]` | ||
| 128 | + | ||
| 129 | +**订单管理页面元素:** | ||
| 130 | +- 订单编号搜索: `input[placeholder="请输入订单编号"]` | ||
| 131 | +- 经销商编码搜索: `input[placeholder="请输入经销商编码"]` | ||
| 132 | +- 经销商名称搜索: `input[placeholder="请输入经销商名称"]` | ||
| 133 | +- 搜索按钮: `button:has-text("🔍 搜索")` | ||
| 134 | +- 重置按钮: `button:has-text("🔄 重置")` | ||
| 135 | +- 数据表格: `.data-table` | ||
| 136 | + | ||
| 137 | +## 扩展测试 | ||
| 138 | + | ||
| 139 | +### 添加新的测试用例 | ||
| 140 | + | ||
| 141 | +1. 继承 `BaseBrowserTest` 类 | ||
| 142 | +2. 实现测试方法 | ||
| 143 | +3. 使用提供的辅助方法进行元素操作 | ||
| 144 | +4. 添加截图和断言 | ||
| 145 | + | ||
| 146 | +```python | ||
| 147 | +class MyTest(BaseBrowserTest): | ||
| 148 | + def test_my_feature(self): | ||
| 149 | + # 设置页面 | ||
| 150 | + self.setup_my_page() | ||
| 151 | + | ||
| 152 | + # 执行测试操作 | ||
| 153 | + self.click_element('button:has-text("我的按钮")') | ||
| 154 | + | ||
| 155 | + # 验证结果 | ||
| 156 | + assert self.is_element_visible('.success-message') | ||
| 157 | + | ||
| 158 | + # 截图 | ||
| 159 | + self.take_screenshot('my_test_result') | ||
| 160 | +``` | ||
| 161 | + | ||
| 162 | +### 自定义测试运行器 | ||
| 163 | + | ||
| 164 | +```python | ||
| 165 | +def run_my_tests(): | ||
| 166 | + test = MyTest(headless=False, slow_mo=500) | ||
| 167 | + | ||
| 168 | + tests = [ | ||
| 169 | + ('我的测试1', test.test_feature_1), | ||
| 170 | + ('我的测试2', test.test_feature_2), | ||
| 171 | + ] | ||
| 172 | + | ||
| 173 | + results = [] | ||
| 174 | + for test_name, test_func in tests: | ||
| 175 | + result = test.run_test(test_name, test_func) | ||
| 176 | + results.append(result) | ||
| 177 | + | ||
| 178 | + return results | ||
| 179 | +``` | ||
| 180 | + | ||
| 181 | +## 故障排除 | ||
| 182 | + | ||
| 183 | +### 常见问题 | ||
| 184 | + | ||
| 185 | +1. **浏览器启动失败** | ||
| 186 | + ```bash | ||
| 187 | + # 重新安装浏览器 | ||
| 188 | + playwright install chromium | ||
| 189 | + ``` | ||
| 190 | + | ||
| 191 | +2. **元素定位失败** | ||
| 192 | + - 检查页面是否完全加载 | ||
| 193 | + - 增加等待时间 | ||
| 194 | + - 使用更精确的选择器 | ||
| 195 | + | ||
| 196 | +3. **截图保存失败** | ||
| 197 | + - 确保 `screenshots/` 目录存在 | ||
| 198 | + - 检查文件权限 | ||
| 199 | + | ||
| 200 | +4. **测试超时** | ||
| 201 | + - 增加超时时间 | ||
| 202 | + - 检查网络连接 | ||
| 203 | + - 确认前端服务正常运行 | ||
| 204 | + | ||
| 205 | +### 调试技巧 | ||
| 206 | + | ||
| 207 | +1. **使用非无头模式** | ||
| 208 | + ```python | ||
| 209 | + test = BaseBrowserTest(headless=False, slow_mo=1000) | ||
| 210 | + ``` | ||
| 211 | + | ||
| 212 | +2. **增加等待时间** | ||
| 213 | + ```python | ||
| 214 | + self.page.wait_for_timeout(2000) # 等待2秒 | ||
| 215 | + ``` | ||
| 216 | + | ||
| 217 | +3. **查看页面内容** | ||
| 218 | + ```python | ||
| 219 | + print(self.page.content()) # 打印页面HTML | ||
| 220 | + ``` | ||
| 221 | + | ||
| 222 | +4. **检查元素状态** | ||
| 223 | + ```python | ||
| 224 | + print(self.page.is_visible('selector')) # 检查元素可见性 | ||
| 225 | + ``` | ||
| 226 | + | ||
| 227 | +## 最佳实践 | ||
| 228 | + | ||
| 229 | +1. **测试独立性**: 每个测试都应该独立运行 | ||
| 230 | +2. **数据清理**: 测试后清理测试数据 | ||
| 231 | +3. **错误处理**: 适当的异常处理和重试机制 | ||
| 232 | +4. **截图记录**: 关键步骤都要截图 | ||
| 233 | +5. **断言验证**: 使用明确的断言验证结果 | ||
| 234 | +6. **性能考虑**: 合理设置等待时间和超时 | ||
| 235 | + | ||
| 236 | +## 持续集成 | ||
| 237 | + | ||
| 238 | +可以将测试集成到CI/CD流程中: | ||
| 239 | + | ||
| 240 | +```yaml | ||
| 241 | +# GitHub Actions 示例 | ||
| 242 | +- name: Run Browser Tests | ||
| 243 | + run: | | ||
| 244 | + cd tests/browser | ||
| 245 | + pip install -r requirements.txt | ||
| 246 | + playwright install chromium | ||
| 247 | + python test_runner.py | ||
| 248 | +``` | ||
| 249 | + | ||
| 250 | +## 许可证 | ||
| 251 | + | ||
| 252 | +本项目遵循 MIT 许可证。 |
tests/browser/__init__.py
0 → 100644
| 1 | +"""Browser automation tests package (Python 3.7.8 compatible).""" |
No preview for this file type
No preview for this file type
No preview for this file type
tests/browser/base_test.py
0 → 100644
| 1 | +""" | ||
| 2 | +基础测试类 - 提供通用的浏览器自动化测试功能 | ||
| 3 | +Python 3.7.8 兼容 | ||
| 4 | +""" | ||
| 5 | + | ||
| 6 | +import os | ||
| 7 | +import time | ||
| 8 | +from datetime import datetime | ||
| 9 | +from typing import Optional, Dict, Any | ||
| 10 | +from playwright.sync_api import sync_playwright, Page, Browser, BrowserContext | ||
| 11 | + | ||
| 12 | + | ||
| 13 | +class BaseBrowserTest: | ||
| 14 | + """浏览器自动化测试基类""" | ||
| 15 | + | ||
| 16 | + def __init__(self, headless: bool = False, slow_mo: int = 100): | ||
| 17 | + """ | ||
| 18 | + 初始化浏览器测试 | ||
| 19 | + | ||
| 20 | + Args: | ||
| 21 | + headless: 是否无头模式运行 | ||
| 22 | + slow_mo: 操作间隔时间(毫秒) | ||
| 23 | + """ | ||
| 24 | + self.headless = headless | ||
| 25 | + self.slow_mo = slow_mo | ||
| 26 | + self.playwright = None | ||
| 27 | + self.browser: Optional[Browser] = None | ||
| 28 | + self.context: Optional[BrowserContext] = None | ||
| 29 | + self.page: Optional[Page] = None | ||
| 30 | + | ||
| 31 | + # 测试配置 | ||
| 32 | + self.base_url = os.environ.get('FRONTEND_URL', 'http://localhost:3001') | ||
| 33 | + self.screenshots_dir = os.path.join(os.path.dirname(__file__), 'screenshots') | ||
| 34 | + self.reports_dir = os.path.join(os.path.dirname(__file__), 'reports') | ||
| 35 | + | ||
| 36 | + # 创建必要的目录 | ||
| 37 | + os.makedirs(self.screenshots_dir, exist_ok=True) | ||
| 38 | + os.makedirs(self.reports_dir, exist_ok=True) | ||
| 39 | + | ||
| 40 | + def setup_browser(self) -> None: | ||
| 41 | + """启动浏览器""" | ||
| 42 | + self.playwright = sync_playwright().start() | ||
| 43 | + | ||
| 44 | + # 启动浏览器(使用Chromium) | ||
| 45 | + self.browser = self.playwright.chromium.launch( | ||
| 46 | + headless=self.headless, | ||
| 47 | + slow_mo=self.slow_mo, | ||
| 48 | + args=['--no-sandbox', '--disable-dev-shm-usage'] | ||
| 49 | + ) | ||
| 50 | + | ||
| 51 | + # 创建浏览器上下文 | ||
| 52 | + self.context = self.browser.new_context( | ||
| 53 | + viewport={'width': 1920, 'height': 1080}, | ||
| 54 | + user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' | ||
| 55 | + ) | ||
| 56 | + | ||
| 57 | + # 创建新页面 | ||
| 58 | + self.page = self.context.new_page() | ||
| 59 | + | ||
| 60 | + # 设置默认超时时间 | ||
| 61 | + self.page.set_default_timeout(30000) # 30秒 | ||
| 62 | + | ||
| 63 | + def teardown_browser(self) -> None: | ||
| 64 | + """关闭浏览器""" | ||
| 65 | + if self.page: | ||
| 66 | + self.page.close() | ||
| 67 | + if self.context: | ||
| 68 | + self.context.close() | ||
| 69 | + if self.browser: | ||
| 70 | + self.browser.close() | ||
| 71 | + if self.playwright: | ||
| 72 | + self.playwright.stop() | ||
| 73 | + | ||
| 74 | + def take_screenshot(self, name: str = None) -> str: | ||
| 75 | + """ | ||
| 76 | + 截图 | ||
| 77 | + | ||
| 78 | + Args: | ||
| 79 | + name: 截图文件名(不包含扩展名) | ||
| 80 | + | ||
| 81 | + Returns: | ||
| 82 | + 截图文件路径 | ||
| 83 | + """ | ||
| 84 | + if not name: | ||
| 85 | + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') | ||
| 86 | + name = f'screenshot_{timestamp}' | ||
| 87 | + | ||
| 88 | + screenshot_path = os.path.join(self.screenshots_dir, f'{name}.png') | ||
| 89 | + self.page.screenshot(path=screenshot_path, full_page=True) | ||
| 90 | + print(f"截图已保存: {screenshot_path}") | ||
| 91 | + return screenshot_path | ||
| 92 | + | ||
| 93 | + def wait_for_element(self, selector: str, timeout: int = 10000) -> None: | ||
| 94 | + """ | ||
| 95 | + 等待元素出现 | ||
| 96 | + | ||
| 97 | + Args: | ||
| 98 | + selector: CSS选择器 | ||
| 99 | + timeout: 超时时间(毫秒) | ||
| 100 | + """ | ||
| 101 | + self.page.wait_for_selector(selector, timeout=timeout) | ||
| 102 | + | ||
| 103 | + def wait_for_text(self, text: str, timeout: int = 10000) -> None: | ||
| 104 | + """ | ||
| 105 | + 等待文本出现 | ||
| 106 | + | ||
| 107 | + Args: | ||
| 108 | + text: 要等待的文本 | ||
| 109 | + timeout: 超时时间(毫秒) | ||
| 110 | + """ | ||
| 111 | + self.page.wait_for_selector(f"text={text}", timeout=timeout) | ||
| 112 | + | ||
| 113 | + def fill_input(self, selector: str, value: str) -> None: | ||
| 114 | + """ | ||
| 115 | + 填充输入框 | ||
| 116 | + | ||
| 117 | + Args: | ||
| 118 | + selector: CSS选择器 | ||
| 119 | + value: 要输入的值 | ||
| 120 | + """ | ||
| 121 | + self.page.fill(selector, value) | ||
| 122 | + | ||
| 123 | + def click_element(self, selector: str) -> None: | ||
| 124 | + """ | ||
| 125 | + 点击元素 | ||
| 126 | + | ||
| 127 | + Args: | ||
| 128 | + selector: CSS选择器 | ||
| 129 | + """ | ||
| 130 | + self.page.click(selector) | ||
| 131 | + | ||
| 132 | + def select_option(self, selector: str, value: str) -> None: | ||
| 133 | + """ | ||
| 134 | + 选择下拉框选项 | ||
| 135 | + | ||
| 136 | + Args: | ||
| 137 | + selector: CSS选择器 | ||
| 138 | + value: 选项值 | ||
| 139 | + """ | ||
| 140 | + self.page.select_option(selector, value) | ||
| 141 | + | ||
| 142 | + def get_text(self, selector: str) -> str: | ||
| 143 | + """ | ||
| 144 | + 获取元素文本 | ||
| 145 | + | ||
| 146 | + Args: | ||
| 147 | + selector: CSS选择器 | ||
| 148 | + | ||
| 149 | + Returns: | ||
| 150 | + 元素文本内容 | ||
| 151 | + """ | ||
| 152 | + return self.page.text_content(selector) | ||
| 153 | + | ||
| 154 | + def get_element_count(self, selector: str) -> int: | ||
| 155 | + """ | ||
| 156 | + 获取元素数量 | ||
| 157 | + | ||
| 158 | + Args: | ||
| 159 | + selector: CSS选择器 | ||
| 160 | + | ||
| 161 | + Returns: | ||
| 162 | + 元素数量 | ||
| 163 | + """ | ||
| 164 | + return self.page.locator(selector).count() | ||
| 165 | + | ||
| 166 | + def is_element_visible(self, selector: str) -> bool: | ||
| 167 | + """ | ||
| 168 | + 检查元素是否可见 | ||
| 169 | + | ||
| 170 | + Args: | ||
| 171 | + selector: CSS选择器 | ||
| 172 | + | ||
| 173 | + Returns: | ||
| 174 | + 元素是否可见 | ||
| 175 | + """ | ||
| 176 | + return self.page.is_visible(selector) | ||
| 177 | + | ||
| 178 | + def navigate_to(self, url: str) -> None: | ||
| 179 | + """ | ||
| 180 | + 导航到指定URL | ||
| 181 | + | ||
| 182 | + Args: | ||
| 183 | + url: 目标URL | ||
| 184 | + """ | ||
| 185 | + self.page.goto(url) | ||
| 186 | + self.page.wait_for_load_state('networkidle') | ||
| 187 | + | ||
| 188 | + def login(self, username: str = 'admin', password: str = 'password') -> bool: | ||
| 189 | + """ | ||
| 190 | + 登录系统 | ||
| 191 | + | ||
| 192 | + Args: | ||
| 193 | + username: 用户名 | ||
| 194 | + password: 密码 | ||
| 195 | + | ||
| 196 | + Returns: | ||
| 197 | + 登录是否成功 | ||
| 198 | + """ | ||
| 199 | + try: | ||
| 200 | + # 导航到登录页面 | ||
| 201 | + self.navigate_to(self.base_url) | ||
| 202 | + | ||
| 203 | + # 等待登录表单加载 | ||
| 204 | + self.wait_for_element('input[placeholder="请输入用户名"]') | ||
| 205 | + | ||
| 206 | + # 填写用户名 | ||
| 207 | + self.fill_input('input[placeholder="请输入用户名"]', username) | ||
| 208 | + | ||
| 209 | + # 填写密码 | ||
| 210 | + self.fill_input('input[placeholder="请输入密码"]', password) | ||
| 211 | + | ||
| 212 | + # 点击登录按钮 | ||
| 213 | + self.click_element('button:has-text("登录")') | ||
| 214 | + | ||
| 215 | + # 等待登录完成(检查是否跳转到首页或出现错误) | ||
| 216 | + try: | ||
| 217 | + # 等待页面跳转或出现成功/失败提示 | ||
| 218 | + self.page.wait_for_load_state('networkidle', timeout=10000) | ||
| 219 | + | ||
| 220 | + # 检查是否成功登录(通过URL或页面元素判断) | ||
| 221 | + current_url = self.page.url | ||
| 222 | + | ||
| 223 | + # 等待页面完全加载 | ||
| 224 | + self.page.wait_for_timeout(2000) | ||
| 225 | + | ||
| 226 | + # 检查是否有错误提示 | ||
| 227 | + error_indicators = [ | ||
| 228 | + 'text=用户名或密码错误', | ||
| 229 | + 'text=登录失败', | ||
| 230 | + 'text=Invalid', | ||
| 231 | + 'text=Error', | ||
| 232 | + '.error', | ||
| 233 | + '.alert-danger' | ||
| 234 | + ] | ||
| 235 | + | ||
| 236 | + has_error = False | ||
| 237 | + for indicator in error_indicators: | ||
| 238 | + if self.is_element_visible(indicator): | ||
| 239 | + has_error = True | ||
| 240 | + print(f"发现错误提示: {indicator}") | ||
| 241 | + break | ||
| 242 | + | ||
| 243 | + # 检查是否跳转到其他页面(表示登录成功) | ||
| 244 | + if has_error: | ||
| 245 | + print(f"登录失败,发现错误提示,当前URL: {current_url}") | ||
| 246 | + return False | ||
| 247 | + elif 'dashboard' in current_url or 'main' in current_url: | ||
| 248 | + print(f"登录成功,当前URL: {current_url}") | ||
| 249 | + return True | ||
| 250 | + elif current_url == self.base_url or current_url == f'{self.base_url}/': | ||
| 251 | + print(f"登录失败,仍在登录页面,当前URL: {current_url}") | ||
| 252 | + return False | ||
| 253 | + else: | ||
| 254 | + print(f"登录状态不明确,当前URL: {current_url}") | ||
| 255 | + return False | ||
| 256 | + | ||
| 257 | + except Exception as e: | ||
| 258 | + print(f"登录等待超时: {e}") | ||
| 259 | + return False | ||
| 260 | + | ||
| 261 | + except Exception as e: | ||
| 262 | + print(f"登录过程出错: {e}") | ||
| 263 | + self.take_screenshot('login_error') | ||
| 264 | + return False | ||
| 265 | + | ||
| 266 | + def run_test(self, test_name: str, test_func) -> Dict[str, Any]: | ||
| 267 | + """ | ||
| 268 | + 运行测试并生成报告 | ||
| 269 | + | ||
| 270 | + Args: | ||
| 271 | + test_name: 测试名称 | ||
| 272 | + test_func: 测试函数 | ||
| 273 | + | ||
| 274 | + Returns: | ||
| 275 | + 测试结果字典 | ||
| 276 | + """ | ||
| 277 | + start_time = datetime.now() | ||
| 278 | + result = { | ||
| 279 | + 'test_name': test_name, | ||
| 280 | + 'start_time': start_time.isoformat(), | ||
| 281 | + 'success': False, | ||
| 282 | + 'error': None, | ||
| 283 | + 'screenshots': [], | ||
| 284 | + 'duration': 0 | ||
| 285 | + } | ||
| 286 | + | ||
| 287 | + try: | ||
| 288 | + print(f"\n{'='*50}") | ||
| 289 | + print(f"开始测试: {test_name}") | ||
| 290 | + print(f"{'='*50}") | ||
| 291 | + | ||
| 292 | + # 设置浏览器 | ||
| 293 | + self.setup_browser() | ||
| 294 | + | ||
| 295 | + # 执行测试 | ||
| 296 | + test_func() | ||
| 297 | + | ||
| 298 | + result['success'] = True | ||
| 299 | + print(f"✅ 测试通过: {test_name}") | ||
| 300 | + | ||
| 301 | + except Exception as e: | ||
| 302 | + result['error'] = str(e) | ||
| 303 | + print(f"❌ 测试失败: {test_name}") | ||
| 304 | + print(f"错误信息: {e}") | ||
| 305 | + | ||
| 306 | + # 失败时截图 | ||
| 307 | + screenshot_path = self.take_screenshot(f'{test_name}_error') | ||
| 308 | + result['screenshots'].append(screenshot_path) | ||
| 309 | + | ||
| 310 | + finally: | ||
| 311 | + # 清理资源 | ||
| 312 | + self.teardown_browser() | ||
| 313 | + | ||
| 314 | + # 计算耗时 | ||
| 315 | + end_time = datetime.now() | ||
| 316 | + result['end_time'] = end_time.isoformat() | ||
| 317 | + result['duration'] = (end_time - start_time).total_seconds() | ||
| 318 | + | ||
| 319 | + print(f"测试耗时: {result['duration']:.2f}秒") | ||
| 320 | + | ||
| 321 | + return result |
tests/browser/requirements.txt
0 → 100644
133 KB
134 KB
144 KB
150 KB
151 KB
181 KB
169 KB
181 KB
74.3 KB
86.3 KB
74.6 KB
169 KB
tests/browser/screenshots/reset_search.png
0 → 100644
169 KB
170 KB
170 KB
145 KB
145 KB
tests/browser/test_login.py
0 → 100644
| 1 | +""" | ||
| 2 | +登录功能自动化测试 | ||
| 3 | +Python 3.7.8 兼容 | ||
| 4 | +""" | ||
| 5 | + | ||
| 6 | +from base_test import BaseBrowserTest | ||
| 7 | + | ||
| 8 | + | ||
| 9 | +class LoginTest(BaseBrowserTest): | ||
| 10 | + """登录测试类""" | ||
| 11 | + | ||
| 12 | + def test_successful_login(self): | ||
| 13 | + """测试成功登录""" | ||
| 14 | + # 测试正常登录 | ||
| 15 | + success = self.login('admin', 'password') | ||
| 16 | + assert success, "登录应该成功" | ||
| 17 | + | ||
| 18 | + # 截图验证登录状态 | ||
| 19 | + self.take_screenshot('login_success') | ||
| 20 | + | ||
| 21 | + # 验证登录后的页面元素 | ||
| 22 | + # 这里可以根据实际登录后的页面结构添加验证 | ||
| 23 | + print("登录成功测试通过") | ||
| 24 | + | ||
| 25 | + def test_invalid_username(self): | ||
| 26 | + """测试无效用户名""" | ||
| 27 | + # 测试错误用户名 | ||
| 28 | + success = self.login('invalid_user', 'password') | ||
| 29 | + assert not success, "使用无效用户名登录应该失败" | ||
| 30 | + | ||
| 31 | + # 截图验证错误状态 | ||
| 32 | + self.take_screenshot('login_invalid_username') | ||
| 33 | + | ||
| 34 | + print("无效用户名测试通过") | ||
| 35 | + | ||
| 36 | + def test_invalid_password(self): | ||
| 37 | + """测试无效密码""" | ||
| 38 | + # 测试错误密码 | ||
| 39 | + success = self.login('admin', 'wrong_password') | ||
| 40 | + assert not success, "使用无效密码登录应该失败" | ||
| 41 | + | ||
| 42 | + # 截图验证错误状态 | ||
| 43 | + self.take_screenshot('login_invalid_password') | ||
| 44 | + | ||
| 45 | + print("无效密码测试通过") | ||
| 46 | + | ||
| 47 | + def test_empty_credentials(self): | ||
| 48 | + """测试空凭据""" | ||
| 49 | + # 导航到登录页面 | ||
| 50 | + self.navigate_to(self.base_url) | ||
| 51 | + | ||
| 52 | + # 等待登录表单 | ||
| 53 | + self.wait_for_element('input[placeholder="请输入用户名"]') | ||
| 54 | + | ||
| 55 | + # 不填写任何信息,直接点击登录 | ||
| 56 | + self.click_element('button:has-text("登录")') | ||
| 57 | + | ||
| 58 | + # 截图验证错误提示 | ||
| 59 | + self.take_screenshot('login_empty_credentials') | ||
| 60 | + | ||
| 61 | + print("空凭据测试通过") | ||
| 62 | + | ||
| 63 | + def test_remember_me(self): | ||
| 64 | + """测试记住我功能""" | ||
| 65 | + # 导航到登录页面 | ||
| 66 | + self.navigate_to(self.base_url) | ||
| 67 | + | ||
| 68 | + # 等待登录表单 | ||
| 69 | + self.wait_for_element('input[placeholder="请输入用户名"]') | ||
| 70 | + | ||
| 71 | + # 填写用户名和密码 | ||
| 72 | + self.fill_input('input[placeholder="请输入用户名"]', 'admin') | ||
| 73 | + self.fill_input('input[placeholder="请输入密码"]', 'password') | ||
| 74 | + | ||
| 75 | + # 勾选记住我 | ||
| 76 | + self.click_element('input[type="checkbox"]') | ||
| 77 | + | ||
| 78 | + # 点击登录 | ||
| 79 | + self.click_element('button:has-text("登录")') | ||
| 80 | + | ||
| 81 | + # 截图验证 | ||
| 82 | + self.take_screenshot('login_remember_me') | ||
| 83 | + | ||
| 84 | + print("记住我功能测试通过") | ||
| 85 | + | ||
| 86 | + | ||
| 87 | +def run_login_tests(): | ||
| 88 | + """运行所有登录测试""" | ||
| 89 | + test = LoginTest(headless=False, slow_mo=500) # 非无头模式,慢速执行便于观察 | ||
| 90 | + | ||
| 91 | + tests = [ | ||
| 92 | + ('成功登录测试', test.test_successful_login), | ||
| 93 | + ('无效用户名测试', test.test_invalid_username), | ||
| 94 | + ('无效密码测试', test.test_invalid_password), | ||
| 95 | + ('空凭据测试', test.test_empty_credentials), | ||
| 96 | + ('记住我功能测试', test.test_remember_me), | ||
| 97 | + ] | ||
| 98 | + | ||
| 99 | + results = [] | ||
| 100 | + for test_name, test_func in tests: | ||
| 101 | + result = test.run_test(test_name, test_func) | ||
| 102 | + results.append(result) | ||
| 103 | + | ||
| 104 | + # 打印测试总结 | ||
| 105 | + print(f"\n{'='*60}") | ||
| 106 | + print("登录测试总结") | ||
| 107 | + print(f"{'='*60}") | ||
| 108 | + | ||
| 109 | + passed = sum(1 for r in results if r['success']) | ||
| 110 | + total = len(results) | ||
| 111 | + | ||
| 112 | + for result in results: | ||
| 113 | + status = "✅ 通过" if result['success'] else "❌ 失败" | ||
| 114 | + print(f"{result['test_name']}: {status} ({result['duration']:.2f}s)") | ||
| 115 | + if result['error']: | ||
| 116 | + print(f" 错误: {result['error']}") | ||
| 117 | + | ||
| 118 | + print(f"\n总计: {passed}/{total} 个测试通过") | ||
| 119 | + | ||
| 120 | + return results | ||
| 121 | + | ||
| 122 | + | ||
| 123 | +if __name__ == '__main__': | ||
| 124 | + run_login_tests() |
tests/browser/test_order_management.py
0 → 100644
This diff is collapsed. Click to expand it.
tests/browser/test_runner.py
0 → 100644
This diff is collapsed. Click to expand it.
-
Please register or login to post a comment