567 lines
21 KiB
Python
567 lines
21 KiB
Python
"""
|
|
UI 集成测试 - 使用 Playwright 测试正则路径替换功能
|
|
|
|
运行前准备:
|
|
1. 启动Docker服务: docker compose up -d
|
|
2. 确保服务运行在 http://localhost:8888
|
|
|
|
安装:
|
|
pip install pytest-playwright
|
|
playwright install
|
|
|
|
运行:
|
|
pytest tests/test_ui_regex_rules.py --headed # 显示浏览器
|
|
pytest tests/test_ui_regex_rules.py # 无头模式
|
|
"""
|
|
|
|
import re
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from playwright.sync_api import Page, expect
|
|
|
|
|
|
# 测试服务器地址 - Docker映射端口
|
|
BASE_URL = "http://localhost:8888"
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def test_server():
|
|
"""启动测试服务器(可选,如果服务器未运行)"""
|
|
# 如果你的服务器已经在运行,直接返回
|
|
# 否则可以在这里启动服务器进程
|
|
yield BASE_URL
|
|
|
|
|
|
@pytest.fixture
|
|
def page(page: Page, test_server):
|
|
"""配置页面并导航到首页"""
|
|
page.goto(test_server)
|
|
page.wait_for_load_state("networkidle")
|
|
return page
|
|
|
|
|
|
class TestRegexRulesUI:
|
|
"""测试正则路径替换规则的 UI 交互 - 适配新React UI"""
|
|
|
|
def _handle_connection_modal(self, page: Page):
|
|
"""处理登录模态框"""
|
|
modal_overlay = page.locator("div.fixed.inset-0.z-50")
|
|
if modal_overlay.is_visible():
|
|
close_button = modal_overlay.locator("button").filter(has=page.locator("svg")).last
|
|
if close_button.is_visible():
|
|
close_button.click()
|
|
else:
|
|
page.keyboard.press("Escape")
|
|
page.wait_for_timeout(500)
|
|
|
|
def _open_strategy_selector(self, page: Page):
|
|
"""打开策略选择器下拉菜单"""
|
|
self._handle_connection_modal(page)
|
|
|
|
dropdown = page.locator("div.absolute.top-14")
|
|
if dropdown.is_visible():
|
|
return
|
|
|
|
strategy_button = page.locator("button[title^='Current Strategy']")
|
|
if strategy_button.count() == 0:
|
|
strategy_button = page.locator("button").filter(has=page.locator("svg")).nth(1)
|
|
|
|
if strategy_button.is_visible():
|
|
strategy_button.click()
|
|
page.wait_for_timeout(300)
|
|
|
|
def _close_strategy_selector(self, page: Page):
|
|
"""关闭策略选择器"""
|
|
page.keyboard.press("Escape")
|
|
page.wait_for_timeout(200)
|
|
|
|
def test_page_loads_successfully(self, page: Page):
|
|
"""测试页面成功加载"""
|
|
expect(page).to_have_title(re.compile("Plex.*Sync", re.I))
|
|
|
|
# 检查关键元素存在 - 新UI的主要元素
|
|
# 检查策略选择器按钮存在
|
|
strategy_button = page.locator("button").filter(has=page.locator("svg")).first
|
|
expect(strategy_button).to_be_visible()
|
|
|
|
def test_add_single_rule(self, page: Page):
|
|
"""测试添加单个规则"""
|
|
# 打开策略选择器
|
|
self._open_strategy_selector(page)
|
|
|
|
# 等待下拉菜单可见
|
|
dropdown = page.locator("div.absolute.top-14")
|
|
expect(dropdown).to_be_visible()
|
|
|
|
# 获取初始规则数量
|
|
initial_count = page.locator("input[placeholder='Regex Pattern']").count()
|
|
|
|
# 点击添加规则按钮
|
|
add_button = page.locator("button[title='Add Rule']")
|
|
if add_button.count() == 0:
|
|
add_button = page.locator("button:has-text('Add Rule')")
|
|
|
|
add_button.click()
|
|
page.wait_for_timeout(200)
|
|
|
|
# 验证规则数量增加
|
|
new_count = page.locator("input[placeholder='Regex Pattern']").count()
|
|
assert new_count == initial_count + 1
|
|
|
|
# 填写规则内容
|
|
pattern_inputs = page.locator("input[placeholder='Regex Pattern']")
|
|
replacement_inputs = page.locator("input[placeholder='Replacement']")
|
|
|
|
pattern_inputs.last.fill(r"/old/path/")
|
|
replacement_inputs.last.fill(r"/new/path/")
|
|
|
|
# 验证填写成功
|
|
assert pattern_inputs.last.input_value() == r"/old/path/"
|
|
assert replacement_inputs.last.input_value() == r"/new/path/"
|
|
|
|
self._close_strategy_selector(page)
|
|
|
|
def test_add_multiple_rules(self, page: Page):
|
|
"""测试添加多个规则"""
|
|
self._open_strategy_selector(page)
|
|
|
|
rules = [
|
|
(r"\\\\nas\\Music", r"N:\\Music"),
|
|
(r"/music/cache/", r"/data/music/"),
|
|
(r"\\", r"/"),
|
|
]
|
|
|
|
# 添加多个规则
|
|
for pattern, replacement in rules:
|
|
add_button = page.locator("button[title='Add Rule']")
|
|
if add_button.count() == 0:
|
|
add_button = page.locator("button:has-text('Add Rule')")
|
|
|
|
add_button.click()
|
|
page.wait_for_timeout(200)
|
|
|
|
pattern_inputs = page.locator("input[placeholder='Regex Pattern']")
|
|
replacement_inputs = page.locator("input[placeholder='Replacement']")
|
|
|
|
pattern_inputs.last.fill(pattern)
|
|
replacement_inputs.last.fill(replacement)
|
|
|
|
# 验证所有规则都已添加
|
|
pattern_inputs = page.locator("input[placeholder='Regex Pattern']")
|
|
assert pattern_inputs.count() >= len(rules)
|
|
|
|
# 验证规则内容
|
|
for i in range(len(rules)):
|
|
idx = pattern_inputs.count() - len(rules) + i
|
|
assert rules[i][0] in pattern_inputs.nth(idx).input_value()
|
|
|
|
self._close_strategy_selector(page)
|
|
|
|
def test_remove_rule(self, page: Page):
|
|
"""测试删除规则"""
|
|
self._open_strategy_selector(page)
|
|
|
|
# 获取初始规则数量
|
|
initial_count = page.locator("input[placeholder='Regex Pattern']").count()
|
|
|
|
# 添加一个规则
|
|
add_button = page.locator("button[title='Add Rule']")
|
|
if add_button.count() == 0:
|
|
add_button = page.locator("button:has-text('Add Rule')")
|
|
|
|
add_button.click()
|
|
page.wait_for_timeout(200)
|
|
|
|
new_count = page.locator("input[placeholder='Regex Pattern']").count()
|
|
assert new_count == initial_count + 1
|
|
|
|
# 找到删除按钮(最后一个规则的删除按钮)
|
|
remove_buttons = page.locator("button[title='Delete Rule']")
|
|
if remove_buttons.count() > 0:
|
|
remove_buttons.last.click()
|
|
page.wait_for_timeout(200)
|
|
|
|
# 验证规则已删除
|
|
final_count = page.locator("input[placeholder='Regex Pattern']").count()
|
|
assert final_count == initial_count
|
|
|
|
self._close_strategy_selector(page)
|
|
|
|
def test_save_rules(self, page: Page):
|
|
"""测试保存规则"""
|
|
self._open_strategy_selector(page)
|
|
|
|
# 清除现有规则
|
|
delete_buttons = page.locator("button[title='Delete Rule']")
|
|
while delete_buttons.count() > 0:
|
|
delete_buttons.first.click()
|
|
page.wait_for_timeout(100)
|
|
delete_buttons = page.locator("button[title='Delete Rule']")
|
|
|
|
# 添加测试规则
|
|
add_button = page.locator("button[title='Add Rule']")
|
|
if add_button.count() == 0:
|
|
add_button = page.locator("button:has-text('Add Rule')")
|
|
|
|
add_button.click()
|
|
page.wait_for_timeout(200)
|
|
|
|
pattern_input = page.locator("input[placeholder='Regex Pattern']").last
|
|
replacement_input = page.locator("input[placeholder='Replacement']").last
|
|
|
|
test_pattern = r"/test/path/"
|
|
test_replacement = r"/new/path/"
|
|
|
|
pattern_input.fill(test_pattern)
|
|
replacement_input.fill(test_replacement)
|
|
page.wait_for_timeout(100)
|
|
|
|
# 点击保存按钮
|
|
save_button = page.locator("button:has-text('Save Changes')")
|
|
expect(save_button).to_be_enabled()
|
|
save_button.click()
|
|
page.wait_for_timeout(500)
|
|
|
|
# 验证成功消息
|
|
toast = page.locator("div:has-text('Regex preprocessing rules have been saved')")
|
|
if toast.count() > 0:
|
|
expect(toast.first).to_be_visible(timeout=3000)
|
|
|
|
self._close_strategy_selector(page)
|
|
|
|
def test_rules_persist_after_save(self, page: Page):
|
|
"""测试规则保存后持久化"""
|
|
self._open_strategy_selector(page)
|
|
|
|
# 清除并添加新规则
|
|
delete_buttons = page.locator("button[title='Delete Rule']")
|
|
while delete_buttons.count() > 0:
|
|
delete_buttons.first.click()
|
|
page.wait_for_timeout(100)
|
|
delete_buttons = page.locator("button[title='Delete Rule']")
|
|
|
|
add_button = page.locator("button[title='Add Rule']")
|
|
if add_button.count() == 0:
|
|
add_button = page.locator("button:has-text('Add Rule')")
|
|
|
|
add_button.click()
|
|
page.wait_for_timeout(200)
|
|
|
|
test_pattern = r"C:\\Music"
|
|
test_replacement = r"D:\\Audio"
|
|
|
|
page.locator("input[placeholder='Regex Pattern']").last.fill(test_pattern)
|
|
page.locator("input[placeholder='Replacement']").last.fill(test_replacement)
|
|
page.wait_for_timeout(100)
|
|
|
|
# 保存
|
|
save_button = page.locator("button:has-text('Save Changes')")
|
|
save_button.click()
|
|
page.wait_for_timeout(500)
|
|
|
|
self._close_strategy_selector(page)
|
|
|
|
# 刷新页面
|
|
page.reload()
|
|
page.wait_for_load_state("networkidle")
|
|
page.wait_for_timeout(1000)
|
|
|
|
# 重新打开策略选择器
|
|
self._open_strategy_selector(page)
|
|
|
|
# 验证规则仍然存在
|
|
pattern_inputs = page.locator("input[placeholder='Regex Pattern']")
|
|
|
|
# 检查是否有匹配的规则
|
|
found = False
|
|
for i in range(pattern_inputs.count()):
|
|
if test_pattern in pattern_inputs.nth(i).input_value():
|
|
found = True
|
|
# 验证对应的替换值
|
|
replacement_inputs = page.locator("input[placeholder='Replacement']")
|
|
replacement_value = replacement_inputs.nth(i).input_value()
|
|
assert test_replacement in replacement_value
|
|
break
|
|
|
|
assert found, f"未找到保存的规则: {test_pattern}"
|
|
|
|
self._close_strategy_selector(page)
|
|
|
|
def test_empty_pattern_validation(self, page: Page):
|
|
"""测试空模式验证 - 新UI在保存时会过滤空规则"""
|
|
self._open_strategy_selector(page)
|
|
|
|
# 添加规则但不填写
|
|
add_button = page.locator("button[title='Add Rule']")
|
|
if add_button.count() == 0:
|
|
add_button = page.locator("button:has-text('Add Rule')")
|
|
|
|
add_button.click()
|
|
page.wait_for_timeout(200)
|
|
|
|
# 只填写替换,不填写模式
|
|
replacement_input = page.locator("input[placeholder='Replacement']").last
|
|
replacement_input.fill("/new/path/")
|
|
page.wait_for_timeout(100)
|
|
|
|
# 尝试保存 - 新UI会自动过滤空模式的规则
|
|
save_button = page.locator("button:has-text('Save Changes')")
|
|
if save_button.is_enabled():
|
|
initial_count = page.locator("input[placeholder='Regex Pattern']").count()
|
|
save_button.click()
|
|
page.wait_for_timeout(500)
|
|
|
|
# 验证空规则被过滤(如果实现了这个逻辑)
|
|
# 注意: 这取决于后端实现
|
|
|
|
self._close_strategy_selector(page)
|
|
|
|
def test_rule_order_preserved(self, page: Page):
|
|
"""测试规则顺序保持"""
|
|
self._open_strategy_selector(page)
|
|
|
|
# 清除现有规则
|
|
delete_buttons = page.locator("button[title='Delete Rule']")
|
|
while delete_buttons.count() > 0:
|
|
delete_buttons.first.click()
|
|
page.wait_for_timeout(100)
|
|
delete_buttons = page.locator("button[title='Delete Rule']")
|
|
|
|
# 按顺序添加多个规则
|
|
rules = [
|
|
("rule1", "replacement1"),
|
|
("rule2", "replacement2"),
|
|
("rule3", "replacement3"),
|
|
]
|
|
|
|
for pattern, replacement in rules:
|
|
add_button = page.locator("button[title='Add Rule']")
|
|
if add_button.count() == 0:
|
|
add_button = page.locator("button:has-text('Add Rule')")
|
|
|
|
add_button.click()
|
|
page.wait_for_timeout(200)
|
|
|
|
page.locator("input[placeholder='Regex Pattern']").last.fill(pattern)
|
|
page.locator("input[placeholder='Replacement']").last.fill(replacement)
|
|
page.wait_for_timeout(100)
|
|
|
|
# 验证顺序
|
|
pattern_inputs = page.locator("input[placeholder='Regex Pattern']")
|
|
count = pattern_inputs.count()
|
|
|
|
for i, (pattern, _) in enumerate(rules):
|
|
idx = count - len(rules) + i
|
|
assert pattern in pattern_inputs.nth(idx).input_value()
|
|
|
|
self._close_strategy_selector(page)
|
|
|
|
|
|
class TestComplexScenarios:
|
|
"""测试复杂场景"""
|
|
|
|
def _handle_connection_modal(self, page: Page):
|
|
"""处理登录模态框"""
|
|
modal_overlay = page.locator("div.fixed.inset-0.z-50")
|
|
if modal_overlay.is_visible():
|
|
close_button = modal_overlay.locator("button").filter(has=page.locator("svg")).last
|
|
if close_button.is_visible():
|
|
close_button.click()
|
|
else:
|
|
page.keyboard.press("Escape")
|
|
page.wait_for_timeout(500)
|
|
|
|
def _open_strategy_selector(self, page: Page):
|
|
"""打开策略选择器下拉菜单"""
|
|
self._handle_connection_modal(page)
|
|
|
|
dropdown = page.locator("div.absolute.top-14")
|
|
if dropdown.is_visible():
|
|
return
|
|
|
|
strategy_button = page.locator("button[title^='Current Strategy']")
|
|
if strategy_button.count() == 0:
|
|
strategy_button = page.locator("button").filter(has=page.locator("svg")).nth(1)
|
|
|
|
if strategy_button.is_visible():
|
|
strategy_button.click()
|
|
page.wait_for_timeout(300)
|
|
|
|
def _close_strategy_selector(self, page: Page):
|
|
"""关闭策略选择器"""
|
|
page.keyboard.press("Escape")
|
|
page.wait_for_timeout(200)
|
|
|
|
def test_windows_to_linux_path_conversion(self, page: Page):
|
|
"""测试 Windows 到 Linux 路径转换场景"""
|
|
self._open_strategy_selector(page)
|
|
|
|
# 清除规则
|
|
delete_buttons = page.locator("button[title='Delete Rule']")
|
|
while delete_buttons.count() > 0:
|
|
delete_buttons.first.click()
|
|
page.wait_for_timeout(100)
|
|
delete_buttons = page.locator("button[title='Delete Rule']")
|
|
|
|
# 添加转换规则
|
|
conversion_rules = [
|
|
(r"C:\\Music", r"/mnt/music"),
|
|
(r"\\", r"/"),
|
|
]
|
|
|
|
for pattern, replacement in conversion_rules:
|
|
add_button = page.locator("button[title='Add Rule']")
|
|
if add_button.count() == 0:
|
|
add_button = page.locator("button:has-text('Add Rule')")
|
|
|
|
add_button.click()
|
|
page.wait_for_timeout(200)
|
|
|
|
page.locator("input[placeholder='Regex Pattern']").last.fill(pattern)
|
|
page.locator("input[placeholder='Replacement']").last.fill(replacement)
|
|
page.wait_for_timeout(100)
|
|
|
|
# 保存
|
|
save_button = page.locator("button:has-text('Save Changes')")
|
|
save_button.click()
|
|
page.wait_for_timeout(500)
|
|
|
|
# 验证保存成功
|
|
toast = page.locator("div:has-text('Regex preprocessing rules have been saved')")
|
|
if toast.count() > 0:
|
|
expect(toast.first).to_be_visible(timeout=3000)
|
|
|
|
self._close_strategy_selector(page)
|
|
|
|
def test_nas_path_normalization(self, page: Page):
|
|
"""测试 NAS 路径规范化"""
|
|
self._open_strategy_selector(page)
|
|
|
|
# 清除规则
|
|
delete_buttons = page.locator("button[title='Delete Rule']")
|
|
while delete_buttons.count() > 0:
|
|
delete_buttons.first.click()
|
|
page.wait_for_timeout(100)
|
|
delete_buttons = page.locator("button[title='Delete Rule']")
|
|
|
|
# NAS 路径规范化规则
|
|
nas_rules = [
|
|
(r"\\\\koha9-nas\\koha9-nas\\Music", r"N:\\Music"),
|
|
(r"/music/cache/", r"/data/music/"),
|
|
(r"\\", r"/"),
|
|
]
|
|
|
|
for pattern, replacement in nas_rules:
|
|
add_button = page.locator("button[title='Add Rule']")
|
|
if add_button.count() == 0:
|
|
add_button = page.locator("button:has-text('Add Rule')")
|
|
|
|
add_button.click()
|
|
page.wait_for_timeout(200)
|
|
|
|
page.locator("input[placeholder='Regex Pattern']").last.fill(pattern)
|
|
page.locator("input[placeholder='Replacement']").last.fill(replacement)
|
|
page.wait_for_timeout(100)
|
|
|
|
# 保存并验证
|
|
save_button = page.locator("button:has-text('Save Changes')")
|
|
save_button.click()
|
|
page.wait_for_timeout(500)
|
|
|
|
# 验证成功
|
|
toast = page.locator("div:has-text('Regex preprocessing rules have been saved')")
|
|
if toast.count() > 0:
|
|
expect(toast.first).to_be_visible(timeout=3000)
|
|
|
|
self._close_strategy_selector(page)
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
# 刷新验证持久化
|
|
page.reload()
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
# 重新打开策略选择器
|
|
self._open_strategy_selector(page)
|
|
|
|
# 验证所有规则都保存了
|
|
pattern_inputs = page.locator("input[placeholder='Regex Pattern']")
|
|
saved_patterns = [pattern_inputs.nth(i).input_value() for i in range(pattern_inputs.count())]
|
|
|
|
for pattern, _ in nas_rules:
|
|
assert any(pattern in saved for saved in saved_patterns), f"规则未保存: {pattern}"
|
|
|
|
|
|
@pytest.mark.slow
|
|
class TestPerformance:
|
|
"""性能测试"""
|
|
|
|
def _handle_connection_modal(self, page: Page):
|
|
"""处理登录模态框"""
|
|
modal_overlay = page.locator("div.fixed.inset-0.z-50")
|
|
if modal_overlay.is_visible():
|
|
close_button = modal_overlay.locator("button").filter(has=page.locator("svg")).last
|
|
if close_button.is_visible():
|
|
close_button.click()
|
|
else:
|
|
page.keyboard.press("Escape")
|
|
page.wait_for_timeout(500)
|
|
|
|
def _open_strategy_selector(self, page: Page):
|
|
"""打开策略选择器下拉菜单"""
|
|
self._handle_connection_modal(page)
|
|
|
|
dropdown = page.locator("div.absolute.top-14")
|
|
if dropdown.is_visible():
|
|
return
|
|
|
|
strategy_button = page.locator("button[title^='Current Strategy']")
|
|
if strategy_button.count() == 0:
|
|
strategy_button = page.locator("button").filter(has=page.locator("svg")).nth(1)
|
|
|
|
if strategy_button.is_visible():
|
|
strategy_button.click()
|
|
page.wait_for_timeout(300)
|
|
|
|
def test_add_many_rules_performance(self, page: Page):
|
|
"""测试添加大量规则的性能"""
|
|
self._open_strategy_selector(page)
|
|
|
|
# 清除规则
|
|
delete_buttons = page.locator("button[title='Delete Rule']")
|
|
while delete_buttons.count() > 0:
|
|
delete_buttons.first.click()
|
|
page.wait_for_timeout(50)
|
|
delete_buttons = page.locator("button[title='Delete Rule']")
|
|
|
|
# 测试添加 20 个规则
|
|
add_button = page.locator("button[title='Add Rule']")
|
|
if add_button.count() == 0:
|
|
add_button = page.locator("button:has-text('Add Rule')")
|
|
|
|
start_time = time.time()
|
|
for i in range(20):
|
|
# 重新定位按钮以确保引用的有效性
|
|
current_add_btn = page.locator("button[title='Add Rule']")
|
|
if current_add_btn.count() == 0:
|
|
current_add_btn = page.locator("button:has-text('Add Rule')")
|
|
|
|
current_add_btn.click()
|
|
# 给一点时间让 React 更新 DOM,避免操作过快导致浏览器崩溃或状态不同步
|
|
page.wait_for_timeout(100)
|
|
|
|
page.locator("input[placeholder='Regex Pattern']").last.fill(f"pattern{i}")
|
|
page.locator("input[placeholder='Replacement']").last.fill(f"replacement{i}")
|
|
|
|
end_time = time.time()
|
|
|
|
# 验证时间合理
|
|
elapsed = end_time - start_time
|
|
assert elapsed < 20, f"添加 20 个规则耗时过长: {elapsed:.2f}s" # 验证数量
|
|
assert page.locator("input[placeholder='Regex Pattern']").count() >= 20
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v", "--headed"])
|