Add UI Test Migration Guide and update test files for new React frontend
This commit is contained in:
+160
-38
@@ -1,11 +1,16 @@
|
||||
"""
|
||||
UI 集成测试 - case_mix:清空规则、设置规则并执行四种同步策略
|
||||
|
||||
运行前准备:
|
||||
1. 启动Docker服务: docker compose up -d
|
||||
2. 确保服务运行在 http://localhost:8888
|
||||
|
||||
运行:
|
||||
pytest tests/test_ui_case_mix.py --headed # 显示浏览器
|
||||
pytest tests/test_ui_case_mix.py # 无头模式
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import time
|
||||
@@ -13,20 +18,92 @@ import time
|
||||
from playwright.sync_api import Page, expect
|
||||
|
||||
|
||||
BASE_URL = "http://localhost:8080"
|
||||
BASE_URL = "http://localhost:8888"
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parent.parent
|
||||
OUTPUT_DIR = PROJECT_ROOT / "dockerapp" / "test_playlists" / "case_mix"
|
||||
OUTPUT_DIR = PROJECT_ROOT / "output_playlists" / "case_mix"
|
||||
EXPECTED_DIR = PROJECT_ROOT / "test_res"
|
||||
|
||||
|
||||
class SyncStrategy(str, Enum):
|
||||
"""同步策略枚举"""
|
||||
LOCAL_OVERWRITE = "LOCAL_OVERWRITE"
|
||||
CLOUD_OVERWRITE = "CLOUD_OVERWRITE"
|
||||
MERGE_LOCAL = "MERGE_LOCAL"
|
||||
MERGE_CLOUD = "MERGE_CLOUD"
|
||||
|
||||
|
||||
def _handle_connection_modal(page: Page):
|
||||
"""处理登录模态框:如果存在则关闭"""
|
||||
# 检查模态框是否存在 (根据 ConnectionModal.tsx 的结构)
|
||||
# 模态框通常有一个全屏的遮罩层
|
||||
modal_overlay = page.locator("div.fixed.inset-0.z-50")
|
||||
|
||||
if modal_overlay.is_visible():
|
||||
print("检测到登录模态框,尝试关闭...")
|
||||
# 尝试找到关闭按钮 (通常在右上角,包含 X 图标)
|
||||
# 在 ConnectionModal.tsx 中,关闭按钮在 Header 里
|
||||
close_button = modal_overlay.locator("button").filter(has=page.locator("svg")).last
|
||||
|
||||
if close_button.is_visible():
|
||||
close_button.click()
|
||||
else:
|
||||
# 如果找不到关闭按钮,尝试按 ESC
|
||||
page.keyboard.press("Escape")
|
||||
|
||||
page.wait_for_timeout(500) # 等待模态框关闭动画
|
||||
|
||||
|
||||
def _open_strategy_selector(page: Page):
|
||||
"""打开策略选择器下拉菜单"""
|
||||
# 1. 先处理可能遮挡的登录模态框
|
||||
_handle_connection_modal(page)
|
||||
|
||||
# 2. 检查下拉菜单是否已经打开
|
||||
# 下拉菜单的特征类名
|
||||
dropdown = page.locator("div.absolute.top-14")
|
||||
|
||||
if dropdown.is_visible():
|
||||
return # 已经打开,无需操作
|
||||
|
||||
# 3. 查找并点击策略选择器按钮
|
||||
# 使用 title 属性定位更准确 (StrategySelector.tsx 中定义了 title="Current Strategy: ...")
|
||||
strategy_button = page.locator("button[title^='Current Strategy']")
|
||||
|
||||
if strategy_button.count() == 0:
|
||||
# 备用定位方式:查找包含特定图标的圆形按钮
|
||||
# 注意:页面上可能有多个按钮,需要小心
|
||||
# 策略按钮在中间,且包含 ChevronDown 小图标
|
||||
strategy_button = page.locator("button").filter(has=page.locator("svg")).nth(1)
|
||||
# nth(0) 可能是 Header 里的连接按钮
|
||||
|
||||
if strategy_button.is_visible():
|
||||
strategy_button.click()
|
||||
page.wait_for_timeout(300) # 等待下拉菜单动画完成
|
||||
else:
|
||||
print("警告: 无法找到策略选择器按钮")
|
||||
|
||||
|
||||
def _clear_all_rules(page: Page):
|
||||
while page.locator(".rule-row").count() > 0:
|
||||
"""清空所有正则规则"""
|
||||
_open_strategy_selector(page)
|
||||
|
||||
# 等待下拉菜单打开
|
||||
dropdown = page.locator("div.absolute.top-14")
|
||||
expect(dropdown).to_be_visible()
|
||||
|
||||
# 查找并点击所有删除按钮
|
||||
delete_buttons = page.locator("button[title='Delete Rule']")
|
||||
while delete_buttons.count() > 0:
|
||||
try:
|
||||
page.locator(".rule-row button[title='删除此规则']").first.click()
|
||||
page.wait_for_timeout(50)
|
||||
delete_buttons.first.click()
|
||||
page.wait_for_timeout(100)
|
||||
except Exception:
|
||||
break
|
||||
|
||||
# 关闭下拉菜单
|
||||
page.keyboard.press("Escape")
|
||||
page.wait_for_timeout(200)
|
||||
|
||||
|
||||
def _normalize_playlist_lines(file_path: Path) -> list[str]:
|
||||
@@ -74,8 +151,12 @@ def test_case_mix_run_all_modes(page: Page):
|
||||
# 导航到首页并确认加载(端口为 8080)
|
||||
page.goto(BASE_URL + "/")
|
||||
page.wait_for_load_state("networkidle")
|
||||
page.wait_for_timeout(1000) # 等待React应用初始化
|
||||
expect(page).to_have_url(BASE_URL + "/")
|
||||
|
||||
# 处理可能出现的登录模态框
|
||||
_handle_connection_modal(page)
|
||||
|
||||
# 1. 清空规则
|
||||
_clear_all_rules(page)
|
||||
|
||||
@@ -87,27 +168,50 @@ def test_case_mix_run_all_modes(page: Page):
|
||||
(r"/", r"\\"), # 斜杠统一为反斜杠(需在映射后)
|
||||
]
|
||||
|
||||
add_button = page.locator("#addRuleBtn")
|
||||
# 打开策略选择器
|
||||
_open_strategy_selector(page)
|
||||
|
||||
# 添加规则
|
||||
for pattern, replacement in rules:
|
||||
# 点击 "Add Rule" 按钮
|
||||
add_button = page.locator("button[title='Add Rule']")
|
||||
if add_button.count() == 0:
|
||||
# 如果没有"Add Rule"按钮,尝试使用带文本的按钮
|
||||
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)
|
||||
page.wait_for_timeout(100)
|
||||
page.locator("input[name='pattern']").last.fill(pattern)
|
||||
page.locator("input[name='replacement']").last.fill(replacement)
|
||||
|
||||
# 保存
|
||||
page.locator("button:has-text('保存规则')").click()
|
||||
page.wait_for_load_state("networkidle")
|
||||
# 成功提示可选校验
|
||||
success_indicator = page.locator(".alert-success, .text-success, :has-text('已保存'), :has-text('保存规则')")
|
||||
if success_indicator.count() > 0:
|
||||
expect(success_indicator.first).to_be_visible()
|
||||
# 保存规则 - 点击 "Save Changes" 按钮
|
||||
save_button = page.locator("button:has-text('Save Changes')")
|
||||
expect(save_button).to_be_enabled()
|
||||
save_button.click()
|
||||
page.wait_for_timeout(500) # 等待保存完成
|
||||
|
||||
# 验证保存成功 - 检查toast通知
|
||||
toast = page.locator("div:has-text('Regex preprocessing rules have been saved')")
|
||||
if toast.count() > 0:
|
||||
expect(toast.first).to_be_visible()
|
||||
|
||||
# 关闭下拉菜单
|
||||
page.keyboard.press("Escape")
|
||||
page.wait_for_timeout(300)
|
||||
|
||||
# 3. 依次执行四种同步模式,每次执行后立即验证
|
||||
test_cases = [
|
||||
("local_force", "case_mix_local_force.m3u"),
|
||||
("remote_force", "case_mix_remote_force.m3u"),
|
||||
("merge_local_primary", "case_mix_merge_local_primary.m3u"),
|
||||
("merge_remote_primary", "case_mix_merge_remote_primary.m3u"),
|
||||
# 策略名称映射: UI中的策略值 -> 测试用例名称
|
||||
strategy_mappings = [
|
||||
(SyncStrategy.LOCAL_OVERWRITE, "Local Overwrite", "case_mix_local_force.m3u"),
|
||||
(SyncStrategy.CLOUD_OVERWRITE, "Cloud Overwrite", "case_mix_remote_force.m3u"),
|
||||
(SyncStrategy.MERGE_LOCAL, "Two-way Merge (Local Priority)", "case_mix_merge_local_primary.m3u"),
|
||||
(SyncStrategy.MERGE_CLOUD, "Two-way Merge (Cloud Priority)", "case_mix_merge_remote_primary.m3u"),
|
||||
]
|
||||
|
||||
# 准备初始 Base(每次测试前恢复)
|
||||
@@ -117,44 +221,62 @@ N:\\Music\\Anime\\CITY THE ANIMATION\\Hello\\01. Hello - Hello.flac
|
||||
N:\\Music\\音ゲー\\USAO\\MEMORIES\\15. Empty Room (Srav3R Remix) - MEMORIES.flac
|
||||
"""
|
||||
|
||||
for mode, expected_file in test_cases:
|
||||
print(f"\n==== 执行同步策略: {mode} ====")
|
||||
for strategy_value, strategy_label, expected_file in strategy_mappings:
|
||||
print(f"\n==== 执行同步策略: {strategy_label} ====")
|
||||
|
||||
# 恢复初始 Base(避免前次同步影响)
|
||||
base_next_path = OUTPUT_DIR / "base_next.m3u8"
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
with open(base_next_path, "w", encoding="utf-8") as f:
|
||||
f.write(initial_base_content)
|
||||
print(f"已恢复初始 Base: {base_next_path}")
|
||||
|
||||
# 选择模式
|
||||
page.select_option("select[name='mode']", value=mode)
|
||||
# 选择策略 - 打开下拉菜单
|
||||
_open_strategy_selector(page)
|
||||
|
||||
# 执行同步
|
||||
page.locator("form[action='/sync'] button[type='submit']").click()
|
||||
page.wait_for_load_state("networkidle")
|
||||
# 点击对应的策略选项 - 更精确的定位
|
||||
# 找到包含策略名称的可点击div (class包含cursor-pointer)
|
||||
strategy_option = page.locator("div.cursor-pointer").filter(has_text=strategy_label)
|
||||
expect(strategy_option.first).to_be_visible()
|
||||
strategy_option.first.click()
|
||||
page.wait_for_timeout(500) # 等待策略保存
|
||||
|
||||
# 等待提示出现(如果存在)
|
||||
alert = page.locator(".alert-success, .alert-info, :has-text('同步完成')")
|
||||
if alert.count() > 0:
|
||||
expect(alert.first).to_be_visible()
|
||||
# 验证策略选择成功的toast
|
||||
toast = page.locator(f"div:has-text('Selected strategy \"{strategy_label}\" has been saved')")
|
||||
if toast.count() > 0:
|
||||
expect(toast.first).to_be_visible(timeout=3000)
|
||||
|
||||
time.sleep(0.5) # 确保文件写入完成
|
||||
# 关闭下拉菜单
|
||||
page.keyboard.press("Escape")
|
||||
page.wait_for_timeout(300)
|
||||
|
||||
# 执行同步 - 通过API触发同步操作
|
||||
# 新UI需要显式调用同步API
|
||||
import requests
|
||||
sync_response = requests.post(
|
||||
f"{BASE_URL}/api/sync",
|
||||
json={"mode": None} # 使用当前配置的策略
|
||||
)
|
||||
assert sync_response.status_code == 200, f"同步API调用失败: {sync_response.text}"
|
||||
print(f"同步API响应: {sync_response.json()}")
|
||||
|
||||
time.sleep(1) # 确保文件写入完成
|
||||
|
||||
# 验证输出文件生成
|
||||
local_result = OUTPUT_DIR / "local_result.m3u8"
|
||||
remote_result = OUTPUT_DIR / "remote_result.m3u8"
|
||||
base_next = OUTPUT_DIR / "base_next.m3u8"
|
||||
|
||||
assert local_result.exists(), f"{mode}: local_result.m3u8 未生成"
|
||||
assert remote_result.exists(), f"{mode}: remote_result.m3u8 未生成"
|
||||
assert base_next.exists(), f"{mode}: base_next.m3u8 未生成"
|
||||
assert local_result.exists(), f"{strategy_label}: local_result.m3u8 未生成"
|
||||
assert remote_result.exists(), f"{strategy_label}: remote_result.m3u8 未生成"
|
||||
assert base_next.exists(), f"{strategy_label}: base_next.m3u8 未生成"
|
||||
|
||||
# 与期望结果对比(使用 local_result.m3u8 作为主要输出)
|
||||
expected_path = EXPECTED_DIR / expected_file
|
||||
match, diff = _compare_playlists(local_result, expected_path)
|
||||
|
||||
# 备份当前输出以便后续检查
|
||||
backup_dir = OUTPUT_DIR / f"backup_{mode}"
|
||||
backup_dir = OUTPUT_DIR / f"backup_{strategy_value}"
|
||||
backup_dir.mkdir(exist_ok=True)
|
||||
shutil.copy(local_result, backup_dir / "local_result.m3u8")
|
||||
shutil.copy(remote_result, backup_dir / "remote_result.m3u8")
|
||||
@@ -163,7 +285,7 @@ N:\\Music\\音ゲー\\USAO\\MEMORIES\\15. Empty Room (Srav3R Remix) - MEMORIES.f
|
||||
print(f"输出已备份到: {backup_dir}")
|
||||
|
||||
# 断言匹配
|
||||
assert match, f"{mode} 输出与期望不符:\n{diff}"
|
||||
print(f"✓ {mode} 验证通过")
|
||||
assert match, f"{strategy_label} 输出与期望不符:\n{diff}"
|
||||
print(f"✓ {strategy_label} 验证通过")
|
||||
|
||||
print("\n==== 全部四种策略测试通过 ====")
|
||||
|
||||
Reference in New Issue
Block a user