using System.Collections.ObjectModel; using System.Collections.Specialized; using XFEToolBox.Client.Models; using XFEToolBox.Client.ViewModel.Pages; namespace XFEToolBox.Client.Wpf.Test; public static class HomeDashboardRefreshTests { [Test] public static void ReturningHomeReusesCardsAndIconsWithoutResettingTheVisualTree() => RunSta(() => { var cards = new ObservableCollection(); MainPageViewModel.UpdateLauncherItems(cards, Enumerable.Range(0, 8).Select(index => Item(index)).ToArray()); var originals = cards.ToArray(); var icons = cards.Select(card => card.IconSource).ToArray(); var iconTasks = cards.Select(card => card.IconLoadingTask).ToArray(); var collectionChanges = 0; cards.CollectionChanged += (_, _) => collectionChanges++; var executedRevision = 0; for (var revision = 1; revision <= 250; revision++) { var currentRevision = revision; MainPageViewModel.UpdateLauncherItems(cards, Enumerable.Range(0, 8).Select(index => Item(index, execute: () => { executedRevision = currentRevision; return Task.CompletedTask; })).ToArray()); } Ensure(collectionChanges == 0, $"重复打开主页触发了 {collectionChanges} 次未变化的卡片集合更新。"); for (var index = 0; index < cards.Count; index++) { Ensure(ReferenceEquals(cards[index], originals[index]), "未变化的卡片没有复用。"); Ensure(ReferenceEquals(cards[index].IconSource, icons[index]), "未变化的图标被重新创建。"); Ensure(ReferenceEquals(cards[index].IconLoadingTask, iconTasks[index]), "未变化的图标触发了重新加载。"); } cards[0].ExecuteAsync().GetAwaiter().GetResult(); Ensure(executedRevision == 250, "复用卡片后执行了旧快照的启动逻辑。"); Console.WriteLine("主页重复刷新 250 次:未变化卡片的集合变更 0 次,8 个图标均复用。"); }); [Test] public static void PinReorderingAndCatalogUpdatesStillRefreshTheAffectedCards() => RunSta(() => { var cards = new ObservableCollection(); MainPageViewModel.UpdateLauncherItems(cards, [Item(0), Item(1), Item(2)]); var originals = cards.ToArray(); var pinNotifications = 0; originals[2].PropertyChanged += (_, args) => { if (args.PropertyName == nameof(LauncherItemViewModel.PinGlyph)) pinNotifications++; }; var resetCount = 0; cards.CollectionChanged += (_, args) => { if (args.Action == NotifyCollectionChangedAction.Reset) resetCount++; }; MainPageViewModel.UpdateLauncherItems(cards, [Item(2, pinned: true), Item(0), Item(1)]); Ensure(ReferenceEquals(cards[0], originals[2]) && ReferenceEquals(cards[1], originals[0]), "固定项排序没有移动已有卡片。"); Ensure(pinNotifications == 1, "固定项变化没有通知星标绑定。"); MainPageViewModel.UpdateLauncherItems(cards, [Item(2, pinned: true), Item(0, title: "更新后的工具"), Item(3)]); Ensure(cards.Select(card => card.Item.TargetId).SequenceEqual(["home-test-2", "home-test-0", "home-test-3"]), "主页卡片没有同步新增、移除或排序变化。"); Ensure(cards[1].Title == "更新后的工具" && !ReferenceEquals(cards[1], originals[0]), "目录修改没有更新对应卡片。"); Ensure(ReferenceEquals(cards[0], originals[2]), "局部修改重建了无关卡片。"); Ensure(resetCount == 0, "局部刷新仍然清空了整张卡片列表。"); MainPageViewModel.UpdateLauncherItems(cards, []); Ensure(cards.Count == 0, "空目录没有清除已移除条目。"); }); [Test] public static void ActivityProgressDoesNotRecreateExistingActivityRows() { var item = new ActivityItem("home-activity", ActivityKind.General, "下载测试", false); var rows = new ObservableCollection(); MainPageViewModel.SynchronizeItems(rows, [item]); var collectionChanges = 0; rows.CollectionChanged += (_, _) => collectionChanges++; for (var progress = 0; progress <= 100; progress++) { item.Progress = progress; MainPageViewModel.SynchronizeItems(rows, [item]); } Ensure(collectionChanges == 0 && ReferenceEquals(rows[0], item) && rows[0].Progress == 100, "活动进度变化重建了行或未保留进度绑定。"); var next = new ActivityItem("next-activity", ActivityKind.General, "构建测试", false); MainPageViewModel.SynchronizeItems(rows, [next, item]); Ensure(rows.Count == 2 && ReferenceEquals(rows[1], item), "新增活动未保留已有行。"); } private static LauncherItem Item(int index, string? title = null, bool pinned = false, Func? execute = null) => new() { Kind = LauncherItemKind.Tool, TargetId = $"home-test-{index}", Title = title ?? $"工具 {index}", IconReference = "/Resources/Image/default_tool_icon.png", IsPinned = pinned, ExecuteAsync = execute ?? (() => Task.CompletedTask) }; private static void RunSta(Action action) { Exception? failure = null; var thread = new Thread(() => { try { action(); } catch (Exception exception) { failure = exception; } }) { IsBackground = true }; thread.SetApartmentState(ApartmentState.STA); thread.Start(); Ensure(thread.Join(TimeSpan.FromSeconds(15)), "主页刷新回归测试超时。"); if (failure is not null) throw new InvalidOperationException("主页刷新回归失败。", failure); } private static void Ensure(bool condition, string message) { if (!condition) throw new InvalidOperationException(message); } }