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