using Paca.Mobile; using System; using System.Collections.Generic; using System.Linq; using Paca.Mobile.Core; using Xunit; namespace Paca.Tests.Mobile; /// /// 모바일 프로덕션 레벨 종합 고도화 게이트 테스트 (Gate 103 ~ Gate 110) /// - 아이콘 시스템, 테마 시스템, 탭 시스템, 즐겨찾기, 영상 찾기, 재시도 큐 관리, 설정, 탭 동기화 /// public class MobileProductionExcellenceGateTests { // [Gate 103] 아이콘 시스템: 이모지 배제 및 시맨틱 아이콘 글리프 매핑 무결성 [Theory] [InlineData(MobileIconKind.Search, "magnifyingglass")] [InlineData(MobileIconKind.Queue, "list.bullet")] [InlineData(MobileIconKind.Download, "arrow.down.circle")] [InlineData(MobileIconKind.Favorites, "star.fill")] [InlineData(MobileIconKind.Settings, "gearshape.fill")] [InlineData(MobileIconKind.Retry, "arrow.clockwise")] public void Gate103_MobileIconSystem_SemanticGlyph_Audit(MobileIconKind kind, string expectedGlyph) { var glyph = MobileIconSystem.GetGlyph(kind); Assert.Equal(expectedGlyph, glyph); // 유니코드 이모지가 포함되지 않았는지 검증 Assert.DoesNotMatch(@"[\uD83C-\uDBFF\uDC00-\uDFFF]", glyph); } // [Gate 104] 테마 시스템: OLED Black을 포함한 4대 테마 토큰 및 명도 대비 [Theory] [InlineData(MobileThemeMode.Dark, "#0B0C10", "#161822", "#FFFFFF", "#5E56E8")] [InlineData(MobileThemeMode.Light, "#F8FAFC", "#FFFFFF", "#0F172A", "#4F46E5")] [InlineData(MobileThemeMode.OLEDBlack, "#000000", "#090A0F", "#FFFFFF", "#00F0FF")] [InlineData(MobileThemeMode.HighContrast, "#000000", "#000000", "#FFFFFF", "#FFFF00")] public void Gate104_MobileThemeSystem_Tokens_And_Contrast_Audit( MobileThemeMode mode, string bg, string card, string text, string accent) { var p = MobileThemeSystem.GetPalette(mode); Assert.Equal(bg, p.Background); Assert.Equal(card, p.CardBackground); Assert.Equal(text, p.TextPrimary); Assert.Equal(accent, p.Accent); } // [Gate 105] 탭 시스템: 탭 추가, 전환, 닫기, 시크릿 탭 모드 라이프사이클 [Fact] public void Gate105_MobileTabManager_MultiTab_Lifecycle_Audit() { var tm = new MobileTabManager(); var tab1 = tm.NewTab("https://www.youtube.com", "YouTube"); var tab2 = tm.NewTab("https://vimeo.com", "Vimeo", isIncognito: true); Assert.Equal(3, tm.Tabs.Count); Assert.Equal(tab2.Id, tm.ActiveTab?.Id); Assert.True(tab2.IsIncognito); // 탭 1로 전환 tm.SwitchTab(tab1.Id); Assert.Equal(tab1.Id, tm.ActiveTab?.Id); // 탭 닫기 tm.CloseTab(tab2.Id); Assert.Equal(2, tm.Tabs.Count); Assert.Equal(tab1.Id, tm.ActiveTab?.Id); } // [Gate 106] 즐겨찾기 & 보관함(Stash): 태그 필터링 및 다운로드 대기 큐 연계 [Fact] public void Gate106_MobileFavoritesManager_TagFilter_And_Stash_Audit() { var fav = new MobileFavoritesManager(); fav.AddFavorite("https://youtube.com/v1", "Rock Festival Live", "음악"); fav.AddFavorite("https://youtube.com/v2", "Dev Conference 2026", "기술"); var f3 = fav.AddFavorite("https://youtube.com/v3", "Jazz Night", "음악"); fav.ToggleStash(f3.Id); var musicList = fav.GetByTag("음악"); Assert.Equal(2, musicList.Count); var stashed = fav.GetStashedItems(); Assert.Single(stashed); Assert.Equal(f3.Id, stashed[0].Id); } // [Gate 107] 영상 찾기(스니퍼 탐색): 플랫폼 감지 및 메타데이터 추출 [Fact] public void Gate107_MobileDiscoveryEngine_PlatformDetection_And_FormatFilter_Audit() { var res1 = MobileDiscoveryEngine.DetectPlatform("https://youtu.be/abcdefg"); Assert.True(res1.IsSupported); Assert.Equal("YouTube", res1.PlatformName); Assert.Contains("1080p", res1.RecommendedQualities); var res2 = MobileDiscoveryEngine.DetectPlatform("https://example.com/live.m3u8"); Assert.True(res2.IsSupported); Assert.Equal("HLS Stream", res2.PlatformName); } // [Gate 108] 영상 재시도 및 큐 관리: 우선순위 핀, 실패 재시도, 일괄 비우기 [Fact] public void Gate108_MobileQueueManager_Priorities_And_FailureRecovery_Audit() { var qm = new MobileQueueManager(); var i1 = qm.Enqueue("https://v1.mp4", "Video 1"); var i2 = qm.Enqueue("https://v2.mp4", "Video 2"); var i3 = qm.Enqueue("https://v3.mp4", "Video 3"); i2.Status = MobileQueueStatus.Failed; i2.ErrorMessage = "Network Timeout"; // 실패 항목만 재시도 var count = qm.RetryFailedItems(); Assert.Equal(1, count); Assert.Equal(MobileQueueStatus.Queued, i2.Status); // 중요 항목 고정 qm.Pin(i3.Id); Assert.Equal(i3.Id, qm.Items[0].Id); // 완료된 항목 일괄 정리 i1.Status = MobileQueueStatus.Completed; qm.ClearCompleted(); Assert.DoesNotContain(qm.Items, x => x.Id == i1.Id); } // [Gate 109] 설정 관리자: 영속성 및 기본값 [Fact] public void Gate109_MobileSettingsManager_Defaults_And_Persistence_Audit() { var settings = new MobileSettingsManager(); Assert.Equal(MobileThemeMode.Dark, settings.ThemeMode); Assert.False(settings.WifiOnlyDownload); Assert.Equal(0, settings.SpeedLimitKbps); // 0 = 무제한 settings.SpeedLimitKbps = 2048; settings.ThemeMode = MobileThemeMode.OLEDBlack; Assert.Equal(2048, settings.SpeedLimitKbps); Assert.Equal(MobileThemeMode.OLEDBlack, settings.ThemeMode); } // [Gate 110] 사용자 탭 동기화: PC 열린 탭 원격 수신 및 동기화 요청 [Fact] public void Gate110_MobileTabSyncEngine_RemoteTabMapping_Audit() { var sync = new MobileTabSyncEngine(); sync.UpdateRemoteTabs(new List { new("t1", "Design System Talk", "https://talk.com/1"), new("t2", "Tutorial Part 2", "https://tutorial.com/2") }); Assert.Equal(2, sync.RemoteTabs.Count); string? requestedUrl = null; sync.OnRemoteDownloadRequested += u => requestedUrl = u; sync.RequestRemoteDownload("t2"); Assert.Equal("https://tutorial.com/2", requestedUrl); } // [Gate 114] 비디오 스니퍼: 다중 스트림 품질 자동 협상 (Quality Negotiation) [Fact] public void Gate114_VideoSniffer_QualityNegotiator_Audit() { var streams = new List { new() { StreamUrl = "https://cdn.com/4k.mp4", Resolution = "4K" }, new() { StreamUrl = "https://cdn.com/1080p.mp4", Resolution = "1080p" }, new() { StreamUrl = "https://cdn.com/720p.mp4", Resolution = "720p" }, new() { StreamUrl = "https://cdn.com/audio.m4a", Resolution = "Audio Only" } }; // 1. 선호 화질 1080p -> 1080p 매칭 var best1080 = VideoSnifferParser.NegotiateBestQuality(streams, "1080p"); Assert.NotNull(best1080); Assert.Equal("1080p", best1080.Resolution); // 2. 선호 화질 1440p (목록에 없음) -> 1440p 이하 최고 화질인 1080p 선택 var fallback = VideoSnifferParser.NegotiateBestQuality(streams, "1440p"); Assert.NotNull(fallback); Assert.Equal("1080p", fallback.Resolution); // 3. 선호 화질 최고 화질("Max") -> 4K 선택 var max = VideoSnifferParser.NegotiateBestQuality(streams, "Max"); Assert.NotNull(max); Assert.Equal("4K", max.Resolution); // 4. 선호 화질 오디오("Audio Only") -> Audio Only 선택 var audio = VideoSnifferParser.NegotiateBestQuality(streams, "Audio Only"); Assert.NotNull(audio); Assert.Equal("Audio Only", audio.Resolution); } }