feat(ads/seo/release): v1.2.0 with live Google Ads, isolated ad profile, ads.txt, and SEO/AEO/GEO optimization

This commit is contained in:
Yun Chan 2026-09-14 00:39:44 +09:00
parent a88aafa1e5
commit 0e568f1f0a
975 changed files with 130593 additions and 16783 deletions

View file

@ -0,0 +1,470 @@
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);
}
}