releases/tests/Paca.Tests/ModernBrowsingMilestone8Phase108Tests.cs

376 lines
16 KiB
C#

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