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,270 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Paca.Mobile.Core;
using Xunit;
namespace Paca.Tests.Mobile;
/// <summary>
/// 데스크톱 Paca와 1:1 대응하는 모바일 히스토리, 즐겨찾기, 탭 관리자, 통합 허브 게이트 (Gate 161 ~ Gate 168)
/// </summary>
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);
}
}