using System; using System.IO; using Paca.Mobile.Core; using Paca.Tests.Infrastructure; using Xunit; namespace Paca.Tests.Mobile; /// /// 모바일 안드로이드 하단 바 조화, 동적 세이프 에어리어 인셋 및 광고(AD) 슬롯 TDD 게이트 테스트 (Gate 191 ~ Gate 193) /// public class MobileAndroidSystemBarHarmonyAndAdGateTests { 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 MobileCoreDir => 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 Gate191_MobileSafeAreaCoordinator_NavigationBarsInset_And_PaddingCalculation_Audit() { var coordinator = new MobileSafeAreaCoordinator(); // 1. 기본 상태 검증 (인셋 0, 기본 여백) Assert.Equal(0.0, coordinator.BottomInsetDp); Assert.Equal(0.0, coordinator.TopInsetDp); Assert.Equal(16.0, coordinator.CalculateBottomPadding(16.0, 6.0)); // 2. 이벤트 발생 여부 추적 bool eventFired = false; coordinator.InsetsChanged += () => eventFired = true; // 3. 제스처 바 인셋 (~24dp) 수신 시 동적 하단 패딩 계산 coordinator.UpdateInsets(bottomDp: 24.0, topDp: 36.0); Assert.True(eventFired); Assert.Equal(24.0, coordinator.BottomInsetDp); Assert.Equal(36.0, coordinator.TopInsetDp); // basePadding: 16.0, extraClearance: 6.0 -> Math.Max(16.0, 24.0 + 6.0) = 30.0 Assert.Equal(30.0, coordinator.CalculateBottomPadding(16.0, 6.0)); Assert.Equal(28.0, coordinator.CalculateBottomPadding(12.0, 4.0)); // 4. 3-버튼 내비게이션 바 인셋 (~48dp) 수신 시 하단 겹침 방지 여백 계산 eventFired = false; coordinator.UpdateInsets(bottomDp: 48.0, topDp: 36.0); Assert.True(eventFired); Assert.Equal(48.0, coordinator.BottomInsetDp); // basePadding: 16.0, extraClearance: 6.0 -> Math.Max(16.0, 48.0 + 6.0) = 54.0 Assert.Equal(54.0, coordinator.CalculateBottomPadding(16.0, 6.0)); // 5. 전체화면 또는 인셋 0 복귀 시 최소 기본 여백 보장 coordinator.UpdateInsets(bottomDp: 0.0, topDp: 0.0); Assert.Equal(16.0, coordinator.CalculateBottomPadding(16.0, 6.0)); // 6. 내비게이션 바 테마 동기화 상태 coordinator.SetNavigationBarTheme("#10111A", isLight: false); Assert.Equal("#10111A", coordinator.NavigationBarColorHex); Assert.False(coordinator.IsNavigationBarLight); coordinator.SetNavigationBarTheme("#FFFFFF", isLight: true); Assert.Equal("#FFFFFF", coordinator.NavigationBarColorHex); Assert.True(coordinator.IsNavigationBarLight); } [Fact] public void Gate192_MobileAdSlotSystem_Models_Dismissal_And_ImpressionTracking_Audit() { var adManager = new MobileAdManager(); // 1. 새 탭 페이지(NTP) 스폰서 카드 슬롯 조회 var ntpAd = adManager.GetAdForPlacement(MobileAdPlacement.NewTabPageSponsoredCard); Assert.NotNull(ntpAd); Assert.Equal("AD", ntpAd.Badge); Assert.False(string.IsNullOrWhiteSpace(ntpAd.Title)); Assert.False(string.IsNullOrWhiteSpace(ntpAd.Advertiser)); Assert.False(string.IsNullOrWhiteSpace(ntpAd.CtaText)); Assert.False(string.IsNullOrWhiteSpace(ntpAd.TargetUrl)); Assert.True(ntpAd.IsVisible); // 2. 인상(Impression) 및 클릭(Click) 추적 int initialImpressions = adManager.GetImpressionCount(ntpAd.Id); int initialClicks = adManager.GetClickCount(ntpAd.Id); adManager.TrackImpression(ntpAd.Id); Assert.Equal(initialImpressions + 1, adManager.GetImpressionCount(ntpAd.Id)); adManager.TrackClick(ntpAd.Id); Assert.Equal(initialClicks + 1, adManager.GetClickCount(ntpAd.Id)); // 3. 닫기(Dismiss) 기능 검증 및 이벤트 통지 bool dismissedEventFired = false; adManager.AdDismissed += adId => { if (adId == ntpAd.Id) dismissedEventFired = true; }; adManager.DismissAd(ntpAd.Id); Assert.True(dismissedEventFired); Assert.False(ntpAd.IsVisible); // 닫힌 후에는 해당 placement 조회 시 null 또는 대체 슬롯 반환 var dismissedAdCheck = adManager.GetAdForPlacement(MobileAdPlacement.NewTabPageSponsoredCard); Assert.Null(dismissedAdCheck); // 4. 다운로드 페이지 인라인 슬롯 조회 검증 var dlAd = adManager.GetAdForPlacement(MobileAdPlacement.DownloadsInlineBanner); Assert.NotNull(dlAd); Assert.Equal("AD", dlAd.Badge); } [Fact] public void Gate193_DesignpacaTokens_And_AndroidPlatformContracts_Audit() { // 1. MobileThemeSystem 내 광고 디자인 토큰 선언 검증 var themeCs = File.ReadAllText(Path.Combine(MobileCoreDir, "MobileThemeSystem.cs")); Assert.Contains("AdBadgeBackground", themeCs); Assert.Contains("AdBadgeText", themeCs); Assert.Contains("AdSurfaceBackground", themeCs); Assert.Contains("AdBorderHairline", themeCs); // 2. Android 플랫폼 내비게이션 바 테마 및 인셋 리스너 계약 검증 var statusBarCs = File.ReadAllText(Path.Combine(MobileProjectDir, "Platforms", "Android", "PacaAndroidStatusBarHelper.cs")); Assert.Contains("SetNavigationBarTheme", statusBarCs); Assert.Contains("AppearanceLightNavigationBars", statusBarCs); Assert.Contains("AttachWindowInsetsListener", statusBarCs); Assert.Contains("NavigationBars", statusBarCs); var mainActivityCs = File.ReadAllText(Path.Combine(MobileProjectDir, "Platforms", "Android", "MainActivity.cs")); Assert.Contains("AttachWindowInsetsListener", mainActivityCs); // 3. BrowserPage 및 DownloadsPage의 MobileAdBannerView 탑재 및 세이프 에어리어 연동 감사 var browserCs = File.ReadAllText(Path.Combine(MobileProjectDir, "BrowserPage.cs")); Assert.Contains("MobileSafeAreaCoordinator", browserCs); Assert.Contains("MobileAdBannerView", browserCs); var downloadsCs = File.ReadAllText(Path.Combine(MobileProjectDir, "DownloadsPage.cs")); Assert.Contains("MobileSafeAreaCoordinator", downloadsCs); Assert.Contains("MobileAdBannerView", downloadsCs); // 4. Obsolete API 전수 방어 검증 (DisplayAlert, Device.StartTimer, new Frame 미사용) Assert.DoesNotContain("DisplayAlert(", browserCs.Replace("DisplayAlertAsync", "")); Assert.DoesNotContain("Device.StartTimer", browserCs); } }