From 00a822eaebee3a64d4e804de36cade1e517b1c64 Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Sat, 15 Aug 2026 12:14:35 +0900 Subject: [PATCH] Phase 0 (T1-3/4/7/9/11): find bar w/ native Find API + search engine + site zoom + force reload + F11, wired into MainWindow, E2E smoke (39 tests) --- VideoDownloader.App/Browser/FindSession.cs | 126 ++++++++++++++++ VideoDownloader.App/MainWindow.xaml | 46 +++++- VideoDownloader.App/MainWindow.xaml.cs | 139 ++++++++++++++++-- VideoDownloader.App/SettingsWindow.xaml | 8 + VideoDownloader.App/SettingsWindow.xaml.cs | 15 ++ .../VideoDownloader.App.csproj | 5 + VideoDownloader.Core/Config/AppConfig.cs | 3 + .../Browser/FindSessionTests.cs | 98 ++++++++++++ .../E2E/MainWindowSmokeE2ETests.cs | 58 ++++++++ .../E2E/WebView2FindE2ETests.cs | 71 +++++++++ .../Infrastructure/WpfTestApp.cs | 33 +++++ 11 files changed, 584 insertions(+), 18 deletions(-) create mode 100644 VideoDownloader.App/Browser/FindSession.cs create mode 100644 VideoDownloader.Tests/Browser/FindSessionTests.cs create mode 100644 VideoDownloader.Tests/E2E/MainWindowSmokeE2ETests.cs create mode 100644 VideoDownloader.Tests/E2E/WebView2FindE2ETests.cs create mode 100644 VideoDownloader.Tests/Infrastructure/WpfTestApp.cs diff --git a/VideoDownloader.App/Browser/FindSession.cs b/VideoDownloader.App/Browser/FindSession.cs new file mode 100644 index 0000000..59a31f5 --- /dev/null +++ b/VideoDownloader.App/Browser/FindSession.cs @@ -0,0 +1,126 @@ +using Microsoft.Web.WebView2.Core; + +namespace VideoDownloader.App.Browser; + +/// 네이티브 Find API 의 최소 추상화(단위 테스트용 fake 교체 가능). +public interface IFindApi +{ + Task StartAsync(string term, bool caseSensitive, bool matchWord, bool highlightAll); + void FindNext(); + void FindPrevious(); + void Stop(); + event EventHandler? MatchCountChanged; + event EventHandler? ActiveMatchIndexChanged; +} + +/// WebView2 네이티브 Find(CoreWebView2.Find) 를 IFindApi 로 감싼 어댑터. +/// 옵션 객체는 public ctor 가 없어 CoreWebView2Environment.CreateFindOptions() 로 생성. +public sealed class WebView2FindApi : IFindApi +{ + private readonly CoreWebView2 _cv; + private readonly CoreWebView2Environment _env; + + public WebView2FindApi(CoreWebView2 cv, CoreWebView2Environment env) + { + _cv = cv; + _env = env; + } + + public Task StartAsync(string term, bool caseSensitive, bool matchWord, bool highlightAll) + { + var o = _env.CreateFindOptions(); + o.FindTerm = term; + o.IsCaseSensitive = caseSensitive; + o.ShouldMatchWord = matchWord; + o.ShouldHighlightAllMatches = highlightAll; + o.SuppressDefaultFindDialog = true; // 자체 WPF 검색 바 사용 + return _cv.Find.StartAsync(o); + } + + public void FindNext() => _cv.Find.FindNext(); + public void FindPrevious() => _cv.Find.FindPrevious(); + public void Stop() => _cv.Find.Stop(); + + // 이벤트 인자가 object 로 전달되므로 매치 수는 Find 프로퍼티에서 직접 읽는다. + public event EventHandler? MatchCountChanged + { + add => _cv.Find.MatchCountChanged += (s, _) => value?.Invoke(s, _cv.Find.MatchCount); + remove { } + } + + public event EventHandler? ActiveMatchIndexChanged + { + add => _cv.Find.ActiveMatchIndexChanged += (s, _) => value?.Invoke(s, _cv.Find.ActiveMatchIndex); + remove { } + } +} + +/// +/// 페이지 내 검색 세션 상태 머신(T1-3). 용어·매치 수·활성 인덱스를 유지하고 +/// UI(검색 바)가 표시할 "3 / 17" 텍스트를 제공한다. +/// +public sealed class FindSession : IDisposable +{ + private readonly IFindApi _api; + + public string Term { get; private set; } = string.Empty; + public int MatchCount { get; private set; } + public int ActiveMatchIndex { get; private set; } + public bool IsCaseSensitive { get; private set; } + public bool MatchWholeWord { get; private set; } + + public event Action? StateChanged; + + public FindSession(IFindApi api) + { + _api = api; + _api.MatchCountChanged += (_, n) => { MatchCount = n; StateChanged?.Invoke(); }; + _api.ActiveMatchIndexChanged += (_, n) => { ActiveMatchIndex = n; StateChanged?.Invoke(); }; + } + + public string StatusText + { + get + { + if (MatchCount <= 0) return "결과 없음"; + var oneBased = ActiveMatchIndex >= 0 ? ActiveMatchIndex + 1 : 0; + return $"{oneBased} / {MatchCount}"; + } + } + + public async Task StartAsync(string term) + { + Term = term ?? ""; + if (string.IsNullOrWhiteSpace(term)) + { + Close(); + return; + } + MatchCount = 0; + ActiveMatchIndex = -1; + await _api.StartAsync(Term, IsCaseSensitive, MatchWholeWord, highlightAll: true); + StateChanged?.Invoke(); + } + + public void SetOptions(bool caseSensitive, bool matchWholeWord) + { + IsCaseSensitive = caseSensitive; + MatchWholeWord = matchWholeWord; + if (!string.IsNullOrWhiteSpace(Term)) + _ = StartAsync(Term); // 옵션 변경은 재시작 필요(네이티브 제약) + } + + public void Next() => _api.FindNext(); + public void Previous() => _api.FindPrevious(); + + public void Close() + { + _api.Stop(); + Term = string.Empty; + MatchCount = 0; + ActiveMatchIndex = -1; + StateChanged?.Invoke(); + } + + public void Dispose() => Close(); +} diff --git a/VideoDownloader.App/MainWindow.xaml b/VideoDownloader.App/MainWindow.xaml index f0cdeb9..eaafcb5 100644 --- a/VideoDownloader.App/MainWindow.xaml +++ b/VideoDownloader.App/MainWindow.xaml @@ -159,7 +159,7 @@ - @@ -217,9 +217,45 @@ - - - + + + + + + + + + + + + + + + + + + +