using VideoDownloader.App.Browser.Features; namespace VideoDownloader.Tests.Browser; // ── T2-1 명령 팔레트: 등록·검색·실행 ── public class CommandPaletteTests { private sealed class CommandHost { public List Executed = new(); public List Tabs = new() { "https://a.com", "https://b.com" }; public List History = new() { "https://news.com" }; public CommandPalette Build() { var palette = new CommandPalette(); palette.Register("새 탭", "tab new open", () => Executed.Add("newtab")); palette.Register("탭 닫기", "close tab", () => Executed.Add("close")); palette.Register("전체화면", "fullscreen f11", () => Executed.Add("fs")); palette.Register("리더 모드", "reader reading", () => Executed.Add("reader")); palette.Register("다크 모드", "dark theme boost", () => Executed.Add("dark")); palette.RegisterSeparator(); // 동적 소스: 탭/히스토리 → 명령으로 palette.RegisterDynamic(() => Tabs.Select(t => new PaletteCommand($"탭: {t}", "tab go", () => Executed.Add("go:" + t))).ToList()); palette.RegisterDynamic(() => History.Select(h => new PaletteCommand($"기록: {h}", "history", () => Executed.Add("hist:" + h))).ToList()); return palette; } } [Fact] public void 등록된_명령을_검색한다_라벨_키워드_부분일치() { var p = new CommandHost().Build(); var hits = p.Search("리더"); Assert.Single(hits); Assert.Equal("리더 모드", hits[0].Title); hits = p.Search("F11"); // 키워드 검색 Assert.Single(hits); hits = p.Search("TAB"); // 대소문자 무시 Assert.Contains(hits, h => h.Title == "새 탭"); } [Fact] public void 동적_소스_탭과_기록도_결과에_포함된다() { var p = new CommandHost().Build(); var hits = p.Search("탭:"); Assert.Equal(2, hits.Count); hits = p.Search("기록:"); Assert.Single(hits); } [Fact] public void 실행은_콜백을_호출한다() { var host = new CommandHost(); var p = host.Build(); p.Execute(p.Search("전체").Single()); Assert.Equal(new[] { "fs" }, host.Executed); } [Fact] public void 빈_질의는_정적_명령_전체를_순서대로_반환() { var p = new CommandHost().Build(); var hits = p.Search(""); Assert.Equal("새 탭", hits[0].Title); Assert.Equal("다크 모드", hits[^1].Title); // 동적 제외, 정적만 } [Fact] public void 매치_순위는_시작_일치_우선() { var p = new CommandPalette(); p.Register("탭 닫기", "close", () => { }); p.Register("닫힌 탭 복원", "reopen closed", () => { }); var hits = p.Search("탭"); Assert.Equal("탭 닫기", hits[0].Title); // "탭"으로 시작하는 것이 우선 } } // ── T2-7 Boost: 사이트별 강제 다크/커스텀 CSS 영속 ── public class BoostStoreTests : IDisposable { private readonly string _path = Path.Combine(Path.GetTempPath(), $"boost_{Guid.NewGuid():N}.json"); [Fact] public void 기본은_빈_부스트() { var s = new BoostStore(_path); Assert.False(s.Get("example.com").ForceDark); Assert.Equal("", s.Get("example.com").CustomCss); } [Fact] public void 강제다크와_CSS를_저장하면_영속된다() { var s = new BoostStore(_path); s.Set("example.com", forceDark: true, css: "body{zoom:1.1}"); var re = new BoostStore(_path); Assert.True(re.Get("example.com").ForceDark); Assert.Equal("body{zoom:1.1}", re.Get("example.com").CustomCss); } [Fact] public void www_정규화_및_빈_부스트는_CSS를_만들지_않는다() { var s = new BoostStore(_path); s.Set("www.example.com", true, ""); Assert.True(s.Get("example.com").ForceDark); // www 정규화 var js = BoostCssBuilder.BuildScript("plain.org", s.Get("plain.org")); Assert.Equal("", js); // 미설정 사이트 → 빈 스크립트 } [Fact] public void 강제다크_스크립트는_반전필터를_포함하고_이미지_비디오를_보호한다() { var js = BoostCssBuilder.BuildScript("d.com", new BoostSettings(ForceDark: true, "h1{color:red}")); Assert.Contains("invert(1)", js); Assert.Contains("img", js); // 미디어 보호 셀렉터 Assert.Contains("video", js); Assert.Contains("h1{color:red}", js); } public void Dispose() { try { File.Delete(_path); } catch { } } }