470 lines
18 KiB
C#
470 lines
18 KiB
C#
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<ArgumentOutOfRangeException>(() => 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<PointD> { 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<PointD> { 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);
|
|
}
|
|
}
|