using System; using System.Collections.Generic; using System.IO; using System.Linq; using Paca.Browser.Panels; using Xunit; namespace Paca.Tests; public sealed class ModernBrowsingMilestone6Phase106Tests : IDisposable { private readonly string _tempDir; public ModernBrowsingMilestone6Phase106Tests() { _tempDir = Path.Combine(Path.GetTempPath(), "paca_m6_test_" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_tempDir); } public void Dispose() { try { if (Directory.Exists(_tempDir)) Directory.Delete(_tempDir, true); } catch { } } [Fact] public void Gate139_FullPageScreenshotCoordinator_CalculatesSlicesAndStitchLayout() { // Arrange var coordinator = new FullPageScreenshotCoordinator(); var viewport = new ViewportDimensions(Width: 1920, Height: 1080, DevicePixelRatio: 1.0); var page = new PageDimensions(ScrollWidth: 1920, ScrollHeight: 3500); // Act 1: Multi-slice page plan var plan = coordinator.PlanCapture(viewport, page); // Assert 1 Assert.NotNull(plan); Assert.Equal(1920, plan.StitchedWidth); Assert.Equal(3500, plan.StitchedHeight); Assert.Equal(4, plan.TotalSlices); // 1080 * 3 = 3240 + 260 = 3500 (4 slices) // Verify slice progression var s0 = plan.Slices[0]; Assert.Equal(0, s0.Index); Assert.Equal(0, s0.ScrollY); Assert.Equal(0, s0.TargetDestY); Assert.Equal(1080, s0.CropHeight); var s1 = plan.Slices[1]; Assert.Equal(1, s1.Index); Assert.Equal(1080, s1.ScrollY); Assert.Equal(1080, s1.TargetDestY); Assert.Equal(1080, s1.CropHeight); var s2 = plan.Slices[2]; Assert.Equal(2, s2.Index); Assert.Equal(2160, s2.ScrollY); Assert.Equal(2160, s2.TargetDestY); Assert.Equal(1080, s2.CropHeight); var lastSlice = plan.Slices[3]; Assert.Equal(3, lastSlice.Index); Assert.Equal(3240, lastSlice.TargetDestY); Assert.Equal(260, lastSlice.CropHeight); // Scrolled to bottom: 3500 - 1080 = 2420, CropSourceY = 1080 - 260 = 820 Assert.Equal(2420, lastSlice.ScrollY); Assert.Equal(820, lastSlice.CropSourceY); // Validation Assert.True(coordinator.ValidatePlan(plan)); // Act 2: Short page fitting in viewport var shortPage = new PageDimensions(ScrollWidth: 1920, ScrollHeight: 800); var shortPlan = coordinator.PlanCapture(viewport, shortPage); Assert.Equal(1, shortPlan.TotalSlices); Assert.Equal(800, shortPlan.StitchedHeight); Assert.Equal(800, shortPlan.Slices[0].CropHeight); // Act 3: Device Pixel Ratio scaling (HiDPI / Retina 2.0x) var hiDpiViewport = new ViewportDimensions(Width: 1920, Height: 1080, DevicePixelRatio: 2.0); var hiDpiPlan = coordinator.PlanCapture(hiDpiViewport, page); Assert.Equal(3840, hiDpiPlan.StitchedWidth); Assert.Equal(7000, hiDpiPlan.StitchedHeight); // Act 4: Limit guard var hugePage = new PageDimensions(ScrollWidth: 1920, ScrollHeight: 50000); coordinator.MaxDimensionLimit = 32767; Assert.Throws(() => coordinator.PlanCapture(viewport, hugePage)); } [Fact] public void Gate140_CaptureAnnotationModel_ManagesEntitiesAndExportCanvas() { // Arrange var canvas = new CaptureAnnotationCanvas( canvasWidth: 1920, canvasHeight: 1080, baseImageUri: "capture://screenshot_9918.png"); // Act 1: Add various annotation types var pen = new CaptureAnnotationItem( id: Guid.NewGuid().ToString("N"), type: AnnotationType.Pen, strokeColorHex: "#FF0000", strokeWidth: 3.5, opacity: 1.0, points: new List { new(10, 10), new(25, 30), new(40, 50) }); var highlighter = new CaptureAnnotationItem( id: Guid.NewGuid().ToString("N"), type: AnnotationType.Highlighter, strokeColorHex: "#FFFF00", strokeWidth: 24.0, opacity: 0.35, points: new List { new(50, 100), new(300, 100) }); var arrow = new CaptureAnnotationItem( id: Guid.NewGuid().ToString("N"), type: AnnotationType.Arrow, strokeColorHex: "#0066FF", strokeWidth: 4.0, opacity: 1.0, startPoint: new PointD(100, 100), endPoint: new PointD(250, 250), headSize: 14.0); var text = new CaptureAnnotationItem( id: Guid.NewGuid().ToString("N"), type: AnnotationType.Text, strokeColorHex: "#111111", strokeWidth: 1.0, opacity: 1.0, position: new PointD(260, 250), text: "Important Callout", fontSize: 18.0, fontFamily: "Segoe UI"); canvas.AddAnnotation(pen); canvas.AddAnnotation(highlighter); canvas.AddAnnotation(arrow); canvas.AddAnnotation(text); // Assert 1: Count & properties Assert.Equal(4, canvas.Annotations.Count); Assert.Equal("capture://screenshot_9918.png", canvas.BaseImageUri); // Act 2: Undo and Redo Assert.True(canvas.CanUndo); canvas.Undo(); Assert.Equal(3, canvas.Annotations.Count); Assert.DoesNotContain(canvas.Annotations, a => a.Id == text.Id); Assert.True(canvas.CanRedo); canvas.Redo(); Assert.Equal(4, canvas.Annotations.Count); Assert.Contains(canvas.Annotations, a => a.Id == text.Id); // Act 3: Remove specific annotation canvas.RemoveAnnotation(highlighter.Id); Assert.Equal(3, canvas.Annotations.Count); Assert.DoesNotContain(canvas.Annotations, a => a.Id == highlighter.Id); // Act 4: Export to JSON and Re-import var json = canvas.ExportToJson(); Assert.False(string.IsNullOrWhiteSpace(json)); var imported = CaptureAnnotationCanvas.ImportFromJson(json); Assert.NotNull(imported); Assert.Equal(canvas.CanvasWidth, imported.CanvasWidth); Assert.Equal(canvas.CanvasHeight, imported.CanvasHeight); Assert.Equal(canvas.BaseImageUri, imported.BaseImageUri); Assert.Equal(3, imported.Annotations.Count); var importedArrow = imported.Annotations.FirstOrDefault(a => a.Type == AnnotationType.Arrow); Assert.NotNull(importedArrow); Assert.Equal("#0066FF", importedArrow.StrokeColorHex); Assert.Equal(14.0, importedArrow.HeadSize); Assert.Equal(100, importedArrow.StartPoint?.X); } [Fact] public void Gate141_SidebarPanelManager_ManagesRegistryDockAndWidthState() { // Arrange var configPath = Path.Combine(_tempDir, "sidebar_panels.json"); var manager = new SidebarPanelManager(configPath); // Act 1: Default preset panels var panels = manager.GetAllPanels(); Assert.Contains(panels, p => p.Id == "papago" && p.Url.Contains("papago.naver.com")); Assert.Contains(panels, p => p.Id == "twitter" && p.Url.Contains("x.com")); Assert.Contains(panels, p => p.Id == "calculator"); Assert.Contains(panels, p => p.Id == "notes"); // Act 2: Dock position & width state Assert.Equal(SidebarDockPosition.Right, manager.DockPosition); manager.SetDockPosition(SidebarDockPosition.Left); Assert.Equal(SidebarDockPosition.Left, manager.DockPosition); Assert.Equal(380, manager.PanelWidth); manager.SetPanelWidth(100); // Below min (250) -> clamps to 250 Assert.Equal(250, manager.PanelWidth); manager.SetPanelWidth(1200); // Above max (800) -> clamps to 800 Assert.Equal(800, manager.PanelWidth); manager.SetPanelWidth(420); Assert.Equal(420, manager.PanelWidth); // Act 3: Toggle panel open/close Assert.False(manager.IsOpen); Assert.Null(manager.ActivePanelId); manager.TogglePanel("papago"); Assert.True(manager.IsOpen); Assert.Equal("papago", manager.ActivePanelId); // Switching to another panel keeps it open manager.TogglePanel("calculator"); Assert.True(manager.IsOpen); Assert.Equal("calculator", manager.ActivePanelId); // Toggling the same panel closes it manager.TogglePanel("calculator"); Assert.False(manager.IsOpen); Assert.Null(manager.ActivePanelId); // Act 4: Custom panel registration and badge counter var customPanel = new SidebarPanel( id: "claude", title: "Claude AI", url: "https://claude.ai/", icon: "bot-sparkle", badgeCount: 0); manager.RegisterPanel(customPanel); Assert.NotNull(manager.GetPanel("claude")); manager.SetBadge("claude", 5); Assert.Equal(5, manager.GetPanel("claude")?.BadgeCount); // Act 5: Save & reload persistence manager.SetDockPosition(SidebarDockPosition.Left); manager.SetPanelWidth(460); manager.Save(); var reloaded = new SidebarPanelManager(configPath); Assert.Equal(SidebarDockPosition.Left, reloaded.DockPosition); Assert.Equal(460, reloaded.PanelWidth); Assert.NotNull(reloaded.GetPanel("claude")); Assert.Equal("Claude AI", reloaded.GetPanel("claude")?.Title); } [Fact] public void Gate142_QuickStylerStore_AppliesPerSiteCssAndUserScripts() { // Arrange var configPath = Path.Combine(_tempDir, "quick_styler.json"); var store = new QuickStylerStore(configPath); var githubDark = new SiteStyleBoost( id: "boost-gh-dark", name: "GitHub OLED Black", hostPattern: "github.com", customCss: "body { background-color: #000000 !important; }", customJs: "console.log('OLED applied');", isEnabled: true); var githubFont = new SiteStyleBoost( id: "boost-gh-font", name: "GitHub Font Customizer", hostPattern: "*.github.com", customCss: "pre, code { font-family: 'JetBrains Mono' !important; }", customJs: string.Empty, isEnabled: true); var redditMinimal = new SiteStyleBoost( id: "boost-reddit", name: "Reddit Minimal", hostPattern: "reddit.com", customCss: "#sidebar { display: none !important; }", customJs: "window.__paca_cleaned = true;", isEnabled: false); // initially disabled store.AddOrUpdateBoost(githubDark); store.AddOrUpdateBoost(githubFont); store.AddOrUpdateBoost(redditMinimal); // Act 1: URL matching var ghMatches = store.GetBoostsForUrl("https://github.com/microsoft/playwright"); Assert.Equal(2, ghMatches.Count); // matches exact github.com and wildcard *.github.com var gistMatches = store.GetBoostsForUrl("https://gist.github.com/user/12345"); Assert.Single(gistMatches); // only matches *.github.com Assert.Equal("boost-gh-font", gistMatches[0].Id); // Act 2: CSS compilation var compiledCss = store.CompileInjectedCss("https://github.com/trending"); Assert.Contains("#000000", compiledCss); Assert.Contains("JetBrains Mono", compiledCss); var compiledJs = store.CompileInjectedScripts("https://github.com/trending"); Assert.Contains("console.log('OLED applied');", compiledJs); // Act 3: Disabled boost handling var redditCssDisabled = store.CompileInjectedCss("https://reddit.com/r/dotnet"); Assert.Empty(redditCssDisabled); // Enable reddit boost store.ToggleBoost("boost-reddit", isEnabled: true); var redditCssEnabled = store.CompileInjectedCss("https://reddit.com/r/dotnet"); Assert.Contains("#sidebar { display: none", redditCssEnabled); // Act 4: Save & Reload store.Save(); var reloaded = new QuickStylerStore(configPath); Assert.Equal(3, reloaded.GetAllBoosts().Count); Assert.True(reloaded.GetAllBoosts().First(b => b.Id == "boost-reddit").IsEnabled); } [Fact] public void Gate143_SiteStorageInspector_InspectsAndDeletesCookiesAndStorage() { // Arrange var inspector = new SiteStorageInspector(); // Cookies inspector.AddOrUpdateCookie(new CookieItem( Domain: "example.com", Name: "session_id", Value: "auth_token_987654", Path: "/", Expires: DateTime.UtcNow.AddDays(7), HttpOnly: true, Secure: true, SameSite: "Lax")); inspector.AddOrUpdateCookie(new CookieItem( Domain: "sub.example.com", Name: "sub_tracking", Value: "track_val_112233", Path: "/app", Expires: null, HttpOnly: false, Secure: true, SameSite: "Strict")); inspector.AddOrUpdateCookie(new CookieItem( Domain: "other.org", Name: "ad_pref", Value: "opt_out", Path: "/", Expires: DateTime.UtcNow.AddDays(30), HttpOnly: false, Secure: false, SameSite: "None")); // LocalStorage inspector.AddOrUpdateLocalStorage(new LocalStorageItem( Origin: "https://example.com", Key: "theme_preference", Value: "dark")); inspector.AddOrUpdateLocalStorage(new LocalStorageItem( Origin: "https://example.com", Key: "draft_comment", Value: "Draft text for issue review")); inspector.AddOrUpdateLocalStorage(new LocalStorageItem( Origin: "https://other.org", Key: "analytics_cache", Value: "{\"v\":1}")); // Act 1: Inspect cookies by domain (hierarchical) var exampleCookies = inspector.GetCookiesForDomain("example.com"); Assert.Equal(2, exampleCookies.Count); // includes example.com and sub.example.com var otherCookies = inspector.GetCookiesForDomain("other.org"); Assert.Single(otherCookies); // Act 2: Search across domain var searchHits = inspector.Search("example.com", "draft"); Assert.Empty(searchHits.Cookies); Assert.Single(searchHits.LocalStorage); Assert.Equal("draft_comment", searchHits.LocalStorage[0].Key); var searchValHits = inspector.Search("example.com", "auth_token"); Assert.Single(searchValHits.Cookies); Assert.Equal("session_id", searchValHits.Cookies[0].Name); // Act 3: Storage summary var summary = inspector.GetSummary("example.com"); Assert.Equal(2, summary.CookieCount); Assert.Equal(2, summary.LocalStorageItemCount); Assert.True(summary.TotalEstimatedBytes > 0); // Act 4: Targeted deletion var deletedCookie = inspector.DeleteCookie("example.com", "session_id", "/"); Assert.True(deletedCookie); Assert.Single(inspector.GetCookiesForDomain("example.com")); var deletedStorage = inspector.DeleteLocalStorage("https://example.com", "theme_preference"); Assert.True(deletedStorage); Assert.Single(inspector.GetLocalStorageForOrigin("https://example.com")); // Act 5: Clear all for domain inspector.ClearAllForDomain("example.com"); Assert.Empty(inspector.GetCookiesForDomain("example.com")); Assert.Empty(inspector.GetLocalStorageForOrigin("https://example.com")); // Other.org remains untouched Assert.Single(inspector.GetCookiesForDomain("other.org")); } [Fact] public void Gate144_NtpCustomizerService_ConfiguresWallpapersAndWidgetVisibility() { // Arrange var configPath = Path.Combine(_tempDir, "ntp_settings.json"); var service = new NtpCustomizerService(configPath); // Act 1: Default configuration var settings = service.CurrentSettings; Assert.Equal(NtpWallpaperSourceType.Default, settings.Wallpaper.SourceType); Assert.True(settings.Widgets.ShowClock); Assert.True(settings.Widgets.ShowShortcuts); Assert.True(settings.Widgets.ShowWeather); Assert.True(settings.Widgets.ShowSearchBox); Assert.False(settings.Widgets.ShowMostVisited); // Act 2: Set Bing Daily wallpaper service.SetBingDailyWallpaper("https://bing.com/th?id=OHR.MountFuji_1920x1080.jpg"); Assert.Equal(NtpWallpaperSourceType.BingDaily, service.CurrentSettings.Wallpaper.SourceType); Assert.Equal("https://bing.com/th?id=OHR.MountFuji_1920x1080.jpg", service.CurrentSettings.Wallpaper.BingDailyUrl); var bingCss = service.GetEffectiveWallpaperCss(); Assert.Contains("url('https://bing.com/th?id=OHR.MountFuji_1920x1080.jpg')", bingCss); // Act 3: Set custom image with blur and dimming overlay var customPath = @"C:\Wallpapers\emerald_lake.jpg"; service.SetCustomImageWallpaper(customPath, blurRadius: 12.0, dimOverlayOpacity: 0.4); Assert.Equal(NtpWallpaperSourceType.CustomImage, service.CurrentSettings.Wallpaper.SourceType); Assert.Equal(customPath, service.CurrentSettings.Wallpaper.CustomImagePath); Assert.Equal(12.0, service.CurrentSettings.Wallpaper.BlurRadius); Assert.Equal(0.4, service.CurrentSettings.Wallpaper.DimOverlayOpacity); var customCss = service.GetEffectiveWallpaperCss(); Assert.Contains("blur(12px)", customCss); Assert.Contains("rgba(0, 0, 0, 0.4)", customCss); // Act 4: Configure widget visibility service.SetWidgetVisibility(weather: false, mostVisited: true); Assert.False(service.CurrentSettings.Widgets.ShowWeather); Assert.True(service.CurrentSettings.Widgets.ShowMostVisited); Assert.True(service.CurrentSettings.Widgets.ShowClock); // Unchanged // Act 5: Persistence & Reset service.Save(); var reloaded = new NtpCustomizerService(configPath); Assert.Equal(NtpWallpaperSourceType.CustomImage, reloaded.CurrentSettings.Wallpaper.SourceType); Assert.False(reloaded.CurrentSettings.Widgets.ShowWeather); Assert.True(reloaded.CurrentSettings.Widgets.ShowMostVisited); // Reset to default reloaded.ResetToDefaults(); Assert.Equal(NtpWallpaperSourceType.Default, reloaded.CurrentSettings.Wallpaper.SourceType); Assert.True(reloaded.CurrentSettings.Widgets.ShowWeather); } }