releases/tests/Paca.Tests/ModernBrowsingMilestone14Phase114Tests.cs

388 lines
17 KiB
C#

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
}
}