Files
PlexPlaylistSync/tests/test_ui_regex_rules.py
T

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"])