releases/tests/Paca.Tests/ModernBrowsingMilestone1Phase101Tests.cs

271 lines
10 KiB
C#

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