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
271
tests/Paca.Tests/ModernBrowsingMilestone1Phase101Tests.cs
Normal file
271
tests/Paca.Tests/ModernBrowsingMilestone1Phase101Tests.cs
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
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");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue