Phase C (T2-1..T3-10): command palette, reader mode (real Readability.js, E2E), boost force-dark, screenshot, split view, vertical tabs, Glance (Alt+click), tab groups (6 colors), workspaces, speed dial, InPrivate tabs, quick find, source view, print, PiP, TTS (94 tests GREEN)

This commit is contained in:
Yun Chan 2026-08-15 15:22:44 +09:00
parent 0caa1111e7
commit 406957088f
14 changed files with 3496 additions and 7 deletions

View file

@ -0,0 +1,85 @@
using System.Windows;
using Microsoft.Web.WebView2.Core;
using VideoDownloader.App.Browser.Features;
using VideoDownloader.Browser.Tabs;
using VideoDownloader.Tests.Infrastructure;
namespace VideoDownloader.Tests.E2E;
/// <summary>
/// E2E (T2-4): 실제 WebView2 + 번들된 Mozilla Readability.js 로
/// 기사 페이지에서 본문을 추출해 리더 뷰로 교체하고 원본 URL 복귀까지 검증.
/// </summary>
[Collection("E2E")]
public class ReaderModeE2ETests
{
private const string ArticleHtml = """
<html><head><title>테스트 기사</title></head><body>
<nav>광고 광고 광고 내비게이션</nav>
<article>
<h1>리더 모드 E2E 기사</h1>
<p>첫 번째 문단입니다. 이 문단은 Readability 가 본문으로 추출해야 합니다. 충분한 길이의 텍스트가 필요하므로 몇 번 더 반복합니다. 본문 본문 본문.</p>
<p>두 번째 문단입니다. 리더 모드는 광고와 내비게이션을 제거하고 본문만 남깁니다. 추출 결과에 이 텍스트가 포함되어야 합니다.</p>
<p>세 번째 문단입니다. 문단이 세 개 이상이면 Readability 의 휴리스틱이 본문으로 판정합니다. 마커텍스트-고유문구-레인보우4747.</p>
</article>
<footer>푸터 링크 모음</footer>
</body></html>
""";
[Fact]
public async Task 기사를_리더로_추출하고_원본으로_복귀한다()
{
using var sta = StaDispatcher.Start();
var udf = Path.Combine(Path.GetTempPath(), $"vd_reader_{Guid.NewGuid():N}");
var ok = await sta.InvokeAsync(async () =>
{
var window = new Window();
var browser = new Microsoft.Web.WebView2.Wpf.WebView2();
window.Content = browser;
window.Show();
var env = await CoreWebView2Environment.CreateAsync(userDataFolder: udf);
await browser.EnsureCoreWebView2Async(env);
var cv = browser.CoreWebView2;
cv.NavigateToString(ArticleHtml);
await WaitNav(cv);
var tab = new TabItem { View = browser };
// 1) 리더 진입
var entered = await ReaderMode.ToggleAsync(tab, lightTheme: false);
await Wait.Until(() => Task.FromResult(cv.DocumentTitle ?? ""), t => t.Contains("테스트 기사"), 15000);
var titleAfter = cv.DocumentTitle ?? "";
var bodyText = await cv.ExecuteScriptAsync("document.body.innerText");
var hasMarker = bodyText.Contains("마커텍스트-고유문구-레인보우4747");
var hasNav = bodyText.Contains("광고 광고 광고");
var isReader = tab.IsReader;
var originalSaved = tab.ReaderOriginalUrl;
// 2) 복귀
var exited = await ReaderMode.ToggleAsync(tab, lightTheme: false);
var restoredReader = tab.IsReader == false;
window.Close();
return (entered: entered == true, titleAfter, hasMarker, navRemoved: !hasNav, isReader, originalSaved, exited: exited == false, restoredReader);
});
try { Directory.Delete(udf, true); } catch { }
Assert.True(ok.entered, "리더 진입 실패");
Assert.Contains("테스트 기사", ok.titleAfter);
Assert.True(ok.hasMarker, "본문 마커 누락");
Assert.True(ok.navRemoved, "내비/광고 제거 안됨");
Assert.True(ok.isReader);
Assert.True(ok.exited);
Assert.True(ok.restoredReader);
}
private static Task WaitNav(CoreWebView2 cv)
{
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
EventHandler<CoreWebView2NavigationCompletedEventArgs>? h = null;
h = (_, _) => { cv.NavigationCompleted -= h; tcs.TrySetResult(); };
cv.NavigationCompleted += h;
return tcs.Task.WaitAsync(TimeSpan.FromSeconds(15));
}
}