201 lines
9.9 KiB
C#
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);
|
|
}
|
|
}
|
|
|