releases/tests/Paca.Tests/Mobile/MobileOrganicSafeAreaAndScrollGateTests.cs

201 lines
9.9 KiB
C#

using System;
using System.IO;
using Paca.Mobile.Core;
using Xunit;
namespace Paca.Tests.Mobile;
public class MobileOrganicSafeAreaAndScrollGateTests
{
private static readonly string RepoRoot = FindRepoRoot();
private static readonly string MobileCoreDir = Path.Combine(RepoRoot, "src", "Paca.Mobile.Core");
private static readonly string MobileDir = Path.Combine(RepoRoot, "src", "Paca.Mobile");
private static string FindRepoRoot()
{
var dir = AppContext.BaseDirectory;
while (!string.IsNullOrEmpty(dir))
{
if (File.Exists(Path.Combine(dir, "Paca.slnx")) || Directory.Exists(Path.Combine(dir, "src", "Paca.Mobile.Core")))
return dir;
var parent = Directory.GetParent(dir);
if (parent == null) break;
dir = parent.FullName;
}
return Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."));
}
[Fact]
public void Gate186_ThemeColorParser_HexRgbNamedAndLuminance_Audit()
{
// 1. Hex parsing
Assert.True(MobileThemeColorParser.TryParseColor("#FF0000", out var red));
Assert.Equal(255, red.R);
Assert.Equal(0, red.G);
Assert.Equal(0, red.B);
Assert.True(MobileThemeColorParser.TryParseColor("#03C75A", out var naverGreen));
Assert.Equal(3, naverGreen.R);
Assert.Equal(199, naverGreen.G);
Assert.Equal(90, naverGreen.B);
// 2. Short hex #RGB
Assert.True(MobileThemeColorParser.TryParseColor("#0F0", out var shortGreen));
Assert.Equal(0, shortGreen.R);
Assert.Equal(255, shortGreen.G);
Assert.Equal(0, shortGreen.B);
// 3. RGB & RGBA formats
Assert.True(MobileThemeColorParser.TryParseColor("rgb(36, 41, 47)", out var githubDark));
Assert.Equal(36, githubDark.R);
Assert.Equal(41, githubDark.G);
Assert.Equal(47, githubDark.B);
Assert.True(MobileThemeColorParser.TryParseColor("rgba(255, 255, 255, 0.9)", out var whiteRgba));
Assert.Equal(255, whiteRgba.R);
Assert.Equal(255, whiteRgba.G);
Assert.Equal(255, whiteRgba.B);
// 4. Named colors
Assert.True(MobileThemeColorParser.TryParseColor("black", out var black));
Assert.Equal(0, black.R);
Assert.Equal(0, black.G);
Assert.Equal(0, black.B);
// 5. Invalid string handling (fallback)
Assert.False(MobileThemeColorParser.TryParseColor("invalid-not-a-color", out _));
Assert.False(MobileThemeColorParser.TryParseColor("", out _));
Assert.False(MobileThemeColorParser.TryParseColor(null, out _));
// 6. Luminance & isDark calculation
Assert.True(MobileThemeColorParser.IsDarkColor(black));
Assert.False(MobileThemeColorParser.IsDarkColor(whiteRgba));
Assert.True(MobileThemeColorParser.IsDarkColor(githubDark));
// 7. Adaptive UI palette generation
var palette = MobileThemeColorParser.CreateAdaptivePalette(naverGreen);
Assert.NotNull(palette);
Assert.False(palette.IsDarkBackground);
Assert.False(string.IsNullOrWhiteSpace(palette.StatusBarHex));
Assert.False(string.IsNullOrWhiteSpace(palette.CapsuleBackgroundHex));
Assert.False(string.IsNullOrWhiteSpace(palette.TextPrimaryHex));
}
[Fact]
public void Gate187_MobileScrollCoordinator_AutoHideAndRestoreLogic_Audit()
{
var coordinator = new MobileScrollCoordinator();
// 초기 상태는 항상 표시(Visible)
Assert.True(coordinator.IsTopBarVisible);
// 새 탭(NTP) 화면에서는 스크롤해도 항상 표시 유지
var ntpAction = coordinator.ProcessScroll(scrollY: 300, deltaY: 50, isNewTabPage: true, isInputFocused: false);
Assert.Equal(TopBarScrollAction.KeepVisible, ntpAction);
Assert.True(coordinator.IsTopBarVisible);
// URL 입력창에 포커스 중일 때도 항상 표시 유지
var focusAction = coordinator.ProcessScroll(scrollY: 300, deltaY: 50, isNewTabPage: false, isInputFocused: true);
Assert.Equal(TopBarScrollAction.KeepVisible, focusAction);
Assert.True(coordinator.IsTopBarVisible);
// 미세 스크롤 노이즈(이동량 < 15px) 무시
var noiseAction = coordinator.ProcessScroll(scrollY: 100, deltaY: 8, isNewTabPage: false, isInputFocused: false);
Assert.Equal(TopBarScrollAction.None, noiseAction);
Assert.True(coordinator.IsTopBarVisible);
// 아래로 유의미하게 스크롤 (deltaY > 20, scrollY > 80) -> 숨김(Hide) 트리거
var hideAction = coordinator.ProcessScroll(scrollY: 200, deltaY: 35, isNewTabPage: false, isInputFocused: false);
Assert.Equal(TopBarScrollAction.Hide, hideAction);
Assert.False(coordinator.IsTopBarVisible);
// 숨겨진 상태에서 계속 아래로 스크롤하면 중복 Hide 방지 (None)
var keepHideAction = coordinator.ProcessScroll(scrollY: 350, deltaY: 40, isNewTabPage: false, isInputFocused: false);
Assert.Equal(TopBarScrollAction.None, keepHideAction);
Assert.False(coordinator.IsTopBarVisible);
// 위로 스크롤 (deltaY < -15) -> 즉각 표시(Show) 트리거
var showAction = coordinator.ProcessScroll(scrollY: 300, deltaY: -25, isNewTabPage: false, isInputFocused: false);
Assert.Equal(TopBarScrollAction.Show, showAction);
Assert.True(coordinator.IsTopBarVisible);
// 숨겨진 상태에서 최상단 복귀 (scrollY <= 20) -> 강제 표시
coordinator.ProcessScroll(scrollY: 500, deltaY: 50, isNewTabPage: false, isInputFocused: false);
Assert.False(coordinator.IsTopBarVisible);
var topAction = coordinator.ProcessScroll(scrollY: 10, deltaY: -5, isNewTabPage: false, isInputFocused: false);
Assert.Equal(TopBarScrollAction.Show, topAction);
Assert.True(coordinator.IsTopBarVisible);
}
[Fact]
public void Gate188_OrganicSafeArea_ThemeSync_And_PlatformContracts_Audit()
{
var browserCs = File.ReadAllText(Path.Combine(MobileDir, "BrowserPage.cs"));
var customizerCs = File.ReadAllText(Path.Combine(MobileDir, "Platforms", "Android", "PacaAndroidWebViewCustomizer.cs"));
var statusBarHelperExists = File.Exists(Path.Combine(MobileDir, "Platforms", "Android", "PacaAndroidStatusBarHelper.cs"));
Assert.True(statusBarHelperExists, "PacaAndroidStatusBarHelper.cs 가 존재해야 합니다.");
// 1. DOM theme-color 메타 태그 추출 스크립트 계약 감사
Assert.Contains("meta[name=\\\"theme-color\\\"]", browserCs);
// 2. Android 상태바 색상 및 인셋 동기화 호출 감사
Assert.Contains("PacaAndroidStatusBarHelper.SetStatusBarTheme", browserCs);
// 3. Webview 네이티브 스크롤 리스너 연동 감사
Assert.Contains("IOnScrollChangeListener", customizerCs);
Assert.Contains("OnScrollChange", customizerCs);
Assert.Contains("MobileScrollCoordinator", browserCs);
}
[Fact]
public void Gate189_WindowInsets_Performance_And_NoLayoutLoop_Audit()
{
var statusBarHelperCs = File.ReadAllText(Path.Combine(MobileDir, "Platforms", "Android", "PacaAndroidStatusBarHelper.cs"));
// ANR 방지 헌법: OnGlobalLayoutListener 사용 엄격 금지 (뷰 레이아웃 트래버설마다 호출되어 UI 루퍼 스타베이션 및 10001ms ANR 유발)
Assert.DoesNotContain("IOnGlobalLayoutListener", statusBarHelperCs);
Assert.DoesNotContain("AddOnGlobalLayoutListener", statusBarHelperCs);
// 정답 아키텍처: AndroidX SetOnApplyWindowInsetsListener 사용 필수 (시스템 바/키보드 인셋 변동 시에만 배타적 트리거)
Assert.Contains("SetOnApplyWindowInsetsListener", statusBarHelperCs);
Assert.Contains("IOnApplyWindowInsetsListener", statusBarHelperCs);
}
[Fact]
public void Gate190_FloatingOverlayLayout_And_NoWebViewVerticalJolt_Audit()
{
var browserCs = File.ReadAllText(Path.Combine(MobileDir, "BrowserPage.cs"));
// 1. Full-Bleed 플로팅 오버레이 아키텍처: 상단 바 컨테이너(_topContainer)와 하단 툴바(_bottomContainer) 분리
Assert.Contains("_topContainer", browserCs);
Assert.Contains("LayoutOptions.Start", browserCs);
Assert.Contains("LayoutOptions.End", browserCs);
// 2. 웹뷰는 컨테이너 이동(Jolt) 금지: webViewNative를 위로 밀어올려 하단에 검은 틈을 만드는 버그 원천 차단
Assert.DoesNotContain("webViewNative.Animate().TranslationY(-height)", browserCs);
Assert.DoesNotContain("webViewNative?.Animate()?.TranslationY(-height)", browserCs);
Assert.Contains("webViewNative.TranslationY = 0f", browserCs);
// 3. 하단 GNB 툴바 동기화 애니메이션: 스크롤 다운 시 하단으로 슬라이드 아웃, 스크롤 업 시 0으로 복귀
Assert.Contains("bottomNative?.Animate()", browserCs);
Assert.Contains("TranslationY(bottomHeight)", browserCs);
}
[Fact]
public void Gate191_GnbFloatingPill_OverWebview_ZeroBlackGap_And_EncodingSanitization_Audit()
{
var browserCs = File.ReadAllText(Path.Combine(MobileDir, "BrowserPage.cs"));
var parserCs = File.ReadAllText(Path.Combine(MobileCoreDir, "VideoSnifferParser.cs"));
// 1. 하단 GNB는 웹페이지 위에 둥둥 떠 있는 투명 컨테이너 구조여야 함 (뒤의 검은 공간 완전 제거)
Assert.Contains("_bottomContainer.Children.Add(_bottomBar)", browserCs);
Assert.Contains("BackgroundColor = Colors.Transparent", browserCs);
// 2. 문자열 인코딩 및 타이틀/URL 새니타이저 연동 감사
Assert.Contains("MobileTextEncodingSanitizer.CleanDecodedString", browserCs);
Assert.Contains("MobileTextEncodingSanitizer.FormatUrlForDisplay", browserCs);
Assert.Contains("MobileTextEncodingSanitizer.CleanDecodedString", parserCs);
}
}