using System; using System.Collections.Generic; using System.IO; using System.Linq; using Paca.Browser.Diagnostics; using Xunit; namespace Paca.Tests; public sealed class ModernBrowsingMilestone14Phase114Tests : IDisposable { private readonly string _tempDir; public ModernBrowsingMilestone14Phase114Tests() { _tempDir = Path.Combine(Path.GetTempPath(), "paca_m14_test_" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_tempDir); } public void Dispose() { try { if (Directory.Exists(_tempDir)) Directory.Delete(_tempDir, true); } catch { } } [Fact] public void Gate195_BatteryEfficiencyModeGovernor_ThrottlesTimersAndLowersFrameRate() { // Arrange var governor = new BatteryEfficiencyModeGovernor( policy: EfficiencyModePolicy.LowBattery, batteryThresholdPercent: 20, normalFrameRate: 60, efficiencyFrameRate: 30, throttleBackgroundTimerInterval: TimeSpan.FromSeconds(10)); // Act & Assert 1: Plugged in and high battery -> Normal 60fps, no throttle governor.UpdateBatteryStatus(batteryPercentage: 85, isPluggedIn: true); Assert.False(governor.IsEfficiencyModeActive); Assert.Equal(60, governor.GetEffectiveFrameRate()); var normalInterval = governor.GetEffectiveTimerInterval(TimeSpan.FromSeconds(1), isBackgroundTab: true); Assert.Equal(TimeSpan.FromSeconds(1), normalInterval); // Act & Assert 2: Unplugged with 15% battery (below 20% threshold) -> Efficiency mode active (30fps) bool eventFired = false; governor.EfficiencyModeChanged += (sender, active) => eventFired = active; governor.UpdateBatteryStatus(batteryPercentage: 15, isPluggedIn: false); Assert.True(governor.IsEfficiencyModeActive); Assert.True(eventFired); Assert.Equal(30, governor.GetEffectiveFrameRate()); // Background timer throttled to at least 10s floor var throttledInterval = governor.GetEffectiveTimerInterval(TimeSpan.FromSeconds(1), isBackgroundTab: true); Assert.Equal(TimeSpan.FromSeconds(10), throttledInterval); // Active foreground tab timer is preserved var foregroundInterval = governor.GetEffectiveTimerInterval(TimeSpan.FromMilliseconds(16), isBackgroundTab: false); Assert.Equal(TimeSpan.FromMilliseconds(16), foregroundInterval); // Act & Assert 3: Manual override always takes precedence governor.SetManualOverride(false); Assert.False(governor.IsEfficiencyModeActive); Assert.Equal(60, governor.GetEffectiveFrameRate()); governor.SetManualOverride(true); Assert.True(governor.IsEfficiencyModeActive); Assert.Equal(30, governor.GetEffectiveFrameRate()); // Act & Assert 4: Clear override resumes policy-driven evaluation governor.SetManualOverride(null); Assert.True(governor.IsEfficiencyModeActive); // still 15% and unplugged } [Fact] public void Gate196_DiscardedTabVisualState_RendersOpacityAndSleepingBadge() { // Act 1: Active tab visual state var activeState = DiscardedTabVisualState.Create(isDiscarded: false, reclaimedMemoryBytes: 0); Assert.False(activeState.IsDiscarded); Assert.Equal(1.0, activeState.Opacity); Assert.False(activeState.ShowSleepingBadge); Assert.Equal(string.Empty, activeState.SleepingBadgeText); Assert.Equal(string.Empty, activeState.SleepingBadgeIcon); Assert.False(activeState.HasReclaimedMemory); // Act 2: Discarded/sleeping tab visual state (spec: opacity 0.6, sleeping badge text/icon) long savedBytes = 145 * 1024 * 1024; // 145 MB var discardedState = DiscardedTabVisualState.Create( isDiscarded: true, reclaimedMemoryBytes: savedBytes, sleepingBadgeText: "절전 중", sleepingBadgeIcon: "💤"); Assert.True(discardedState.IsDiscarded); Assert.Equal(0.6, discardedState.Opacity); Assert.True(discardedState.ShowSleepingBadge); Assert.Equal("절전 중", discardedState.SleepingBadgeText); Assert.Equal("💤", discardedState.SleepingBadgeIcon); Assert.True(discardedState.HasReclaimedMemory); Assert.Contains("145.0 MB", discardedState.FormattedReclaimedMemory); Assert.Contains("절전", discardedState.TooltipText); // Act 3: CSS styling helper verification string css = discardedState.ToCssStyle(); Assert.Contains("opacity: 0.6", css); string activeCss = activeState.ToCssStyle(); Assert.Contains("opacity: 1", activeCss); } [Fact] public void Gate197_SiteStorageQuotaInspector_CalculatesUsageAndGeneratesCleanupOrders() { // Arrange var inspector = new SiteStorageQuotaInspector(perDomainQuotaBytes: 200 * 1024 * 1024); // 200MB quota // Domain 1: 350MB total (Over Quota) inspector.RegisterUsage(new SiteStorageItem( Domain: "video.streaming.com", IndexedDbBytes: 150 * 1024 * 1024, CacheStorageBytes: 180 * 1024 * 1024, CookieBytes: 20 * 1024 * 1024, LastAccessedUtc: DateTime.UtcNow.AddDays(-10))); // Domain 2: 80MB total (Under Quota) inspector.RegisterUsage(new SiteStorageItem( Domain: "news.portal.com", IndexedDbBytes: 10 * 1024 * 1024, CacheStorageBytes: 68 * 1024 * 1024, CookieBytes: 2 * 1024 * 1024, LastAccessedUtc: DateTime.UtcNow.AddDays(-2))); // Domain 3: 250MB total (Over Quota, older) inspector.RegisterUsage(new SiteStorageItem( Domain: "heavy.webapp.org", IndexedDbBytes: 200 * 1024 * 1024, CacheStorageBytes: 49 * 1024 * 1024, CookieBytes: 1 * 1024 * 1024, LastAccessedUtc: DateTime.UtcNow.AddDays(-30))); // Act 1: Verify total storage computation long totalBytes = inspector.GetTotalStorageBytes(); Assert.Equal(680 * 1024 * 1024, totalBytes); // Act 2: Detect over-quota domains var overQuota = inspector.GetOverQuotaDomains(); Assert.Equal(2, overQuota.Count); Assert.Contains(overQuota, d => d.Domain == "video.streaming.com"); Assert.Contains(overQuota, d => d.Domain == "heavy.webapp.org"); Assert.DoesNotContain(overQuota, d => d.Domain == "news.portal.com"); // Act 3: Generate over-quota cleanup orders var overQuotaOrders = inspector.GenerateOverQuotaCleanupOrders(); Assert.Equal(2, overQuotaOrders.Count); var videoOrder = overQuotaOrders.First(o => o.Domain == "video.streaming.com"); Assert.True(videoOrder.ClearCacheStorage); Assert.True(videoOrder.TargetBytesToReclaim > 0); // Act 4: Generate target free cleanup plan (Free 300MB from LRU domains) var cleanupPlan = inspector.GenerateCleanupPlan(targetBytesToFree: 300 * 1024 * 1024); Assert.NotEmpty(cleanupPlan); long plannedReclamation = cleanupPlan.Sum(p => p.TargetBytesToReclaim); Assert.True(plannedReclamation >= 300 * 1024 * 1024); // heavy.webapp.org is oldest (-30 days), should be cleaned first Assert.Equal("heavy.webapp.org", cleanupPlan[0].Domain); // Act 5: Execute cleanup order and verify state mutation long freed = inspector.ExecuteCleanupOrder(videoOrder); Assert.True(freed > 0); var updatedVideo = inspector.GetUsage("video.streaming.com"); Assert.NotNull(updatedVideo); Assert.True(updatedVideo.TotalBytes < 350 * 1024 * 1024); } [Fact] public void Gate198_DoNotDisturbNotificationPolicy_SuppressesPushDuringQuietHoursAndFullscreen() { // Arrange var policy = new DoNotDisturbNotificationPolicy( quietHoursStart: new TimeOnly(22, 0), // 10:00 PM quietHoursEnd: new TimeOnly(7, 0), // 07:00 AM quietHoursEnabled: true, suppressDuringFullscreen: true); policy.AddWhitelistedOrigin("https://critical-alerts.corp.internal"); // Act & Assert 1: Day time (14:30) outside quiet hours, windowed mode -> Allow var resDay = policy.EvaluateNotification( origin: "https://social.example.com", isFullscreen: false, currentTime: new TimeOnly(14, 30), priority: NotificationPriority.Normal); Assert.False(resDay.IsSuppressed); Assert.Equal(DndSuppressionReason.None, resDay.Reason); Assert.Equal(NotificationDeliveryAction.DeliverImmediately, resDay.Action); // Act & Assert 2: Quiet hours (23:15) -> Suppress & Queue var resNight = policy.EvaluateNotification( origin: "https://social.example.com", isFullscreen: false, currentTime: new TimeOnly(23, 15), priority: NotificationPriority.Normal); Assert.True(resNight.IsSuppressed); Assert.Equal(DndSuppressionReason.QuietHours, resNight.Reason); Assert.Equal(NotificationDeliveryAction.QueueForLater, resNight.Action); // Act & Assert 3: Fullscreen active during daytime (15:00) -> Suppress var resFullscreen = policy.EvaluateNotification( origin: "https://social.example.com", isFullscreen: true, currentTime: new TimeOnly(15, 0), priority: NotificationPriority.Normal); Assert.True(resFullscreen.IsSuppressed); Assert.Equal(DndSuppressionReason.Fullscreen, resFullscreen.Reason); // Act & Assert 4: Whitelisted origin bypasses quiet hours and fullscreen var resWhitelisted = policy.EvaluateNotification( origin: "https://critical-alerts.corp.internal", isFullscreen: true, currentTime: new TimeOnly(3, 30), priority: NotificationPriority.Normal); Assert.False(resWhitelisted.IsSuppressed); Assert.Equal(NotificationDeliveryAction.DeliverImmediately, resWhitelisted.Action); // Act & Assert 5: Critical priority bypasses DND var resCritical = policy.EvaluateNotification( origin: "https://random.news.com", isFullscreen: true, currentTime: new TimeOnly(2, 0), priority: NotificationPriority.Critical); Assert.False(resCritical.IsSuppressed); // Act & Assert 6: Queuing and flushing suppressed notifications var item1 = new WebPushNotification("notif-1", "https://social.example.com", "New Message", "Hello", NotificationPriority.Normal, DateTime.UtcNow); policy.EnqueueSuppressedNotification(item1); Assert.Single(policy.GetSuppressedQueue()); var flushed = policy.FlushSuppressedQueue(); Assert.Single(flushed); Assert.Equal("notif-1", flushed[0].Id); Assert.Empty(policy.GetSuppressedQueue()); } [Fact] public void Gate199_PrintBackgroundGraphicsPolicy_PreservesBackgroundColorsImagesAndBorders() { // Arrange var policy = new PrintBackgroundGraphicsPolicy { PreserveBackgroundGraphics = true, PreserveBackgroundColors = true, PreserveBackgroundImages = true, PreserveBorders = true, ColorMode = PrintColorMode.Color, Orientation = PrintOrientation.Portrait, DisplayHeaderFooter = false }; // Act 1: Verify CSS generator outputs exact color adjust rules string css = policy.GeneratePrintCss(); Assert.Contains("-webkit-print-color-adjust: exact", css); Assert.Contains("print-color-adjust: exact", css); Assert.Contains("@media print", css); Assert.Contains("background-color", css); Assert.Contains("background-image", css); // Act 2: Export CDP / WebView2 print parameters var cdpParams = policy.ToChromiumPrintParameters(); Assert.True(cdpParams.ContainsKey("printBackground")); Assert.True((bool)cdpParams["printBackground"]); Assert.False((bool)cdpParams["displayHeaderFooter"]); Assert.False((bool)cdpParams["landscape"]); // Portrait // Act 3: JSON persistence / serialization string jsonPath = Path.Combine(_tempDir, "print_policy.json"); policy.SaveToFile(jsonPath); Assert.True(File.Exists(jsonPath)); var loaded = PrintBackgroundGraphicsPolicy.LoadFromFile(jsonPath); Assert.True(loaded.PreserveBackgroundGraphics); Assert.True(loaded.PreserveBackgroundColors); Assert.True(loaded.PreserveBackgroundImages); Assert.True(loaded.PreserveBorders); Assert.Equal(PrintColorMode.Color, loaded.ColorMode); // Act 4: Turning off graphics preservation policy.PreserveBackgroundGraphics = false; var offParams = policy.ToChromiumPrintParameters(); Assert.False((bool)offParams["printBackground"]); string offCss = policy.GeneratePrintCss(); Assert.DoesNotContain("-webkit-print-color-adjust: exact", offCss); } [Fact] public void Gate200_AdvancedProcessTaskInspector_CategorizesProcessesAndComputesResourceUsage() { // Arrange var inspector = new AdvancedProcessTaskInspector(); // Act 1: Categorization logic for command line switches Assert.Equal(BrowserProcessCategory.Browser, inspector.CategorizeProcess("PacaBrowser.exe", "")); Assert.Equal(BrowserProcessCategory.Gpu, inspector.CategorizeProcess("PacaBrowser.exe", "--type=gpu-process --enable-vulkan")); Assert.Equal(BrowserProcessCategory.Renderer, inspector.CategorizeProcess("PacaBrowser.exe", "--type=renderer --site-per-process")); Assert.Equal(BrowserProcessCategory.Audio, inspector.CategorizeProcess("PacaBrowser.exe", "--type=utility --utility-sub-type=audio.mojom.AudioService")); Assert.Equal(BrowserProcessCategory.Utility, inspector.CategorizeProcess("PacaBrowser.exe", "--type=utility --utility-sub-type=network.mojom.NetworkService")); // Act 2: Register task descriptors inspector.RegisterProcess(new ProcessTaskSnapshot( ProcessId: 1001, ProcessName: "PacaBrowser.exe", Category: BrowserProcessCategory.Browser, Title: "Browser Main (Paca)", CommandLine: "", WorkingSetBytes: 150 * 1024 * 1024, CpuPercent: 1.5)); inspector.RegisterProcess(new ProcessTaskSnapshot( ProcessId: 1002, ProcessName: "PacaBrowser.exe", Category: BrowserProcessCategory.Gpu, Title: "GPU Process", CommandLine: "--type=gpu-process", WorkingSetBytes: 100 * 1024 * 1024, CpuPercent: 2.0)); inspector.RegisterProcess(new ProcessTaskSnapshot( ProcessId: 1003, ProcessName: "PacaBrowser.exe", Category: BrowserProcessCategory.Renderer, Title: "Tab: Dashboard", CommandLine: "--type=renderer", WorkingSetBytes: 250 * 1024 * 1024, CpuPercent: 8.5)); inspector.RegisterProcess(new ProcessTaskSnapshot( ProcessId: 1004, ProcessName: "PacaBrowser.exe", Category: BrowserProcessCategory.Audio, Title: "Audio Service", CommandLine: "--type=utility --utility-sub-type=audio.mojom.AudioService", WorkingSetBytes: 30 * 1024 * 1024, CpuPercent: 0.5)); inspector.RegisterProcess(new ProcessTaskSnapshot( ProcessId: 1005, ProcessName: "PacaBrowser.exe", Category: BrowserProcessCategory.Utility, Title: "Network Service", CommandLine: "--type=utility --utility-sub-type=network.mojom.NetworkService", WorkingSetBytes: 45 * 1024 * 1024, CpuPercent: 0.8)); // Act 3: Aggregation and metrics Assert.Equal(575 * 1024 * 1024, inspector.GetTotalWorkingSetBytes()); Assert.Equal(13.3, Math.Round(inspector.GetTotalCpuPercent(), 1)); // Category filtering var renderers = inspector.GetProcessesByCategory(BrowserProcessCategory.Renderer); Assert.Single(renderers); Assert.Equal("Tab: Dashboard", renderers[0].Title); var audioList = inspector.GetProcessesByCategory(BrowserProcessCategory.Audio); Assert.Single(audioList); Assert.Equal(BrowserProcessCategory.Audio, audioList[0].Category); // Act 4: Group summary by category var summary = inspector.GetCategorySummaries(); Assert.Equal(5, summary.Count); var rendererSummary = summary.First(s => s.Category == BrowserProcessCategory.Renderer); Assert.Equal(1, rendererSummary.ProcessCount); Assert.Equal(250 * 1024 * 1024, rendererSummary.TotalWorkingSetBytes); Assert.Equal(8.5, rendererSummary.TotalCpuPercent); // Act 5: Heavy process detection (thresholds: 200MB or 5% CPU) var heavy = inspector.FindHeavyProcesses(memoryThresholdBytes: 200 * 1024 * 1024, cpuThresholdPercent: 5.0); Assert.Single(heavy); Assert.Equal(1003, heavy[0].ProcessId); // Act 6: Sorting var sortedByMem = inspector.Sort(ProcessSortColumn.WorkingSet, descending: true); Assert.Equal(1003, sortedByMem[0].ProcessId); // 250MB Assert.Equal(1004, sortedByMem.Last().ProcessId); // 30MB } }