feat(ads/seo/release): v1.2.0 with live Google Ads, isolated ad profile, ads.txt, and SEO/AEO/GEO optimization

This commit is contained in:
Yun Chan 2026-09-14 00:39:44 +09:00
parent a88aafa1e5
commit 0e568f1f0a
975 changed files with 130593 additions and 16783 deletions

View file

@ -0,0 +1,146 @@
using System;
using System.IO;
using Paca.Mobile.Core;
using Paca.Tests.Infrastructure;
using Xunit;
namespace Paca.Tests.Mobile;
/// <summary>
/// 모바일 안드로이드 하단 바 조화, 동적 세이프 에어리어 인셋 및 광고(AD) 슬롯 TDD 게이트 테스트 (Gate 191 ~ Gate 193)
/// </summary>
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);
}
}