using System; using System.IO; using System.Text.RegularExpressions; using Paca.Tests.Infrastructure; using Xunit; namespace Paca.Tests.Mobile; /// /// 모바일 프로덕션 UI 뷰, 5대 탭, 컨트롤 실존 및 레이아웃 무결성 게이트 (Gate 93 ~ Gate 97) /// public class MobileUiProductionGateTests { private static string MobileProjectDir => Directory.Exists(Path.Combine(SourceTree.Root, "src", "Paca.Mobile")) ? Path.Combine(SourceTree.Root, "src", "Paca.Mobile") : Path.Combine(SourceTree.Root, "Paca.Mobile"); private static string MobileCoreProjectDir => Directory.Exists(Path.Combine(SourceTree.Root, "src", "Paca.Mobile.Core")) ? Path.Combine(SourceTree.Root, "src", "Paca.Mobile.Core") : Path.Combine(SourceTree.Root, "Paca.Mobile.Core"); [Fact] public void Gate93_TabbedRootPage_FiveTabs_Completeness_Audit() { var rootCs = File.ReadAllText(Path.Combine(MobileProjectDir, "TabbedRootPage.cs")); Assert.Contains("QueuePage", rootCs); Assert.Contains("DiscoverPage", rootCs); Assert.Contains("BrowserPage", rootCs); Assert.Contains("FavoritesPage", rootCs); Assert.Contains("SyncSettingsPage", rootCs); } [Fact] public void Gate94_DiscoverPage_SearchControls_Audit() { var pagesCs = File.ReadAllText(Path.Combine(MobileProjectDir, "Pages.cs")); Assert.Contains("class DiscoverPage", pagesCs); Assert.Contains("_searchEntry", pagesCs); Assert.Contains("_platformChips", pagesCs); Assert.Contains("YouTube", pagesCs); Assert.Contains("Vimeo", pagesCs); } [Fact] public void Gate95_FavoritesPage_Stash_And_Tagging_Ui_Audit() { var pagesCs = File.ReadAllText(Path.Combine(MobileProjectDir, "Pages.cs")); Assert.Contains("class FavoritesPage", pagesCs); Assert.Contains("_tagFilter", pagesCs); Assert.Contains("_favoritesList", pagesCs); } [Fact] public void Gate96_QueuePage_EmptyIllustration_And_Retry_Audit() { var pagesCs = File.ReadAllText(Path.Combine(MobileProjectDir, "Pages.cs")); Assert.Contains("paca_empty_queue.png", pagesCs); Assert.Contains("재시도", pagesCs); } [Fact] public void Gate97_SyncSettingsPage_RemoteTabs_Ui_Audit() { var pagesCs = File.ReadAllText(Path.Combine(MobileProjectDir, "Pages.cs")); Assert.Contains("class SyncSettingsPage", pagesCs); Assert.Contains("_remoteTabsList", pagesCs); Assert.Contains("PC 탭 동기화", pagesCs); } [Fact] public void Gate111_DiscoverPage_Enqueue_And_Favorite_Action_Buttons_Audit() { var pagesCs = File.ReadAllText(Path.Combine(MobileProjectDir, "Pages.cs")); Assert.Contains("_enqueueBtn", pagesCs); Assert.Contains("_favoriteBtn", pagesCs); Assert.Contains("다운로드 큐 추가", pagesCs); Assert.Contains("즐겨찾기 저장", pagesCs); } [Fact] public void Gate112_SyncSettingsPage_ThemeOptions_Includes_OLEDBlack() { var pagesCs = File.ReadAllText(Path.Combine(MobileProjectDir, "Pages.cs")); Assert.Contains("OLED 블랙", pagesCs); Assert.Contains("_wifiOnlySwitch", pagesCs); Assert.Contains("_speedLimitEntry", pagesCs); } [Fact] public void Gate113_AppIcon_And_Splash_Assets_Exist_And_Referenced() { var appIconPath = Path.Combine(MobileProjectDir, "Resources", "AppIcon", "paca_app_icon.png"); var splashPath = Path.Combine(MobileProjectDir, "Resources", "Splash", "paca_splash_screen.png"); Assert.True(File.Exists(appIconPath), "paca_app_icon.png must exist"); Assert.True(File.Exists(splashPath), "paca_splash_screen.png must exist"); } [Fact] public void Gate114_ConnectPage_LanAutoDiscovery_And_WanConnectCode_Ui_Audit() { var pagesCs = File.ReadAllText(Path.Combine(MobileProjectDir, "Pages.cs")); Assert.Contains("주변 PC 자동 검색", pagesCs); Assert.Contains("원클릭 연결", pagesCs); Assert.Contains("ConnectCode", pagesCs); Assert.Contains("3G / 4G / 5G", pagesCs); } [Fact] public void Gate151_QueuePage_SmartClipboardBanner_Ui_Audit() { var pagesCs = File.ReadAllText(Path.Combine(MobileProjectDir, "Pages.cs")); Assert.Contains("_clipboardBanner", pagesCs); Assert.Contains("클립보드 감지", pagesCs); Assert.Contains("PC로 다운로드", pagesCs); } [Fact] public void Gate152_LibraryPage_OfflineStorageDashboard_Ui_Audit() { var pagesCs = File.ReadAllText(Path.Combine(MobileProjectDir, "Pages.cs")); Assert.Contains("_storageProgressBar", pagesCs); Assert.Contains("_storageSummaryLabel", pagesCs); Assert.Contains("오프라인 보관함", pagesCs); } [Fact] public void Gate160_ApiClient_AudioStreamUrl_Supported() { var targetFile = File.Exists(Path.Combine(MobileCoreProjectDir, "ApiClient.cs")) ? Path.Combine(MobileCoreProjectDir, "ApiClient.cs") : Path.Combine(MobileProjectDir, "ApiClient.cs"); var apiClientCs = File.ReadAllText(targetFile); Assert.Contains("AudioUrl", apiClientCs); Assert.Contains("/api/audio/", apiClientCs); } [Fact] public void Gate161_BrowserPage_BrandLogo_Asset_And_Source_Audit() { var logoAssetPath = Path.Combine(MobileProjectDir, "Resources", "Images", "paca_logo.png"); Assert.True(File.Exists(logoAssetPath), "paca_logo.png must exist in Resources/Images"); var browserCs = File.ReadAllText(Path.Combine(MobileProjectDir, "BrowserPage.cs")); Assert.Contains("paca_logo.png", browserCs); } [Fact] public void Gate162_BrowserPage_BottomScrollPadding_AvoidsClipping_Audit() { var browserCs = File.ReadAllText(Path.Combine(MobileProjectDir, "BrowserPage.cs")); // CreateNewTabPageView rootLayout bottom padding must be >= 100 to avoid clipping with bottom floating bar var match = Regex.Match(browserCs, @"rootLayout[\s\S]*?Padding\s*=\s*new\s*Thickness\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*(\d+)\s*\)"); Assert.True(match.Success, "Padding definition must exist on rootLayout"); int bottomPadding = int.Parse(match.Groups[1].Value); Assert.True(bottomPadding >= 100, $"Bottom padding must be at least 100 to clear floating bottom bar, but was {bottomPadding}"); } [Fact] public void Gate163_MobileApp_NoUnderlineEntry_Handler_Audit() { var mauiProgramCs = File.ReadAllText(Path.Combine(MobileProjectDir, "MauiProgram.cs")); Assert.Contains("EntryHandler", mauiProgramCs); Assert.Contains("Background", mauiProgramCs); } [Fact] public void Gate164_BrowserPage_SpeedDials_Modern_Lucide_Glyph_Audit() { var browserCs = File.ReadAllText(Path.Combine(MobileProjectDir, "BrowserPage.cs")); Assert.Contains("MobileIconSystem.FontFamily", browserCs); Assert.Contains("MobileIconSystem.Play", browserCs); } [Fact] public void Gate165_AndroidManifest_WebView_PackageVisibility_Queries_Audit() { var manifestPath = Path.Combine(MobileProjectDir, "Platforms", "Android", "AndroidManifest.xml"); var manifestText = File.ReadAllText(manifestPath); Assert.Contains("", manifestText); Assert.Contains("com.google.android.webview", manifestText); } [Fact] public void Gate166_BrowserPage_LazyWebView_FaultTolerant_Audit() { var browserCs = File.ReadAllText(Path.Combine(MobileProjectDir, "BrowserPage.cs")); Assert.Contains("private bool EnsureWebView()", browserCs); Assert.Contains("if (!EnsureWebView()", browserCs); } [Fact] public void Gate167_BrowserPage_SnifferBadge_EmeraldColor_And_Glyph_Audit() { // DownloadsPage 안내문구: '웹 브라우징 중 상단 에메랄드 비디오 뱃지를 눌러 고화질 영상을 다운로드하세요.' // 에메랄드 컬러(#059669 또는 #10B981) 적용 및 비디오 글리프/텍스트 매핑 검증 var browserCs = File.ReadAllText(Path.Combine(MobileProjectDir, "BrowserPage.cs")); Assert.True(browserCs.Contains("#059669") || browserCs.Contains("#10B981"), "BrowserPage _snifferBadge는 DownloadsPage 안내문구와 100% 일치하는 에메랄드(#059669 또는 #10B981) 배경색이어야 합니다."); } [Fact] public void Gate168_BrowserPage_DesktopModeBtn_Decoupled_From_PcSync_Audit() { // 데스크톱 모드 전환 버튼과 PC 연결 상태 인디케이터의 변수/로직 분리 감사 var browserCs = File.ReadAllText(Path.Combine(MobileProjectDir, "BrowserPage.cs")); Assert.Contains("_desktopModeBtn", browserCs); Assert.DoesNotContain("_pcStatusIndicator.TextColor = Color.FromArgb(\"#34D399\")", browserCs); } [Fact] public void Gate171_BrowserPage_Sniffer_Detects_Supported_Platforms_Audit() { // 플랫폼 비디오(유튜브/치지직/숲 등) 진입 시 DOM blob 여부와 무관하게 플랫폼 비디오가 스니퍼 뱃지에 반영되는지 감사 var browserCs = File.ReadAllText(Path.Combine(MobileProjectDir, "BrowserPage.cs")); Assert.Contains("MobileDiscoveryEngine.DetectPlatform", browserCs); } [Fact] public void Gate172_BrowserPage_ShareAction_Invokes_Native_Share_Audit() { // 더보기 메뉴의 '링크 복사 및 공유' 클릭 시 클립보드 복사뿐 아니라 OS 네이티브 공유 시트(Share.Default.RequestAsync)를 연동하는지 감사 var browserCs = File.ReadAllText(Path.Combine(MobileProjectDir, "BrowserPage.cs")); Assert.Contains("Share.Default.RequestAsync", browserCs); } [Fact] public void Gate173_BrowserPage_Sniffer_Extracts_True_Url_Not_Source_ToString_Audit() { // MAUI UrlWebViewSource.ToString()은 클래스명을 반환하므로 반드시 window.location.href 또는 UrlWebViewSource.Url/e.Url을 사용하여 실제 URL을 추출해야 함 var browserCs = File.ReadAllText(Path.Combine(MobileProjectDir, "BrowserPage.cs")); Assert.DoesNotContain("_webView.Source?.ToString() ?? e.Url", browserCs); Assert.Contains("window.location.href", browserCs); } [Fact] public void Gate174_SyncSettingsPage_LanDiscovery_Ui_And_AutoConnect_Pills_Audit() { var syncCs = File.ReadAllText(Path.Combine(MobileProjectDir, "SyncSettingsPage.cs")); // 원클릭 LAN 자동 발견 프로버 연동 및 UI 구성요소 감사 Assert.Contains("LanDiscoveryProber", syncCs); Assert.Contains("로컬 네트워크 탐색", syncCs); Assert.Contains("10.0.2.2:5000", syncCs); // 에뮬레이터 퀵 페어링 지원 Assert.Contains("OnDeviceFound", syncCs); } [Fact] public void Gate175_ApiClient_DirectHost_And_OptionalToken_Support_Audit() { var targetFile = File.Exists(Path.Combine(MobileCoreProjectDir, "ApiClient.cs")) ? Path.Combine(MobileCoreProjectDir, "ApiClient.cs") : Path.Combine(MobileProjectDir, "ApiClient.cs"); var apiCs = File.ReadAllText(targetFile); // 토큰 없는 로컬 LAN 직접 연결(10.0.2.2:5000, 192.168.x.x) 및 URI 정규화 지원 감사 Assert.Contains("lan_direct", apiCs); Assert.Contains("!host.StartsWith(\"http", apiCs); } [Fact] public void Gate176_MobileTabManager_SpeedDial_Deduplication_And_DomainMatching_Audit() { var browserCs = File.ReadAllText(Path.Combine(MobileProjectDir, "BrowserPage.cs")); // 바로가기(Speed Dial) 탭 시 중복 생성 방지 및 탭 전환 감사 Assert.Contains("FindTabByUrlOrHost", browserCs); } [Fact] public void Gate177_MobileProject_16KbPageSize_And_AssemblyStore_Audit() { var csproj = File.ReadAllText(Path.Combine(MobileProjectDir, "Paca.Mobile.csproj")); var manifest = File.ReadAllText(Path.Combine(MobileProjectDir, "Platforms", "Android", "AndroidManifest.xml")); // 최신 안드로이드 15/16 16KB 페이지 사이즈 호환 및 어셈블리 스토어(.so 래핑 방지) 감사 Assert.True(csproj.Contains("")); Assert.True(csproj.Contains("")); Assert.Contains("android:targetSdkVersion=\"36\"", manifest); } [Fact] public void Gate181_DownloadsPage_SingleRail_Compact_Filters_Audit() { var downloadsCs = File.ReadAllText(Path.Combine(MobileProjectDir, "DownloadsPage.cs")); // 과밀한 4줄 버튼 대신 단일 레일 컴팩트 알약 스크롤 뷰 및 34dp 슬림 서치박스 감사 Assert.Contains("SingleRailPillsScrollView", downloadsCs); Assert.Contains("FilterAll", downloadsCs); Assert.Contains("FilterActive", downloadsCs); Assert.Contains("FilterDone", downloadsCs); Assert.Contains("FilterFailed", downloadsCs); Assert.Contains("ClearFinishedDownloadsBtn", downloadsCs); } [Fact] public void Gate182_QueuePage_CompactClipboardBanner_And_DoubleBezelCards_Audit() { var pagesCs = File.ReadAllText(Path.Combine(MobileProjectDir, "Pages.cs")); // 비대한 48px 카드 대신 슬림한 클립보드 센서 알약 및 28px/32px 컴팩트 큐 컨트롤 감사 Assert.Contains("_clipboardBanner", pagesCs); Assert.Contains("paca_empty_queue.png", pagesCs); Assert.Contains("재시도", pagesCs); Assert.Contains("CompactQueueCard", pagesCs); } [Fact] public void Gate183_TabbedRootPage_Modern_FiveTabs_Integrity_Audit() { var rootCs = File.ReadAllText(Path.Combine(MobileProjectDir, "TabbedRootPage.cs")); // 8개 탭 산발적 나열 대신 5대 핵심 탭 정렬 및 서브 탭 자연스러운 수용 감사 Assert.Contains("QueuePage", rootCs); Assert.Contains("DiscoverPage", rootCs); Assert.Contains("BrowserPage", rootCs); Assert.Contains("FavoritesPage", rootCs); Assert.Contains("SyncSettingsPage", rootCs); Assert.Contains("Paca Mobile", rootCs); } [Fact] public void Gate184_SpeedDial_TouchReliability_And_FullHitTest_Audit() { var browserCs = File.ReadAllText(Path.Combine(MobileProjectDir, "BrowserPage.cs")); // 스피드 다이얼 항목 터치 시 자식 요소(아이콘, 라벨 등)가 터치를 가로채지 않도록 InputTransparent 설정 감사 Assert.Contains("InputTransparent = true", browserCs); Assert.Contains("CascadeInputTransparent = true", browserCs); Assert.Contains("iconCircle.GestureRecognizers.Add(tap);", browserCs); Assert.Contains("itemContainer.GestureRecognizers.Add(tap);", browserCs); } [Fact] public void Gate185_Browser_Navigation_And_BackKeyHandling_Audit() { var browserCs = File.ReadAllText(Path.Combine(MobileProjectDir, "BrowserPage.cs")); var tabbedRootCs = File.ReadAllText(Path.Combine(MobileProjectDir, "TabbedRootPage.cs")); // 1. _isNavigating 락으로 인한 터치 무시/잠금 버그 완전 배제 Assert.DoesNotContain("_isNavigating", browserCs); // 2. UI 스레드 안전 비동기 전환 보증 Assert.Contains("MainThread.BeginInvokeOnMainThread", browserCs); // 3. 브라우저 페이지 및 탭 루트의 뒤로가기 제어 감사 Assert.Contains("public bool HandleBackButton()", browserCs); Assert.Contains("browserPage.HandleBackButton()", tabbedRootCs); } [Fact] public void Gate239_ApiClient_AutoRestore_Constructor_Audit() { var targetFile = File.Exists(Path.Combine(MobileCoreProjectDir, "ApiClient.cs")) ? Path.Combine(MobileCoreProjectDir, "ApiClient.cs") : Path.Combine(MobileProjectDir, "ApiClient.cs"); var apiCs = File.ReadAllText(targetFile); // ApiClient의 순수 생성자 및 영속 상태 복원 Restore() 메서드 계약 감사 Assert.Contains("public ApiClient(", apiCs); Assert.Contains("public void Restore()", apiCs); } }