using System; using System.Collections.Generic; using System.IO; using System.Linq; using Paca.Mobile.Core; using Xunit; namespace Paca.Tests.Mobile; /// /// 데스크톱 Paca와 1:1 대응하는 모바일 히스토리, 즐겨찾기, 탭 관리자, 통합 허브 게이트 (Gate 161 ~ Gate 168) /// public class MobileDesktopParityBrowserGateTests { private static readonly string MobileProjectDir = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "src", "Paca.Mobile")); [Fact] public void Gate161_DesktopParitySideNavHub_Navigation_Audit() { var hub = new DesktopParitySideNavHub(); Assert.Equal(DesktopSideNavTab.Downloads, hub.ActiveTab); hub.SelectTab(DesktopSideNavTab.History); Assert.Equal(DesktopSideNavTab.History, hub.ActiveTab); hub.SelectTab(DesktopSideNavTab.Bookmarks); Assert.Equal(DesktopSideNavTab.Bookmarks, hub.ActiveTab); } [Fact] public void Gate162_MobileBrowserBottomBarParity_Buttons_Audit() { var browserPageCs = File.ReadAllText(Path.Combine(MobileProjectDir, "BrowserPage.cs")); // 하단 바 7대 핵심 버튼 검증 (뒤로, 앞으로, 홈, 북마크, 히스토리, 다운로드, 탭창) Assert.Contains("ToolbarBookmarkBtn", browserPageCs); Assert.Contains("ToolbarHistoryBtn", browserPageCs); Assert.Contains("ToolbarDownloadsBtn", browserPageCs); Assert.Contains("ToolbarTabsBtn", browserPageCs); } [Fact] public void Gate163_MobileBookmarkManager_Parity_Audit() { var manager = new DesktopParityBookmarkManager(); var url = "https://github.com/videodownloader"; var title = "Paca Video Downloader"; Assert.False(manager.IsBookmarked(url)); // 북마크 토글 (추가) bool added = manager.ToggleBookmark(url, title); Assert.True(added); Assert.True(manager.IsBookmarked(url)); Assert.Single(manager.Bookmarks); Assert.Equal(title, manager.Bookmarks[0].Title); // 북마크 토글 (제거) bool removed = manager.ToggleBookmark(url, title); Assert.False(removed); Assert.False(manager.IsBookmarked(url)); Assert.Empty(manager.Bookmarks); } [Fact] public void Gate164_MobileHistoryManager_Parity_Audit() { var history = new DesktopParityHistoryManager(); history.RecordVisit("https://youtube.com", "YouTube - Home"); history.RecordVisit("https://naver.com", "NAVER"); history.RecordVisit("https://chzzk.naver.com", "CHZZK Live"); Assert.Equal(3, history.Entries.Count); // 검색 var searchResult = history.Search("naver"); Assert.Equal(2, searchResult.Count); // 특정 항목 삭제 history.DeleteEntry("https://naver.com"); Assert.Equal(2, history.Entries.Count); // 전체 삭제 history.ClearAll(); Assert.Empty(history.Entries); } [Fact] public void Gate165_MobileTabManager_Parity_Audit() { var tabManager = new DesktopParityTabManager(); Assert.Single(tabManager.Tabs); // 기본 1개 탭 존재 Assert.NotNull(tabManager.ActiveTab); // 새 탭 생성 var tab2 = tabManager.CreateTab("https://chzzk.naver.com", "CHZZK"); Assert.Equal(2, tabManager.Tabs.Count); Assert.Equal(tab2.Id, tabManager.ActiveTab.Id); // 탭 전환 tabManager.SwitchTab(tabManager.Tabs[0].Id); Assert.Equal(tabManager.Tabs[0].Id, tabManager.ActiveTab.Id); // 탭 닫기 tabManager.CloseTab(tab2.Id); Assert.Single(tabManager.Tabs); Assert.Equal(tabManager.Tabs[0].Id, tabManager.ActiveTab.Id); } [Fact] public void Gate166_DownloadsPage_DesktopParitySideTabs_Audit() { var downloadsPageCs = File.ReadAllText(Path.Combine(MobileProjectDir, "DownloadsPage.cs")); // 데스크톱과 동일한 사이드 세그먼트 탭(SideTabDownloads, SideTabHistory, SideTabBookmarks) 존재 검증 Assert.Contains("SideTabDownloads", downloadsPageCs); Assert.Contains("SideTabHistory", downloadsPageCs); Assert.Contains("SideTabBookmarks", downloadsPageCs); } [Fact] public void Gate167_HistoryBookmarksPage_TabSwitching_Audit() { var historyPageCs = File.ReadAllText(Path.Combine(MobileProjectDir, "HistoryBookmarksPage.cs")); // 방문 기록 & 즐겨찾기 스위처 버튼 검증 Assert.Contains("_tabHistoryBtn", historyPageCs); Assert.Contains("_tabBookmarkBtn", historyPageCs); } [Fact] public void Gate168_EndToEndDesktopParityFullCycle_Audit() { var bookmarkMgr = new DesktopParityBookmarkManager(); var historyMgr = new DesktopParityHistoryManager(); var tabMgr = new DesktopParityTabManager(); var hub = new DesktopParitySideNavHub(); // 1. 첫 탭에서 유튜브 접속 및 방문 기록 추가 tabMgr.ActiveTab.Navigate("https://youtube.com/watch?v=123", "Amazing 4K Video"); historyMgr.RecordVisit(tabMgr.ActiveTab.Url, tabMgr.ActiveTab.Title); // 2. 즐겨찾기 등록 bookmarkMgr.ToggleBookmark(tabMgr.ActiveTab.Url, tabMgr.ActiveTab.Title); Assert.True(bookmarkMgr.IsBookmarked(tabMgr.ActiveTab.Url)); // 3. 새 탭 열기 var tab2 = tabMgr.CreateTab("https://naver.com", "NAVER"); historyMgr.RecordVisit(tab2.Url, tab2.Title); Assert.Equal(2, tabMgr.Tabs.Count); // 4. 통합 허브에서 방문 기록과 북마크 확인 hub.SelectTab(DesktopSideNavTab.History); Assert.Equal(DesktopSideNavTab.History, hub.ActiveTab); Assert.Equal(2, historyMgr.Entries.Count); hub.SelectTab(DesktopSideNavTab.Bookmarks); Assert.Equal(DesktopSideNavTab.Bookmarks, hub.ActiveTab); Assert.Single(bookmarkMgr.Bookmarks); Assert.Equal("Amazing 4K Video", bookmarkMgr.Bookmarks[0].Title); } [Fact] public void Gate177_MobileBrowserNavigationAndBackKeyLifecycle_Audit() { var browserPageCs = File.ReadAllText(Path.Combine(MobileProjectDir, "BrowserPage.cs")); // 1. Android 하드웨어 Back 키 누름 시 앱 강제 종료 방지 및 웹뷰 뒤로가기/홈 복귀 생명주기 오버라이드 검증 Assert.Contains("protected override bool OnBackButtonPressed()", browserPageCs); Assert.Contains("CanGoBack", browserPageCs); Assert.Contains("GoBack()", browserPageCs); // 2. 툴바 뒤로가기 버튼(‹)에 홈 복귀 폴백이 구현되어 있는지 검증 Assert.Contains("paca://newtab", browserPageCs); // 3. 독립된 원터치 데스크톱/모바일 모드 토글 버튼 검증 Assert.Contains("_isDesktopMode", browserPageCs); Assert.Contains("SetDesktopMode", browserPageCs); } [Fact] public void Gate178_MobileWebFullscreenVideo_CustomView_Audit() { var customizerCs = File.ReadAllText(Path.Combine(MobileProjectDir, "Platforms", "Android", "PacaAndroidWebViewCustomizer.cs")); // HTML5 전체화면 비디오 재생 시 뷰 얹기/제거 및 가로모드 전환 생명주기 검증 Assert.Contains("OnShowCustomView", customizerCs); Assert.Contains("OnHideCustomView", customizerCs); Assert.Contains("DecorView", customizerCs); Assert.Contains("RequestedOrientation", customizerCs); } [Fact] public void Gate179_DesignpacaMinimalistUi_Audit() { var mobileProjCs = File.ReadAllText(Path.Combine(MobileProjectDir, "Paca.Mobile.csproj")); var splashSvg = File.ReadAllText(Path.Combine(MobileProjectDir, "Resources", "Splash", "splash.svg")); var browserPageCs = File.ReadAllText(Path.Combine(MobileProjectDir, "BrowserPage.cs")); // 1. .NET 기본 스플래시 및 파란색(#2563eb) 완전 제거, 딥 옵시디언 다크(#0A0A0E) 및 Paca 브랜드 스플래시 적용 검증 Assert.DoesNotContain("Color=\"#2563eb\"", mobileProjCs); Assert.Contains("Color=\"#0A0A0E\"", mobileProjCs); Assert.DoesNotContain("280.08446 H 195.99044", splashSvg); // .NET 글자 패스 삭제 확인 Assert.Contains("paca-splash-mark", splashSvg); // 2. 툴바 버튼에서 42x42 네모 상자 Border 컨테이너 제거 및 보더리스 플랫 터치 타깃 검증 Assert.DoesNotContain("BackgroundColor = Color.FromArgb(\"#141420\"),", browserPageCs); Assert.DoesNotContain("StrokeThickness = 0.5,", browserPageCs); // 3. 네온 헤어라인 보더(Stroke) 남발 제거 검증 Assert.DoesNotContain("Stroke = Color.FromArgb(\"#8B7CFF\"),", browserPageCs); // 네온 바이올렛 스트로크 Assert.DoesNotContain("Stroke = Color.FromArgb(\"#34D399\"),", browserPageCs); // 네온 그린 스트로크 Assert.DoesNotContain("더블 베젤 앰비언트 글로우", browserPageCs); // 4. 스피드 다이얼에서 억지 94px 카드 박스 컨테이너 및 내부 이중 박스 제거, 보더리스 플랫 레이아웃 검증 Assert.DoesNotContain("AddLuxurySpeedDialTile", browserPageCs); Assert.Contains("AddMinimalSpeedDialItem", browserPageCs); } [Fact] public void Gate180_NoEmoji_LucideIconSystem_Audit() { var appDir = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "src", "Paca.App")); var mobileDir = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "src", "Paca.Mobile")); var mobileCoreDir = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "src", "Paca.Mobile.Core")); // 1. Lucide 폰트 에셋 존재 및 정상 용량 검증 var mobileFont = Path.Combine(mobileDir, "Resources", "Fonts", "lucide.ttf"); var appFont = Path.Combine(appDir, "Assets", "Fonts", "lucide.ttf"); Assert.True(File.Exists(mobileFont), "Paca.Mobile에 lucide.ttf가 존재해야 합니다."); Assert.True(File.Exists(appFont), "Paca.App에 lucide.ttf가 존재해야 합니다."); Assert.True(new FileInfo(mobileFont).Length > 500_000, "lucide.ttf는 500KB 이상의 온전한 폰트 파일이어야 합니다."); Assert.True(new FileInfo(appFont).Length > 500_000, "lucide.ttf는 500KB 이상의 온전한 폰트 파일이어야 합니다."); // 2. 모바일 및 모바일 코어 프로덕션 코드 이모지 0건 전면 척결 검증 var emojiRegex = new System.Text.RegularExpressions.Regex(@"[\uD83C-\uDBFF\uDC00-\uDFFF]"); var mobileFiles = Directory.GetFiles(mobileDir, "*.cs", SearchOption.AllDirectories) .Concat(Directory.GetFiles(mobileDir, "*.xaml", SearchOption.AllDirectories)) .Concat(Directory.GetFiles(mobileCoreDir, "*.cs", SearchOption.AllDirectories)) .Where(f => !f.Contains("\\obj\\") && !f.Contains("\\bin\\")); foreach (var file in mobileFiles) { var text = File.ReadAllText(file); var match = emojiRegex.Match(text); Assert.False(match.Success, $"모바일 프로덕션 파일({Path.GetFileName(file)})에 이모지가 잔류해선 안 됩니다: '{match.Value}'"); } // 3. 데스크톱 앱 프로덕션 코드 이모지 0건 전면 척결 검증 var appFiles = Directory.GetFiles(appDir, "*.cs", SearchOption.AllDirectories) .Concat(Directory.GetFiles(appDir, "*.xaml", SearchOption.AllDirectories)) .Where(f => !f.Contains("\\obj\\") && !f.Contains("\\bin\\") && !f.Contains("min.js")); foreach (var file in appFiles) { var text = File.ReadAllText(file); var match = emojiRegex.Match(text); Assert.False(match.Success, $"데스크톱 프로덕션 파일({Path.GetFileName(file)})에 이모지가 잔류해선 안 됩니다: '{match.Value}'"); } // 4. 공식 마스코트 스플래시 화면 무결성 검증 var splashSvg = File.ReadAllText(Path.Combine(mobileDir, "Resources", "Splash", "splash.svg")); Assert.Contains("paca-splash-mark", splashSvg); Assert.Contains("image/png;base64", splashSvg); // 5. MobileIconSystem의 Lucide 폰트 패밀리 지정 검증 Assert.Equal("Lucide", MobileIconSystem.FontFamily); } }