using System; using System.Collections.Generic; using System.IO; using System.Linq; using Paca.Browser.Profiles; using Paca.Browser.Tabs; using Xunit; namespace Paca.Tests; public sealed class ModernBrowsingMilestone1Phase101Tests : IDisposable { private readonly string _tempDir; public ModernBrowsingMilestone1Phase101Tests() { _tempDir = Path.Combine(Path.GetTempPath(), "paca_m1_test_" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_tempDir); } public void Dispose() { try { if (Directory.Exists(_tempDir)) Directory.Delete(_tempDir, true); } catch { } } [Fact] public void Gate101_DuplicateTabDetector_IdentifiesAndCleansDuplicatesCorrectly() { // Arrange var tabs = new List { new("tab-1", "https://example.com/page?utm_source=feed", "Example Page", IsActive: false, Index: 0), new("tab-2", "https://github.com/trending", "GitHub Trending", IsActive: false, Index: 1), new("tab-3", "https://example.com/page#section", "Example Page - Diff Hash", IsActive: true, Index: 2), // duplicate of tab-1 after normalisation new("tab-4", "https://example.com/other", "Other Page", IsActive: false, Index: 3), new("tab-5", "https://github.com/trending?utm_medium=email", "GitHub Trending Copy", IsActive: false, Index: 4) // duplicate of tab-2 }; // Act var detector = new DuplicateTabDetector(); var duplicates = detector.FindDuplicates(tabs); // Assert: 2 groups of duplicates Assert.Equal(2, duplicates.Count); // Policy: Keep active or first var toCloseKeepActive = detector.GetTabsToClose(tabs, DuplicateKeepPolicy.KeepActiveOrFirst).ToList(); Assert.Contains(toCloseKeepActive, t => t.Id == "tab-1"); // tab-3 is active, so tab-1 should close Assert.Contains(toCloseKeepActive, t => t.Id == "tab-5"); // tab-2 is first, so tab-5 should close Assert.DoesNotContain(toCloseKeepActive, t => t.Id == "tab-3"); Assert.DoesNotContain(toCloseKeepActive, t => t.Id == "tab-2"); Assert.DoesNotContain(toCloseKeepActive, t => t.Id == "tab-4"); } [Fact] public void Gate102_TabPreviewThumbnailService_CapturesAndMaintainsThumbnailCache() { // Arrange var service = new TabPreviewThumbnailService(); var sampleBytes = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; // PNG header // Act service.SetThumbnail("tab-1", sampleBytes, "Sample Title", "https://example.com"); var preview = service.GetThumbnail("tab-1"); // Assert Assert.NotNull(preview); Assert.Equal("Sample Title", preview.Title); Assert.Equal("https://example.com", preview.Url); Assert.Equal(sampleBytes, preview.PngBytes); // Remove service.RemoveThumbnail("tab-1"); Assert.Null(service.GetThumbnail("tab-1")); } [Fact] public void Gate103_SleepingTabsPolicy_RespectsWhitelistAndNeverSleepDomains() { // Arrange var policyFile = Path.Combine(_tempDir, "never_sleep.json"); var policy = new SleepingTabsPolicy(policyFile); // Act policy.AddNeverSleepPattern("*.tradingview.com"); policy.AddNeverSleepPattern("spotify.com"); policy.AddNeverSleepPattern("https://music.youtube.com/*"); // Assert Assert.True(policy.IsNeverSleep("https://kr.tradingview.com/chart/abc")); Assert.True(policy.IsNeverSleep("https://spotify.com/playlist")); Assert.True(policy.IsNeverSleep("https://music.youtube.com/watch?v=123")); Assert.False(policy.IsNeverSleep("https://news.ycombinator.com")); // Persistence check var reloaded = new SleepingTabsPolicy(policyFile); Assert.True(reloaded.IsNeverSleep("https://spotify.com")); } [Fact] public void Gate104_AutoTabArchiver_StashesExpiredUnpinnedTabs() { // Arrange var archiveFile = Path.Combine(_tempDir, "archived_tabs.json"); var archiver = new AutoTabArchiver(archiveFile); var now = DateTime.UtcNow; var tabs = new List { new("tab-1", "https://doc.com", "Active Doc", IsActive: true, Index: 0, IsPinned: false, LastActiveUtc: now.AddHours(-1)), new("tab-2", "https://pinned.com", "Pinned Dashboard", IsActive: false, Index: 1, IsPinned: true, LastActiveUtc: now.AddHours(-30)), // Pinned -> Never archive new("tab-3", "https://old.com/post", "Old Article", IsActive: false, Index: 2, IsPinned: false, LastActiveUtc: now.AddHours(-25)) // Unpinned & >24h -> Should archive }; // Act var stashed = archiver.CheckAndArchive(tabs, TimeSpan.FromHours(24), now); // Assert Assert.Single(stashed); Assert.Equal("tab-3", stashed[0].TabId); Assert.Equal("https://old.com/post", stashed[0].Url); // Verify query & restore var allStashed = archiver.GetArchivedTabs(); Assert.Single(allStashed); var restored = archiver.Restore(stashed[0].ArchiveId); Assert.NotNull(restored); Assert.Empty(archiver.GetArchivedTabs()); } [Fact] public void Gate105_SessionCrashRecoveryService_DetectsDirtyShutdownAndRecoversTabs() { // Arrange var sessionDir = Path.Combine(_tempDir, "session_state"); var recovery = new SessionCrashRecoveryService(sessionDir); var tabs = new List { new("t1", "https://github.com", "GitHub", false, 0), new("t2", "https://stackoverflow.com", "StackOverflow", true, 1) }; // Act 1: Simulate active session with snapshot recovery.MarkSessionStart(); recovery.SaveSessionSnapshot(tabs); // Simulate crash (no MarkCleanExit called) var recoveryAfterCrash = new SessionCrashRecoveryService(sessionDir); // Assert 1: Crash detected Assert.True(recoveryAfterCrash.HasCrashSession()); var recoveredTabs = recoveryAfterCrash.GetCrashRecoveryTabs(); Assert.Equal(2, recoveredTabs.Count); Assert.Equal("https://github.com", recoveredTabs[0].Url); // Act 2: Clean exit simulation recoveryAfterCrash.MarkCleanExit(); var normalService = new SessionCrashRecoveryService(sessionDir); Assert.False(normalService.HasCrashSession()); } [Fact] public void Gate106_ClosedTabTimelineManager_MaintainsTimelineAndRestoresItems() { // Arrange var timelineFile = Path.Combine(_tempDir, "timeline.json"); var manager = new ClosedTabTimelineManager(timelineFile, maxEntries: 3); // Act manager.RecordClosedTab("Tab 1", "https://1.com", index: 0); manager.RecordClosedTab("Tab 2", "https://2.com", index: 1); manager.RecordClosedTab("Tab 3", "https://3.com", index: 2); manager.RecordClosedTab("Tab 4", "https://4.com", index: 3); // should drop Tab 1 (FIFO for capacity) // Assert: Cap at 3 Assert.Equal(3, manager.Timeline.Count); Assert.Equal("Tab 4", manager.Timeline[0].Title); // LIFO order Assert.Equal("Tab 3", manager.Timeline[1].Title); Assert.Equal("Tab 2", manager.Timeline[2].Title); // Restore single entry var entryToRestore = manager.Timeline[1]; // Tab 3 var restored = manager.RestoreTab(entryToRestore.Id); Assert.NotNull(restored); Assert.Equal("Tab 3", restored.Title); Assert.Equal(2, manager.Timeline.Count); } [Fact] public void Gate107_NavigationHistoryStack_ExtractsJumpListEntries() { // Arrange var stack = new NavigationHistoryStack(); // Act: Navigate A -> B -> C -> D stack.Push("https://a.com", "Site A"); stack.Push("https://b.com", "Site B"); stack.Push("https://c.com", "Site C"); stack.Push("https://d.com", "Site D"); // Assert Assert.Equal("https://d.com", stack.Current?.Url); Assert.True(stack.CanGoBack); Assert.False(stack.CanGoForward); var backList = stack.GetBackList(10); Assert.Equal(3, backList.Count); Assert.Equal("Site C", backList[0].Title); Assert.Equal("Site B", backList[1].Title); Assert.Equal("Site A", backList[2].Title); // Jump to Site B (skip Site C) var jumped = stack.JumpTo(backList[1].Id); Assert.NotNull(jumped); Assert.Equal("https://b.com", stack.Current?.Url); Assert.True(stack.CanGoBack); Assert.True(stack.CanGoForward); var forwardList = stack.GetForwardList(10); Assert.Equal(2, forwardList.Count); Assert.Equal("Site C", forwardList[0].Title); Assert.Equal("Site D", forwardList[1].Title); } [Fact] public void Gate108_ExternalBrowserDataImporter_DiscoversAndParsesChromeEdgeBookmarks() { // Arrange var chromeJson = """ { "roots": { "bookmark_bar": { "children": [ { "date_added": "13254123456789", "name": "Google", "type": "url", "url": "https://www.google.com" }, { "children": [ { "name": "GitHub", "type": "url", "url": "https://github.com" } ], "name": "Dev Folder", "type": "folder" } ] } } } """; // Act var importer = new ExternalBrowserDataImporter(); var bookmarks = importer.ParseChromiumBookmarksJson(chromeJson, "Chrome"); // Assert Assert.Equal(2, bookmarks.Count); Assert.Contains(bookmarks, b => b.Title == "Google" && b.Url == "https://www.google.com" && b.Folder == "bookmark_bar"); Assert.Contains(bookmarks, b => b.Title == "GitHub" && b.Url == "https://github.com" && b.Folder == "bookmark_bar/Dev Folder"); } }