422 lines
15 KiB
Python
422 lines
15 KiB
Python
"""
|
|
UAT用户体验验收测试
|
|
|
|
测试范围:
|
|
1. 界面友好性验证
|
|
2. 操作便捷性验证
|
|
3. 错误提示友好性验证
|
|
4. 响应时间验收
|
|
5. 可访问性验证
|
|
"""
|
|
|
|
import pytest
|
|
import time
|
|
import asyncio
|
|
from typing import Dict, Any
|
|
from playwright.async_api import async_playwright, Page, expect
|
|
from api.auth_api import AuthAPI
|
|
from api.user_api import UserAPI
|
|
from api.role_api import RoleAPI
|
|
from config.settings import settings
|
|
|
|
|
|
@pytest.mark.uat
|
|
@pytest.mark.user_experience
|
|
@pytest.mark.asyncio
|
|
class TestUserExperienceUAT:
|
|
"""用户体验验收测试类"""
|
|
|
|
@pytest.fixture
|
|
async def browser(self):
|
|
"""浏览器fixture"""
|
|
async with async_playwright() as p:
|
|
browser = await p.chromium.launch(headless=True)
|
|
yield browser
|
|
await browser.close()
|
|
|
|
@pytest.fixture
|
|
async def context(self, browser):
|
|
"""浏览器上下文fixture"""
|
|
context = await browser.new_context(
|
|
viewport={"width": 1920, "height": 1080},
|
|
locale="zh-CN"
|
|
)
|
|
yield context
|
|
await context.close()
|
|
|
|
@pytest.fixture
|
|
async def page(self, context):
|
|
"""页面fixture"""
|
|
page = await context.new_page()
|
|
page.set_default_timeout(30000)
|
|
yield page
|
|
await page.close()
|
|
|
|
@pytest.fixture
|
|
async def authenticated_page(self, page):
|
|
"""已认证的页面fixture"""
|
|
await page.goto(f"{settings.FRONTEND_URL}/login")
|
|
await page.wait_for_load_state("networkidle")
|
|
|
|
await page.fill('input[placeholder="请输入用户名"]', settings.TEST_USERNAME)
|
|
await page.fill('input[placeholder="请输入密码"]', settings.TEST_PASSWORD)
|
|
await page.click('button[type="submit"]')
|
|
|
|
await page.wait_for_url("**/")
|
|
await page.wait_for_load_state("networkidle")
|
|
|
|
yield page
|
|
|
|
async def test_ue_interface_friendly_login(self, page):
|
|
"""
|
|
UE-01: 登录界面友好性验证
|
|
|
|
验证点:
|
|
- 登录页面布局合理
|
|
- 输入框提示清晰
|
|
- 按钮位置合理
|
|
- 错误提示友好
|
|
"""
|
|
await page.goto(f"{settings.FRONTEND_URL}/login")
|
|
await page.wait_for_load_state("networkidle")
|
|
|
|
username_input = await page.query_selector('input[placeholder="请输入用户名"]')
|
|
assert username_input is not None, "用户名输入框应存在"
|
|
|
|
password_input = await page.query_selector('input[placeholder="请输入密码"]')
|
|
assert password_input is not None, "密码输入框应存在"
|
|
|
|
submit_button = await page.query_selector('button[type="submit"]')
|
|
assert submit_button is not None, "登录按钮应存在"
|
|
|
|
await page.fill('input[placeholder="请输入用户名"]', "wrong_user")
|
|
await page.fill('input[placeholder="请输入密码"]', "wrong_pass")
|
|
await page.click('button[type="submit"]')
|
|
|
|
await asyncio.sleep(1)
|
|
|
|
error_message = await page.query_selector('.el-message--error')
|
|
assert error_message is not None, "应显示错误提示"
|
|
|
|
error_text = await error_message.text_content()
|
|
assert len(error_text) > 0, "错误提示应有内容"
|
|
|
|
async def test_ue_interface_friendly_dashboard(self, authenticated_page):
|
|
"""
|
|
UE-02: 仪表盘界面友好性验证
|
|
|
|
验证点:
|
|
- 页面布局清晰
|
|
- 导航菜单易用
|
|
- 数据展示直观
|
|
- 响应式设计
|
|
"""
|
|
page = authenticated_page
|
|
|
|
sidebar = await page.query_selector('.sidebar-container')
|
|
assert sidebar is not None, "侧边栏应存在"
|
|
|
|
navbar = await page.query_selector('.navbar')
|
|
assert navbar is not None, "导航栏应存在"
|
|
|
|
main_content = await page.query_selector('.app-main')
|
|
assert main_content is not None, "主内容区应存在"
|
|
|
|
await page.set_viewport_size({"width": 768, "height": 1024})
|
|
await asyncio.sleep(0.5)
|
|
|
|
mobile_menu = await page.query_selector('.mobile-menu')
|
|
assert mobile_menu is not None or sidebar is not None, "移动端应显示菜单按钮或侧边栏"
|
|
|
|
async def test_ue_operation_convenience_user_management(self, authenticated_page):
|
|
"""
|
|
UE-03: 用户管理操作便捷性验证
|
|
|
|
验证点:
|
|
- 列表加载快速
|
|
- 搜索功能便捷
|
|
- 操作按钮明显
|
|
- 表单填写简单
|
|
"""
|
|
page = authenticated_page
|
|
|
|
start_time = time.time()
|
|
await page.click('text=用户管理')
|
|
await page.wait_for_load_state("networkidle")
|
|
load_time = time.time() - start_time
|
|
|
|
assert load_time < 3.0, f"用户管理页面加载时间应小于3秒,实际: {load_time:.2f}秒"
|
|
|
|
search_input = await page.query_selector('input[placeholder*="搜索"]')
|
|
if search_input:
|
|
await search_input.fill("admin")
|
|
await asyncio.sleep(0.5)
|
|
|
|
table_rows = await page.query_selector_all('.el-table__row')
|
|
assert len(table_rows) > 0, "搜索应返回结果"
|
|
|
|
create_button = await page.query_selector('button:has-text("新增")')
|
|
assert create_button is not None, "新增按钮应存在且明显"
|
|
|
|
await create_button.click()
|
|
await page.wait_for_load_state("networkidle")
|
|
|
|
dialog = await page.query_selector('.el-dialog')
|
|
assert dialog is not None, "新增用户对话框应弹出"
|
|
|
|
form_items = await dialog.query_selector_all('.el-form-item')
|
|
assert len(form_items) > 0, "表单应包含必填项"
|
|
|
|
async def test_ue_operation_convenience_role_management(self, authenticated_page):
|
|
"""
|
|
UE-04: 角色管理操作便捷性验证
|
|
|
|
验证点:
|
|
- 角色列表清晰
|
|
- 权限树易操作
|
|
- 批量操作支持
|
|
"""
|
|
page = authenticated_page
|
|
|
|
await page.click('text=角色管理')
|
|
await page.wait_for_load_state("networkidle")
|
|
|
|
role_table = await page.query_selector('.el-table')
|
|
assert role_table is not None, "角色表格应存在"
|
|
|
|
edit_button = await page.query_selector('button:has-text("编辑")')
|
|
if edit_button:
|
|
await edit_button.click()
|
|
await page.wait_for_load_state("networkidle")
|
|
|
|
permission_tree = await page.query_selector('.el-tree')
|
|
assert permission_tree is not None, "权限树应存在"
|
|
|
|
tree_checkboxes = await permission_tree.query_selector_all('.el-checkbox')
|
|
assert len(tree_checkboxes) > 0, "权限树应包含可选项"
|
|
|
|
async def test_ue_error_message_friendly(self, page):
|
|
"""
|
|
UE-05: 错误提示友好性验证
|
|
|
|
验证点:
|
|
- 错误信息清晰
|
|
- 错误位置明确
|
|
- 解决建议提供
|
|
"""
|
|
await page.goto(f"{settings.FRONTEND_URL}/login")
|
|
await page.wait_for_load_state("networkidle")
|
|
|
|
await page.click('button[type="submit"]')
|
|
await asyncio.sleep(1)
|
|
|
|
validation_errors = await page.query_selector_all('.el-form-item__error')
|
|
assert len(validation_errors) > 0, "应显示表单验证错误"
|
|
|
|
for error in validation_errors:
|
|
error_text = await error.text_content()
|
|
assert len(error_text) > 0, "错误信息应有内容"
|
|
assert "请" in error_text or "不能为空" in error_text, "错误信息应友好"
|
|
|
|
async def test_ue_response_time_acceptance(self, authenticated_page):
|
|
"""
|
|
UE-06: 响应时间验收
|
|
|
|
验证点:
|
|
- 页面加载时间 < 3秒
|
|
- API响应时间 < 1秒
|
|
- 列表查询时间 < 2秒
|
|
"""
|
|
page = authenticated_page
|
|
|
|
pages_to_test = [
|
|
("用户管理", "用户管理页面"),
|
|
("角色管理", "角色管理页面"),
|
|
("菜单管理", "菜单管理页面")
|
|
]
|
|
|
|
for menu_text, page_name in pages_to_test:
|
|
start_time = time.time()
|
|
await page.click(f'text={menu_text}')
|
|
await page.wait_for_load_state("networkidle")
|
|
load_time = time.time() - start_time
|
|
|
|
assert load_time < 3.0, f"{page_name}加载时间应小于3秒,实际: {load_time:.2f}秒"
|
|
|
|
async def test_ue_accessibility_verification(self, authenticated_page):
|
|
"""
|
|
UE-07: 可访问性验证
|
|
|
|
验证点:
|
|
- 键盘导航支持
|
|
- ARIA标签存在
|
|
- 对比度合理
|
|
- 字体大小合适
|
|
"""
|
|
page = authenticated_page
|
|
|
|
await page.keyboard.press('Tab')
|
|
await asyncio.sleep(0.2)
|
|
|
|
focused_element = await page.query_selector(':focus')
|
|
assert focused_element is not None, "应支持键盘导航"
|
|
|
|
buttons = await page.query_selector_all('button')
|
|
for button in buttons[:5]:
|
|
aria_label = await button.get_attribute('aria-label')
|
|
text_content = await button.text_content()
|
|
assert aria_label or text_content, "按钮应有ARIA标签或文本内容"
|
|
|
|
body = await page.query_selector('body')
|
|
font_size = await body.evaluate('el => window.getComputedStyle(el).fontSize')
|
|
font_size_value = float(font_size.replace('px', ''))
|
|
assert font_size_value >= 14, f"字体大小应不小于14px,实际: {font_size_value}px"
|
|
|
|
async def test_ue_form_validation_feedback(self, authenticated_page):
|
|
"""
|
|
UE-08: 表单验证反馈验证
|
|
|
|
验证点:
|
|
- 实时验证反馈
|
|
- 验证规则清晰
|
|
- 错误位置标记
|
|
"""
|
|
page = authenticated_page
|
|
|
|
await page.click('text=用户管理')
|
|
await page.wait_for_load_state("networkidle")
|
|
|
|
create_button = await page.query_selector('button:has-text("新增")')
|
|
await create_button.click()
|
|
await page.wait_for_load_state("networkidle")
|
|
|
|
username_input = await page.query_selector('input[placeholder*="用户名"]')
|
|
if username_input:
|
|
await username_input.fill("a")
|
|
await username_input.blur()
|
|
await asyncio.sleep(0.5)
|
|
|
|
error_message = await page.query_selector('.el-form-item__error')
|
|
if error_message:
|
|
error_text = await error_message.text_content()
|
|
assert len(error_text) > 0, "应显示验证错误信息"
|
|
|
|
async def test_ue_loading_state_feedback(self, authenticated_page):
|
|
"""
|
|
UE-09: 加载状态反馈验证
|
|
|
|
验证点:
|
|
- 加载动画显示
|
|
- 加载提示清晰
|
|
- 禁用重复提交
|
|
"""
|
|
page = authenticated_page
|
|
|
|
await page.click('text=用户管理')
|
|
await page.wait_for_load_state("networkidle")
|
|
|
|
loading_overlay = await page.query_selector('.el-loading-mask')
|
|
|
|
create_button = await page.query_selector('button:has-text("新增")')
|
|
if create_button:
|
|
is_disabled = await create_button.is_disabled()
|
|
assert not is_disabled, "按钮应可点击"
|
|
|
|
async def test_ue_confirmation_dialog(self, authenticated_page):
|
|
"""
|
|
UE-10: 确认对话框验证
|
|
|
|
验证点:
|
|
- 危险操作有确认
|
|
- 确认信息清晰
|
|
- 取消操作支持
|
|
"""
|
|
page = authenticated_page
|
|
|
|
await page.click('text=用户管理')
|
|
await page.wait_for_load_state("networkidle")
|
|
|
|
delete_buttons = await page.query_selector_all('button:has-text("删除")')
|
|
|
|
if len(delete_buttons) > 0:
|
|
await delete_buttons[0].click()
|
|
await asyncio.sleep(0.5)
|
|
|
|
confirm_dialog = await page.query_selector('.el-message-box')
|
|
assert confirm_dialog is not None, "删除操作应弹出确认对话框"
|
|
|
|
cancel_button = await confirm_dialog.query_selector('button:has-text("取消")')
|
|
assert cancel_button is not None, "确认对话框应有取消按钮"
|
|
|
|
await cancel_button.click()
|
|
await asyncio.sleep(0.5)
|
|
|
|
dialog_closed = await page.query_selector('.el-message-box')
|
|
assert dialog_closed is None, "点击取消应关闭对话框"
|
|
|
|
|
|
@pytest.mark.uat
|
|
@pytest.mark.user_experience
|
|
@pytest.mark.regression
|
|
class TestUserExperienceRegression:
|
|
"""用户体验回归测试"""
|
|
|
|
@pytest.fixture
|
|
async def browser(self):
|
|
async with async_playwright() as p:
|
|
browser = await p.chromium.launch(headless=True)
|
|
yield browser
|
|
await browser.close()
|
|
|
|
@pytest.fixture
|
|
async def context(self, browser):
|
|
context = await browser.new_context(viewport={"width": 1920, "height": 1080})
|
|
yield context
|
|
await context.close()
|
|
|
|
@pytest.fixture
|
|
async def page(self, context):
|
|
page = await context.new_page()
|
|
page.set_default_timeout(30000)
|
|
yield page
|
|
await page.close()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ue_browser_compatibility(self, page):
|
|
"""
|
|
UE-REG-01: 浏览器兼容性验证
|
|
|
|
验证点:
|
|
- Chrome浏览器兼容
|
|
- 主要功能正常
|
|
"""
|
|
await page.goto(f"{settings.FRONTEND_URL}/login")
|
|
await page.wait_for_load_state("networkidle")
|
|
|
|
login_form = await page.query_selector('.login-form')
|
|
assert login_form is not None, "登录表单应正常显示"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ue_responsive_design(self, page):
|
|
"""
|
|
UE-REG-02: 响应式设计验证
|
|
|
|
验证点:
|
|
- 桌面端显示正常
|
|
- 平板端显示正常
|
|
- 移动端显示正常
|
|
"""
|
|
viewports = [
|
|
{"width": 1920, "height": 1080, "name": "桌面端"},
|
|
{"width": 768, "height": 1024, "name": "平板端"},
|
|
{"width": 375, "height": 667, "name": "移动端"}
|
|
]
|
|
|
|
for viewport in viewports:
|
|
await page.set_viewport_size({"width": viewport["width"], "height": viewport["height"]})
|
|
await page.goto(f"{settings.FRONTEND_URL}/login")
|
|
await page.wait_for_load_state("networkidle")
|
|
|
|
login_form = await page.query_selector('.login-form')
|
|
assert login_form is not None, f"{viewport['name']}登录表单应正常显示"
|