using System; using System.IO; using Paca.App.Browser.Features; using Paca.Browser.Ads; using Paca.Browser.NewTab; using Paca.Tests.Infrastructure; using Xunit; namespace Paca.Tests.Browser; /// /// 데스크톱 광고 슬롯 시스템, NTP 스폰서 카드 주입 및 배너 컨트롤 UI TDD 게이트 테스트 (Gate 197 ~ Gate 199) /// public class DesktopAdSlotAndNtpSponsoredGateTests { private static string AppDir => Directory.Exists(Path.Combine(SourceTree.Root, "src", "Paca.App")) ? Path.Combine(SourceTree.Root, "src", "Paca.App") : Path.Combine(SourceTree.Root, "Paca.App"); private static string BrowserDir => Directory.Exists(Path.Combine(SourceTree.Root, "src", "Paca.Browser")) ? Path.Combine(SourceTree.Root, "src", "Paca.Browser") : Path.Combine(SourceTree.Root, "Paca.Browser"); [Fact] public void Gate197_DesktopAdManager_Models_Placement_And_Dismissal_Audit() { var adManager = new DesktopAdManager(); // 1. 새 탭 페이지(NTP) 스폰서 슬롯 조회 var ntpAd = adManager.GetAdForPlacement(DesktopAdPlacement.NewTabPageSponsoredCard); Assert.NotNull(ntpAd); Assert.True(ntpAd.Badge == "AD" || ntpAd.Badge == "SPONSORED"); Assert.False(string.IsNullOrWhiteSpace(ntpAd.Title)); Assert.False(string.IsNullOrWhiteSpace(ntpAd.Advertiser)); Assert.False(string.IsNullOrWhiteSpace(ntpAd.TargetUrl)); Assert.True(ntpAd.IsVisible); // 2. 다운로드 뷰 배너 슬롯 조회 var dlAd = adManager.GetAdForPlacement(DesktopAdPlacement.DownloadViewBanner); Assert.NotNull(dlAd); Assert.True(dlAd.IsVisible); // 3. 인상(Impression) 및 클릭(Click) 트래킹 int initImpressions = adManager.GetImpressionCount(ntpAd.Id); int initClicks = adManager.GetClickCount(ntpAd.Id); adManager.TrackImpression(ntpAd.Id); Assert.Equal(initImpressions + 1, adManager.GetImpressionCount(ntpAd.Id)); adManager.TrackClick(ntpAd.Id); Assert.Equal(initClicks + 1, adManager.GetClickCount(ntpAd.Id)); // 4. 닫기(Dismiss) 및 가시성 제어 bool dismissedFired = false; adManager.AdDismissed += id => { if (id == ntpAd.Id) dismissedFired = true; }; adManager.DismissAd(ntpAd.Id); Assert.True(dismissedFired); Assert.False(ntpAd.IsVisible); // 닫힌 후 조회 시 null var dismissedCheck = adManager.GetAdForPlacement(DesktopAdPlacement.NewTabPageSponsoredCard); Assert.Null(dismissedCheck); } [Fact] public void Gate198_NewTabPageBuilder_SponsoredCard_Markup_And_Accessibility_Audit() { // 1. NewTabPageBuilder에 스폰서 광고 데이터가 제공될 때 HTML 생성 검증 var adData = new DesktopAdData { Id = "test-desktop-sponsor", Badge = "SPONSORED", Title = "PACA Cloud Pro 10Gbps 가속", Description = "초고속 클라우드 다운로드 릴레이 및 8K 인코딩", Advertiser = "Paca Media Lab", CtaText = "체험하기", TargetUrl = "https://paca.cloud/pro", IsVisible = true, IsDismissible = true }; var html = NewTabPageBuilder.BuildNtp( shortcuts: null, settings: null, recentHistory: null, recentDownloads: null, isDark: true, sponsoredAd: adData ); // 2. 스폰서 컨테이너 및 속성 감사 Assert.Contains("ntp-sponsored-container", html); Assert.Contains("data-sponsored-ad=\"true\"", html); Assert.Contains("PACA Cloud Pro 10Gbps 가속", html); Assert.Contains("Paca Media Lab", html); Assert.Contains("https://paca.cloud/pro", html); Assert.Contains("SPONSORED", html); // 3. WCAG 2.2 접근성 계약: aria-label 및 dismiss 버튼 Assert.Contains("aria-label=\"스폰서 추천\"", html); Assert.Contains("aria-label=\"광고 닫기\"", html); // 4. 스폰서 광고가 비활성화(null)되었을 때는 렌더링되지 않아야 함 var cleanHtml = NewTabPageBuilder.BuildNtp( shortcuts: null, settings: null, recentHistory: null, recentDownloads: null, isDark: true, sponsoredAd: null ); Assert.DoesNotContain("data-sponsored-ad=\"true\"", cleanHtml); } [Fact] public void Gate199_DesktopAdBannerControl_Xaml_And_DesignSystem_Audit() { // 1. WPF DesktopAdBannerControl XAML 및 코드비하인드 파일 존재 확인 var xamlPath = Path.Combine(AppDir, "Controls", "DesktopAdBannerControl.xaml"); var csPath = Path.Combine(AppDir, "Controls", "DesktopAdBannerControl.xaml.cs"); Assert.True(File.Exists(xamlPath), "DesktopAdBannerControl.xaml 파일이 존재해야 합니다."); Assert.True(File.Exists(csPath), "DesktopAdBannerControl.xaml.cs 파일이 존재해야 합니다."); var xaml = File.ReadAllText(xamlPath); var cs = File.ReadAllText(csPath); // 2. Designpaca 25대 게이트 감사 // - 최소 32x32px 클릭 타깃 및 마진 8px 배수 Assert.Contains("Height=\"32\"", xaml); // - AutomationProperties.Name 스크린리더 계약 Assert.Contains("AutomationProperties.Name", xaml); // - 닫기 버튼 및 CTA 버튼 Assert.Contains("PART_DismissBtn", xaml); Assert.Contains("PART_CtaBtn", xaml); // - 폰트 및 라운드 코너 Assert.Contains("CornerRadius", xaml); // 3. 코드비하인드에서 DesktopAdManager 연동 및 트래킹 호출 감사 Assert.Contains("TrackImpression", cs); Assert.Contains("TrackClick", cs); Assert.Contains("DismissAd", cs); } [Fact] public void Gate202_DesktopAdConfig_AdMobUnitIds_And_EffectiveTestAd_Audit() { // 1. AdMob 콘솔에서 생성한 실제 배너 광고 단위 ID 일치성 단정 var config = DesktopAdConfig.PacaProduction; Assert.Equal("ca-app-pub-1039714767792854~5680462775", config.AppId); Assert.Equal("ca-app-pub-1039714767792854/1521297311", config.DownloadBannerAdUnitId); Assert.Equal("ca-app-pub-1039714767792854/4387520844", config.NtpBannerAdUnitId); // 2. 검토 대기(스토어 미연결) 상태 기본 테스트 모드: Google 공식 실시간 웹 디스플레이 테스트 슬롯 라우팅 Assert.True(config.ForceTestAds); Assert.Equal(DesktopAdConfig.GoogleTestSlot, config.EffectiveDownloadBannerUnitId); Assert.Equal(DesktopAdConfig.GoogleTestSlot, config.EffectiveNtpBannerUnitId); // 3. 프로덕션 강제 지정 시 실제 콘솔 광고 단위 ID 반환 var prodConfig = config with { ForceTestAds = false }; Assert.Equal("ca-app-pub-1039714767792854/1521297311", prodConfig.EffectiveDownloadBannerUnitId); Assert.Equal("ca-app-pub-1039714767792854/4387520844", prodConfig.EffectiveNtpBannerUnitId); // 4. Google Publisher Tag (GPT) 스니펫 생성기 검증 var snippet = DesktopAdConfig.GenerateGoogleTagSnippet(DesktopAdConfig.GoogleTestSlot, 728, 90, "div-gpt-test"); Assert.Contains("securepubads.g.doubleclick.net/tag/js/gpt.js", snippet); Assert.Contains($"googletag.defineSlot('{DesktopAdConfig.GoogleTestSlot}'", snippet); Assert.Contains("[728, 90]", snippet); Assert.Contains("googletag.display('div-gpt-test')", snippet); // 5. DesktopAdData 실광고 속성 모델 검증 var ad = new DesktopAdData { AdUnitId = "ca-app-pub-1039714767792854/1521297311", IsRealAd = true, HtmlEmbedSnippet = snippet }; Assert.True(ad.IsRealAd); Assert.Equal("ca-app-pub-1039714767792854/1521297311", ad.AdUnitId); Assert.NotNull(ad.HtmlEmbedSnippet); } [Fact] public void Gate203_NewTabPageBuilder_RealAdUnit_GooglePublisherTag_Audit() { // Arrange: 실시간 구글 광고 태그가 포함된 DesktopAdData 구성 var realAd = new DesktopAdData { Id = "paca-desktop-ntp-real-ad", AdUnitId = "ca-app-pub-1039714767792854/4387520844", IsRealAd = true, Badge = "AD", Title = "Google Sponsor Network", Advertiser = "Google Ad Manager", HtmlEmbedSnippet = DesktopAdConfig.GenerateGoogleTagSnippet(DesktopAdConfig.GoogleTestSlot, 728, 90, "div-gpt-ad-ntp"), IsVisible = true }; // Act: NewTabPageBuilder HTML 생성 var html = NewTabPageBuilder.BuildNtp( shortcuts: null, settings: null, recentHistory: null, recentDownloads: null, isDark: true, sponsoredAd: realAd ); // Assert: Google Publisher Tag 비동기 스크립트 및 실시간 광고 슬롯 컨테이너 마크업 검증 Assert.Contains("ntp-sponsored-container", html); Assert.Contains("ntp-real-ad-container", html); Assert.Contains("data-real-ad=\"true\"", html); Assert.Contains("securepubads.g.doubleclick.net/tag/js/gpt.js", html); Assert.Contains($"googletag.defineSlot('{DesktopAdConfig.GoogleTestSlot}'", html); Assert.Contains("googletag.display('div-gpt-ad-ntp')", html); } [Fact] public void Gate204_DesktopAdBannerControl_RealAdHost_And_WebView2_Audit() { var xamlPath = Path.Combine(AppDir, "Controls", "DesktopAdBannerControl.xaml"); var csPath = Path.Combine(AppDir, "Controls", "DesktopAdBannerControl.xaml.cs"); var xaml = File.ReadAllText(xamlPath); var cs = File.ReadAllText(csPath); // 1. 실광고 호스트 및 네이티브 카드 2단 계층 XAML 감사 Assert.Contains("PART_RealAdHost", xaml); Assert.Contains("PART_NativeCard", xaml); // 2. 코드비하인드에서 WebView2 환경 주입 및 실광고 내비게이션 지원 감사 Assert.Contains("AttachEnvironment", cs); Assert.Contains("GenerateGoogleTagSnippet", cs); } [Fact] public void Gate205_DownloadsPanelView_BottomAdBanner_And_RichImageCard_Audit() { // 1. MainWindow.xaml 다운로드 뷰 하단(Grid.Row=2) 배너 배치 감사 var mainWindowXamlPath = Path.Combine(AppDir, "MainWindow.xaml"); Assert.True(File.Exists(mainWindowXamlPath), "MainWindow.xaml 파일이 존재해야 합니다."); var mainXaml = File.ReadAllText(mainWindowXamlPath); // DownloadsPanelView 그리드 행 정의에 Row 2(Auto)가 추가되어 있어야 함 Assert.Contains(" 120 && c.G > 120 && c.B > 160) { textPixels++; } } } Assert.True(textPixels > 50, $"BottomAppBanner must contain visible text pixels in the center content area (Found {textPixels} text pixels)"); // 1. 다운로드 배너가 단색 빈 화면(762바이트)이 아니어야 함 var dlInfo = new FileInfo(dlFile); Assert.True(dlInfo.Length > 2000, $"DownloadViewBanner preview must not be a blank solid rectangle (Length was {dlInfo.Length} bytes)"); } [Fact] public void Gate209_GoogleAds_AdsTxt_And_AppAdsTxt_Verification_Audit() { // 1. site/ads.txt 및 site/app-ads.txt 배포 파일 존재 및 내용 검증 var siteDir = Path.Combine(SourceTree.Root, "site"); var adsTxtPath = Path.Combine(siteDir, "ads.txt"); var appAdsTxtPath = Path.Combine(siteDir, "app-ads.txt"); Assert.True(File.Exists(adsTxtPath), "site/ads.txt 파일이 배포 루트에 존재해야 합니다."); Assert.True(File.Exists(appAdsTxtPath), "site/app-ads.txt 파일이 배포 루트에 존재해야 합니다."); var adsTxt = File.ReadAllText(adsTxtPath).Trim(); var appAdsTxt = File.ReadAllText(appAdsTxtPath).Trim(); // Google 공식 인증 레코드: google.com, pub-1039714767792854, DIRECT, f08c47fec0942fa0 const string expectedRecord = "google.com, pub-1039714767792854, DIRECT, f08c47fec0942fa0"; Assert.Contains(expectedRecord, adsTxt); Assert.Contains(expectedRecord, appAdsTxt); // 2. DesktopAdConfig 공식 퍼블리셔 ID 및 클라이언트 ID 단정 Assert.Equal("pub-1039714767792854", DesktopAdConfig.PublisherId); Assert.Equal("ca-pub-1039714767792854", DesktopAdConfig.AdClient); } [Fact] public void Gate210_ZeroMockAdvertising_And_RealGoogleOrigin_Audit() { // 1. DesktopAdModels.cs에 가짜 모의 광고(쿠팡, 삼성 등)가 완전히 제거되었는지 감사 var modelsCsPath = Path.Combine(BrowserDir, "Ads", "DesktopAdModels.cs"); Assert.True(File.Exists(modelsCsPath)); var modelsCs = File.ReadAllText(modelsCsPath); Assert.DoesNotContain("쿠팡", modelsCs); Assert.DoesNotContain("Coupang", modelsCs); Assert.DoesNotContain("Galaxy S24", modelsCs); Assert.DoesNotContain("삼성전자", modelsCs); // 2. DesktopAdBannerControl.xaml.cs에서 가짜 목업이 아닌 실시간 Google 태그 호스팅 및 가상 HTTPS 오리진 감사 var bannerCsPath = Path.Combine(AppDir, "Controls", "DesktopAdBannerControl.xaml.cs"); Assert.True(File.Exists(bannerCsPath)); var bannerCs = File.ReadAllText(bannerCsPath); Assert.Contains("ads.paca.cloud", bannerCs); Assert.Contains("SetVirtualHostNameToFolderMapping", bannerCs); Assert.Contains("ad-google-wrapper", bannerCs); Assert.DoesNotContain("ad-logo-box", bannerCs); Assert.DoesNotContain("ad-logo-fallback", bannerCs); // 3. AdSense 스니펫 생성기 동작 검증 var adsenseSnippet = DesktopAdConfig.GenerateAdSenseSnippet(DesktopAdConfig.AdClient, "4387520844", 728, 90); Assert.Contains("pagead2.googlesyndication.com/pagead/js/adsbygoogle.js", adsenseSnippet); Assert.Contains("data-ad-client=\"ca-pub-1039714767792854\"", adsenseSnippet); Assert.Contains("data-ad-slot=\"4387520844\"", adsenseSnippet); } }