using System.Windows; using System.Windows.Controls; using Paca.App; using Paca.App.Browser.Features; using Paca.App.Core; using Paca.App.Services; using Paca.App.ViewModels; using Paca.Core.Config; using Paca.Tests.Infrastructure; using WpfApp = Paca.App.App; namespace Paca.Tests.E2E; /// /// E2E: "UI 복잡 버전"(docs/UI_USECASES.md §A~§J) GREEN 구현의 실창 실증 — /// MainWindowSmokeE2ETests 와 동일한 StaDispatcher/WpfTestApp 패턴으로 실제 윈도우를 띄워 /// ① 신규 유즈케이스 컨트롤이 실 트리에 존재하는지 ② 테마가 재시작 없이 라이브 전환되는지(UC-T03/T04) /// ③ 다운로드 일시정지/재개 상태 전이(UC-D06/D07) ④ 설정창 검색 필터·테마 즉시 적용(UC-S01) /// ⑤ 토스트 방해 금지 억제·알림센터 히스토리(UC-N14/N15)를 검증한다. /// [Collection("E2E")] public class UiUsecaseLiveE2ETests { private static string TempDataDir() { var dir = Path.Combine(Path.GetTempPath(), $"vd_uc_e2e_{Guid.NewGuid():N}"); Environment.SetEnvironmentVariable("VD_DATA_DIR", dir); return dir; } [Fact] public async Task MainWindow_유즈케이스_컨트롤_실존과_라이브_테마_전환_E2E() { var dataDir = TempDataDir(); try { using var sta = StaDispatcher.Start(); WpfTestApp.Ensure(sta.Dispatcher); var ok = await sta.InvokeAsync(async () => { var window = new MainWindow(); window.Show(); var first = await Wait.Until( () => Task.FromResult(window.Tabs.ActiveTab), t => t?.Core != null, 30000); // ── §C/§D/§E/§I/§J 신규 컨트롤이 실 트리에 존재(최상위 x:Name 필드) ── var mustExist = new FrameworkElement?[] { // 툴바 window.CompactModeBtn, window.LibraryBtn, window.PairingBtn, window.DownloadsBtnBadge, window.UpdateBadge, // 탭 스트립 window.WorkspaceIndicator, window.WorkspaceSwitcher, window.NewTabMoreBtn, window.ArchiveBtn, window.TabInsertIndicator, // 다운로드 패널 window.DownloadFilterTabs, window.DownloadSearchBox, window.QueueSummaryText, window.GlobalSpeedText, window.SpeedSparkline, // 상태바·오버레이 window.StatusRight, window.ZoomIndicator, window.ServerStatusIcon, window.NotificationCenter, window.ShortcutHelpBtn, window.AggregateProgress, window.ToastHost, window.ErrorBanner, window.ErrorDetailExpander, window.ClipboardHint, window.Onboarding, // 팝업 본문 window.ArchivePopup, window.ArchiveSearchBox, window.LibraryPopup, window.LibrarySearchBox, window.LibraryViewToggle, window.LibrarySortMenu, window.BookmarkSearchBox, window.HistorySearchBox, window.NotesSearchBox, window.PlaylistSearchBox, window.PlaylistShuffle, window.ShortcutHelpPopup, window.NotificationCenterPopup, window.TabVolumePopup, }; var missing = mustExist.Count(e => e == null); // ── §J 초기 상태 계약: 온보딩 표시(세션 없음)·오류배너·클립보드힌트 숨김 ── var onboardingShown = window.Onboarding.Visibility == Visibility.Visible; var errorHidden = window.ErrorBanner.Visibility == Visibility.Collapsed; var clipboardHidden = window.ClipboardHint.Visibility == Visibility.Collapsed; // ── §A 라이브 테마 전환(UC-T03/T04) — 창을 다시 만들지 않고 즉시 ── window.ApplyTheme("light"); var lightApplied = WpfApp.CurrentTheme == "light" && WpfApp.IsLightTheme; var aliveInLight = window.IsLoaded; window.ApplyTheme("dark"); var darkApplied = WpfApp.CurrentTheme == "dark" && !WpfApp.IsLightTheme; var aliveInDark = window.IsLoaded; var lightRestore = WpfApp.CurrentTheme == "dark"; // 원복 확인 window.Close(); await Task.Delay(800); // WebView2 파일 핸들 해제 여유(smoke 테스트 패턴) var fails = new List(); if (missing != 0) fails.Add($"컨트롤 누락 {missing}개"); if (!onboardingShown) fails.Add("온보딩 미표시"); if (!errorHidden) fails.Add("오류배너 초기 미숨김"); if (!clipboardHidden) fails.Add("클립보드힌트 초기 미숨김"); if (!lightApplied) fails.Add("라이트 테마 미적용"); if (!aliveInLight) fails.Add("라이트 전환 후 창 사망"); if (!darkApplied) fails.Add("다크 원복 미적용"); if (!aliveInDark) fails.Add("다크 전환 후 창 사망"); return string.Join(" | ", fails); }); Assert.True(string.IsNullOrEmpty(ok), $"MainWindow 유즈케이스 E2E 실패: {ok}"); } finally { Environment.SetEnvironmentVariable("VD_DATA_DIR", null); try { Directory.Delete(dataDir, recursive: true); } catch { } // WebView2 파일 점유 시 남김(smoke 테스트 패턴) } } [Fact] public void DownloadManager_전체_일시정지_및_재개_상태전이_E2E() { var dataDir = TempDataDir(); var outDir = Path.Combine(dataDir, "out"); try { using var sta = StaDispatcher.Start(); WpfTestApp.Ensure(sta.Dispatcher); var ok = sta.Invoke(() => { var config = new AppConfig { OutputFolder = outDir, MaxConcurrentDownloads = 2 }; var manager = new DownloadManager( config, _ => Task.FromResult(new List()), _ => { }); var running = new DownloadItemViewModel("진행 중 항목") { Status = "다운로드 중", Started = true, MediaUrl = new Uri("https://127.0.0.1:9/video.mp4"), // 비라우팅 — 재개가 네트워크를 치지 않게 OutputBase = Path.Combine(outDir, "video"), }; var queued = new DownloadItemViewModel("대기 항목") { Status = "대기", MediaUrl = new Uri("https://example.com/video2.mp4"), OutputBase = Path.Combine(outDir, "video2"), }; manager.Items.Add(running); manager.Items.Add(queued); // UC-D06: 전체 일시정지 — 진행 중만 "일시정지" 전이, 대기 항목은 그대로 manager.PauseAll(); var pausedStatus = running.Status == "일시정지" && running.PausedForResume; var queuedUntouched = queued.Status == "대기"; // UC-D07: 전체 재개 — 일시정지 항목만 재투입(PausedForResume 해제 + Started 재설정) manager.ResumeAll(); var resumed = !running.PausedForResume && running.Started; // 재시작 큐 진입 manager.Cancel(running); // 실측 목적 달성 — 원격 접속 시도 중단 var queuedStillQueued = queued.Started == false && queued.PausedForResume == false; var fails = new List(); if (!pausedStatus) fails.Add($"PauseAll 후 상태={running.Status}/PausedForResume={running.PausedForResume}"); if (!queuedUntouched) fails.Add($"대기 항목이 건드려짐(상태={queued.Status})"); if (!resumed) fails.Add($"ResumeAll 후 재투입 안 됨(Started={running.Started}/PausedForResume={running.PausedForResume})"); if (!queuedStillQueued) fails.Add("대기 항목이 재개 대상이 됨"); return string.Join(" | ", fails); }); Assert.True(string.IsNullOrEmpty(ok), $"일시정지/재개 E2E 실패: {ok}"); } finally { Environment.SetEnvironmentVariable("VD_DATA_DIR", null); try { Directory.Delete(dataDir, recursive: true); } catch { } // WebView2 파일 점유 시 남김(smoke 테스트 패턴) } } [Fact] public void SettingsWindow_검색_필터와_테마_즉시적용_E2E() { var dataDir = TempDataDir(); try { using var sta = StaDispatcher.Start(); WpfTestApp.Ensure(sta.Dispatcher); var ok = sta.Invoke(() => { var config = new AppConfig(); // 기본 테마 dark var win = new SettingsWindow(config); // 생성 시점 로드: 테마 dark 반영 + 검색창 비어 전 섹션 표시 var initialTheme = WpfApp.CurrentTheme == "dark"; var allVisible = win.SectionFont.Visibility == Visibility.Visible && win.SectionDownload.Visibility == Visibility.Visible; // UC-T01/T02: 콤보박스로 라이트 선택 → 앱 테마가 즉시 전환(재시작 없음) win.ThemeModeBox.SelectedValue = "light"; var lightLive = WpfApp.CurrentTheme == "light" && WpfApp.IsLightTheme; win.ThemeModeBox.SelectedValue = "dark"; var backToDark = WpfApp.CurrentTheme == "dark"; // UC-S01: 설정 검색 — 해당 섹션만 노출 win.SettingsSearchBox.Text = "글꼴"; var fontOnly = win.SectionFont.Visibility == Visibility.Visible && win.SectionDownload.Visibility == Visibility.Collapsed && win.SectionNotify.Visibility == Visibility.Collapsed; win.SettingsSearchBox.Text = "알림"; var downloadOnly = win.SectionNotify.Visibility == Visibility.Visible && win.SectionDownload.Visibility == Visibility.Collapsed && win.SectionAccessibility.Visibility == Visibility.Collapsed; win.SettingsSearchBox.Text = ""; var restored = win.SectionFont.Visibility == Visibility.Visible && win.SectionDownload.Visibility == Visibility.Visible; win.Close(); var fails = new List(); if (!initialTheme) fails.Add($"초기 테마 dark 아님(현재 {WpfApp.CurrentTheme})"); if (!allVisible) fails.Add("초기 전 섹션 표시 아님"); if (!lightLive) fails.Add($"라이트 즉시 적용 실패(현재 {WpfApp.CurrentTheme}/{WpfApp.IsLightTheme})"); if (!backToDark) fails.Add($"다크 원복 실패(현재 {WpfApp.CurrentTheme})"); if (!fontOnly) fails.Add("'글꼴' 검색 시 글꼴 섹션만 표시 아님"); if (!downloadOnly) fails.Add("'알림' 검색 시 알림 섹션만 표시 아님"); if (!restored) fails.Add("빈 검색어로 복원 안 됨"); return string.Join(" | ", fails); }); Assert.True(string.IsNullOrEmpty(ok), $"설정창 E2E 실패: {ok}"); } finally { Environment.SetEnvironmentVariable("VD_DATA_DIR", null); try { Directory.Delete(dataDir, recursive: true); } catch { } // WebView2 파일 점유 시 남김(smoke 테스트 패턴) } } [Fact] public void ToastNotifier_방해금지_억제와_알림센터_히스토리_E2E() { using var sta = StaDispatcher.Start(); WpfTestApp.Ensure(sta.Dispatcher); var ok = sta.Invoke(() => { var host = new StackPanel(); var toast = new ToastNotifier(host) { Duration = TimeSpan.FromMilliseconds(500) }; // 일반 표시: 토스트 1장 + 히스토리 1건(UC-N02) toast.Show("완료: 첫 번째", "success"); var shown = host.Children.Count == 1 && toast.History.Count == 1; // UC-N15 방해 금지: 표시는 억제되고 히스토리에만 기록(UC-N14 알림 센터 원천) toast.DoNotDisturb = true; toast.Show("실패: 두 번째", "error"); var suppressed = host.Children.Count == 1 && toast.History.Count == 2; return shown && suppressed; }); Assert.True(ok, "토스트 방해 금지/히스토리 계약 위반"); } [Fact] public void MainWindow_우측패널_접기_시_모든내부요소_숨김_E2E() { var dataDir = TempDataDir(); try { using var sta = StaDispatcher.Start(); WpfTestApp.Ensure(sta.Dispatcher); var ok = sta.Invoke(() => { var window = new MainWindow(); // 1. 펼침 상태 확인 window.ApplyPanelState(false); Assert.Equal(Visibility.Visible, window.SideTabsContainer.Visibility); Assert.Equal(Visibility.Visible, window.PanelBodyContainer.Visibility); Assert.Equal(Visibility.Visible, window.PanelCountBadge.Visibility); Assert.Equal(Visibility.Visible, window.RightSplitter.Visibility); // 2. 접힘 상태 확인 - 툴바, 요약 텍스트, 바디, 세그먼트 탭 등 모든 내부 요소가 완전히 숨겨져야 함 window.ApplyPanelState(true); Assert.Equal(Visibility.Collapsed, window.SideTabsContainer.Visibility); Assert.Equal(Visibility.Collapsed, window.PanelBodyContainer.Visibility); Assert.Equal(Visibility.Collapsed, window.PanelCountBadge.Visibility); Assert.Equal(Visibility.Collapsed, window.RightSplitter.Visibility); Assert.Equal(56, window.RightCol.Width.Value); window.Close(); return true; }); Assert.True(ok); } finally { Environment.SetEnvironmentVariable("VD_DATA_DIR", null); try { Directory.Delete(dataDir, recursive: true); } catch { } } } }