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