releases/tests/Paca.Tests/ModernBrowsingMilestone13Phase113Tests.cs

416 lines
18 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Paca.Browser.Theming;
using Xunit;
namespace Paca.Tests;
public sealed class ModernBrowsingMilestone13Phase113Tests : IDisposable
{
private readonly string _tempDir;
public ModernBrowsingMilestone13Phase113Tests()
{
_tempDir = Path.Combine(Path.GetTempPath(), "paca_m13_test_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(_tempDir);
}
public void Dispose()
{
try
{
if (Directory.Exists(_tempDir)) Directory.Delete(_tempDir, true);
}
catch { }
}
[Fact]
public void Gate189_ToolbarPaletteManager_ManagesOrderVisibilityAndPersistence()
{
// Arrange
var configPath = Path.Combine(_tempDir, "toolbar_palette.json");
var manager = new ToolbarPaletteManager(configPath);
// Act 1: Verify default items
var items = manager.GetAllItems();
Assert.NotEmpty(items);
Assert.Contains(items, x => x.Id == "back");
Assert.Contains(items, x => x.Id == "forward");
Assert.Contains(items, x => x.Id == "omnibox");
Assert.Contains(items, x => x.Id == "settings");
Assert.Contains(items, x => x.Id == "bookmarks");
// Act 2: Visibility toggle
var layoutChangedFired = false;
manager.LayoutChanged += (_, _) => layoutChangedFired = true;
var bookmarksItem = manager.GetItem("bookmarks");
Assert.NotNull(bookmarksItem);
Assert.True(bookmarksItem.IsVisible);
var toggled = manager.SetVisibility("bookmarks", false);
Assert.True(toggled);
Assert.True(layoutChangedFired);
var visibleItems = manager.GetVisibleItems();
Assert.DoesNotContain(visibleItems, x => x.Id == "bookmarks");
Assert.Contains(manager.GetAllItems(), x => x.Id == "bookmarks" && !x.IsVisible);
// Omnibox is essential, cannot be hidden if CanHide is false
var omniboxItem = manager.GetItem("omnibox");
Assert.NotNull(omniboxItem);
Assert.False(omniboxItem.CanHide);
var hideOmniboxResult = manager.SetVisibility("omnibox", false);
Assert.False(hideOmniboxResult);
Assert.True(manager.GetItem("omnibox")!.IsVisible);
// Act 3: Move item (drag and drop)
// Move "settings" to index 0
var moved = manager.MoveItem("settings", 0);
Assert.True(moved);
Assert.Equal("settings", manager.GetAllItems()[0].Id);
// Act 4: Bulk reorder
var currentIds = manager.GetAllItems().Select(i => i.Id).Reverse().ToList();
var reordered = manager.Reorder(currentIds);
Assert.True(reordered);
Assert.Equal(currentIds, manager.GetAllItems().Select(i => i.Id).ToList());
// Act 5: Save & reload persistence
manager.Save();
Assert.True(File.Exists(configPath));
var reloaded = new ToolbarPaletteManager(configPath);
Assert.Equal(currentIds, reloaded.GetAllItems().Select(i => i.Id).ToList());
Assert.False(reloaded.GetItem("bookmarks")!.IsVisible);
// Act 6: Reset to default
reloaded.ResetToDefault();
Assert.True(reloaded.GetItem("bookmarks")!.IsVisible);
Assert.Equal("back", reloaded.GetAllItems()[0].Id);
}
[Fact]
public void Gate190_StatusBarModuleCoordinator_CoordinatesZoomHostAndResourceStats()
{
// Arrange
var coordinator = new StatusBarModuleCoordinator();
var stateChangedCount = 0;
coordinator.StateChanged += (_, _) => stateChangedCount++;
// Act 1: Zoom slider defaults and controls
Assert.Equal(1.0, coordinator.Zoom.ZoomFactor);
Assert.Equal(100, coordinator.Zoom.ZoomPercentage);
Assert.True(coordinator.IsZoomModuleVisible);
coordinator.ZoomIn();
Assert.True(coordinator.Zoom.ZoomFactor > 1.0);
Assert.True(stateChangedCount > 0);
coordinator.ZoomOut();
coordinator.ZoomOut();
Assert.True(coordinator.Zoom.ZoomFactor < 1.0);
coordinator.SetZoom(2.5);
Assert.Equal(2.5, coordinator.Zoom.ZoomFactor);
Assert.Equal(250, coordinator.Zoom.ZoomPercentage);
// Zoom clamping
coordinator.SetZoom(10.0);
Assert.Equal(coordinator.Zoom.MaxZoom, coordinator.Zoom.ZoomFactor);
coordinator.SetZoom(0.01);
Assert.Equal(coordinator.Zoom.MinZoom, coordinator.Zoom.ZoomFactor);
coordinator.ResetZoom();
Assert.Equal(1.0, coordinator.Zoom.ZoomFactor);
Assert.Equal(100, coordinator.Zoom.ZoomPercentage);
// Act 2: Host / IP Indicator
coordinator.UpdateHost("github.com", "140.82.121.4", isSecure: true, port: 443);
Assert.Equal("github.com", coordinator.HostIndicator.Host);
Assert.Equal("140.82.121.4", coordinator.HostIndicator.IpAddress);
Assert.True(coordinator.HostIndicator.IsSecure);
Assert.Equal(443, coordinator.HostIndicator.Port);
Assert.Contains("github.com", coordinator.HostIndicator.DisplayText);
Assert.Contains("140.82.121.4", coordinator.HostIndicator.DisplayText);
coordinator.ClearHost();
Assert.Equal(string.Empty, coordinator.HostIndicator.Host);
Assert.Null(coordinator.HostIndicator.IpAddress);
// Act 3: Resource Size & Request Counter
coordinator.AddResource(1024 * 512); // 512 KB
Assert.Equal(512 * 1024, coordinator.ResourceCounter.TransferredBytes);
Assert.Equal(1, coordinator.ResourceCounter.RequestCount);
Assert.Contains("512 KB", coordinator.ResourceCounter.FormattedSize);
coordinator.AddResource(1024 * 1024 * 2); // +2 MB -> ~2.5 MB
Assert.Equal(2, coordinator.ResourceCounter.RequestCount);
Assert.Contains("2.5 MB", coordinator.ResourceCounter.FormattedSize);
Assert.Contains("2 requests", coordinator.ResourceCounter.FormattedSummary);
coordinator.ResetResourceStats();
Assert.Equal(0, coordinator.ResourceCounter.TransferredBytes);
Assert.Equal(0, coordinator.ResourceCounter.RequestCount);
Assert.Equal("0 B", coordinator.ResourceCounter.FormattedSize);
// Act 4: Module Visibility Toggles
coordinator.IsResourceCounterVisible = false;
Assert.False(coordinator.IsResourceCounterVisible);
}
[Fact]
public void Gate191_ThemeAutoScheduler_CalculatesScheduledAndSolarThemes()
{
// Arrange
var scheduler = new ThemeAutoScheduler();
// Act 1: Explicit theme modes
scheduler.Mode = ThemeMode.Light;
Assert.Equal(AppTheme.Light, scheduler.GetEffectiveTheme(DateTime.Parse("2026-09-13 23:00:00")));
scheduler.Mode = ThemeMode.Dark;
Assert.Equal(AppTheme.Dark, scheduler.GetEffectiveTheme(DateTime.Parse("2026-09-13 12:00:00")));
scheduler.Mode = ThemeMode.System;
Assert.Equal(AppTheme.Light, scheduler.GetEffectiveTheme(DateTime.Now, systemTheme: AppTheme.Light));
Assert.Equal(AppTheme.Dark, scheduler.GetEffectiveTheme(DateTime.Now, systemTheme: AppTheme.Dark));
// Act 2: Scheduled Window spanning midnight (20:00 to 07:00 is Dark)
scheduler.Mode = ThemeMode.Scheduled;
scheduler.ScheduledWindow = new TimeWindow(
DarkStartTime: new TimeOnly(20, 0),
DarkEndTime: new TimeOnly(7, 0));
// 20:00 -> Dark
Assert.Equal(AppTheme.Dark, scheduler.GetEffectiveTheme(DateTime.Parse("2026-09-13 20:00:00")));
// 23:30 -> Dark
Assert.Equal(AppTheme.Dark, scheduler.GetEffectiveTheme(DateTime.Parse("2026-09-13 23:30:00")));
// 03:15 -> Dark
Assert.Equal(AppTheme.Dark, scheduler.GetEffectiveTheme(DateTime.Parse("2026-09-13 03:15:00")));
// 06:59 -> Dark
Assert.Equal(AppTheme.Dark, scheduler.GetEffectiveTheme(DateTime.Parse("2026-09-13 06:59:59")));
// 07:00 -> Light
Assert.Equal(AppTheme.Light, scheduler.GetEffectiveTheme(DateTime.Parse("2026-09-13 07:00:00")));
// 12:00 -> Light
Assert.Equal(AppTheme.Light, scheduler.GetEffectiveTheme(DateTime.Parse("2026-09-13 12:00:00")));
// 19:59 -> Light
Assert.Equal(AppTheme.Light, scheduler.GetEffectiveTheme(DateTime.Parse("2026-09-13 19:59:59")));
// Act 3: Solar Sunrise/Sunset calculation (Seoul, Lat 37.5665, Lon 126.9780)
scheduler.Mode = ThemeMode.Solar;
scheduler.Coordinates = new SolarCoordinates(37.5665, 126.9780);
var date = new DateTime(2026, 9, 13);
var solarTimes = scheduler.CalculateSolarTimes(date, 37.5665, 126.9780);
// Sunrise should be morning (~6 AM), Sunset evening (~18:30 PM)
Assert.True(solarTimes.Sunrise.Hour >= 5 && solarTimes.Sunrise.Hour <= 7, $"Sunrise was {solarTimes.Sunrise}");
Assert.True(solarTimes.Sunset.Hour >= 18 && solarTimes.Sunset.Hour <= 20, $"Sunset was {solarTimes.Sunset}");
// Midday 12:00 -> Light
var noon = new DateTime(2026, 9, 13, 12, 0, 0);
Assert.Equal(AppTheme.Light, scheduler.GetEffectiveTheme(noon));
// Midnight 00:00 -> Dark
var midnight = new DateTime(2026, 9, 13, 0, 0, 0);
Assert.Equal(AppTheme.Dark, scheduler.GetEffectiveTheme(midnight));
// 23:00 -> Dark
var night = new DateTime(2026, 9, 13, 23, 0, 0);
Assert.Equal(AppTheme.Dark, scheduler.GetEffectiveTheme(night));
}
[Fact]
public void Gate192_SpeedDialFolderManager_ManagesThumbnailsAndHierarchies()
{
// Arrange
var configPath = Path.Combine(_tempDir, "speed_dial.json");
var manager = new SpeedDialFolderManager(configPath);
// Act 1: Add root tiles
var githubTile = manager.AddTile("GitHub", "https://github.com");
var hnTile = manager.AddTile("Hacker News", "https://news.ycombinator.com");
Assert.NotNull(githubTile);
Assert.NotNull(hnTile);
Assert.Equal(2, manager.GetTilesInFolder(null).Count);
// Act 2: Custom thumbnail override
var customThumb = "https://assets.example.com/thumbnails/gh-custom.png";
var thumbUpdated = manager.SetCustomThumbnail(githubTile.Id, customThumb);
Assert.True(thumbUpdated);
Assert.Equal(customThumb, manager.GetTile(githubTile.Id)!.CustomThumbnailUrl);
// Act 3: Create folder grouping hierarchies
var devFolder = manager.CreateFolder("Development");
var newsFolder = manager.CreateFolder("News");
var aiFolder = manager.CreateFolder("AI Tools", parentFolderId: devFolder.Id);
Assert.NotNull(devFolder);
Assert.NotNull(newsFolder);
Assert.NotNull(aiFolder);
// Subfolder query
var devSubfolders = manager.GetSubFolders(devFolder.Id);
Assert.Single(devSubfolders);
Assert.Equal("AI Tools", devSubfolders[0].Name);
// Breadcrumbs for nested folder
var breadcrumbs = manager.GetBreadcrumbs(aiFolder.Id);
Assert.Equal(2, breadcrumbs.Count);
Assert.Equal("Development", breadcrumbs[0].Name);
Assert.Equal("AI Tools", breadcrumbs[1].Name);
// Act 4: Move tile into folders
manager.MoveTile(githubTile.Id, devFolder.Id);
var openaiTile = manager.AddTile("OpenAI", "https://openai.com", folderId: aiFolder.Id);
var devTiles = manager.GetTilesInFolder(devFolder.Id);
Assert.Single(devTiles);
Assert.Equal("GitHub", devTiles[0].Title);
var aiTiles = manager.GetTilesInFolder(aiFolder.Id);
Assert.Single(aiTiles);
Assert.Equal("OpenAI", aiTiles[0].Title);
// Root tiles now only contains Hacker News
var rootTiles = manager.GetTilesInFolder(null);
Assert.Single(rootTiles);
Assert.Equal("Hacker News", rootTiles[0].Title);
// Act 5: Persistence save & reload
manager.Save();
Assert.True(File.Exists(configPath));
var reloaded = new SpeedDialFolderManager(configPath);
Assert.Equal(customThumb, reloaded.GetTile(githubTile.Id)!.CustomThumbnailUrl);
Assert.Equal(devFolder.Id, reloaded.GetTile(githubTile.Id)!.FolderId);
Assert.Single(reloaded.GetTilesInFolder(aiFolder.Id));
// Act 6: Folder deletion handling (without deleting children: moves children to parent folder)
var removed = reloaded.RemoveFolder(aiFolder.Id, deleteChildren: false);
Assert.True(removed);
// OpenAI tile should now be moved up to Development folder
var devTilesAfterDelete = reloaded.GetTilesInFolder(devFolder.Id);
Assert.Contains(devTilesAfterDelete, t => t.Title == "OpenAI");
}
[Fact]
public void Gate193_UiLanguageSwitcher_SwitchesLanguagesAndLocalizesStrings()
{
// Arrange
var switcher = new UiLanguageSwitcher(defaultCulture: "en-US");
// Act 1: Default English strings
Assert.Equal("en-US", switcher.CurrentLanguage);
Assert.Contains("ko-KR", switcher.SupportedLanguages);
Assert.Contains("ja-JP", switcher.SupportedLanguages);
Assert.Equal("Search or enter address", switcher.GetString("Omnibox_Placeholder"));
Assert.Equal("Settings", switcher["Settings_Title"]);
// Act 2: Switch to Korean (ko-KR)
string? previousLang = null;
string? newLang = null;
switcher.LanguageChanged += (_, e) =>
{
previousLang = e.PreviousLanguage;
newLang = e.NewLanguage;
};
var switchResultKo = switcher.SetLanguage("ko-KR");
Assert.True(switchResultKo);
Assert.Equal("ko-KR", switcher.CurrentLanguage);
Assert.Equal("en-US", previousLang);
Assert.Equal("ko-KR", newLang);
Assert.Equal("검색어 또는 웹 주소 입력", switcher.GetString("Omnibox_Placeholder"));
Assert.Equal("설정", switcher["Settings_Title"]);
Assert.Equal("새 탭", switcher["NewTab_Title"]);
// Act 3: Switch to Japanese (ja-JP)
var switchResultJa = switcher.SetLanguage("ja-JP");
Assert.True(switchResultJa);
Assert.Equal("ja-JP", switcher.CurrentLanguage);
Assert.Equal("検索またはアドレスを入力", switcher.GetString("Omnibox_Placeholder"));
Assert.Equal("設定", switcher["Settings_Title"]);
// Act 4: Fallback for missing string
// If a key doesn't exist in ja-JP, it falls back to en-US, or returns key itself
Assert.Equal("NonExistent_Key_XYZ", switcher.GetString("NonExistent_Key_XYZ"));
// Act 5: Custom translation registration and string formatting
switcher.RegisterTranslations("fr-FR", new Dictionary<string, string>
{
["Omnibox_Placeholder"] = "Rechercher ou entrer une adresse",
["Tab_Count_Format"] = "{0} onglets ouverts"
});
switcher.SetLanguage("fr-FR");
Assert.Equal("Rechercher ou entrer une adresse", switcher.GetString("Omnibox_Placeholder"));
Assert.Equal("5 onglets ouverts", switcher.GetString("Tab_Count_Format", 5));
}
[Fact]
public void Gate194_ScrollAutoHideCoordinator_ThresholdsAndAnimatesHeader()
{
// Arrange
// Header height = 60, scroll threshold = 20
var coordinator = new ScrollAutoHideCoordinator(headerHeight: 60.0, threshold: 20.0);
var offsetEvents = new List<double>();
coordinator.OffsetChanged += (_, offset) => offsetEvents.Add(offset);
// Act 1: Initial state
Assert.Equal(0.0, coordinator.CurrentOffset);
Assert.Equal(1.0, coordinator.VisibilityFraction);
Assert.Equal(HeaderVisibilityState.FullyVisible, coordinator.State);
// Act 2: Downward scroll below threshold (10px < 20px threshold)
coordinator.ProcessScrollDelta(deltaY: 10.0, currentScrollY: 100.0);
Assert.Equal(0.0, coordinator.CurrentOffset);
Assert.Equal(1.0, coordinator.VisibilityFraction);
Assert.Equal(HeaderVisibilityState.FullyVisible, coordinator.State);
// Act 3: Downward scroll exceeding threshold (+25px total delta > 20px threshold -> 15px over)
// Accumulated 10 + 25 = 35 -> 15px over threshold -> offset = -15
coordinator.ProcessScrollDelta(deltaY: 25.0, currentScrollY: 125.0);
Assert.Equal(-15.0, coordinator.CurrentOffset);
Assert.Equal(45.0 / 60.0, coordinator.VisibilityFraction);
Assert.Equal(HeaderVisibilityState.PartiallyHidden, coordinator.State);
Assert.NotEmpty(offsetEvents);
// Act 4: Scroll down further until fully hidden
coordinator.ProcessScrollDelta(deltaY: 100.0, currentScrollY: 225.0);
Assert.Equal(-60.0, coordinator.CurrentOffset);
Assert.Equal(0.0, coordinator.VisibilityFraction);
Assert.Equal(HeaderVisibilityState.FullyHidden, coordinator.State);
// Act 5: Upward scroll below threshold (-10px upward < 20px threshold)
coordinator.ProcessScrollDelta(deltaY: -10.0, currentScrollY: 215.0);
Assert.Equal(-60.0, coordinator.CurrentOffset);
Assert.Equal(HeaderVisibilityState.FullyHidden, coordinator.State);
// Act 6: Upward scroll exceeding threshold (-25px upward -> accumulated -35 -> 15px over)
// Offset moves from -60 to -60 + 15 = -45
coordinator.ProcessScrollDelta(deltaY: -25.0, currentScrollY: 190.0);
Assert.Equal(-45.0, coordinator.CurrentOffset);
Assert.Equal(15.0 / 60.0, coordinator.VisibilityFraction);
Assert.Equal(HeaderVisibilityState.PartiallyHidden, coordinator.State);
// Act 7: Snap to fully visible when scrolled to top of page (currentScrollY <= 0)
coordinator.ProcessScrollDelta(deltaY: 5.0, currentScrollY: 0.0);
Assert.Equal(0.0, coordinator.CurrentOffset);
Assert.Equal(1.0, coordinator.VisibilityFraction);
Assert.Equal(HeaderVisibilityState.FullyVisible, coordinator.State);
// Act 8: Pinning disables auto-hide
coordinator.IsPinned = true;
coordinator.ProcessScrollDelta(deltaY: 200.0, currentScrollY: 500.0);
Assert.Equal(0.0, coordinator.CurrentOffset);
Assert.Equal(HeaderVisibilityState.FullyVisible, coordinator.State);
}
}