416 lines
18 KiB
C#
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);
|
|
}
|
|
}
|