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:
parent
a88aafa1e5
commit
0e568f1f0a
975 changed files with 130593 additions and 16783 deletions
376
tests/Paca.Tests/ModernBrowsingMilestone8Phase108Tests.cs
Normal file
376
tests/Paca.Tests/ModernBrowsingMilestone8Phase108Tests.cs
Normal file
|
|
@ -0,0 +1,376 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Paca.Browser.Layouts;
|
||||
using Xunit;
|
||||
|
||||
namespace Paca.Tests;
|
||||
|
||||
public sealed class ModernBrowsingMilestone8Phase108Tests : IDisposable
|
||||
{
|
||||
private readonly string _tempDir;
|
||||
|
||||
public ModernBrowsingMilestone8Phase108Tests()
|
||||
{
|
||||
_tempDir = Path.Combine(Path.GetTempPath(), "paca_m8_test_" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(_tempDir);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(_tempDir)) Directory.Delete(_tempDir, true);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate151_SpatialNavigationCoordinator_Navigates2DGridByElementBounds()
|
||||
{
|
||||
// Arrange
|
||||
var coordinator = new SpatialNavigationCoordinator();
|
||||
|
||||
// Register a 3x3 grid of elements:
|
||||
// A (0,0) B (120,0) C (240,0)
|
||||
// D (0,80) E (120,80) F (240,80)
|
||||
// G (0,160) H (120,160) I (240,160)
|
||||
var elements = new List<SpatialElement>
|
||||
{
|
||||
new("elem-A", new LayoutRect(0, 0, 100, 50)),
|
||||
new("elem-B", new LayoutRect(120, 0, 100, 50)),
|
||||
new("elem-C", new LayoutRect(240, 0, 100, 50)),
|
||||
new("elem-D", new LayoutRect(0, 80, 100, 50)),
|
||||
new("elem-E", new LayoutRect(120, 80, 100, 50)),
|
||||
new("elem-F", new LayoutRect(240, 80, 100, 50)),
|
||||
new("elem-G", new LayoutRect(0, 160, 100, 50)),
|
||||
new("elem-H", new LayoutRect(120, 160, 100, 50)),
|
||||
new("elem-I", new LayoutRect(240, 160, 100, 50))
|
||||
};
|
||||
|
||||
coordinator.RegisterElements(elements);
|
||||
coordinator.SetFocus("elem-A");
|
||||
|
||||
// Act 1: Move Right twice -> A -> B -> C
|
||||
var next1 = coordinator.MoveFocus(SpatialDirection.Right);
|
||||
Assert.Equal("elem-B", next1);
|
||||
Assert.Equal("elem-B", coordinator.CurrentFocusId);
|
||||
|
||||
var next2 = coordinator.MoveFocus(SpatialDirection.Right);
|
||||
Assert.Equal("elem-C", next2);
|
||||
|
||||
// Act 2: Move Down -> C -> F
|
||||
var next3 = coordinator.MoveFocus(SpatialDirection.Down);
|
||||
Assert.Equal("elem-F", next3);
|
||||
|
||||
// Act 3: Move Left -> F -> E
|
||||
var next4 = coordinator.MoveFocus(SpatialDirection.Left);
|
||||
Assert.Equal("elem-E", next4);
|
||||
|
||||
// Act 4: Move Down -> E -> H
|
||||
var next5 = coordinator.MoveFocus(SpatialDirection.Down);
|
||||
Assert.Equal("elem-H", next5);
|
||||
|
||||
// Act 5: Boundary check without wrapping
|
||||
coordinator.WrapAround = false;
|
||||
var next6 = coordinator.MoveFocus(SpatialDirection.Down);
|
||||
Assert.Null(next6); // No element below H
|
||||
Assert.Equal("elem-H", coordinator.CurrentFocusId); // Stays on H
|
||||
|
||||
// Act 6: Disabled / Unfocusable element skipping
|
||||
coordinator.RegisterElement(new SpatialElement("elem-disabled", new LayoutRect(120, 240, 100, 50), IsFocusable: false));
|
||||
coordinator.RegisterElement(new SpatialElement("elem-active", new LayoutRect(120, 320, 100, 50), IsFocusable: true));
|
||||
var next7 = coordinator.MoveFocus(SpatialDirection.Down);
|
||||
Assert.Equal("elem-active", next7); // Skips disabled element
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate152_TabTilingCoordinator_ComputesViewportLayoutsFor2To4Tabs()
|
||||
{
|
||||
// Arrange
|
||||
var coordinator = new TabTilingCoordinator();
|
||||
var viewport = new LayoutRect(0, 0, 1920, 1080);
|
||||
|
||||
// Act 1: Vertical split for 2 tabs (50/50 split)
|
||||
var tabs2 = new[] { "tab-1", "tab-2" };
|
||||
var vSplit = coordinator.CalculateTileLayout(TabTilingMode.VerticalSplit, tabs2, viewport);
|
||||
Assert.Equal(2, vSplit.Count);
|
||||
Assert.Equal(new LayoutRect(0, 0, 960, 1080), vSplit["tab-1"]);
|
||||
Assert.Equal(new LayoutRect(960, 0, 960, 1080), vSplit["tab-2"]);
|
||||
|
||||
// Act 2: Custom split ratio (e.g. 0.3 / 0.7)
|
||||
coordinator.SplitRatio = 0.3;
|
||||
var customSplit = coordinator.CalculateTileLayout(TabTilingMode.VerticalSplit, tabs2, viewport);
|
||||
Assert.Equal(576, customSplit["tab-1"].Width, 0.01);
|
||||
Assert.Equal(1344, customSplit["tab-2"].Width, 0.01);
|
||||
|
||||
// Act 3: Horizontal split for 2 tabs
|
||||
coordinator.SplitRatio = 0.5;
|
||||
var hSplit = coordinator.CalculateTileLayout(TabTilingMode.HorizontalSplit, tabs2, viewport);
|
||||
Assert.Equal(new LayoutRect(0, 0, 1920, 540), hSplit["tab-1"]);
|
||||
Assert.Equal(new LayoutRect(0, 540, 1920, 540), hSplit["tab-2"]);
|
||||
|
||||
// Act 4: Quad grid layout for 4 tabs
|
||||
var tabs4 = new[] { "tab-1", "tab-2", "tab-3", "tab-4" };
|
||||
var quad = coordinator.CalculateTileLayout(TabTilingMode.QuadGrid, tabs4, viewport);
|
||||
Assert.Equal(4, quad.Count);
|
||||
Assert.Equal(new LayoutRect(0, 0, 960, 540), quad["tab-1"]); // Top-Left
|
||||
Assert.Equal(new LayoutRect(960, 0, 960, 540), quad["tab-2"]); // Top-Right
|
||||
Assert.Equal(new LayoutRect(0, 540, 960, 540), quad["tab-3"]); // Bottom-Left
|
||||
Assert.Equal(new LayoutRect(960, 540, 960, 540), quad["tab-4"]); // Bottom-Right
|
||||
|
||||
// Act 5: Vertical split with 3 tabs
|
||||
var tabs3 = new[] { "t1", "t2", "t3" };
|
||||
var vSplit3 = coordinator.CalculateTileLayout(TabTilingMode.VerticalSplit, tabs3, viewport);
|
||||
Assert.Equal(3, vSplit3.Count);
|
||||
Assert.Equal(640, vSplit3["t1"].Width, 0.01);
|
||||
Assert.Equal(640, vSplit3["t2"].Width, 0.01);
|
||||
Assert.Equal(640, vSplit3["t3"].Width, 0.01);
|
||||
|
||||
// Act 6: Tile gap support
|
||||
var vSplitWithGap = coordinator.CalculateTileLayout(TabTilingMode.VerticalSplit, tabs2, viewport, gap: 10);
|
||||
Assert.Equal(955, vSplitWithGap["tab-1"].Width, 0.01);
|
||||
Assert.Equal(965, vSplitWithGap["tab-2"].X, 0.01);
|
||||
Assert.Equal(955, vSplitWithGap["tab-2"].Width, 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate153_TwoLevelTabStackManager_ManagesHierarchyAndSubStripVisibility()
|
||||
{
|
||||
// Arrange
|
||||
var manager = new TwoLevelTabStackManager();
|
||||
|
||||
// Create a stack: parent stack with 3 children
|
||||
manager.CreateStack("stack-alpha", new[] { "tab-c1", "tab-c2", "tab-c3" }, title: "Research");
|
||||
// Register a standalone tab
|
||||
manager.RegisterSoloTab("tab-solo");
|
||||
|
||||
// Act 1: Select standalone tab
|
||||
manager.SelectTab("tab-solo");
|
||||
Assert.Equal("tab-solo", manager.ActiveTabId);
|
||||
Assert.False(manager.IsSubStripVisible);
|
||||
Assert.Null(manager.ActiveParentStackId);
|
||||
Assert.Empty(manager.SubStripTabIds);
|
||||
|
||||
// Act 2: Select stack parent
|
||||
manager.SelectTab("stack-alpha");
|
||||
Assert.True(manager.IsSubStripVisible);
|
||||
Assert.Equal("stack-alpha", manager.ActiveParentStackId);
|
||||
Assert.Equal(3, manager.SubStripTabIds.Count);
|
||||
Assert.Contains("tab-c1", manager.SubStripTabIds);
|
||||
|
||||
// Act 3: Select child tab directly
|
||||
manager.SelectTab("tab-c2");
|
||||
Assert.Equal("tab-c2", manager.ActiveTabId);
|
||||
Assert.True(manager.IsSubStripVisible);
|
||||
Assert.Equal("stack-alpha", manager.ActiveParentStackId);
|
||||
|
||||
// Act 4: Add and remove child tabs
|
||||
manager.AddChildToStack("stack-alpha", "tab-c4");
|
||||
Assert.Equal(4, manager.GetStackChildren("stack-alpha").Count);
|
||||
|
||||
manager.RemoveChildFromStack("stack-alpha", "tab-c1");
|
||||
Assert.Equal(3, manager.GetStackChildren("stack-alpha").Count);
|
||||
Assert.DoesNotContain("tab-c1", manager.GetStackChildren("stack-alpha"));
|
||||
|
||||
// Act 5: Ungroup stack
|
||||
manager.UngroupStack("stack-alpha");
|
||||
Assert.Null(manager.GetStack("stack-alpha"));
|
||||
Assert.False(manager.IsSubStripVisible);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate154_TabLayoutSwitcher_TogglesHorizontalAndVerticalSidebarModes()
|
||||
{
|
||||
// Arrange
|
||||
var configPath = Path.Combine(_tempDir, "tab_layout_config.json");
|
||||
var switcher = new TabLayoutSwitcher(configPath);
|
||||
|
||||
// Act 1: Default configuration is HorizontalTop
|
||||
Assert.Equal(TabLayoutMode.HorizontalTop, switcher.CurrentMode);
|
||||
|
||||
// Act 2: Switch to VerticalSidebar
|
||||
var modeChangedFired = false;
|
||||
switcher.ModeChanged += (s, mode) => modeChangedFired = true;
|
||||
|
||||
switcher.SetMode(TabLayoutMode.VerticalSidebar);
|
||||
Assert.Equal(TabLayoutMode.VerticalSidebar, switcher.CurrentMode);
|
||||
Assert.True(modeChangedFired);
|
||||
|
||||
// Act 3: Adjust sidebar options
|
||||
switcher.SidebarPosition = SidebarPosition.Right;
|
||||
switcher.SidebarWidth = 260.0;
|
||||
switcher.IsCollapsed = true;
|
||||
switcher.TreeIndentationPx = 18.0;
|
||||
|
||||
// Act 4: Save & Reload
|
||||
switcher.Save();
|
||||
var reloaded = new TabLayoutSwitcher(configPath);
|
||||
|
||||
Assert.Equal(TabLayoutMode.VerticalSidebar, reloaded.CurrentMode);
|
||||
Assert.Equal(SidebarPosition.Right, reloaded.SidebarPosition);
|
||||
Assert.Equal(260.0, reloaded.SidebarWidth);
|
||||
Assert.True(reloaded.IsCollapsed);
|
||||
Assert.Equal(18.0, reloaded.TreeIndentationPx);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate155_TabStripScrollSwitcher_CyclesTabsWithWrappingAndBoundaryClamping()
|
||||
{
|
||||
// Arrange
|
||||
var switcher = new TabStripScrollSwitcher();
|
||||
var tabs = new[] { "tab-1", "tab-2", "tab-3", "tab-4" };
|
||||
|
||||
switcher.SetTabs(tabs, activeTabId: "tab-1");
|
||||
switcher.CycleMode = ScrollCycleMode.ClampAtBoundary;
|
||||
|
||||
// Act 1: Scroll down / right (+120 delta) -> moves to tab-2
|
||||
var next1 = switcher.OnScrollWheel(120);
|
||||
Assert.Equal("tab-2", next1);
|
||||
Assert.Equal("tab-2", switcher.ActiveTabId);
|
||||
|
||||
// Act 2: Scroll up / left (-120 delta) -> moves to tab-1
|
||||
var prev1 = switcher.OnScrollWheel(-120);
|
||||
Assert.Equal("tab-1", prev1);
|
||||
|
||||
// Act 3: Clamping boundary (scrolling left past index 0)
|
||||
var clamped1 = switcher.OnScrollWheel(-120);
|
||||
Assert.Equal("tab-1", clamped1);
|
||||
|
||||
// Scroll to the end
|
||||
switcher.OnScrollWheel(120); // tab-2
|
||||
switcher.OnScrollWheel(120); // tab-3
|
||||
switcher.OnScrollWheel(120); // tab-4
|
||||
Assert.Equal("tab-4", switcher.ActiveTabId);
|
||||
|
||||
var clampedEnd = switcher.OnScrollWheel(120);
|
||||
Assert.Equal("tab-4", clampedEnd); // Stops at boundary
|
||||
|
||||
// Act 4: Enable WrapAround mode
|
||||
switcher.CycleMode = ScrollCycleMode.WrapAround;
|
||||
var wrappedNext = switcher.OnScrollWheel(120);
|
||||
Assert.Equal("tab-1", wrappedNext); // Wraps tab-4 -> tab-1
|
||||
|
||||
var wrappedPrev = switcher.OnScrollWheel(-120);
|
||||
Assert.Equal("tab-4", wrappedPrev); // Wraps tab-1 -> tab-4
|
||||
|
||||
// Act 5: Invert scroll direction
|
||||
switcher.InvertScrollDirection = true;
|
||||
var inverted = switcher.OnScrollWheel(120); // normally next, now previous -> tab-3
|
||||
Assert.Equal("tab-3", inverted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate156_PinnedTabRailPolicy_EnforcesCloseSuppressionAndFaviconMode()
|
||||
{
|
||||
// Arrange
|
||||
var policy = new PinnedTabRailPolicy();
|
||||
policy.PinTab("pinned-1");
|
||||
policy.PinTab("pinned-2");
|
||||
|
||||
// Act 1: Pin status
|
||||
Assert.True(policy.IsPinned("pinned-1"));
|
||||
Assert.False(policy.IsPinned("normal-1"));
|
||||
|
||||
// Act 2: Close button suppression
|
||||
Assert.False(policy.ShouldShowCloseButton("pinned-1"));
|
||||
Assert.True(policy.ShouldShowCloseButton("normal-1"));
|
||||
|
||||
// Act 3: Favicon only mode
|
||||
Assert.True(policy.IsFaviconOnlyMode("pinned-1"));
|
||||
Assert.False(policy.IsFaviconOnlyMode("normal-1"));
|
||||
|
||||
// Act 4: Tab width calculations
|
||||
Assert.Equal(36.0, policy.GetTabDisplayWidth("pinned-1", normalWidth: 200.0));
|
||||
Assert.Equal(200.0, policy.GetTabDisplayWidth("normal-1", normalWidth: 200.0));
|
||||
|
||||
// Act 5: Unclosable guard
|
||||
Assert.False(policy.CanCloseTab("pinned-1"));
|
||||
Assert.True(policy.CanCloseTab("normal-1"));
|
||||
|
||||
Assert.False(policy.TryCloseTab("pinned-1")); // Normal close blocked
|
||||
Assert.True(policy.TryCloseTab("pinned-1", force: true)); // Force close allowed
|
||||
|
||||
// Act 6: Rail order sorting (pinned tabs must precede normal tabs)
|
||||
var unordered = new[] { "normal-1", "pinned-2", "normal-2", "pinned-1" };
|
||||
var sorted = policy.SortTabsForRail(unordered);
|
||||
Assert.Equal(new[] { "pinned-2", "pinned-1", "normal-1", "normal-2" }, sorted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate157_WindowTabMergeCoordinator_DetachesAndMergesWindows()
|
||||
{
|
||||
// Arrange
|
||||
var coordinator = new WindowTabMergeCoordinator();
|
||||
coordinator.CreateWindow("win-1", new[] { "tab-1", "tab-2", "tab-3", "tab-4" }, activeTabId: "tab-1");
|
||||
|
||||
// Act 1: Detach tab-3 into a new standalone window
|
||||
var win2 = coordinator.DetachTab("win-1", "tab-3", newWindowId: "win-2");
|
||||
Assert.NotNull(win2);
|
||||
Assert.Equal("win-2", win2.WindowId);
|
||||
Assert.Single(win2.TabIds);
|
||||
Assert.Equal("tab-3", win2.ActiveTabId);
|
||||
|
||||
var win1 = coordinator.GetWindow("win-1");
|
||||
Assert.NotNull(win1);
|
||||
Assert.Equal(new[] { "tab-1", "tab-2", "tab-4" }, win1.TabIds);
|
||||
|
||||
// Act 2: Move tab between windows
|
||||
coordinator.MoveTab(sourceWindowId: "win-1", targetWindowId: "win-2", tabId: "tab-2");
|
||||
win1 = coordinator.GetWindow("win-1");
|
||||
win2 = coordinator.GetWindow("win-2");
|
||||
Assert.Equal(new[] { "tab-1", "tab-4" }, win1!.TabIds);
|
||||
Assert.Equal(new[] { "tab-3", "tab-2" }, win2!.TabIds);
|
||||
|
||||
// Act 3: Merge win-2 into win-1
|
||||
var merged = coordinator.MergeWindows(targetWindowId: "win-1", sourceWindowId: "win-2");
|
||||
Assert.Equal(new[] { "tab-1", "tab-4", "tab-3", "tab-2" }, merged.TabIds);
|
||||
Assert.Null(coordinator.GetWindow("win-2"));
|
||||
|
||||
// Act 4: MergeAllIntoWindow
|
||||
coordinator.CreateWindow("win-3", new[] { "tab-5" });
|
||||
coordinator.CreateWindow("win-4", new[] { "tab-6" });
|
||||
var finalWin = coordinator.MergeAllIntoWindow("win-1");
|
||||
|
||||
Assert.Equal(6, finalWin.TabIds.Count);
|
||||
Assert.Single(coordinator.GetAllWindows());
|
||||
Assert.Equal("win-1", coordinator.GetAllWindows()[0].WindowId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate158_StartupRestorePolicy_ResolvesStartupUrlsAndPersistsOptions()
|
||||
{
|
||||
// Arrange
|
||||
var configPath = Path.Combine(_tempDir, "startup_policy.json");
|
||||
var policy = new StartupRestorePolicy(configPath);
|
||||
|
||||
// Act 1: Default mode is OpenNewTabPage
|
||||
Assert.Equal(StartupRestoreMode.OpenNewTabPage, policy.Mode);
|
||||
var urls1 = policy.ResolveStartupUrls(previousSessionUrls: new[] { "https://siteA.com", "https://siteB.com" });
|
||||
Assert.Equal(new[] { "paca://newtab" }, urls1);
|
||||
|
||||
// Act 2: Switch to RestorePreviousSession
|
||||
policy.SetMode(StartupRestoreMode.RestorePreviousSession);
|
||||
var urls2 = policy.ResolveStartupUrls(previousSessionUrls: new[] { "https://siteA.com", "https://siteB.com" });
|
||||
Assert.Equal(new[] { "https://siteA.com", "https://siteB.com" }, urls2);
|
||||
|
||||
// When previous session is empty, falls back to New Tab page
|
||||
var emptyFallback = policy.ResolveStartupUrls(previousSessionUrls: Array.Empty<string>());
|
||||
Assert.Equal(new[] { "paca://newtab" }, emptyFallback);
|
||||
|
||||
// Act 3: Switch to OpenSpecificUrlSet
|
||||
policy.SetMode(StartupRestoreMode.OpenSpecificUrlSet);
|
||||
policy.SetCustomUrls(new[] { "https://github.com", "https://news.ycombinator.com" });
|
||||
var urls3 = policy.ResolveStartupUrls(previousSessionUrls: new[] { "https://siteA.com" });
|
||||
Assert.Equal(new[] { "https://github.com", "https://news.ycombinator.com" }, urls3);
|
||||
|
||||
// Act 4: Save & Reload
|
||||
policy.Save();
|
||||
var reloaded = new StartupRestorePolicy(configPath);
|
||||
Assert.Equal(StartupRestoreMode.OpenSpecificUrlSet, reloaded.Mode);
|
||||
Assert.Equal(2, reloaded.CustomUrls.Count);
|
||||
Assert.Contains("https://github.com", reloaded.CustomUrls);
|
||||
Assert.Contains("https://news.ycombinator.com", reloaded.CustomUrls);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue