From 56afcf8abda79c00b3a4b37276a8d2f9845ac977 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Tue, 2 Jun 2026 06:12:43 +0200 Subject: [PATCH] Factor repeated headless-test setup into shared helpers Nearly every headless UI test opened with the same four-line prologue (resolve the shared MainWindow, show it, cast its DataContext, wait for the assembly list), then repeated the corelib lookup, the EnsureLazyChildren + Children.OfType().Single() drill, the registry single-by-header lookups, and the open-an-assembly-and-wait dance. The duplication made the intent of each test hard to see and every signature tweak a suite-wide edit. Collapse those into TestHarness (BootAsync, OpenAssemblyAsync, GetCommand, GetEntry) and TreeNavigation extensions (FindCoreLib, GetChild, Expand), then apply them across the suite. Net ~865 lines of boilerplate removed with no change in behaviour; the full headless suite stays green. --- .../Analyzers/AnalyzeContextMenuTests.cs | 46 +- .../Analyzers/AnalyzedMethodTreeNodeTests.cs | 10 +- .../Analyzers/AnalyzedTypeTreeNodeTests.cs | 16 +- .../Analyzers/AnalyzerConstructorUsesTests.cs | 15 +- .../Analyzers/AnalyzerErrorDisplayTests.cs | 14 +- .../Analyzers/AnalyzerNavigationTests.cs | 10 +- .../Analyzers/AnalyzerPaneCopyResultsTests.cs | 24 +- .../Analyzers/AnalyzerPaneRemoveTests.cs | 10 +- .../Analyzers/AnalyzerResultHighlightTests.cs | 10 +- .../Analyzers/AnalyzerSearchTreeNodeTests.cs | 13 +- .../Analyzers/AnalyzerTreeNodeTests.cs | 13 +- .../AssemblyTreeContextMenuTests.cs | 13 +- .../AssemblyTreeDragReorderTests.cs | 33 +- .../AssemblyTreeExpanderHitboxTests.cs | 11 +- .../AssemblyList/AssemblyTreeFileDropTests.cs | 33 +- ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs | 423 ++++-------------- .../ReloadAssemblyContextMenuTests.cs | 15 +- .../RemoveAssemblyContextMenuTests.cs | 20 +- .../AssemblyList/ShowMetadataTokensTests.cs | 7 +- .../TreeViewSettingsLiveReactivityTests.cs | 7 +- ...UseNestedNamespaceNodesGridVerification.cs | 7 +- .../UseNestedNamespaceNodesLiveTests.cs | 7 +- .../UseNestedNamespaceNodesScreenshot.cs | 7 +- .../UseNestedNamespaceNodesTests.cs | 12 +- ILSpy.Tests/Docking/LayoutPersistenceTests.cs | 16 +- .../Docking/PreviewTabPromotionTests.cs | 60 +-- .../Docking/SingletonDocumentTabTests.cs | 3 +- .../MainWindow/MainToolBarLayoutTests.cs | 11 +- ILSpy.Tests/MainWindow/MainWindowTests.cs | 5 +- .../WindowMenuOpenDocumentsTests.cs | 7 +- .../Metadata/DebugDirectoryChildNodesTests.cs | 35 +- ILSpy.Tests/Metadata/EmbeddedPdbTreeTests.cs | 50 +-- ILSpy.Tests/Metadata/HeapTreeTests.cs | 60 +-- .../Metadata/HideEmptyMetadataTablesTests.cs | 18 +- .../HideEmptyMetadataTablesUiBindingTests.cs | 13 +- .../MetadataDisablesLanguageSwitchingTests.cs | 15 +- ILSpy.Tests/Metadata/MetadataFilterTests.cs | 21 +- .../Metadata/MetadataNodeIconParityTests.cs | 16 +- .../MetadataProtocolHandlerDrillDownTests.cs | 19 +- .../Metadata/MetadataRowActivationTests.cs | 38 +- .../MetadataTabSessionRestoreTests.cs | 55 +-- .../Metadata/MetadataTablesTreeTests.cs | 46 +- .../Metadata/MetadataTokenNavigationTests.cs | 38 +- .../Metadata/MetadataTreeShapeTests.cs | 24 +- ILSpy.Tests/Metadata/PEHeaderTreeTests.cs | 105 ++--- .../ShowInMetadataContextMenuTests.cs | 13 +- .../Metadata/TypedMetadataTableTreeTests.cs | 73 +-- .../BrowseBackForwardCommandTests.cs | 5 +- ILSpy.Tests/Navigation/NavigationTests.cs | 18 +- .../Navigation/ViewStateRoundTripTests.cs | 18 +- .../Options/ApiVisibilityFilterTests.cs | 20 +- .../Options/OptionsPageScrollReachTests.cs | 6 +- ILSpy.Tests/Options/OptionsTabTests.cs | 43 +- ILSpy.Tests/Search/ScopeSearchToTests.cs | 24 +- ILSpy.Tests/Search/SearchInputFocusTests.cs | 10 +- .../SearchPaneAssemblyListChangedTests.cs | 10 +- .../Search/SearchPaneStreamingTests.cs | 28 +- ILSpy.Tests/Search/SearchProgressTests.cs | 10 +- .../Search/SearchResultNavigationTests.cs | 5 +- .../Search/SearchResultSortOrderTests.cs | 5 +- ILSpy.Tests/Search/ShowSearchCommandTests.cs | 5 +- ILSpy.Tests/TestHarness.cs | 112 +++++ ILSpy.Tests/TreeNavigation.cs | 51 +++ 63 files changed, 567 insertions(+), 1320 deletions(-) create mode 100644 ILSpy.Tests/TestHarness.cs diff --git a/ILSpy.Tests/Analyzers/AnalyzeContextMenuTests.cs b/ILSpy.Tests/Analyzers/AnalyzeContextMenuTests.cs index 3ee946fae..bcd2468ff 100644 --- a/ILSpy.Tests/Analyzers/AnalyzeContextMenuTests.cs +++ b/ILSpy.Tests/Analyzers/AnalyzeContextMenuTests.cs @@ -43,10 +43,7 @@ public class AnalyzeContextMenuTests [AvaloniaTest] public async Task Analyze_Entry_Is_Registered_With_The_Localised_Header() { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + await TestHarness.BootAsync(); var registry = AppComposition.Current.GetExport(); registry.Entries.Should().Contain(e => e.Metadata.Header == nameof(Resources.Analyze), @@ -59,15 +56,10 @@ public class AnalyzeContextMenuTests // AnalyzeContextMenuEntry visibility contract: visible when every selected node is // an IMemberTreeNode (types, methods, fields, properties, events), hidden otherwise. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); - var registry = AppComposition.Current.GetExport(); - var entry = registry.Entries - .Single(e => e.Metadata.Header == nameof(Resources.Analyze)) - .Value; + var entry = AppComposition.Current.GetExport() + .GetEntry(nameof(Resources.Analyze)); var typeNode = vm.AssemblyTreeModel.FindNode( "System.Linq", "System.Linq", "System.Linq.Enumerable"); @@ -91,15 +83,10 @@ public class AnalyzeContextMenuTests // selection. The pane's Root.Children must hold one AnalyzedTypeTreeNode wrapping // the same type definition after the call. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); - var registry = AppComposition.Current.GetExport(); - var entry = registry.Entries - .Single(e => e.Metadata.Header == nameof(Resources.Analyze)) - .Value; + var entry = AppComposition.Current.GetExport() + .GetEntry(nameof(Resources.Analyze)); var typeNode = vm.AssemblyTreeModel.FindNode( "System.Linq", "System.Linq", "System.Linq.Enumerable"); @@ -121,10 +108,7 @@ public class AnalyzeContextMenuTests // Ctrl+R while a member is selected on the assembly tree pane must surface the // member in the analyzer pane — the same end-state as right-click + Analyze. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (window, vm) = await TestHarness.BootAsync(); var pane = await window.WaitForComponent(); var typeNode = vm.AssemblyTreeModel.FindNode( @@ -153,15 +137,12 @@ public class AnalyzeContextMenuTests // pane's Root.Children, but if the pane is hidden the user doesn't see anything // happen. Execute should call DockWorkspace.ShowToolPane so the pane surfaces. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); var dockWorkspace = AppComposition.Current.GetExport(); var analyzerVm = AppComposition.Current.GetExport(); var entry = AppComposition.Current.GetExport() - .Entries.Single(e => e.Metadata.Header == nameof(Resources.Analyze)).Value; + .GetEntry(nameof(Resources.Analyze)); // Pick a method that won't already be in the analyzer pane. var typeNode = vm.AssemblyTreeModel.FindNode( @@ -208,14 +189,11 @@ public class AnalyzeContextMenuTests // the leaf result rows below had perfectly good icons. Every materialised node in // the analyzer tree must report a non-null Icon for the cell template to render. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); var analyzerVm = AppComposition.Current.GetExport(); var entry = AppComposition.Current.GetExport() - .Entries.Single(e => e.Metadata.Header == nameof(Resources.Analyze)).Value; + .GetEntry(nameof(Resources.Analyze)); var typeNode = vm.AssemblyTreeModel.FindNode( "System.Linq", "System.Linq", "System.Linq.Enumerable"); diff --git a/ILSpy.Tests/Analyzers/AnalyzedMethodTreeNodeTests.cs b/ILSpy.Tests/Analyzers/AnalyzedMethodTreeNodeTests.cs index 445879657..365c193fe 100644 --- a/ILSpy.Tests/Analyzers/AnalyzedMethodTreeNodeTests.cs +++ b/ILSpy.Tests/Analyzers/AnalyzedMethodTreeNodeTests.cs @@ -47,10 +47,7 @@ public class AnalyzedMethodTreeNodeTests // in the pane rather than the bare method name. An optional `prefix` argument lets // accessor / event-helper nodes prepend a tag without touching the entity string. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); var typeNode = vm.AssemblyTreeModel.FindNode( "System.Linq", "System.Linq", "System.Linq.Enumerable"); @@ -74,10 +71,7 @@ public class AnalyzedMethodTreeNodeTests // are "Uses" (MethodUsesAnalyzer, body scanner) and "Used By" (MethodUsedByAnalyzer, // call-site scanner). - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); var typeNode = vm.AssemblyTreeModel.FindNode( "System.Linq", "System.Linq", "System.Linq.Enumerable"); diff --git a/ILSpy.Tests/Analyzers/AnalyzedTypeTreeNodeTests.cs b/ILSpy.Tests/Analyzers/AnalyzedTypeTreeNodeTests.cs index 6bf8a4cef..4fc7a9c70 100644 --- a/ILSpy.Tests/Analyzers/AnalyzedTypeTreeNodeTests.cs +++ b/ILSpy.Tests/Analyzers/AnalyzedTypeTreeNodeTests.cs @@ -47,13 +47,9 @@ public class AnalyzedTypeTreeNodeTests // the rest of the assembly tree uses — so users see "System.Object" exactly the way // it appears under the assembly node, not a raw ReflectionName. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); + var assemblyNode = vm.AssemblyTreeModel.FindCoreLib(); var module = assemblyNode.LoadedAssembly.GetMetadataFileOrNull(); module.Should().NotBeNull(); var typeSystem = module!.GetTypeSystemOrNull(); @@ -76,13 +72,9 @@ public class AnalyzedTypeTreeNodeTests // non-static reference type like System.Object, "Used By", "Instantiated By", "Exposed // By", and "Extension Methods" all match. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); + var assemblyNode = vm.AssemblyTreeModel.FindCoreLib(); var module = assemblyNode.LoadedAssembly.GetMetadataFileOrNull(); module.Should().NotBeNull(); var typeSystem = module!.GetTypeSystemOrNull(); diff --git a/ILSpy.Tests/Analyzers/AnalyzerConstructorUsesTests.cs b/ILSpy.Tests/Analyzers/AnalyzerConstructorUsesTests.cs index 559c63bc4..1989c68e2 100644 --- a/ILSpy.Tests/Analyzers/AnalyzerConstructorUsesTests.cs +++ b/ILSpy.Tests/Analyzers/AnalyzerConstructorUsesTests.cs @@ -49,10 +49,7 @@ public class AnalyzerConstructorUsesTests [AvaloniaTest] public async Task Used_By_Analyzer_Runs_Over_A_Constructor_Without_Throwing() { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); // Pick any concrete type with at least one public instance ctor; the type itself // doesn't matter — what matters is that the symbol fed to the analyzer is a method @@ -93,10 +90,7 @@ public class AnalyzerConstructorUsesTests // MethodUsesAnalyzer can return type/method/field/property/event ISymbols — the // path most likely to surface a switch-arm hole for some result shape we missed. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); var typeNode = vm.AssemblyTreeModel.FindNode( "System.Linq", "System.Linq", "System.Linq.Lookup`2"); @@ -122,10 +116,7 @@ public class AnalyzerConstructorUsesTests [AvaloniaTest] public async Task Analyze_Pushes_A_Constructor_Onto_The_Pane_Without_Throwing() { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); var typeNode = vm.AssemblyTreeModel.FindNode( "System.Linq", "System.Linq", "System.Linq.Lookup`2"); diff --git a/ILSpy.Tests/Analyzers/AnalyzerErrorDisplayTests.cs b/ILSpy.Tests/Analyzers/AnalyzerErrorDisplayTests.cs index 9e0d3b121..568e6e686 100644 --- a/ILSpy.Tests/Analyzers/AnalyzerErrorDisplayTests.cs +++ b/ILSpy.Tests/Analyzers/AnalyzerErrorDisplayTests.cs @@ -75,22 +75,16 @@ public class AnalyzerErrorDisplayTests [AvaloniaTest] public async Task CopyErrorMessage_Entry_Is_Visible_Only_For_Error_Nodes() { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); - var registry = AppComposition.Current.GetExport(); - var entry = registry.Entries - .Single(e => e.Metadata.Header == nameof(Resources.CopyErrorMessage)) - .Value; + var entry = AppComposition.Current.GetExport() + .GetEntry(nameof(Resources.CopyErrorMessage)); var errorNode = new AnalyzerErrorNode(new InvalidOperationException("boom")); entry.IsVisible(new TextViewContext { SelectedTreeNodes = new SharpTreeNode[] { errorNode } }) .Should().BeTrue("Copy Error Message must surface for AnalyzerErrorNode rows"); - var unrelatedNode = vm.AssemblyTreeModel.FindNode( - typeof(object).Assembly.GetName().Name!); + var unrelatedNode = vm.AssemblyTreeModel.FindCoreLib(); entry.IsVisible(new TextViewContext { SelectedTreeNodes = new SharpTreeNode[] { unrelatedNode } }) .Should().BeFalse("non-error rows must not see the Copy Error Message entry"); } diff --git a/ILSpy.Tests/Analyzers/AnalyzerNavigationTests.cs b/ILSpy.Tests/Analyzers/AnalyzerNavigationTests.cs index 54d6fa090..7368568ad 100644 --- a/ILSpy.Tests/Analyzers/AnalyzerNavigationTests.cs +++ b/ILSpy.Tests/Analyzers/AnalyzerNavigationTests.cs @@ -50,10 +50,7 @@ public class AnalyzerNavigationTests // Double-click on an analyser result must navigate back to the entity's home in // the assembly tree — same UX as clicking a hyperlink in the decompiler view. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); var typeNode = vm.AssemblyTreeModel.FindNode( "System.Linq", "System.Linq", "System.Linq.Enumerable"); @@ -69,10 +66,7 @@ public class AnalyzerNavigationTests [AvaloniaTest] public async Task ActivateItem_On_An_AnalyzedMethodTreeNode_Selects_The_MethodTreeNode_Sibling() { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); var typeNode = vm.AssemblyTreeModel.FindNode( "System.Linq", "System.Linq", "System.Linq.Enumerable"); diff --git a/ILSpy.Tests/Analyzers/AnalyzerPaneCopyResultsTests.cs b/ILSpy.Tests/Analyzers/AnalyzerPaneCopyResultsTests.cs index c60aa3aef..68e80ba38 100644 --- a/ILSpy.Tests/Analyzers/AnalyzerPaneCopyResultsTests.cs +++ b/ILSpy.Tests/Analyzers/AnalyzerPaneCopyResultsTests.cs @@ -42,15 +42,10 @@ public class AnalyzerPaneCopyResultsTests [AvaloniaTest] public async Task CopyResults_Entry_Is_Visible_For_AnalyzerSearchTreeNode_And_Hidden_Otherwise() { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); - var registry = AppComposition.Current.GetExport(); - var entry = registry.Entries - .Single(e => e.Metadata.Header == "Copy results") - .Value; + var entry = AppComposition.Current.GetExport() + .GetEntry("Copy results"); var typeNode = vm.AssemblyTreeModel.FindNode( "System.Linq", "System.Linq", "System.Linq.Enumerable"); @@ -71,15 +66,10 @@ public class AnalyzerPaneCopyResultsTests [AvaloniaTest] public async Task CopyResults_Execute_Writes_Each_Child_On_Its_Own_Line() { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); - - var registry = AppComposition.Current.GetExport(); - var entry = registry.Entries - .Single(e => e.Metadata.Header == "Copy results") - .Value; + var (_, vm) = await TestHarness.BootAsync(); + + var entry = AppComposition.Current.GetExport() + .GetEntry("Copy results"); // Build a search node and stuff some deterministic children in directly, bypassing // the analyzer altogether — the copy entry should only care about Text values. diff --git a/ILSpy.Tests/Analyzers/AnalyzerPaneRemoveTests.cs b/ILSpy.Tests/Analyzers/AnalyzerPaneRemoveTests.cs index ccd7e0d38..9a40eebbf 100644 --- a/ILSpy.Tests/Analyzers/AnalyzerPaneRemoveTests.cs +++ b/ILSpy.Tests/Analyzers/AnalyzerPaneRemoveTests.cs @@ -42,10 +42,7 @@ public class AnalyzerPaneRemoveTests [AvaloniaTest] public async Task Remove_Entry_Is_Visible_Only_For_Top_Level_Analysed_Entities() { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); var registry = AppComposition.Current.GetExport(); var entry = registry.Entries @@ -68,10 +65,7 @@ public class AnalyzerPaneRemoveTests [AvaloniaTest] public async Task Remove_Execute_Drops_The_Selected_Root_Children() { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); var registry = AppComposition.Current.GetExport(); var entry = registry.Entries diff --git a/ILSpy.Tests/Analyzers/AnalyzerResultHighlightTests.cs b/ILSpy.Tests/Analyzers/AnalyzerResultHighlightTests.cs index 7eed5011c..47439f2b6 100644 --- a/ILSpy.Tests/Analyzers/AnalyzerResultHighlightTests.cs +++ b/ILSpy.Tests/Analyzers/AnalyzerResultHighlightTests.cs @@ -57,10 +57,7 @@ public class AnalyzerResultHighlightTests // MessageBus → AssemblyTreeModel's subscription selects the Reference's tree node // AND pushes Source onto the active decompiler tab's HighlightedReference. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); var coreLibName = typeof(object).Assembly.GetName().Name!; var stringNode = vm.AssemblyTreeModel.FindNode( @@ -94,10 +91,7 @@ public class AnalyzerResultHighlightTests // so the subscriber must skip the HighlightedReference assignment rather than blow // away an active highlight. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); var coreLibName = typeof(object).Assembly.GetName().Name!; var stringNode = vm.AssemblyTreeModel.FindNode( diff --git a/ILSpy.Tests/Analyzers/AnalyzerSearchTreeNodeTests.cs b/ILSpy.Tests/Analyzers/AnalyzerSearchTreeNodeTests.cs index ebb22728e..0b315296d 100644 --- a/ILSpy.Tests/Analyzers/AnalyzerSearchTreeNodeTests.cs +++ b/ILSpy.Tests/Analyzers/AnalyzerSearchTreeNodeTests.cs @@ -50,10 +50,7 @@ public class AnalyzerSearchTreeNodeTests // the elapsed-time stamp ("Used By (N in M ms)"). This single test exercises all // three contracts on a small fixture so it stays fast. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); // Pick a small target so the analyzer completes quickly in headless. The "Used By" // analyzer applies to every concrete type; we use a relatively unused type from @@ -96,14 +93,10 @@ public class AnalyzerSearchTreeNodeTests // a clean fetch. The "Used By" analyzer over a deeply-referenced type takes long // enough that we can race a collapse against it. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); // Object is referenced by almost every type — the analyzer streams for a while. - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); + var assemblyNode = vm.AssemblyTreeModel.FindCoreLib(); var module = assemblyNode.LoadedAssembly.GetMetadataFileOrNull(); module.Should().NotBeNull(); var typeSystem = module!.GetTypeSystemOrNull(); diff --git a/ILSpy.Tests/Analyzers/AnalyzerTreeNodeTests.cs b/ILSpy.Tests/Analyzers/AnalyzerTreeNodeTests.cs index 58baebbb6..06cb413f3 100644 --- a/ILSpy.Tests/Analyzers/AnalyzerTreeNodeTests.cs +++ b/ILSpy.Tests/Analyzers/AnalyzerTreeNodeTests.cs @@ -49,10 +49,7 @@ public class AnalyzerTreeNodeTests // reaches the list, (b) the order is monotonically non-decreasing by Metadata.Order // so users see headers in the same sequence the WPF host produces. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + await TestHarness.BootAsync(); var analyzers = AnalyzerTreeNode.Analyzers.ToArray(); analyzers.Should().HaveCountGreaterThanOrEqualTo(15, @@ -76,14 +73,10 @@ public class AnalyzerTreeNodeTests // every Analyzed*TreeNode's symbol references a module the new list may not // contain. The root must wipe its children rather than risk a stale navigation. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); var root = new AnalyzerRootNode(); - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); + var assemblyNode = vm.AssemblyTreeModel.FindCoreLib(); var module = assemblyNode.LoadedAssembly.GetMetadataFileOrNull(); module.Should().NotBeNull("the test needs a loaded CoreLib module to seed a sample analyzed entry"); diff --git a/ILSpy.Tests/AssemblyList/AssemblyTreeContextMenuTests.cs b/ILSpy.Tests/AssemblyList/AssemblyTreeContextMenuTests.cs index ad2a598c1..3ea4f296d 100644 --- a/ILSpy.Tests/AssemblyList/AssemblyTreeContextMenuTests.cs +++ b/ILSpy.Tests/AssemblyList/AssemblyTreeContextMenuTests.cs @@ -28,11 +28,8 @@ using AwesomeAssertions; using ICSharpCode.ILSpyX.TreeView; using ILSpy; -using ILSpy.AppEnv; using ILSpy.AssemblyTree; using ILSpy.TreeNodes; -using ILSpy.ViewModels; -using ILSpy.Views; using NUnit.Framework; @@ -49,10 +46,7 @@ public class AssemblyTreeContextMenuTests // without further per-entry plumbing. // Arrange + Act — boot the window so the pane materialises. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (window, vm) = await TestHarness.BootAsync(); // Assert — TreeGrid carries a ContextMenu. (The menu may be empty if no entries are // registered yet; verifies only that the host is in place.) @@ -71,10 +65,7 @@ public class AssemblyTreeContextMenuTests // Arrange — boot, select an assembly node, install a stub entry that records the // context it sees on Execute. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (window, vm) = await TestHarness.BootAsync(); var pane = await window.WaitForComponent(); var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq"); vm.AssemblyTreeModel.SelectNode(assemblyNode); diff --git a/ILSpy.Tests/AssemblyList/AssemblyTreeDragReorderTests.cs b/ILSpy.Tests/AssemblyList/AssemblyTreeDragReorderTests.cs index ab4dc5e01..af900c5d9 100644 --- a/ILSpy.Tests/AssemblyList/AssemblyTreeDragReorderTests.cs +++ b/ILSpy.Tests/AssemblyList/AssemblyTreeDragReorderTests.cs @@ -29,11 +29,8 @@ using AwesomeAssertions; using ICSharpCode.ILSpyX; -using ILSpy.AppEnv; using ILSpy.AssemblyTree; using ILSpy.TreeNodes; -using ILSpy.ViewModels; -using ILSpy.Views; using NUnit.Framework; @@ -48,10 +45,7 @@ public class AssemblyTreeDragReorderTests // Mirrors WPF's SharpTreeView AllowDropOrder=True — the assembly tree must opt in to // ProDataGrid's row-drag-drop machinery (otherwise the handler we wire below is never // asked to validate anything). - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (window, vm) = await TestHarness.BootAsync(); var pane = await window.WaitForComponent(); var grid = await pane.WaitForComponent(); @@ -71,10 +65,7 @@ public class AssemblyTreeDragReorderTests { // The pane owns the handler instance — it builds one from the model's AssemblyList so // dropping into the grid mutates the same list that file-open and Unload mutate. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (window, vm) = await TestHarness.BootAsync(); var pane = await window.WaitForComponent(); var grid = await pane.WaitForComponent(); @@ -88,10 +79,7 @@ public class AssemblyTreeDragReorderTests // the underlying AssemblyList reordered. The handler is responsible for turning // HierarchicalNode wrappers (or bare AssemblyTreeNodes) into LoadedAssembly refs and // calling AssemblyList.Move with the correct insert index. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 2); + var (window, vm) = await TestHarness.BootAsync(2); var pane = await window.WaitForComponent(); var grid = await pane.WaitForComponent(); var list = vm.AssemblyTreeModel.AssemblyList!; @@ -123,10 +111,7 @@ public class AssemblyTreeDragReorderTests // "Inside" would mean dropping one assembly as a child of another — there's no such // relationship in the model, so the handler must refuse it (the grid then renders the // "not allowed" cursor). - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 2); + var (window, vm) = await TestHarness.BootAsync(2); var pane = await window.WaitForComponent(); var grid = await pane.WaitForComponent(); @@ -143,10 +128,7 @@ public class AssemblyTreeDragReorderTests { // Dropping onto a child of an assembly (a namespace or type) must not reorder anything // — that target doesn't live in AssemblyList at all. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (window, vm) = await TestHarness.BootAsync(); var pane = await window.WaitForComponent(); var grid = await pane.WaitForComponent(); @@ -165,10 +147,7 @@ public class AssemblyTreeDragReorderTests { // Sub-nodes (namespaces, types, etc.) must not be picked up by the reorder gesture — // only top-level AssemblyTreeNodes are eligible source items. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 2); + var (window, vm) = await TestHarness.BootAsync(2); var pane = await window.WaitForComponent(); var grid = await pane.WaitForComponent(); diff --git a/ILSpy.Tests/AssemblyList/AssemblyTreeExpanderHitboxTests.cs b/ILSpy.Tests/AssemblyList/AssemblyTreeExpanderHitboxTests.cs index be18d3068..82b5d7828 100644 --- a/ILSpy.Tests/AssemblyList/AssemblyTreeExpanderHitboxTests.cs +++ b/ILSpy.Tests/AssemblyList/AssemblyTreeExpanderHitboxTests.cs @@ -33,11 +33,8 @@ using AwesomeAssertions; using ICSharpCode.ILSpyX.TreeView; -using ILSpy.AppEnv; using ILSpy.AssemblyTree; using ILSpy.TreeNodes; -using ILSpy.ViewModels; -using ILSpy.Views; using NUnit.Framework; @@ -56,14 +53,10 @@ public class AssemblyTreeExpanderHitboxTests // occupy 16x16 of layout while only the 9x9 glyph receives input. // Arrange — boot, wait for assemblies, expand a node so an expandable row is realised. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (window, vm) = await TestHarness.BootAsync(3); var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq"); - assemblyNode.EnsureLazyChildren(); - assemblyNode.IsExpanded = true; + assemblyNode.Expand(); vm.AssemblyTreeModel.SelectNode(assemblyNode); var pane = await window.WaitForComponent(); diff --git a/ILSpy.Tests/AssemblyList/AssemblyTreeFileDropTests.cs b/ILSpy.Tests/AssemblyList/AssemblyTreeFileDropTests.cs index 745c25196..4b0543c95 100644 --- a/ILSpy.Tests/AssemblyList/AssemblyTreeFileDropTests.cs +++ b/ILSpy.Tests/AssemblyList/AssemblyTreeFileDropTests.cs @@ -28,11 +28,8 @@ using Avalonia.Input; using AwesomeAssertions; -using ILSpy.AppEnv; using ILSpy.AssemblyTree; using ILSpy.TreeNodes; -using ILSpy.ViewModels; -using ILSpy.Views; using NUnit.Framework; @@ -47,10 +44,7 @@ public class AssemblyTreeFileDropTests // Avalonia's standard drag-drop pipeline routes Explorer file drops through // DragDrop.AllowDrop / DragDrop.DropEvent. The grid must opt in or external drops // produce a "no" cursor and never reach our handler. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (window, vm) = await TestHarness.BootAsync(); var pane = await window.WaitForComponent(); var grid = await pane.WaitForComponent(); @@ -63,10 +57,7 @@ public class AssemblyTreeFileDropTests // Simulating Windows Explorer dropping a .dll on empty space inside the tree. // No target row → the new entry is appended (WPF's AssemblyListTreeNode.Drop with // no target index does the same). - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (window, vm) = await TestHarness.BootAsync(3); var pane = await window.WaitForComponent(); var list = vm.AssemblyTreeModel.AssemblyList!; @@ -92,10 +83,7 @@ public class AssemblyTreeFileDropTests { // Drop position is honoured: dragging onto the first row with Position=Before lands // the opened assembly at index 0. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (window, vm) = await TestHarness.BootAsync(3); var pane = await window.WaitForComponent(); var list = vm.AssemblyTreeModel.AssemblyList!; @@ -125,10 +113,7 @@ public class AssemblyTreeFileDropTests public async Task File_Drop_After_A_Target_Row_Inserts_Past_That_Index() { // Position=After: insertion lands at targetIndex + 1. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (window, vm) = await TestHarness.BootAsync(3); var pane = await window.WaitForComponent(); var list = vm.AssemblyTreeModel.AssemblyList!; @@ -158,10 +143,7 @@ public class AssemblyTreeFileDropTests // AssemblyListTreeNode.Drop (which ends in AssemblyTreeModel.SelectNodes). // Without this, the user gets no visual confirmation that the drop took, and // the decompiler view stays parked on whatever was selected before. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (window, vm) = await TestHarness.BootAsync(3); var pane = await window.WaitForComponent(); var list = vm.AssemblyTreeModel.AssemblyList!; @@ -192,10 +174,7 @@ public class AssemblyTreeFileDropTests { // AssemblyList.OpenAssembly returns null for non-existent / non-PE paths; the // handler must filter those out and never raise on the caller's behalf. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (window, vm) = await TestHarness.BootAsync(3); var pane = await window.WaitForComponent(); var list = vm.AssemblyTreeModel.AssemblyList!; diff --git a/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs b/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs index 6320054ed..3a027735c 100644 --- a/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs +++ b/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs @@ -48,7 +48,6 @@ using ILSpy.Metadata.CorTables; using ILSpy.Search; using ILSpy.TreeNodes; using ILSpy.ViewModels; -using ILSpy.Views; using NUnit.Framework; @@ -66,22 +65,16 @@ public class AssemblyTreeTests // ResourceTreeNode — both are ILSpyTreeNodes). // Arrange — boot, wait for assemblies. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); // Act — pick CoreLib (always ships localised error-message tables), expand it. - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - assemblyNode.IsExpanded = true; + var assemblyNode = vm.AssemblyTreeModel.FindCoreLib(); + assemblyNode.Expand(); // Assert — exactly one Resources folder, named, non-empty, every child an ILSpyTreeNode. var resources = assemblyNode.Children.OfType().Single(); resources.Text.Should().Be(Resources._Resources); - resources.EnsureLazyChildren(); - resources.IsExpanded = true; + resources.Expand(); resources.Children.Should().NotBeEmpty(); resources.Children.Should().AllBeAssignableTo(); } @@ -93,10 +86,7 @@ public class AssemblyTreeTests // Ctrl-click to multi-select rows. // Arrange — boot, wait for assemblies + AssemblyListPane to materialise. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (window, vm) = await TestHarness.BootAsync(3); // Act — locate the realised DataGrid inside the AssemblyListPane. var pane = await window.WaitForComponent(); @@ -115,15 +105,11 @@ public class AssemblyTreeTests // flip its IsSelected back without affecting siblings. // Arrange — boot, wait for assemblies, expand Enumerable, grab two methods. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var typeNode = vm.AssemblyTreeModel.FindNode( "System.Linq", "System.Linq", "System.Linq.Enumerable"); - typeNode.EnsureLazyChildren(); - typeNode.IsExpanded = true; + typeNode.Expand(); var first = typeNode.Children.OfType() .First(m => m.MethodDefinition.Name == "ElementAt"); var second = typeNode.Children.OfType() @@ -155,10 +141,7 @@ public class AssemblyTreeTests // Arrange — boot, wait for assemblies, build a fixture zip with a flat file + two // folders containing one file each. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var tempDir = Path.Combine(Path.GetTempPath(), "ILSpy.Tests", System.Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); @@ -174,22 +157,10 @@ public class AssemblyTreeTests } // Act — open the zip via the OpenCommand and wait for it to load. - var registry = AppComposition.Current.GetExport(); - var openCommand = registry.Commands - .Single(c => c.Metadata.Header == nameof(Resources._Open)) - .CreateExport().Value; - openCommand.Execute(zipPath); - - await Waiters.WaitForAsync(() => - vm.AssemblyTreeModel.AssemblyList!.GetAssemblies().Any(a => - string.Equals(a.FileName, zipPath, System.StringComparison.OrdinalIgnoreCase))); - var loaded = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() - .First(a => string.Equals(a.FileName, zipPath, System.StringComparison.OrdinalIgnoreCase)); - await loaded.GetLoadResultAsync(); + var loaded = await vm.OpenAssemblyAsync(zipPath); var assemblyNode = vm.AssemblyTreeModel.FindNode(loaded.ShortName); - assemblyNode.EnsureLazyChildren(); - assemblyNode.IsExpanded = true; + assemblyNode.Expand(); // Assert — both nested folders show as PackageFolderTreeNodes; the flat file shows as // a ResourceTreeNode with its original entry name. @@ -212,18 +183,12 @@ public class AssemblyTreeTests // string-table or inner ResourceEntryNode children (depending on payload type). // Arrange — boot, wait for assemblies, expand CoreLib's Resources folder. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - assemblyNode.IsExpanded = true; + var assemblyNode = vm.AssemblyTreeModel.FindCoreLib(); + assemblyNode.Expand(); var resources = assemblyNode.Children.OfType().Single(); - resources.EnsureLazyChildren(); - resources.IsExpanded = true; + resources.Expand(); // Act — look up the first .resources file and select it. var resourceFileNode = resources.Children.OfType().FirstOrDefault(); @@ -246,14 +211,10 @@ public class AssemblyTreeTests // AssemblyReferenceTreeNodes — one per AssemblyRef row. // Arrange — boot, wait for assemblies, expand System.Linq. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq"); - assemblyNode.EnsureLazyChildren(); - assemblyNode.IsExpanded = true; + assemblyNode.Expand(); // Act — find the References folder. var refFolder = assemblyNode.Children.OfType().Single(); @@ -261,8 +222,7 @@ public class AssemblyTreeTests // Assert — folder is named, expands to non-empty AssemblyReferenceTreeNode children. refFolder.Text.Should().Be(Resources.References); - refFolder.EnsureLazyChildren(); - refFolder.IsExpanded = true; + refFolder.Expand(); refFolder.Children.Should().NotBeEmpty(); refFolder.Children.Should().AllBeAssignableTo(); } @@ -277,14 +237,10 @@ public class AssemblyTreeTests // Arrange — boot, wait for assemblies, force lazy children on assembly + References. // Stay model-only (no IsExpanded = true) — visually expanding rows would render the // icons, triggering the resolver and losing the lazy "loading" state we're asserting. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq"); - assemblyNode.EnsureLazyChildren(); - var refFolder = assemblyNode.Children.OfType().Single(); + var refFolder = assemblyNode.GetChild(); refFolder.EnsureLazyChildren(); var refNode = refFolder.Children.OfType() @@ -309,27 +265,20 @@ public class AssemblyTreeTests // TypeRef rows the host assembly imports from that reference. // Arrange — boot, wait for assemblies, expand System.Linq → References → System.Runtime. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq"); - assemblyNode.EnsureLazyChildren(); - assemblyNode.IsExpanded = true; + assemblyNode.Expand(); var refFolder = assemblyNode.Children.OfType().Single(); - refFolder.EnsureLazyChildren(); - refFolder.IsExpanded = true; + refFolder.Expand(); var refNode = refFolder.Children.OfType() .First(n => n.AssemblyReference.Name == "System.Runtime"); - refNode.EnsureLazyChildren(); - refNode.IsExpanded = true; + refNode.Expand(); // Act — find the ReferencedTypes subnode and expand it. var typesNode = refNode.Children.OfType().Single(); - typesNode.EnsureLazyChildren(); - typesNode.IsExpanded = true; + typesNode.Expand(); // Assert — header text contains the expected label, and the children include at least // one TypeReferenceTreeNode. @@ -345,14 +294,10 @@ public class AssemblyTreeTests // search to land on a specific namespace. // Arrange — boot, wait for assemblies, expand System.Linq. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq"); - assemblyNode.EnsureLazyChildren(); - assemblyNode.IsExpanded = true; + assemblyNode.Expand(); // Act — look up the System.Linq namespace. var ns = assemblyNode.FindNamespaceNode("System.Linq"); @@ -370,14 +315,10 @@ public class AssemblyTreeTests // to land on a specific type by ITypeDefinition. // Arrange — boot, wait for assemblies, expand System.Linq. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq"); - assemblyNode.EnsureLazyChildren(); - assemblyNode.IsExpanded = true; + assemblyNode.Expand(); // Act — resolve System.Linq.Enumerable's ITypeDefinition, then look up its tree node. var module = assemblyNode.LoadedAssembly.GetMetadataFileOrNull()!; @@ -399,17 +340,12 @@ public class AssemblyTreeTests // top of the underlying Method/Field glyph — verifies that overlay routing works. // Arrange — boot, wait for assemblies, expand System.Linq → References. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq"); - assemblyNode.EnsureLazyChildren(); - assemblyNode.IsExpanded = true; + assemblyNode.Expand(); var refFolder = assemblyNode.Children.OfType().Single(); - refFolder.EnsureLazyChildren(); - refFolder.IsExpanded = true; + refFolder.Expand(); // Act — walk every reference's ReferencedTypes subtree until we find a TypeRef with // at least one MemberReference. System.Linq imports plenty (delegates, Func ctors) @@ -417,17 +353,14 @@ public class AssemblyTreeTests MemberReferenceTreeNode? memberRefNode = null; foreach (var refNode in refFolder.Children.OfType()) { - refNode.EnsureLazyChildren(); - refNode.IsExpanded = true; + refNode.Expand(); var typesNode = refNode.Children.OfType().FirstOrDefault(); if (typesNode == null) continue; - typesNode.EnsureLazyChildren(); - typesNode.IsExpanded = true; + typesNode.Expand(); foreach (var typeRef in typesNode.Children.OfType()) { - typeRef.EnsureLazyChildren(); - typeRef.IsExpanded = true; + typeRef.Expand(); memberRefNode = typeRef.Children.OfType().FirstOrDefault(); if (memberRefNode != null) break; @@ -452,10 +385,7 @@ public class AssemblyTreeTests // Arrange — boot, wait for assemblies. Open mscorlib explicitly: it's the canonical // type-forwarder facade and we need a known forwarder regardless of default startup. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var coreLibDir = System.IO.Path.GetDirectoryName(typeof(object).Assembly.Location)!; var mscorlibPath = System.IO.Path.Combine(coreLibDir, "mscorlib.dll"); @@ -464,37 +394,22 @@ public class AssemblyTreeTests Assert.Inconclusive($"mscorlib.dll not present next to System.Private.CoreLib at {coreLibDir} — runtime layout differs."); return; } - var registry = AppComposition.Current.GetExport(); - var openCommand = registry.Commands - .Single(c => c.Metadata.Header == nameof(Resources._Open)) - .CreateExport().Value; - openCommand.Execute(mscorlibPath); - - await Waiters.WaitForAsync(() => - vm.AssemblyTreeModel.AssemblyList!.GetAssemblies().Any(a => - string.Equals(a.FileName, mscorlibPath, System.StringComparison.OrdinalIgnoreCase))); - var loaded = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() - .First(a => string.Equals(a.FileName, mscorlibPath, System.StringComparison.OrdinalIgnoreCase)); - await loaded.GetLoadResultAsync(); + var loaded = await vm.OpenAssemblyAsync(mscorlibPath); var assemblyNode = vm.AssemblyTreeModel.FindNode(loaded.ShortName); - assemblyNode.EnsureLazyChildren(); - assemblyNode.IsExpanded = true; + assemblyNode.Expand(); var refFolder = assemblyNode.Children.OfType().Single(); - refFolder.EnsureLazyChildren(); - refFolder.IsExpanded = true; + refFolder.Expand(); // Act — walk every reference's ReferencedTypes for an ExportedTypeTreeNode. ExportedTypeTreeNode? exportedNode = null; foreach (var refNode in refFolder.Children.OfType()) { - refNode.EnsureLazyChildren(); - refNode.IsExpanded = true; + refNode.Expand(); var typesNode = refNode.Children.OfType().FirstOrDefault(); if (typesNode == null) continue; - typesNode.EnsureLazyChildren(); - typesNode.IsExpanded = true; + typesNode.Expand(); exportedNode = typesNode.Children.OfType().FirstOrDefault(); if (exportedNode != null) break; @@ -514,25 +429,19 @@ public class AssemblyTreeTests // — building a transitive reference tree the user can drill into. // Arrange — boot, wait for assemblies, expand System.Linq → References. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq"); - assemblyNode.EnsureLazyChildren(); - assemblyNode.IsExpanded = true; + assemblyNode.Expand(); var refFolder = assemblyNode.Children.OfType().Single(); - refFolder.EnsureLazyChildren(); - refFolder.IsExpanded = true; + refFolder.Expand(); // Act — many BCL refs resolve to facade assemblies with zero transitive references; // iterate until we find one that has its own AssemblyRefs. bool foundTransitive = false; foreach (var refNode in refFolder.Children.OfType()) { - refNode.EnsureLazyChildren(); - refNode.IsExpanded = true; + refNode.Expand(); if (refNode.Children.OfType().Any()) { foundTransitive = true; @@ -551,17 +460,12 @@ public class AssemblyTreeTests // assembly's AssemblyTreeNode in the tree — auto-loading it if necessary. // Arrange — boot, wait for assemblies, expand System.Linq → References. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq"); - assemblyNode.EnsureLazyChildren(); - assemblyNode.IsExpanded = true; + assemblyNode.Expand(); var refFolder = assemblyNode.Children.OfType().Single(); - refFolder.EnsureLazyChildren(); - refFolder.IsExpanded = true; + refFolder.Expand(); var refNode = refFolder.Children.OfType() .Single(n => n.AssemblyReference.Name == "System.Runtime"); @@ -591,25 +495,10 @@ public class AssemblyTreeTests // Arrange — boot, wait for assemblies, open System.Net.Http (its CancelPendingRequests // pulls in plenty of currently-unreferenced types). - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var newAsmPath = typeof(System.Net.Http.HttpClient).Assembly.Location; - var registry = AppComposition.Current.GetExport(); - var openCommand = registry.Commands - .Single(c => c.Metadata.Header == nameof(Resources._Open)) - .CreateExport().Value; - openCommand.Execute(newAsmPath); - - await Waiters.WaitForAsync(() => - vm.AssemblyTreeModel.AssemblyList!.GetAssemblies().Any(a => - string.Equals(a.FileName, newAsmPath, System.StringComparison.OrdinalIgnoreCase))); - - var loaded = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() - .First(a => string.Equals(a.FileName, newAsmPath, System.StringComparison.OrdinalIgnoreCase)); - await loaded.GetLoadResultAsync(); + var loaded = await vm.OpenAssemblyAsync(newAsmPath); var initialFiles = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() .Select(a => a.FileName) @@ -619,8 +508,7 @@ public class AssemblyTreeTests // assemblies to appear. var typeNode = vm.AssemblyTreeModel.FindNode( loaded.ShortName, "System.Net.Http", "System.Net.Http.HttpClient"); - typeNode.EnsureLazyChildren(); - typeNode.IsExpanded = true; + typeNode.Expand(); var methodNode = typeNode.Children.OfType() .First(m => m.MethodDefinition.Name == "CancelPendingRequests"); @@ -653,30 +541,14 @@ public class AssemblyTreeTests // user actually opened. // Arrange — boot, wait for assemblies, open + select a method that triggers auto-load. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var newAsmPath = typeof(System.Net.Http.HttpClient).Assembly.Location; - var registry = AppComposition.Current.GetExport(); - var openCommand = registry.Commands - .Single(c => c.Metadata.Header == nameof(Resources._Open)) - .CreateExport().Value; - openCommand.Execute(newAsmPath); - - await Waiters.WaitForAsync(() => - vm.AssemblyTreeModel.AssemblyList!.GetAssemblies().Any(a => - string.Equals(a.FileName, newAsmPath, System.StringComparison.OrdinalIgnoreCase))); - - var loaded = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() - .First(a => string.Equals(a.FileName, newAsmPath, System.StringComparison.OrdinalIgnoreCase)); - await loaded.GetLoadResultAsync(); + var loaded = await vm.OpenAssemblyAsync(newAsmPath); var typeNode = vm.AssemblyTreeModel.FindNode( loaded.ShortName, "System.Net.Http", "System.Net.Http.HttpClient"); - typeNode.EnsureLazyChildren(); - typeNode.IsExpanded = true; + typeNode.Expand(); var methodNode = typeNode.Children.OfType() .First(m => m.MethodDefinition.Name == "CancelPendingRequests"); @@ -717,28 +589,13 @@ public class AssemblyTreeTests // matching tree node, and select that node so the user sees their newly-opened file. // Arrange — boot, wait for assemblies, snapshot the initial count. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var newAsmPath = typeof(System.Net.Http.HttpClient).Assembly.Location; var initialCount = vm.AssemblyTreeModel.AssemblyList!.Count; // Act — fire OpenCommand on a new path and wait for the load to complete. - var registry = AppComposition.Current.GetExport(); - var openCommand = registry.Commands - .Single(c => c.Metadata.Header == nameof(Resources._Open)) - .CreateExport().Value; - openCommand.Execute(newAsmPath); - - await Waiters.WaitForAsync(() => - vm.AssemblyTreeModel.AssemblyList!.GetAssemblies().Any(a => - string.Equals(a.FileName, newAsmPath, System.StringComparison.OrdinalIgnoreCase))); - - var loaded = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() - .First(a => string.Equals(a.FileName, newAsmPath, System.StringComparison.OrdinalIgnoreCase)); - await loaded.GetLoadResultAsync(); + var loaded = await vm.OpenAssemblyAsync(newAsmPath); // Assert — count incremented by one; the corresponding tree node points at the // freshly-loaded assembly and is selected. @@ -758,24 +615,18 @@ public class AssemblyTreeTests // Arrange — boot, wait for assemblies, expand a sub-tree large enough to scroll. Park // the viewport mid-list so clicks are at non-zero offset. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (window, vm) = await TestHarness.BootAsync(3); var enumerable = vm.AssemblyTreeModel.FindNode( "System.Linq", "System.Linq", "System.Linq.Enumerable"); - enumerable.EnsureLazyChildren(); - enumerable.IsExpanded = true; + enumerable.Expand(); var ns = (NamespaceTreeNode)enumerable.Parent!; - ns.EnsureLazyChildren(); - ns.IsExpanded = true; + ns.Expand(); var asm = (AssemblyTreeNode)ns.Parent!; asm.IsExpanded = true; foreach (var child in ns.Children.OfType()) { - child.EnsureLazyChildren(); - child.IsExpanded = true; + child.Expand(); } var pane = await window.WaitForComponent(); @@ -838,19 +689,13 @@ public class AssemblyTreeTests // method — that's how nodes (e.g. resources) opt into a custom save dialog/format. // Arrange — boot, wait for assemblies, plant a probe node that records when Save runs. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var probe = new SaveProbeNode(); vm.AssemblyTreeModel.SelectNode(probe); // Act — fire the Save Code main-menu command. - var registry = AppComposition.Current.GetExport(); - var saveCmd = registry.Commands - .Single(c => c.Metadata.Header == nameof(Resources._SaveCode)) - .CreateExport().Value; + var saveCmd = AppComposition.Current.GetExport().GetCommand(nameof(Resources._SaveCode)); saveCmd.Execute(null); // Assert — probe's Save() ran. @@ -877,15 +722,11 @@ public class AssemblyTreeTests // write the resulting C# to the user-picked path. // Arrange — boot, wait for assemblies, select a method, wait for the in-tab decompile. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var typeNode = vm.AssemblyTreeModel.FindNode( "System.Linq", "System.Linq", "System.Linq.Enumerable"); - typeNode.EnsureLazyChildren(); - typeNode.IsExpanded = true; + typeNode.Expand(); var method = typeNode.Children.OfType() .First(m => m.MethodDefinition.Name == "AsEnumerable"); vm.AssemblyTreeModel.SelectNode(method); @@ -929,10 +770,7 @@ public class AssemblyTreeTests // Arrange — boot, wait for assemblies, pick the assembly to evict + a sibling we // expect to survive, select the sacrificial one, focus the grid. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (window, vm) = await TestHarness.BootAsync(3); var sacrificialName = typeof(System.Linq.Enumerable).Assembly.GetName().Name!; var survivorName = typeof(object).Assembly.GetName().Name!; @@ -980,17 +818,11 @@ public class AssemblyTreeTests // The Clear Assembly List menu command must drop every entry from the active list. // Arrange — boot, wait for assemblies, baseline the non-empty count. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); vm.AssemblyTreeModel.AssemblyList!.Count.Should().BeGreaterThan(0); // Act — fire the Clear command. - var registry = AppComposition.Current.GetExport(); - var clearCmd = registry.Commands - .Single(c => c.Metadata.Header == nameof(Resources.ClearAssemblyList)) - .CreateExport().Value; + var clearCmd = AppComposition.Current.GetExport().GetCommand(nameof(Resources.ClearAssemblyList)); clearCmd.CanExecute(null).Should().BeTrue("non-empty list must enable Clear"); clearCmd.Execute(null); @@ -1008,10 +840,7 @@ public class AssemblyTreeTests // Arrange — boot, wait for assemblies, plant a "broken" entry: a real file that isn't // a PE binary. AssemblyList records it but the load fails — that's the state the // command targets. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var brokenPath = System.IO.Path.Combine( System.IO.Path.GetTempPath(), @@ -1035,10 +864,7 @@ public class AssemblyTreeTests validBefore.Should().NotBeEmpty("at least one real assembly must remain to verify it's spared"); // Act — fire the command. - var registry = AppComposition.Current.GetExport(); - var cmd = registry.Commands - .Single(c => c.Metadata.Header == nameof(Resources._RemoveAssembliesWithLoadErrors)) - .CreateExport().Value; + var cmd = AppComposition.Current.GetExport().GetCommand(nameof(Resources._RemoveAssembliesWithLoadErrors)); cmd.CanExecute(null).Should().BeTrue("a broken entry must enable the command"); cmd.Execute(null); @@ -1069,10 +895,7 @@ public class AssemblyTreeTests // Arrange — boot, wait for assemblies, locate the realised DataGrid and the // HierarchicalModel wrapper for the assembly node. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (window, vm) = await TestHarness.BootAsync(3); var pane = await window.WaitForComponent(); var grid = await pane.WaitForComponent(); @@ -1084,8 +907,7 @@ public class AssemblyTreeTests hNode!.IsExpanded.Should().BeFalse("baseline: top-level assembly rows start collapsed"); // Act — production-shaped action: just toggle the model property. - assemblyNode.EnsureLazyChildren(); - assemblyNode.IsExpanded = true; + assemblyNode.Expand(); // Drain the dispatcher so the wiring (PropertyChanged → hm.Expand) settles. for (int i = 0; i < 5; i++) @@ -1106,10 +928,7 @@ public class AssemblyTreeTests // tab opens with the supplied node decompiled, the existing tab keeps its content, // and the assembly-tree selection is pulled across to the new tab's source node // (the active tab and the tree are kept in lockstep). - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (window, vm) = await TestHarness.BootAsync(3); var typeNode = vm.AssemblyTreeModel.FindNode( "System.Linq", "System.Linq", "System.Linq.Enumerable"); @@ -1147,10 +966,7 @@ public class AssemblyTreeTests { // Activate two tabs (each carrying a different SourceNode), then flip the active // dockable back to the first one and confirm the assembly-tree selection follows. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var typeNode = vm.AssemblyTreeModel.FindNode( "System.Linq", "System.Linq", "System.Linq.Enumerable"); @@ -1188,19 +1004,12 @@ public class AssemblyTreeTests // DecompilerTabPageModel. Exercises the regression behind "there can't be multiple // metadata views": before the OpenNodeInNewTab consolidation, the new-tab path // always built a decompiler tab and the metadata page-type couldn't be carved out. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); - metadataNode.EnsureLazyChildren(); - var tablesNode = metadataNode.Children.OfType().Single(); - tablesNode.EnsureLazyChildren(); - var typeDefNode = tablesNode.Children.OfType().Single(); + var tablesNode = vm.AssemblyTreeModel.FindCoreLib() + .GetChild() + .GetChild(); + var typeDefNode = tablesNode.GetChild(); // First metadata view via tree-selection (active tab gets MetadataTablePageModel content). vm.AssemblyTreeModel.SelectNode(typeDefNode); @@ -1229,10 +1038,7 @@ public class AssemblyTreeTests // ILSpy. Without this, tree-based navigation can't reach get_X / set_X — and // MMB on a property-accessor row in the metadata grid silently no-ops because // FindTreeNode can't resolve the IMethod to a tree node. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); var coreLibName = typeof(object).Assembly.GetName().Name!; var stringTypeNode = vm.AssemblyTreeModel.FindNode( @@ -1257,10 +1063,7 @@ public class AssemblyTreeTests // Events surface add_X / remove_X (and invoke_X if present) as MethodTreeNode // children, mirroring WPF ILSpy. Same regression-protection as the property // case above. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); var coreLibName = typeof(object).Assembly.GetName().Name!; // AppDomain.AssemblyLoad is a public, well-known event in CoreLib. @@ -1285,10 +1088,7 @@ public class AssemblyTreeTests // the per-node Filter must hide them under the default ShowApiLevel — they're // rendered as part of the parent property in the C# surface, so the tree should // not duplicate them as standalone rows. ShowAll is the explicit opt-in. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); var coreLibName = typeof(object).Assembly.GetName().Name!; var stringTypeNode = vm.AssemblyTreeModel.FindNode( @@ -1317,10 +1117,7 @@ public class AssemblyTreeTests // an IMethod whose AccessorOwner is the IProperty. Without the AccessorOwner-aware // branch in FindMemberNode, the lookup falls through to "MethodTreeNode child of the // type" and returns null because accessors live under the property, not the type. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); var coreLibName = typeof(object).Assembly.GetName().Name!; var stringTypeNode = vm.AssemblyTreeModel.FindNode( @@ -1346,10 +1143,7 @@ public class AssemblyTreeTests // expect a single BaseTypesEntryNode whose label ends in "Object". // Arrange — boot, wait for assemblies, drill into System.Exception. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var coreLibName = typeof(object).Assembly.GetName().Name!; var typeNode = vm.AssemblyTreeModel.FindNode( @@ -1371,10 +1165,7 @@ public class AssemblyTreeTests // Activating a BaseTypesEntryNode jumps the assembly-tree selection to the underlying // type node — same gesture the WPF tree uses to walk an inheritance chain. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var coreLibName = typeof(object).Assembly.GetName().Name!; var typeNode = vm.AssemblyTreeModel.FindNode( @@ -1404,10 +1195,7 @@ public class AssemblyTreeTests // scan the assembly list synchronously — for the small fixture loaded in tests this // yields a few hits per common base (e.g. SystemException is derived from Exception). - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var coreLibName = typeof(object).Assembly.GetName().Name!; var typeNode = vm.AssemblyTreeModel.FindNode( @@ -1425,10 +1213,7 @@ public class AssemblyTreeTests { // Sealed types can't be derived from, so the DerivedTypes sub-tree must not appear. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var coreLibName = typeof(object).Assembly.GetName().Name!; var stringNode = vm.AssemblyTreeModel.FindNode( @@ -1444,10 +1229,7 @@ public class AssemblyTreeTests { // System.Object has no base types, so the BaseTypes sub-tree must not appear. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var coreLibName = typeof(object).Assembly.GetName().Name!; var objectNode = vm.AssemblyTreeModel.FindNode( @@ -1498,10 +1280,7 @@ public class AssemblyTreeTests // must NOT return Hidden purely because the field's name doesn't contain the active // SearchTerm — only ShowApiLevel + ShowMember remain valid hiding criteria. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); var settings = AppComposition.Current.GetExport() .SessionSettings.LanguageSettings; @@ -1533,13 +1312,9 @@ public class AssemblyTreeTests // NOT as bare TypeTreeNodes hanging directly off the assembly node. // Arrange — boot, wait for assemblies, locate CoreLib. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); + var assemblyNode = vm.AssemblyTreeModel.FindCoreLib(); assemblyNode.EnsureLazyChildren(); // Assert — exactly one global-namespace node, labelled "-", non-empty contents. @@ -1570,10 +1345,7 @@ public class AssemblyTreeTests // the current cell to the last row (the user reported having to press it twice). // Arrange — boot, wait for assemblies, realise the grid. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (window, vm) = await TestHarness.BootAsync(3); var pane = await window.WaitForComponent(); var grid = await pane.WaitForComponent(); @@ -1619,10 +1391,7 @@ public class AssemblyTreeTests // one row must collapse the selection down to just that row. // Arrange — boot, wait for assemblies, realise the grid, select everything. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (window, vm) = await TestHarness.BootAsync(3); var pane = await window.WaitForComponent(); var grid = await pane.WaitForComponent(); @@ -1670,16 +1439,10 @@ public class AssemblyTreeTests // to do nothing. // Arrange -- boot, open System.Net.Http (it references many assemblies not in the list). - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var newAsmPath = typeof(System.Net.Http.HttpClient).Assembly.Location; - var registry = AppComposition.Current.GetExport(); - var openCommand = registry.Commands - .Single(c => c.Metadata.Header == nameof(Resources._Open)) - .CreateExport().Value; + var openCommand = AppComposition.Current.GetExport().GetCommand(nameof(Resources._Open)); openCommand.Execute(newAsmPath); await Waiters.WaitForAsync(() => vm.AssemblyTreeModel.AssemblyList!.GetAssemblies().Any(a => diff --git a/ILSpy.Tests/AssemblyList/ReloadAssemblyContextMenuTests.cs b/ILSpy.Tests/AssemblyList/ReloadAssemblyContextMenuTests.cs index 685090a7f..59cd1b8bf 100644 --- a/ILSpy.Tests/AssemblyList/ReloadAssemblyContextMenuTests.cs +++ b/ILSpy.Tests/AssemblyList/ReloadAssemblyContextMenuTests.cs @@ -32,7 +32,6 @@ using ILSpy; using ILSpy.AppEnv; using ILSpy.AssemblyTree; using ILSpy.TreeNodes; -using ILSpy.ViewModels; using ILSpy.Views; using NUnit.Framework; @@ -68,14 +67,9 @@ public class ReloadAssemblyContextMenuTests // pollute their right-click menus. // Arrange — boot, locate the registered entry. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var registry = AppComposition.Current.GetExport(); - var reloadEntry = registry.Entries - .Single(e => e.Metadata.Header == nameof(Resources._Reload)) - .Value; + var reloadEntry = registry.GetEntry(nameof(Resources._Reload)); var asm = vm.AssemblyTreeModel.FindNode("System.Linq"); var typeNode = vm.AssemblyTreeModel.FindNode( @@ -106,10 +100,7 @@ public class ReloadAssemblyContextMenuTests // the same file name. // Arrange — boot, snapshot the LoadedAssembly we'll reload. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (window, vm) = await TestHarness.BootAsync(3); var pane = await window.WaitForComponent(); var registry = AppComposition.Current.GetExport(); diff --git a/ILSpy.Tests/AssemblyList/RemoveAssemblyContextMenuTests.cs b/ILSpy.Tests/AssemblyList/RemoveAssemblyContextMenuTests.cs index 780fec6e1..921de9a12 100644 --- a/ILSpy.Tests/AssemblyList/RemoveAssemblyContextMenuTests.cs +++ b/ILSpy.Tests/AssemblyList/RemoveAssemblyContextMenuTests.cs @@ -33,7 +33,6 @@ using ILSpy; using ILSpy.AppEnv; using ILSpy.AssemblyTree; using ILSpy.TreeNodes; -using ILSpy.ViewModels; using ILSpy.Views; using NUnit.Framework; @@ -69,14 +68,9 @@ public class RemoveAssemblyContextMenuTests // e.g. method/type tree contexts. // Arrange — boot, locate the registered entry. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var registry = AppComposition.Current.GetExport(); - var removeEntry = registry.Entries - .Single(e => e.Metadata.Header == nameof(Resources._Remove)) - .Value; + var removeEntry = registry.GetEntry(nameof(Resources._Remove)); var asm = vm.AssemblyTreeModel.FindNode("System.Linq"); var typeNode = vm.AssemblyTreeModel.FindNode( @@ -110,10 +104,7 @@ public class RemoveAssemblyContextMenuTests public async Task Right_Click_Then_Remove_On_An_Assembly_Unloads_It_From_The_List() { // Arrange — boot, snapshot a sacrificial assembly + a survivor. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (window, vm) = await TestHarness.BootAsync(3); var pane = await window.WaitForComponent(); var registry = AppComposition.Current.GetExport(); @@ -152,10 +143,7 @@ public class RemoveAssemblyContextMenuTests // when right-clicking on a selected assembly node. // Arrange — boot, select an assembly node. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (window, vm) = await TestHarness.BootAsync(); await Waiters.WaitForAsync( () => window.GetVisualDescendants().OfType().Any()); var pane = await window.WaitForComponent(); diff --git a/ILSpy.Tests/AssemblyList/ShowMetadataTokensTests.cs b/ILSpy.Tests/AssemblyList/ShowMetadataTokensTests.cs index ca9c67511..f297a3840 100644 --- a/ILSpy.Tests/AssemblyList/ShowMetadataTokensTests.cs +++ b/ILSpy.Tests/AssemblyList/ShowMetadataTokensTests.cs @@ -27,8 +27,6 @@ using AwesomeAssertions; using ILSpy; using ILSpy.AppEnv; using ILSpy.TreeNodes; -using ILSpy.ViewModels; -using ILSpy.Views; using NUnit.Framework; @@ -46,10 +44,7 @@ public class ShowMetadataTokensTests // empty otherwise. var settings = AppComposition.Current.GetExport().DisplaySettings; - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); // Drill into a known type and grab a method node. var typeNode = vm.AssemblyTreeModel.FindNode( diff --git a/ILSpy.Tests/AssemblyList/TreeViewSettingsLiveReactivityTests.cs b/ILSpy.Tests/AssemblyList/TreeViewSettingsLiveReactivityTests.cs index 681eb422a..09fb5973d 100644 --- a/ILSpy.Tests/AssemblyList/TreeViewSettingsLiveReactivityTests.cs +++ b/ILSpy.Tests/AssemblyList/TreeViewSettingsLiveReactivityTests.cs @@ -29,8 +29,6 @@ using ICSharpCode.ILSpyX.TreeView; using ILSpy; using ILSpy.AppEnv; using ILSpy.TreeNodes; -using ILSpy.ViewModels; -using ILSpy.Views; using NUnit.Framework; @@ -48,10 +46,7 @@ public class TreeViewSettingsLiveReactivityTests // 'Text' fires when the setting flips. var settings = AppComposition.Current.GetExport().DisplaySettings; - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var typeNode = vm.AssemblyTreeModel.FindNode( "System.Linq", "System.Linq", "System.Linq.Enumerable"); diff --git a/ILSpy.Tests/AssemblyList/UseNestedNamespaceNodesGridVerification.cs b/ILSpy.Tests/AssemblyList/UseNestedNamespaceNodesGridVerification.cs index 42147ef5c..682735bc5 100644 --- a/ILSpy.Tests/AssemblyList/UseNestedNamespaceNodesGridVerification.cs +++ b/ILSpy.Tests/AssemblyList/UseNestedNamespaceNodesGridVerification.cs @@ -27,8 +27,6 @@ using ILSpy; using ILSpy.AppEnv; using ILSpy.AssemblyTree; using ILSpy.TreeNodes; -using ILSpy.ViewModels; -using ILSpy.Views; using NUnit.Framework; @@ -49,10 +47,7 @@ public class UseNestedNamespaceNodesGridVerification var settings = AppComposition.Current.GetExport().DisplaySettings; settings.UseNestedNamespaceNodes = false; - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (window, vm) = await TestHarness.BootAsync(3); var pane = await window.WaitForComponent(); var grid = pane.FindControl("TreeGrid")!; diff --git a/ILSpy.Tests/AssemblyList/UseNestedNamespaceNodesLiveTests.cs b/ILSpy.Tests/AssemblyList/UseNestedNamespaceNodesLiveTests.cs index 43e51e594..74c6226af 100644 --- a/ILSpy.Tests/AssemblyList/UseNestedNamespaceNodesLiveTests.cs +++ b/ILSpy.Tests/AssemblyList/UseNestedNamespaceNodesLiveTests.cs @@ -26,8 +26,6 @@ using AwesomeAssertions; using ILSpy; using ILSpy.AppEnv; using ILSpy.TreeNodes; -using ILSpy.ViewModels; -using ILSpy.Views; using NUnit.Framework; @@ -45,10 +43,7 @@ public class UseNestedNamespaceNodesLiveTests var settings = AppComposition.Current.GetExport().DisplaySettings; settings.UseNestedNamespaceNodes = false; - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); try { diff --git a/ILSpy.Tests/AssemblyList/UseNestedNamespaceNodesScreenshot.cs b/ILSpy.Tests/AssemblyList/UseNestedNamespaceNodesScreenshot.cs index 90cd4c7c8..4130ff88a 100644 --- a/ILSpy.Tests/AssemblyList/UseNestedNamespaceNodesScreenshot.cs +++ b/ILSpy.Tests/AssemblyList/UseNestedNamespaceNodesScreenshot.cs @@ -26,8 +26,6 @@ using Avalonia.Threading; using ILSpy; using ILSpy.AppEnv; using ILSpy.TreeNodes; -using ILSpy.ViewModels; -using ILSpy.Views; using NUnit.Framework; @@ -43,10 +41,7 @@ public class UseNestedNamespaceNodesScreenshot var settings = AppComposition.Current.GetExport().DisplaySettings; settings.UseNestedNamespaceNodes = false; - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (window, vm) = await TestHarness.BootAsync(3); var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq"); assemblyNode.IsExpanded = true; diff --git a/ILSpy.Tests/AssemblyList/UseNestedNamespaceNodesTests.cs b/ILSpy.Tests/AssemblyList/UseNestedNamespaceNodesTests.cs index cd6a91e3a..1043daabf 100644 --- a/ILSpy.Tests/AssemblyList/UseNestedNamespaceNodesTests.cs +++ b/ILSpy.Tests/AssemblyList/UseNestedNamespaceNodesTests.cs @@ -26,8 +26,6 @@ using AwesomeAssertions; using ILSpy; using ILSpy.AppEnv; using ILSpy.TreeNodes; -using ILSpy.ViewModels; -using ILSpy.Views; using NUnit.Framework; @@ -44,10 +42,7 @@ public class UseNestedNamespaceNodesTests // "System.IO" siblings the default flat mode produces. var settings = AppComposition.Current.GetExport().DisplaySettings; - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); try { @@ -88,10 +83,7 @@ public class UseNestedNamespaceNodesTests var settings = AppComposition.Current.GetExport().DisplaySettings; settings.UseNestedNamespaceNodes = false; - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq"); assemblyNode.Children.Clear(); diff --git a/ILSpy.Tests/Docking/LayoutPersistenceTests.cs b/ILSpy.Tests/Docking/LayoutPersistenceTests.cs index 2aef713e4..a03740c8c 100644 --- a/ILSpy.Tests/Docking/LayoutPersistenceTests.cs +++ b/ILSpy.Tests/Docking/LayoutPersistenceTests.cs @@ -30,7 +30,6 @@ using Dock.Model.Core; using ILSpy.AppEnv; using ILSpy.Docking; using ILSpy.ViewModels; -using ILSpy.Views; using NUnit.Framework; @@ -59,10 +58,7 @@ public class LayoutPersistenceTests [AvaloniaTest] public async Task SaveLayout_Followed_By_LoadLayout_Round_Trips_The_Live_Layout() { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + _ = await TestHarness.BootAsync(); var dockWorkspace = AppComposition.Current.GetExport(); var path = Path.Combine(Path.GetTempPath(), $"ILSpy.Layout.test.{System.Guid.NewGuid():N}.json"); @@ -127,10 +123,7 @@ public class LayoutPersistenceTests // `if (factory.MainTab is not { } main) return;` and the user sees an empty // editor when they click a tree node. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + _ = await TestHarness.BootAsync(); var dockWorkspace = AppComposition.Current.GetExport(); var path = Path.Combine(Path.GetTempPath(), $"ILSpy.Layout.test.{System.Guid.NewGuid():N}.json"); @@ -184,10 +177,7 @@ public class LayoutPersistenceTests // DocumentDock comes back with a single fresh MainTab; user's tree selection // re-projects through ShowSelectedNode as on first launch. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + _ = await TestHarness.BootAsync(); var dockWorkspace = AppComposition.Current.GetExport(); diff --git a/ILSpy.Tests/Docking/PreviewTabPromotionTests.cs b/ILSpy.Tests/Docking/PreviewTabPromotionTests.cs index b5ea16ee2..9849102f1 100644 --- a/ILSpy.Tests/Docking/PreviewTabPromotionTests.cs +++ b/ILSpy.Tests/Docking/PreviewTabPromotionTests.cs @@ -58,10 +58,7 @@ public class PreviewTabPromotionTests { // Open-in-new-tab tabs are explicit user intent — they should never be replaced // by subsequent tree-node selections, so they're born pinned (IsPreview=false). - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var typeNode = vm.AssemblyTreeModel.FindNode( "System.Linq", "System.Linq", "System.Linq.Enumerable"); @@ -81,10 +78,7 @@ public class PreviewTabPromotionTests // preview tab spawns at pin time. A fresh preview tab opens lazily later, when a // tree-selection change finds the active tab frozen — see // Selecting_A_Different_Node_After_Pin_Opens_A_New_Preview_Tab below. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var factory = (ILSpyDockFactory)vm.DockWorkspace.Factory; var previousMainTab = factory.MainTab!; @@ -124,10 +118,7 @@ public class PreviewTabPromotionTests // Pin holds the current tab's content. Selecting a different tree node while // the (now-pinned) tab is active must spawn a fresh preview tab beside it and // route the new content there — never overwrite the pinned tab. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var factory = (ILSpyDockFactory)vm.DockWorkspace.Factory; @@ -175,10 +166,7 @@ public class PreviewTabPromotionTests // Visual contract: the DocumentTabStripItem for the preview MainTab binds its // FontStyle to ContentTabPage.IsPreview via BoolToFontStyleConverter.Italic. // Pinned tabs and tool-pane tabs fall through to FontStyle.Normal. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (window, vm) = await TestHarness.BootAsync(); // Wait for the tab strip to realise its items. await Waiters.WaitForAsync(() => window.GetVisualDescendants().OfType().Any(), @@ -223,10 +211,7 @@ public class PreviewTabPromotionTests // tab so both MainTab and carve-out exist; close button becomes visible; verify // the pin's right edge stays inside the tab's right edge. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (window, vm) = await TestHarness.BootAsync(3); // Open a carve-out so close buttons become visible on both tabs. var typeNode = vm.AssemblyTreeModel.FindNode( @@ -297,10 +282,7 @@ public class PreviewTabPromotionTests // width if the title isn't capped. The pin button must still fit inside the tab's // right edge regardless of title length. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (window, vm) = await TestHarness.BootAsync(3); // Open carve-out so close button is visible. var typeNode = vm.AssemblyTreeModel.FindNode( @@ -365,10 +347,7 @@ public class PreviewTabPromotionTests [AvaloniaTest] public async Task Inline_Pin_Button_Appears_On_Preview_Tab_And_Pins_When_Clicked() { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (window, vm) = await TestHarness.BootAsync(); await Waiters.WaitForAsync(() => window.GetVisualDescendants().OfType().Any(), System.TimeSpan.FromSeconds(10)); @@ -409,10 +388,7 @@ public class PreviewTabPromotionTests { // The right-click context-menu Pin entry must be visible on the preview MainTab // and hidden (its IsVisible flips) on pinned carve-out tabs. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (window, vm) = await TestHarness.BootAsync(3); var typeNode = vm.AssemblyTreeModel.FindNode( "System.Linq", "System.Linq", "System.Linq.Enumerable"); vm.DockWorkspace.OpenNodeInNewTab(typeNode); @@ -442,10 +418,7 @@ public class PreviewTabPromotionTests // because the App.axaml setter used a static `FontStyle="Italic"` value instead // of the binding to IsPreview. A carve-out tab (IsPreview=false) must render // Normal, even when sitting next to a preview MainTab. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (window, vm) = await TestHarness.BootAsync(3); // Open a carve-out tab via the production code path. var typeNode = vm.AssemblyTreeModel.FindNode( @@ -497,10 +470,7 @@ public class PreviewTabPromotionTests // (Open / Save / Remove / Exit groups in the File menu, ...). With the menu now living // as a NativeMenu, separator rendering is the platform's job; this test pins only // the structural claim that separators are inserted between groups. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (window, _) = await TestHarness.BootAsync(); var nativeMenu = global::Avalonia.Controls.NativeMenu.GetMenu(window) ?? throw new System.InvalidOperationException("MainMenu.Attach should have set NativeMenu on the window"); @@ -519,10 +489,7 @@ public class PreviewTabPromotionTests // hover background. Both are plain Avalonia.Controls.Button instances; the close // button gets its visual identity from a ControlTheme applied by Dock's tab // template. Pin should copy that Theme rather than carry its own custom style. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (window, vm) = await TestHarness.BootAsync(3); // Open a carve-out so the close button is visible alongside the pin (single-tab // scenario hides the close button). @@ -561,10 +528,7 @@ public class PreviewTabPromotionTests // selections while any of those are active must spawn a fresh preview tab and // route the new content there. This test uses a carve-out as a stand-in for the // frozen-tab family — the IsWritablePreview check returns null for all three. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var factory = (ILSpyDockFactory)vm.DockWorkspace.Factory; diff --git a/ILSpy.Tests/Docking/SingletonDocumentTabTests.cs b/ILSpy.Tests/Docking/SingletonDocumentTabTests.cs index c28ec06c4..7deed3367 100644 --- a/ILSpy.Tests/Docking/SingletonDocumentTabTests.cs +++ b/ILSpy.Tests/Docking/SingletonDocumentTabTests.cs @@ -45,8 +45,7 @@ public class SingletonDocumentTabTests { static void Invoke(MainWindow window, string header) { - var registry = AppComposition.Current.GetExport(); - registry.Commands.Single(c => c.Metadata.Header == header).CreateExport().Value.Execute(null); + AppComposition.Current.GetExport().GetCommand(header).Execute(null); Avalonia.Threading.Dispatcher.UIThread.RunJobs(); } diff --git a/ILSpy.Tests/MainWindow/MainToolBarLayoutTests.cs b/ILSpy.Tests/MainWindow/MainToolBarLayoutTests.cs index 6cb1575ca..6acb01864 100644 --- a/ILSpy.Tests/MainWindow/MainToolBarLayoutTests.cs +++ b/ILSpy.Tests/MainWindow/MainToolBarLayoutTests.cs @@ -32,7 +32,6 @@ using ILSpy; using ILSpy.AppEnv; using ILSpy.Commands; using ILSpy.Search; -using ILSpy.ViewModels; using ILSpy.Views; using NUnit.Framework; @@ -92,10 +91,7 @@ public class MainToolBarLayoutTests // Avalonia port only had the key-binding wiring — this test pins the toolbar surface area // and verifies executing the button activates the search pane. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (window, _) = await TestHarness.BootAsync(); var toolbar = await window.WaitForComponent(); var rootPanel = toolbar.GetVisualDescendants().OfType() @@ -160,10 +156,7 @@ public class MainToolBarLayoutTests // the assembly-list combo each live inside a Grid wrapper (axaml lines 223 / 232 / 245), // so we tag those positions by descending into the Grid for the ComboBox name. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (window, _) = await TestHarness.BootAsync(); var toolbar = await window.WaitForComponent(); var rootPanel = toolbar.GetVisualDescendants().OfType() diff --git a/ILSpy.Tests/MainWindow/MainWindowTests.cs b/ILSpy.Tests/MainWindow/MainWindowTests.cs index cb8fbcefd..55cf164c4 100644 --- a/ILSpy.Tests/MainWindow/MainWindowTests.cs +++ b/ILSpy.Tests/MainWindow/MainWindowTests.cs @@ -137,10 +137,7 @@ public class MainWindowTests // transition to Indeterminate, and once done it must drop back to None. // Arrange — boot window, wait for assemblies, subscribe to TaskbarProgressService events. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var service = AppComposition.Current.GetExport(); var states = new List(); diff --git a/ILSpy.Tests/MainWindow/WindowMenuOpenDocumentsTests.cs b/ILSpy.Tests/MainWindow/WindowMenuOpenDocumentsTests.cs index 56a6c3473..6315b87b0 100644 --- a/ILSpy.Tests/MainWindow/WindowMenuOpenDocumentsTests.cs +++ b/ILSpy.Tests/MainWindow/WindowMenuOpenDocumentsTests.cs @@ -27,11 +27,9 @@ using AwesomeAssertions; using ICSharpCode.ILSpy.Properties; -using ILSpy.AppEnv; using ILSpy.Docking; using ILSpy.TreeNodes; using ILSpy.ViewModels; -using ILSpy.Views; using NUnit.Framework; @@ -49,10 +47,7 @@ public class WindowMenuOpenDocumentsTests // (NativeMenuBar's visual presenter renders these on Windows / Linux, and the // system menu bar projects them on macOS). - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (window, vm) = await TestHarness.BootAsync(3); var firstAsm = vm.AssemblyTreeModel.FindNode("System.Linq"); vm.AssemblyTreeModel.SelectNode(firstAsm); diff --git a/ILSpy.Tests/Metadata/DebugDirectoryChildNodesTests.cs b/ILSpy.Tests/Metadata/DebugDirectoryChildNodesTests.cs index d466349f5..8b0445b6b 100644 --- a/ILSpy.Tests/Metadata/DebugDirectoryChildNodesTests.cs +++ b/ILSpy.Tests/Metadata/DebugDirectoryChildNodesTests.cs @@ -23,11 +23,7 @@ using Avalonia.Headless.NUnit; using AwesomeAssertions; -using ILSpy.AppEnv; using ILSpy.Metadata; -using ILSpy.TreeNodes; -using ILSpy.ViewModels; -using ILSpy.Views; using NUnit.Framework; @@ -96,37 +92,26 @@ public class DebugDirectoryChildNodesTests static async Task GetDebugDirectoryForCoreLib() { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); - - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); - metadataNode.EnsureLazyChildren(); - var debugDir = metadataNode.Children.OfType().Single(); + var (_, vm) = await TestHarness.BootAsync(); + + var debugDir = vm.AssemblyTreeModel.FindCoreLib() + .GetChild() + .GetChild(); debugDir.EnsureLazyChildren(); return debugDir; } static async Task<(DebugDirectoryTreeNode node, int rawCount)> GetDebugDirectoryAndRawCountForCoreLib() { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); + var assemblyNode = vm.AssemblyTreeModel.FindCoreLib(); var module = (ICSharpCode.Decompiler.Metadata.PEFile)assemblyNode.LoadedAssembly.GetMetadataFileOrNull()!; int rawCount = module.Reader.ReadDebugDirectory().Length; - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); - metadataNode.EnsureLazyChildren(); - var debugDir = metadataNode.Children.OfType().Single(); + var debugDir = assemblyNode + .GetChild() + .GetChild(); debugDir.EnsureLazyChildren(); return (debugDir, rawCount); } diff --git a/ILSpy.Tests/Metadata/EmbeddedPdbTreeTests.cs b/ILSpy.Tests/Metadata/EmbeddedPdbTreeTests.cs index 4d70196d3..930e33d9a 100644 --- a/ILSpy.Tests/Metadata/EmbeddedPdbTreeTests.cs +++ b/ILSpy.Tests/Metadata/EmbeddedPdbTreeTests.cs @@ -25,13 +25,9 @@ using AwesomeAssertions; using ICSharpCode.ILSpy.Properties; -using ILSpy.AppEnv; -using ILSpy.Commands; using ILSpy.Metadata; using ILSpy.Metadata.DebugTables; using ILSpy.TreeNodes; -using ILSpy.ViewModels; -using ILSpy.Views; using NUnit.Framework; @@ -51,38 +47,21 @@ public class EmbeddedPdbTreeTests // own Tables subtree contains the PDB-only tables (Document and friends) — the // canonical entry point for browsing debug metadata in the live app. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); // ICSharpCode.Decompiler ships with an embedded portable PDB (no separate .pdb in // the build output) — use it as the fixture rather than the test DLL itself, which // the test SDK builds with portable + sidecar PDB. var testDllPath = typeof(ICSharpCode.Decompiler.Metadata.MetadataFile).Assembly.Location; - var registry = AppComposition.Current.GetExport(); - var openCommand = registry.Commands - .Single(c => c.Metadata.Header == nameof(Resources._Open)) - .CreateExport().Value; - openCommand.Execute(testDllPath); - - await Waiters.WaitForAsync(() => - vm.AssemblyTreeModel.AssemblyList!.GetAssemblies().Any(a => - string.Equals(a.FileName, testDllPath, System.StringComparison.OrdinalIgnoreCase))); - var loaded = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() - .First(a => string.Equals(a.FileName, testDllPath, System.StringComparison.OrdinalIgnoreCase)); - await loaded.GetLoadResultAsync(); + var loaded = await vm.OpenAssemblyAsync(testDllPath); var assemblyNode = vm.AssemblyTreeModel.FindNode(loaded.ShortName); - assemblyNode.EnsureLazyChildren(); // AssemblyTreeNode now surfaces two MetadataTreeNode children for assemblies with an // embedded PDB (host metadata + a top-level PDB sibling). This test cares about the // nested-under-DebugDirectory path, so pick the host metadata explicitly by label. - var metadataNode = assemblyNode.Children.OfType() - .Single(n => (string?)n.Text == Resources.Metadata); - metadataNode.EnsureLazyChildren(); + var metadataNode = assemblyNode.GetChild(n => (string?)n.Text == Resources.Metadata); - var debugDirectoryNode = metadataNode.Children.OfType().Single(); + var debugDirectoryNode = metadataNode.GetChild(); debugDirectoryNode.EnsureLazyChildren(); // The embedded-PDB sub-tree is a MetadataTreeNode whose own children include a @@ -92,8 +71,7 @@ public class EmbeddedPdbTreeTests "the test DLL ships with an embedded portable PDB, so the Debug Directory must expose its metadata as a nested MetadataTreeNode"); embeddedPdbNode!.Text.Should().Be("Debug Metadata (Embedded)"); - embeddedPdbNode.EnsureLazyChildren(); - var pdbTables = embeddedPdbNode.Children.OfType().Single(); + var pdbTables = embeddedPdbNode.GetChild(); pdbTables.EnsureLazyChildren(); pdbTables.Children.OfType().Should().NotBeEmpty( "the embedded PDB's Tables subtree must expose the Document table — the lead debug-only table that always has rows in a populated PDB"); @@ -107,24 +85,10 @@ public class EmbeddedPdbTreeTests // folder), not only nested under DebugDirectory. Without this, users have to drill // three levels deep to reach PDB metadata — same content, harder to find. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); var testDllPath = typeof(ICSharpCode.Decompiler.Metadata.MetadataFile).Assembly.Location; - var registry = AppComposition.Current.GetExport(); - var openCommand = registry.Commands - .Single(c => c.Metadata.Header == nameof(Resources._Open)) - .CreateExport().Value; - openCommand.Execute(testDllPath); - - await Waiters.WaitForAsync(() => - vm.AssemblyTreeModel.AssemblyList!.GetAssemblies().Any(a => - string.Equals(a.FileName, testDllPath, System.StringComparison.OrdinalIgnoreCase))); - var loaded = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() - .First(a => string.Equals(a.FileName, testDllPath, System.StringComparison.OrdinalIgnoreCase)); - await loaded.GetLoadResultAsync(); + var loaded = await vm.OpenAssemblyAsync(testDllPath); var assemblyNode = vm.AssemblyTreeModel.FindNode(loaded.ShortName); assemblyNode.EnsureLazyChildren(); diff --git a/ILSpy.Tests/Metadata/HeapTreeTests.cs b/ILSpy.Tests/Metadata/HeapTreeTests.cs index 707129ef0..d23ee8d2a 100644 --- a/ILSpy.Tests/Metadata/HeapTreeTests.cs +++ b/ILSpy.Tests/Metadata/HeapTreeTests.cs @@ -23,11 +23,7 @@ using Avalonia.Headless.NUnit; using AwesomeAssertions; -using ILSpy.AppEnv; using ILSpy.Metadata; -using ILSpy.TreeNodes; -using ILSpy.ViewModels; -using ILSpy.Views; using NUnit.Framework; @@ -45,15 +41,9 @@ public class HeapTreeTests // reachable from the Metadata folder regardless of whether the file's debug // metadata is present. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); + var metadataNode = vm.AssemblyTreeModel.FindCoreLib().GetChild(); metadataNode.EnsureLazyChildren(); metadataNode.Children.OfType().Should().ContainSingle(); @@ -69,17 +59,11 @@ public class HeapTreeTests // row through Avalonia's built-in row virtualisation, so unlike the Phase 1 text // dump there's no preview cap and no truncation footer. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); - metadataNode.EnsureLazyChildren(); - var heapNode = metadataNode.Children.OfType().Single(); + var heapNode = vm.AssemblyTreeModel.FindCoreLib() + .GetChild() + .GetChild(); vm.AssemblyTreeModel.SelectNode(heapNode); var tab = await vm.DockWorkspace.WaitForMetadataTabAsync(); @@ -93,17 +77,11 @@ public class HeapTreeTests [AvaloniaTest] public async Task GuidHeapTreeNode_Opens_Grid_Tab_With_Index_Length_Value_Columns() { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); - - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); - metadataNode.EnsureLazyChildren(); - var heapNode = metadataNode.Children.OfType().Single(); + var (_, vm) = await TestHarness.BootAsync(); + + var heapNode = vm.AssemblyTreeModel.FindCoreLib() + .GetChild() + .GetChild(); vm.AssemblyTreeModel.SelectNode(heapNode); var tab = await vm.DockWorkspace.WaitForMetadataTabAsync(); @@ -118,17 +96,11 @@ public class HeapTreeTests [AvaloniaTest] public async Task BlobHeapTreeNode_Opens_Grid_Tab_With_Offset_Length_Value_Columns() { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); - - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); - metadataNode.EnsureLazyChildren(); - var heapNode = metadataNode.Children.OfType().Single(); + var (_, vm) = await TestHarness.BootAsync(); + + var heapNode = vm.AssemblyTreeModel.FindCoreLib() + .GetChild() + .GetChild(); vm.AssemblyTreeModel.SelectNode(heapNode); var tab = await vm.DockWorkspace.WaitForMetadataTabAsync(); diff --git a/ILSpy.Tests/Metadata/HideEmptyMetadataTablesTests.cs b/ILSpy.Tests/Metadata/HideEmptyMetadataTablesTests.cs index c9b09105b..f99505333 100644 --- a/ILSpy.Tests/Metadata/HideEmptyMetadataTablesTests.cs +++ b/ILSpy.Tests/Metadata/HideEmptyMetadataTablesTests.cs @@ -29,7 +29,6 @@ using ILSpy.AppEnv; using ILSpy.Metadata; using ILSpy.TreeNodes; using ILSpy.ViewModels; -using ILSpy.Views; using NUnit.Framework; @@ -47,10 +46,7 @@ public class HideEmptyMetadataTablesTests var settings = AppComposition.Current.GetExport().DisplaySettings; settings.HideEmptyMetadataTables = true; - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); var coreLibName = typeof(object).Assembly.GetName().Name!; var tables = LoadTablesNode(vm, coreLibName, out var metadata); @@ -71,10 +67,7 @@ public class HideEmptyMetadataTablesTests settings.HideEmptyMetadataTables = false; try { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); var coreLibName = typeof(object).Assembly.GetName().Name!; var tables = LoadTablesNode(vm, coreLibName, out var _); @@ -94,10 +87,9 @@ public class HideEmptyMetadataTablesTests out System.Reflection.Metadata.MetadataReader metadata) { var assemblyNode = vm.AssemblyTreeModel.FindNode(assemblyName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); - metadataNode.EnsureLazyChildren(); - var tables = metadataNode.Children.OfType().Single(); + var tables = assemblyNode + .GetChild() + .GetChild(); tables.EnsureLazyChildren(); metadata = assemblyNode.LoadedAssembly.GetMetadataFileOrNull()!.Metadata; return tables; diff --git a/ILSpy.Tests/Metadata/HideEmptyMetadataTablesUiBindingTests.cs b/ILSpy.Tests/Metadata/HideEmptyMetadataTablesUiBindingTests.cs index 2fb44dfe5..f51193a00 100644 --- a/ILSpy.Tests/Metadata/HideEmptyMetadataTablesUiBindingTests.cs +++ b/ILSpy.Tests/Metadata/HideEmptyMetadataTablesUiBindingTests.cs @@ -28,7 +28,6 @@ using ILSpy.AppEnv; using ILSpy.Metadata; using ILSpy.TreeNodes; using ILSpy.ViewModels; -using ILSpy.Views; using NUnit.Framework; @@ -46,10 +45,7 @@ public class HideEmptyMetadataTablesUiBindingTests // SessionSettings.HideEmptyMetadataTables (an orphaned parallel setting). var settings = AppComposition.Current.GetExport(); - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); var coreLibName = typeof(object).Assembly.GetName().Name!; @@ -80,10 +76,9 @@ public class HideEmptyMetadataTablesUiBindingTests var assemblyNode = vm.AssemblyTreeModel.FindNode(assemblyName); assemblyNode.Children.Clear(); assemblyNode.LazyLoading = true; - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); - metadataNode.EnsureLazyChildren(); - var tables = metadataNode.Children.OfType().Single(); + var tables = assemblyNode + .GetChild() + .GetChild(); tables.EnsureLazyChildren(); return tables.Children.OfType().Count(); } diff --git a/ILSpy.Tests/Metadata/MetadataDisablesLanguageSwitchingTests.cs b/ILSpy.Tests/Metadata/MetadataDisablesLanguageSwitchingTests.cs index 7da3a00c8..202c3022e 100644 --- a/ILSpy.Tests/Metadata/MetadataDisablesLanguageSwitchingTests.cs +++ b/ILSpy.Tests/Metadata/MetadataDisablesLanguageSwitchingTests.cs @@ -26,11 +26,9 @@ using Avalonia.VisualTree; using AwesomeAssertions; using ILSpy; -using ILSpy.AppEnv; using ILSpy.Metadata; using ILSpy.TreeNodes; using ILSpy.ViewModels; -using ILSpy.Views; using NUnit.Framework; @@ -88,10 +86,7 @@ public class MetadataDisablesLanguageSwitchingTests // DockWorkspace.ActiveContentTabPage.SupportsLanguageSwitching must flip to false. // Selecting a decompilable type after that must flip it back to true. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (window, vm) = await TestHarness.BootAsync(); var toolbar = await window.WaitForComponent(); var languageCombo = toolbar.GetVisualDescendants().OfType() @@ -103,11 +98,9 @@ public class MetadataDisablesLanguageSwitchingTests // Navigate to a metadata node (DOS Header is a stable choice — every PE has one). var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); - metadataNode.EnsureLazyChildren(); - var dosHeader = metadataNode.Children.OfType().Single(); + var dosHeader = vm.AssemblyTreeModel.FindCoreLib() + .GetChild() + .GetChild(); vm.AssemblyTreeModel.SelectNode(dosHeader); await Waiters.WaitForAsync( diff --git a/ILSpy.Tests/Metadata/MetadataFilterTests.cs b/ILSpy.Tests/Metadata/MetadataFilterTests.cs index b36576e84..837576e54 100644 --- a/ILSpy.Tests/Metadata/MetadataFilterTests.cs +++ b/ILSpy.Tests/Metadata/MetadataFilterTests.cs @@ -28,10 +28,8 @@ using Avalonia.VisualTree; using AwesomeAssertions; -using ILSpy.AppEnv; using ILSpy.Metadata; using ILSpy.Metadata.CorTables; -using ILSpy.TreeNodes; using ILSpy.ViewModels; using ILSpy.Views; @@ -295,19 +293,12 @@ public class MetadataFilterTests // assert on the live DataGridCollectionView's Count (what the user actually sees on // screen) — not on the predicate evaluated over Items, which can pass even when the // CollectionView's Filter is silently null. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); - - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); - metadataNode.EnsureLazyChildren(); - var tablesNode = metadataNode.Children.OfType().Single(); - tablesNode.EnsureLazyChildren(); - var typeDefNode = tablesNode.Children.OfType().Single(); + var (window, vm) = await TestHarness.BootAsync(); + + var typeDefNode = vm.AssemblyTreeModel.FindCoreLib() + .GetChild() + .GetChild() + .GetChild(); vm.AssemblyTreeModel.SelectNode(typeDefNode); var tab = await vm.DockWorkspace.WaitForMetadataTabAsync(); diff --git a/ILSpy.Tests/Metadata/MetadataNodeIconParityTests.cs b/ILSpy.Tests/Metadata/MetadataNodeIconParityTests.cs index 6fd073a23..228777a37 100644 --- a/ILSpy.Tests/Metadata/MetadataNodeIconParityTests.cs +++ b/ILSpy.Tests/Metadata/MetadataNodeIconParityTests.cs @@ -24,11 +24,7 @@ using Avalonia.Media; using AwesomeAssertions; -using ILSpy.AppEnv; using ILSpy.Metadata; -using ILSpy.TreeNodes; -using ILSpy.ViewModels; -using ILSpy.Views; using NUnit.Framework; @@ -47,15 +43,9 @@ public class MetadataNodeIconParityTests // restored per-node parity. // Arrange — boot, expand CoreLib's Metadata folder. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); - - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); + var (_, vm) = await TestHarness.BootAsync(); + + var metadataNode = vm.AssemblyTreeModel.FindCoreLib().GetChild(); metadataNode.EnsureLazyChildren(); var children = metadataNode.Children; diff --git a/ILSpy.Tests/Metadata/MetadataProtocolHandlerDrillDownTests.cs b/ILSpy.Tests/Metadata/MetadataProtocolHandlerDrillDownTests.cs index b1c70c1d1..9a34c4c0a 100644 --- a/ILSpy.Tests/Metadata/MetadataProtocolHandlerDrillDownTests.cs +++ b/ILSpy.Tests/Metadata/MetadataProtocolHandlerDrillDownTests.cs @@ -28,7 +28,6 @@ using AwesomeAssertions; using ILSpy.AppEnv; using ILSpy.Metadata; using ILSpy.Metadata.CorTables; -using ILSpy.TreeNodes; using ILSpy.ViewModels; using ILSpy.Views; @@ -117,18 +116,11 @@ public class MetadataProtocolHandlerDrillDownTests static async Task GetMetadataTreeNodeForCoreLib() { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); - - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); + var (_, vm) = await TestHarness.BootAsync(); + + var metadataNode = vm.AssemblyTreeModel.FindCoreLib().GetChild(); // Drill-down needs the children realised; EnsureLazyChildren cascades only one level. - metadataNode.EnsureLazyChildren(); - var tablesNode = metadataNode.Children.OfType().Single(); + var tablesNode = metadataNode.GetChild(); tablesNode.EnsureLazyChildren(); return metadataNode; } @@ -137,8 +129,7 @@ public class MetadataProtocolHandlerDrillDownTests { var metadataNode = await GetMetadataTreeNodeForCoreLib(); var vm = (MainWindowViewModel)AppComposition.Current.GetExport().DataContext!; - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); + var assemblyNode = vm.AssemblyTreeModel.FindCoreLib(); var module = assemblyNode.LoadedAssembly.GetMetadataFileOrNull()!; // The handler is exported as IProtocolHandler, not as itself — fish out the // MetadataProtocolHandler concrete impl from the contract list. diff --git a/ILSpy.Tests/Metadata/MetadataRowActivationTests.cs b/ILSpy.Tests/Metadata/MetadataRowActivationTests.cs index bedde39b7..a33aaa4ca 100644 --- a/ILSpy.Tests/Metadata/MetadataRowActivationTests.cs +++ b/ILSpy.Tests/Metadata/MetadataRowActivationTests.cs @@ -47,19 +47,12 @@ public class MetadataRowActivationTests // RowActivated, and confirm the assembly tree's TypeTreeNode for System.Object // is now selected — that's the gateway to the decompiler view. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); - - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); - metadataNode.EnsureLazyChildren(); - var tablesNode = metadataNode.Children.OfType().Single(); - tablesNode.EnsureLazyChildren(); - var typeDefNode = tablesNode.Children.OfType().Single(); + var (_, vm) = await TestHarness.BootAsync(); + + var typeDefNode = vm.AssemblyTreeModel.FindCoreLib() + .GetChild() + .GetChild() + .GetChild(); vm.AssemblyTreeModel.SelectNode(typeDefNode); var tab = await vm.DockWorkspace.WaitForMetadataTabAsync(); @@ -84,19 +77,12 @@ public class MetadataRowActivationTests // Shift+double-click on a metadata row should NOT replace the active tab — it should // open a brand-new decompiler tab containing the row's entity. The single-tab-reuse // path stays put. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); - - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); - metadataNode.EnsureLazyChildren(); - var tablesNode = metadataNode.Children.OfType().Single(); - tablesNode.EnsureLazyChildren(); - var typeDefNode = tablesNode.Children.OfType().Single(); + var (_, vm) = await TestHarness.BootAsync(); + + var typeDefNode = vm.AssemblyTreeModel.FindCoreLib() + .GetChild() + .GetChild() + .GetChild(); vm.AssemblyTreeModel.SelectNode(typeDefNode); var metadataTab = await vm.DockWorkspace.WaitForMetadataTabAsync(); diff --git a/ILSpy.Tests/Metadata/MetadataTabSessionRestoreTests.cs b/ILSpy.Tests/Metadata/MetadataTabSessionRestoreTests.cs index 69ec2961f..dd00e4aba 100644 --- a/ILSpy.Tests/Metadata/MetadataTabSessionRestoreTests.cs +++ b/ILSpy.Tests/Metadata/MetadataTabSessionRestoreTests.cs @@ -25,12 +25,9 @@ using AwesomeAssertions; using ILSpy; using ILSpy.AppEnv; -using ILSpy.Commands; using ILSpy.Metadata; using ILSpy.Metadata.CorTables; using ILSpy.TreeNodes; -using ILSpy.ViewModels; -using ILSpy.Views; using NUnit.Framework; @@ -46,19 +43,12 @@ public class MetadataTabSessionRestoreTests // restart (or a tree-path lookup) can land back on the same node. The metadata // sub-tree's ToString() values must be stable for that round-trip to work. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); - metadataNode.EnsureLazyChildren(); - var tablesNode = metadataNode.Children.OfType().Single(); - tablesNode.EnsureLazyChildren(); - var typeDefNode = tablesNode.Children.OfType().Single(); + var typeDefNode = vm.AssemblyTreeModel.FindCoreLib() + .GetChild() + .GetChild() + .GetChild(); vm.AssemblyTreeModel.SelectNode(typeDefNode); @@ -82,40 +72,21 @@ public class MetadataTabSessionRestoreTests // The path-restore must walk through both Metadata folders and land on a debug-only // table. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); var testDllPath = typeof(ICSharpCode.Decompiler.Metadata.MetadataFile).Assembly.Location; - var registry = AppComposition.Current.GetExport(); - var openCommand = registry.Commands - .Single(c => c.Metadata.Header == nameof(ICSharpCode.ILSpy.Properties.Resources._Open)) - .CreateExport().Value; - openCommand.Execute(testDllPath); - - await Waiters.WaitForAsync(() => - vm.AssemblyTreeModel.AssemblyList!.GetAssemblies().Any(a => - string.Equals(a.FileName, testDllPath, System.StringComparison.OrdinalIgnoreCase))); - var loaded = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() - .First(a => string.Equals(a.FileName, testDllPath, System.StringComparison.OrdinalIgnoreCase)); - await loaded.GetLoadResultAsync(); + var loaded = await vm.OpenAssemblyAsync(testDllPath); var assemblyNode = vm.AssemblyTreeModel.FindNode(loaded.ShortName); - assemblyNode.EnsureLazyChildren(); // AssemblyTreeNode surfaces the embedded PDB's metadata as a second top-level // MetadataTreeNode sibling, so disambiguate by label — this test exercises the // nested-under-DebugDirectory path. - var metadataNode = assemblyNode.Children.OfType() - .Single(n => (string?)n.Text == ICSharpCode.ILSpy.Properties.Resources.Metadata); - metadataNode.EnsureLazyChildren(); - var debugDirectoryNode = metadataNode.Children.OfType().Single(); - debugDirectoryNode.EnsureLazyChildren(); - var embeddedPdbNode = debugDirectoryNode.Children.OfType().Single(); - embeddedPdbNode.EnsureLazyChildren(); - var pdbTables = embeddedPdbNode.Children.OfType().Single(); - pdbTables.EnsureLazyChildren(); - var documentTable = pdbTables.Children.OfType().Single(); + var documentTable = assemblyNode + .GetChild(n => (string?)n.Text == ICSharpCode.ILSpy.Properties.Resources.Metadata) + .GetChild() + .GetChild() + .GetChild() + .GetChild(); vm.AssemblyTreeModel.SelectNode(documentTable); diff --git a/ILSpy.Tests/Metadata/MetadataTablesTreeTests.cs b/ILSpy.Tests/Metadata/MetadataTablesTreeTests.cs index 08bc801f3..8dc5e302e 100644 --- a/ILSpy.Tests/Metadata/MetadataTablesTreeTests.cs +++ b/ILSpy.Tests/Metadata/MetadataTablesTreeTests.cs @@ -24,11 +24,7 @@ using Avalonia.Headless.NUnit; using AwesomeAssertions; -using ILSpy.AppEnv; using ILSpy.Metadata; -using ILSpy.TreeNodes; -using ILSpy.ViewModels; -using ILSpy.Views; using NUnit.Framework; @@ -45,15 +41,9 @@ public class MetadataTablesTreeTests // Phase 1 lazy-loads its children with a generic placeholder per table; Phase 1e // replaces them with per-table typed leaves carrying the actual row data. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); + var metadataNode = vm.AssemblyTreeModel.FindCoreLib().GetChild(); metadataNode.EnsureLazyChildren(); var tablesNode = metadataNode.Children.OfType().Single(); @@ -68,17 +58,11 @@ public class MetadataTablesTreeTests [AvaloniaTest] public async Task MetadataTablesTreeNode_LazyLoads_One_Child_Per_NonEmpty_Table() { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); - - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); - metadataNode.EnsureLazyChildren(); - var tablesNode = metadataNode.Children.OfType().Single(); + var (_, vm) = await TestHarness.BootAsync(); + + var tablesNode = vm.AssemblyTreeModel.FindCoreLib() + .GetChild() + .GetChild(); tablesNode.EnsureLazyChildren(); var tableChildren = tablesNode.Children.OfType().ToList(); @@ -93,17 +77,11 @@ public class MetadataTablesTreeTests [AvaloniaTest] public async Task MetadataTablesTreeNode_Decompiles_To_A_Per_Table_Row_Count_Summary() { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); - - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); - metadataNode.EnsureLazyChildren(); - var tablesNode = metadataNode.Children.OfType().Single(); + var (_, vm) = await TestHarness.BootAsync(); + + var tablesNode = vm.AssemblyTreeModel.FindCoreLib() + .GetChild() + .GetChild(); vm.AssemblyTreeModel.SelectNode(tablesNode); var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); diff --git a/ILSpy.Tests/Metadata/MetadataTokenNavigationTests.cs b/ILSpy.Tests/Metadata/MetadataTokenNavigationTests.cs index 65ebcae70..59cbdace1 100644 --- a/ILSpy.Tests/Metadata/MetadataTokenNavigationTests.cs +++ b/ILSpy.Tests/Metadata/MetadataTokenNavigationTests.cs @@ -24,10 +24,8 @@ using Avalonia.Headless.NUnit; using AwesomeAssertions; -using ILSpy.AppEnv; using ILSpy.Metadata; using ILSpy.Metadata.CorTables; -using ILSpy.TreeNodes; using ILSpy.ViewModels; using ILSpy.Views; @@ -45,19 +43,12 @@ public class MetadataTokenNavigationTests // must dispatch through the docking host to the TypeRef / TypeDef / TypeSpec table // the token belongs to and scroll the receiving grid to the referenced row. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); - metadataNode.EnsureLazyChildren(); - var tablesNode = metadataNode.Children.OfType().Single(); - tablesNode.EnsureLazyChildren(); - var typeDefNode = tablesNode.Children.OfType().Single(); + var typeDefNode = vm.AssemblyTreeModel.FindCoreLib() + .GetChild() + .GetChild() + .GetChild(); vm.AssemblyTreeModel.SelectNode(typeDefNode); var tab = await vm.DockWorkspace.WaitForMetadataTabAsync(); @@ -94,19 +85,12 @@ public class MetadataTokenNavigationTests // entry. It should fire the same NavigateToCellRequested path as the hyperlink-style // click, with the focused (or last-clicked) token cell as the target. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); - - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); - metadataNode.EnsureLazyChildren(); - var tablesNode = metadataNode.Children.OfType().Single(); - tablesNode.EnsureLazyChildren(); - var typeDefNode = tablesNode.Children.OfType().Single(); + var (window, vm) = await TestHarness.BootAsync(); + + var typeDefNode = vm.AssemblyTreeModel.FindCoreLib() + .GetChild() + .GetChild() + .GetChild(); vm.AssemblyTreeModel.SelectNode(typeDefNode); var tab = await vm.DockWorkspace.WaitForMetadataTabAsync(); diff --git a/ILSpy.Tests/Metadata/MetadataTreeShapeTests.cs b/ILSpy.Tests/Metadata/MetadataTreeShapeTests.cs index e70cc0249..8ce187311 100644 --- a/ILSpy.Tests/Metadata/MetadataTreeShapeTests.cs +++ b/ILSpy.Tests/Metadata/MetadataTreeShapeTests.cs @@ -23,11 +23,8 @@ using Avalonia.Headless.NUnit; using AwesomeAssertions; -using ILSpy.AppEnv; using ILSpy.Metadata; using ILSpy.TreeNodes; -using ILSpy.ViewModels; -using ILSpy.Views; using NUnit.Framework; @@ -45,17 +42,12 @@ public class MetadataTreeShapeTests // MetadataTreeNode instance ready to lazy-load its children. // Arrange — boot, wait for assemblies, expand CoreLib (always a PE). - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); + var assemblyNode = vm.AssemblyTreeModel.FindCoreLib(); // Assert — exactly one MetadataTreeNode child and it precedes the References folder. - var metadataNode = assemblyNode.Children.OfType().Single(); + var metadataNode = assemblyNode.GetChild(); var children = assemblyNode.Children.ToList(); children.IndexOf(metadataNode).Should().BeLessThan( children.IndexOf(assemblyNode.Children.OfType().Single()), @@ -71,15 +63,9 @@ public class MetadataTreeShapeTests // pipeline rather than introducing a new tab type for v1. // Arrange — boot, find the Metadata folder under CoreLib. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); + var metadataNode = vm.AssemblyTreeModel.FindCoreLib().GetChild(); // Act — select it through the helper that mirrors a click (loads lazy children + assigns // SelectedItem), wait for the decompile to finish. diff --git a/ILSpy.Tests/Metadata/PEHeaderTreeTests.cs b/ILSpy.Tests/Metadata/PEHeaderTreeTests.cs index 0cfa68f4e..4c3cc4bc2 100644 --- a/ILSpy.Tests/Metadata/PEHeaderTreeTests.cs +++ b/ILSpy.Tests/Metadata/PEHeaderTreeTests.cs @@ -23,11 +23,7 @@ using Avalonia.Headless.NUnit; using AwesomeAssertions; -using ILSpy.AppEnv; using ILSpy.Metadata; -using ILSpy.TreeNodes; -using ILSpy.ViewModels; -using ILSpy.Views; using NUnit.Framework; @@ -44,15 +40,9 @@ public class PEHeaderTreeTests // user shifting between hosts sees the same tree shape. Each node is the entry point // to a header / table view; Phase 1 renders text, Phase 2 swaps to a DataGrid. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); + var metadataNode = vm.AssemblyTreeModel.FindCoreLib().GetChild(); metadataNode.EnsureLazyChildren(); metadataNode.Children.OfType().Should().ContainSingle(); @@ -78,17 +68,11 @@ public class PEHeaderTreeTests // row per field, columns reflected from the Entry shape (Member / Offset / Size / // Value / Meaning). - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); - metadataNode.EnsureLazyChildren(); - var dosNode = metadataNode.Children.OfType().Single(); + var dosNode = vm.AssemblyTreeModel.FindCoreLib() + .GetChild() + .GetChild(); vm.AssemblyTreeModel.SelectNode(dosNode); var tab = await vm.DockWorkspace.WaitForMetadataTabAsync(); @@ -113,17 +97,12 @@ public class PEHeaderTreeTests // decompiler tab but never restored it as the user-visible page. Both round-trip // directions should swap the active dockable to the right tab type. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); - metadataNode.EnsureLazyChildren(); - var dosNode = metadataNode.Children.OfType().Single(); + var assemblyNode = vm.AssemblyTreeModel.FindCoreLib(); + var dosNode = assemblyNode + .GetChild() + .GetChild(); // Step 1 — pick a regular entity node (the assembly itself decompiles to its // AssemblyDef metadata via the decompiler text path). Decompiler tab should be active. @@ -151,17 +130,12 @@ public class PEHeaderTreeTests // the docking host can lazily create and activate the decompiler tab even when the // document area starts out holding only the metadata grid. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); - metadataNode.EnsureLazyChildren(); - var dosNode = metadataNode.Children.OfType().Single(); + var assemblyNode = vm.AssemblyTreeModel.FindCoreLib(); + var dosNode = assemblyNode + .GetChild() + .GetChild(); vm.AssemblyTreeModel.SelectNode(dosNode); await vm.DockWorkspace.WaitForMetadataTabAsync(); @@ -183,18 +157,12 @@ public class PEHeaderTreeTests // previous inner view rendered (the user would see a "Decompiling" spinner that // never goes away because the prior decompiler tab is still on screen). - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); - metadataNode.EnsureLazyChildren(); - var dosNode = metadataNode.Children.OfType().Single(); - var coffNode = metadataNode.Children.OfType().Single(); + var assemblyNode = vm.AssemblyTreeModel.FindCoreLib(); + var metadataNode = assemblyNode.GetChild(); + var dosNode = metadataNode.GetChild(); + var coffNode = metadataNode.GetChild(); var factoryFactory = (global::ILSpy.Docking.ILSpyDockFactory)vm.DockWorkspace.Factory; var documents = factoryFactory.Documents!; var mainTab = factoryFactory.MainTab!; @@ -228,19 +196,12 @@ public class PEHeaderTreeTests // decompiler tab reuses itself across tree-node selections. Otherwise every click // piles up a new dockable and the tab strip grows without bound. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); - metadataNode.EnsureLazyChildren(); + var metadataNode = vm.AssemblyTreeModel.FindCoreLib().GetChild(); - var dosNode = metadataNode.Children.OfType().Single(); - var coffNode = metadataNode.Children.OfType().Single(); + var dosNode = metadataNode.GetChild(); + var coffNode = metadataNode.GetChild(); vm.AssemblyTreeModel.SelectNode(dosNode); var firstTab = await vm.DockWorkspace.WaitForMetadataTabAsync(); @@ -263,17 +224,11 @@ public class PEHeaderTreeTests [AvaloniaTest] public async Task CoffHeaderTreeNode_Opens_A_DataGrid_Tab_With_Machine_And_Characteristics_Rows() { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); - - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); - metadataNode.EnsureLazyChildren(); - var coffNode = metadataNode.Children.OfType().Single(); + var (_, vm) = await TestHarness.BootAsync(); + + var coffNode = vm.AssemblyTreeModel.FindCoreLib() + .GetChild() + .GetChild(); vm.AssemblyTreeModel.SelectNode(coffNode); var tab = await vm.DockWorkspace.WaitForMetadataTabAsync(); diff --git a/ILSpy.Tests/Metadata/ShowInMetadataContextMenuTests.cs b/ILSpy.Tests/Metadata/ShowInMetadataContextMenuTests.cs index dc6f19f9a..bfe503d28 100644 --- a/ILSpy.Tests/Metadata/ShowInMetadataContextMenuTests.cs +++ b/ILSpy.Tests/Metadata/ShowInMetadataContextMenuTests.cs @@ -27,14 +27,9 @@ using AwesomeAssertions; using ICSharpCode.Decompiler.TypeSystem; using ILSpy; -using ILSpy.AppEnv; using ILSpy.Commands; using ILSpy.Metadata; -using ILSpy.Metadata.CorTables; using ILSpy.TextView; -using ILSpy.TreeNodes; -using ILSpy.ViewModels; -using ILSpy.Views; using NUnit.Framework; @@ -51,13 +46,9 @@ public class ShowInMetadataContextMenuTests // verify the entry's IsVisible/Execute drives the dock workspace into the right // metadata table for the entity's MetadataToken.kind. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); + var assemblyNode = vm.AssemblyTreeModel.FindCoreLib(); // Resolve System.Object's IEntity via the type system, since the entry only acts // on IEntity-bearing references. diff --git a/ILSpy.Tests/Metadata/TypedMetadataTableTreeTests.cs b/ILSpy.Tests/Metadata/TypedMetadataTableTreeTests.cs index 60ebf05d6..ca70ec5a4 100644 --- a/ILSpy.Tests/Metadata/TypedMetadataTableTreeTests.cs +++ b/ILSpy.Tests/Metadata/TypedMetadataTableTreeTests.cs @@ -24,12 +24,9 @@ using Avalonia.Headless.NUnit; using AwesomeAssertions; -using ILSpy.AppEnv; using ILSpy.Metadata; using ILSpy.Metadata.CorTables; using ILSpy.TreeNodes; -using ILSpy.ViewModels; -using ILSpy.Views; using NUnit.Framework; @@ -46,18 +43,12 @@ public class TypedMetadataTableTreeTests // tab whose Items list mirrors the table's rows; a known reference's display name // must show up on at least one row. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); - var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq"); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); - metadataNode.EnsureLazyChildren(); - var tablesNode = metadataNode.Children.OfType().Single(); - tablesNode.EnsureLazyChildren(); - var assemblyRefNode = tablesNode.Children.OfType().Single(); + var assemblyRefNode = vm.AssemblyTreeModel.FindNode("System.Linq") + .GetChild() + .GetChild() + .GetChild(); assemblyRefNode.Kind.Should().Be(TableIndex.AssemblyRef); assemblyRefNode.RowCount.Should().BeGreaterThan(0); @@ -76,19 +67,12 @@ public class TypedMetadataTableTreeTests [AvaloniaTest] public async Task TypeDefTableTreeNode_Opens_A_Grid_Including_System_Object() { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); - - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); - metadataNode.EnsureLazyChildren(); - var tablesNode = metadataNode.Children.OfType().Single(); - tablesNode.EnsureLazyChildren(); - var typeDefNode = tablesNode.Children.OfType().Single(); + var (_, vm) = await TestHarness.BootAsync(); + + var typeDefNode = vm.AssemblyTreeModel.FindCoreLib() + .GetChild() + .GetChild() + .GetChild(); typeDefNode.Kind.Should().Be(TableIndex.TypeDef); typeDefNode.RowCount.Should().BeGreaterThan(1000); @@ -107,17 +91,11 @@ public class TypedMetadataTableTreeTests // Field / MethodDef are by far the largest tables in any real assembly. The typed // leaves should report the right kind and expose row data through the grid view. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); - - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); - metadataNode.EnsureLazyChildren(); - var tablesNode = metadataNode.Children.OfType().Single(); + var (_, vm) = await TestHarness.BootAsync(); + + var tablesNode = vm.AssemblyTreeModel.FindCoreLib() + .GetChild() + .GetChild(); tablesNode.EnsureLazyChildren(); tablesNode.Children.OfType().Should().ContainSingle(); @@ -137,19 +115,12 @@ public class TypedMetadataTableTreeTests [AvaloniaTest] public async Task ModuleTableTreeNode_Opens_A_Grid_With_The_Single_Module_Row() { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); - - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); - assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); - metadataNode.EnsureLazyChildren(); - var tablesNode = metadataNode.Children.OfType().Single(); - tablesNode.EnsureLazyChildren(); - var moduleNode = tablesNode.Children.OfType().Single(); + var (_, vm) = await TestHarness.BootAsync(); + + var moduleNode = vm.AssemblyTreeModel.FindCoreLib() + .GetChild() + .GetChild() + .GetChild(); moduleNode.RowCount.Should().Be(1, "Module is a one-row table by spec"); diff --git a/ILSpy.Tests/Navigation/BrowseBackForwardCommandTests.cs b/ILSpy.Tests/Navigation/BrowseBackForwardCommandTests.cs index a95fc7d75..178e5d44d 100644 --- a/ILSpy.Tests/Navigation/BrowseBackForwardCommandTests.cs +++ b/ILSpy.Tests/Navigation/BrowseBackForwardCommandTests.cs @@ -79,10 +79,7 @@ public class BrowseBackForwardCommandTests // Execute pops one entry. Verifies via the live MenuItem that ends up in the View menu. // Arrange — boot the window, load assemblies, expand Enumerable, capture two methods. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (window, vm) = await TestHarness.BootAsync(3); var typeNode = vm.AssemblyTreeModel.FindNode( "System.Linq", "System.Linq", "System.Linq.Enumerable"); diff --git a/ILSpy.Tests/Navigation/NavigationTests.cs b/ILSpy.Tests/Navigation/NavigationTests.cs index 247118911..17b19af2a 100644 --- a/ILSpy.Tests/Navigation/NavigationTests.cs +++ b/ILSpy.Tests/Navigation/NavigationTests.cs @@ -25,10 +25,7 @@ using Avalonia.VisualTree; using AwesomeAssertions; -using ILSpy.AppEnv; using ILSpy.TreeNodes; -using ILSpy.ViewModels; -using ILSpy.Views; using NUnit.Framework; @@ -46,10 +43,7 @@ public class NavigationTests // Arrange — boot the window, wait for assemblies, expand Enumerable, capture two methods // that we'll bounce between. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var typeNode = vm.AssemblyTreeModel.FindNode( "System.Linq", "System.Linq", "System.Linq.Enumerable"); @@ -98,10 +92,7 @@ public class NavigationTests // displaced entries pushed onto the forward stack). // Arrange — boot, wait for assemblies, expand Enumerable, capture three methods. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (window, vm) = await TestHarness.BootAsync(3); var typeNode = vm.AssemblyTreeModel.FindNode( "System.Linq", "System.Linq", "System.Linq.Enumerable"); @@ -167,10 +158,7 @@ public class NavigationTests // Arrange — boot, wait for assemblies, expand System.Exception so we can select its // BaseTypesTreeNode child. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (window, vm) = await TestHarness.BootAsync(3); var coreLibName = typeof(object).Assembly.GetName().Name!; var typeNode = vm.AssemblyTreeModel.FindNode( diff --git a/ILSpy.Tests/Navigation/ViewStateRoundTripTests.cs b/ILSpy.Tests/Navigation/ViewStateRoundTripTests.cs index 580238224..8b8d7b731 100644 --- a/ILSpy.Tests/Navigation/ViewStateRoundTripTests.cs +++ b/ILSpy.Tests/Navigation/ViewStateRoundTripTests.cs @@ -23,12 +23,9 @@ using Avalonia.Headless.NUnit; using AwesomeAssertions; -using ILSpy.AppEnv; using ILSpy.Navigation; using ILSpy.TextView; using ILSpy.TreeNodes; -using ILSpy.ViewModels; -using ILSpy.Views; using NUnit.Framework; @@ -47,10 +44,7 @@ public class ViewStateRoundTripTests [AvaloniaTest] public async Task Back_Restores_Caret_Position_The_User_Left_On_The_Previous_Node() { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); var dockWorkspace = vm.DockWorkspace; @@ -111,10 +105,7 @@ public class ViewStateRoundTripTests // a deterministic snapshot, exercise the navigation pipeline, and assert the snapshot // survives the capture -> entry -> pending hops. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); var dockWorkspace = vm.DockWorkspace; var coreLibName = typeof(object).Assembly.GetName().Name!; @@ -165,10 +156,7 @@ public class ViewStateRoundTripTests // as missing, but it was already implemented. This test makes a regression in any // of the three wirings (command, toolbar button, Alt+Right) detectable. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); ((object?)vm.DockWorkspace.NavigateForwardCommand).Should().NotBeNull(); ((object?)vm.DockWorkspace.NavigateBackCommand).Should().NotBeNull(); diff --git a/ILSpy.Tests/Options/ApiVisibilityFilterTests.cs b/ILSpy.Tests/Options/ApiVisibilityFilterTests.cs index ece9d04c6..1baa83a20 100644 --- a/ILSpy.Tests/Options/ApiVisibilityFilterTests.cs +++ b/ILSpy.Tests/Options/ApiVisibilityFilterTests.cs @@ -153,10 +153,7 @@ public class ApiVisibilityFilterTests // re-evaluates visibility when ShowApiLevel changes. // Arrange — boot, find a CoreLib type with mixed accessibility (String has many). - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var coreLibName = typeof(object).Assembly.GetName().Name!; var stringType = vm.AssemblyTreeModel.FindNode(coreLibName, "System", "System.String"); @@ -187,10 +184,7 @@ public class ApiVisibilityFilterTests // new HierarchicalModel reference whenever the setting changes. // Arrange — boot, locate the pane. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (window, _) = await TestHarness.BootAsync(); await Waiters.WaitForAsync( () => window.GetVisualDescendants().OfType().Any()); var pane = await window.WaitForComponent(); @@ -219,10 +213,7 @@ public class ApiVisibilityFilterTests // than Foreground brush) avoids depending on theme brush resolution. // Arrange — boot, ensure ShowApiLevel=All so non-public methods are visible at all. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (window, vm) = await TestHarness.BootAsync(); var settings = AppComposition.Current.GetExport().SessionSettings.LanguageSettings; settings.ShowApiLevel = ApiVisibility.All; @@ -308,10 +299,7 @@ public class ApiVisibilityFilterTests static async Task<(MainWindowViewModel vm, TypeTreeNode enumerableNode)> BootAndExpandEnumerableAsync() { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); var node = vm.AssemblyTreeModel.FindNode( "System.Linq", "System.Linq", "System.Linq.Enumerable"); node.IsExpanded = true; diff --git a/ILSpy.Tests/Options/OptionsPageScrollReachTests.cs b/ILSpy.Tests/Options/OptionsPageScrollReachTests.cs index 94300190c..eee17488c 100644 --- a/ILSpy.Tests/Options/OptionsPageScrollReachTests.cs +++ b/ILSpy.Tests/Options/OptionsPageScrollReachTests.cs @@ -58,10 +58,8 @@ public class OptionsPageScrollReachTests window.Show(); Dispatcher.UIThread.RunJobs(); - var registry = AppComposition.Current.GetExport(); - var command = registry.Commands - .Single(c => c.Metadata.Header == nameof(Resources._Options)) - .CreateExport().Value; + var command = AppComposition.Current.GetExport() + .GetCommand(nameof(Resources._Options)); command.Execute(null); Dispatcher.UIThread.RunJobs(); diff --git a/ILSpy.Tests/Options/OptionsTabTests.cs b/ILSpy.Tests/Options/OptionsTabTests.cs index f4551b63d..7527abb3e 100644 --- a/ILSpy.Tests/Options/OptionsTabTests.cs +++ b/ILSpy.Tests/Options/OptionsTabTests.cs @@ -66,10 +66,8 @@ public class OptionsTabTests var window = AppComposition.Current.GetExport(); window.Show(); var vm = (MainWindowViewModel)window.DataContext!; - var registry = AppComposition.Current.GetExport(); - var command = registry.Commands - .Single(c => c.Metadata.Header == nameof(Resources._Options)) - .CreateExport().Value; + var command = AppComposition.Current.GetExport() + .GetCommand(nameof(Resources._Options)); command.Execute(null); @@ -92,15 +90,11 @@ public class OptionsTabTests // document is still Options — so the user sees no visible change. The fix: // ShowSelectedNode pulls focus back to MainTab whenever a tree-selection writes // new content there. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + var (_, vm) = await TestHarness.BootAsync(3); // Open Options and confirm it's the active document. - var registry = AppComposition.Current.GetExport(); - registry.Commands.Single(c => c.Metadata.Header == nameof(Resources._Options)) - .CreateExport().Value.Execute(null); + AppComposition.Current.GetExport() + .GetCommand(nameof(Resources._Options)).Execute(null); var documents = vm.DockWorkspace.Documents!; var optionsTab = documents.VisibleDockables!.OfType() @@ -133,9 +127,8 @@ public class OptionsTabTests // tab should use the bare Resources.Options ("Options") instead. var window = AppComposition.Current.GetExport(); window.Show(); - var registry = AppComposition.Current.GetExport(); - registry.Commands.Single(c => c.Metadata.Header == nameof(Resources._Options)) - .CreateExport().Value.Execute(null); + AppComposition.Current.GetExport() + .GetCommand(nameof(Resources._Options)).Execute(null); var vm = (MainWindowViewModel)window.DataContext!; var model = (OptionsPageModel)vm.DockWorkspace.Documents!.VisibleDockables! @@ -157,10 +150,8 @@ public class OptionsTabTests // byte-for-byte. var window = AppComposition.Current.GetExport(); window.Show(); - var registry = AppComposition.Current.GetExport(); - var command = registry.Commands - .Single(c => c.Metadata.Header == nameof(Resources._Options)) - .CreateExport().Value; + var command = AppComposition.Current.GetExport() + .GetCommand(nameof(Resources._Options)); command.Execute(null); var vm = (MainWindowViewModel)window.DataContext!; @@ -181,10 +172,8 @@ public class OptionsTabTests var window = AppComposition.Current.GetExport(); window.Show(); var vm = (MainWindowViewModel)window.DataContext!; - var registry = AppComposition.Current.GetExport(); - var command = registry.Commands - .Single(c => c.Metadata.Header == nameof(Resources._Options)) - .CreateExport().Value; + var command = AppComposition.Current.GetExport() + .GetCommand(nameof(Resources._Options)); command.Execute(null); command.Execute(null); @@ -205,12 +194,11 @@ public class OptionsTabTests var window = AppComposition.Current.GetExport(); window.Show(); var settings = AppComposition.Current.GetExport(); - var registry = AppComposition.Current.GetExport(); var liveBefore = settings.DecompilerSettings.UsingDeclarations; - registry.Commands.Single(c => c.Metadata.Header == nameof(Resources._Options)) - .CreateExport().Value.Execute(null); + AppComposition.Current.GetExport() + .GetCommand(nameof(Resources._Options)).Execute(null); var vm = (MainWindowViewModel)window.DataContext!; var model = (OptionsPageModel)vm.DockWorkspace.Documents!.VisibleDockables! @@ -238,9 +226,8 @@ public class OptionsTabTests // default; Display and Misc panels are untouched (verified by leaving them alone). var window = AppComposition.Current.GetExport(); window.Show(); - var registry = AppComposition.Current.GetExport(); - registry.Commands.Single(c => c.Metadata.Header == nameof(Resources._Options)) - .CreateExport().Value.Execute(null); + AppComposition.Current.GetExport() + .GetCommand(nameof(Resources._Options)).Execute(null); var vm = (MainWindowViewModel)window.DataContext!; var model = (OptionsPageModel)vm.DockWorkspace.Documents!.VisibleDockables! diff --git a/ILSpy.Tests/Search/ScopeSearchToTests.cs b/ILSpy.Tests/Search/ScopeSearchToTests.cs index 8a9ba5d9c..6fe26966d 100644 --- a/ILSpy.Tests/Search/ScopeSearchToTests.cs +++ b/ILSpy.Tests/Search/ScopeSearchToTests.cs @@ -43,15 +43,10 @@ public class ScopeSearchToTests [AvaloniaTest] public async Task Scope_Search_To_Assembly_Prepends_InAssembly_To_The_Search_Term() { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); - var registry = AppComposition.Current.GetExport(); - var entry = registry.Entries - .Single(e => e.Metadata.Header == nameof(Resources.ScopeSearchToThisAssembly)) - .Value; + var entry = AppComposition.Current.GetExport() + .GetEntry(nameof(Resources.ScopeSearchToThisAssembly)); var asm = vm.AssemblyTreeModel.FindNode("System.Linq"); var search = AppComposition.Current.GetExport(); @@ -71,15 +66,10 @@ public class ScopeSearchToTests [AvaloniaTest] public async Task Scope_Search_To_Namespace_Prepends_InNamespace_To_The_Search_Term() { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); - - var registry = AppComposition.Current.GetExport(); - var entry = registry.Entries - .Single(e => e.Metadata.Header == nameof(Resources.ScopeSearchToThisNamespace)) - .Value; + var (_, vm) = await TestHarness.BootAsync(); + + var entry = AppComposition.Current.GetExport() + .GetEntry(nameof(Resources.ScopeSearchToThisNamespace)); var ns = vm.AssemblyTreeModel.FindNode("System.Linq", "System.Linq"); var search = AppComposition.Current.GetExport(); diff --git a/ILSpy.Tests/Search/SearchInputFocusTests.cs b/ILSpy.Tests/Search/SearchInputFocusTests.cs index 1b612f3cd..d11f137de 100644 --- a/ILSpy.Tests/Search/SearchInputFocusTests.cs +++ b/ILSpy.Tests/Search/SearchInputFocusTests.cs @@ -44,10 +44,7 @@ public class SearchInputFocusTests // can start typing immediately. The VM raises FocusRequested; the SearchPane // code-behind subscribes and calls Focus() on the TextBox. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + await TestHarness.BootAsync(); var search = AppComposition.Current.GetExport(); var dockWorkspace = AppComposition.Current.GetExport(); @@ -64,10 +61,7 @@ public class SearchInputFocusTests [AvaloniaTest] public async Task SearchInput_Is_Focused_After_ShowSearchCommand_Pumps_Through_The_Dispatcher() { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (window, _) = await TestHarness.BootAsync(); var pane = await window.WaitForComponent(); var dockWorkspace = AppComposition.Current.GetExport(); diff --git a/ILSpy.Tests/Search/SearchPaneAssemblyListChangedTests.cs b/ILSpy.Tests/Search/SearchPaneAssemblyListChangedTests.cs index 0765bf674..ea0a89ecc 100644 --- a/ILSpy.Tests/Search/SearchPaneAssemblyListChangedTests.cs +++ b/ILSpy.Tests/Search/SearchPaneAssemblyListChangedTests.cs @@ -48,10 +48,7 @@ public class SearchPaneAssemblyListChangedTests [AvaloniaTest] public async Task Search_Restarts_When_A_User_Loaded_Assembly_Is_Added() { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); var search = AppComposition.Current.GetExport(); search.SearchTerm = "Object"; @@ -82,10 +79,7 @@ public class SearchPaneAssemblyListChangedTests // Auto-loaded dependencies fire from result navigation in a large assembly; restarting // the search there would feed back into more loads → more events → flicker. The // handler MUST take the early-out path. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); var search = AppComposition.Current.GetExport(); search.SearchTerm = "Object"; diff --git a/ILSpy.Tests/Search/SearchPaneStreamingTests.cs b/ILSpy.Tests/Search/SearchPaneStreamingTests.cs index 55df1cc65..f80be5820 100644 --- a/ILSpy.Tests/Search/SearchPaneStreamingTests.cs +++ b/ILSpy.Tests/Search/SearchPaneStreamingTests.cs @@ -45,13 +45,9 @@ public class SearchPaneStreamingTests // logic works for the fixture. If this fails, the bug is in the strategy / fixture; // if it passes but the orchestrator test below fails, the bug is in our wiring. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); - var coreLibName = typeof(object).Assembly.GetName().Name!; - var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); + var assemblyNode = vm.AssemblyTreeModel.FindCoreLib(); var module = await assemblyNode.LoadedAssembly.GetMetadataFileAsync(); var queue = new System.Collections.Concurrent.ConcurrentQueue(); @@ -82,10 +78,7 @@ public class SearchPaneStreamingTests // search across the loaded fixture assemblies, and wait for at least one matching // result to land in the Results collection. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + await TestHarness.BootAsync(); var search = AppComposition.Current.GetExport(); search.Results.Clear(); @@ -107,10 +100,7 @@ public class SearchPaneStreamingTests // Switching to an empty term tears down the running search and clears the visible // results. The user shouldn't see stale rows after they've explicitly cleared the box. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + await TestHarness.BootAsync(); var search = AppComposition.Current.GetExport(); search.Results.Clear(); @@ -131,10 +121,7 @@ public class SearchPaneStreamingTests // contain". Confirm end-to-end: a Type search for "String -Builder" returns names // containing "String" (System.String, StringComparer, …) but never StringBuilder. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + await TestHarness.BootAsync(); var search = AppComposition.Current.GetExport(); search.Results.Clear(); @@ -159,10 +146,7 @@ public class SearchPaneStreamingTests // (length-aware; ignores IL backtick generic arity). Confirm: =String must match // System.String but NOT StringBuilder or StringComparer. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + await TestHarness.BootAsync(); var search = AppComposition.Current.GetExport(); search.Results.Clear(); diff --git a/ILSpy.Tests/Search/SearchProgressTests.cs b/ILSpy.Tests/Search/SearchProgressTests.cs index 13e5d73bb..c567cde45 100644 --- a/ILSpy.Tests/Search/SearchProgressTests.cs +++ b/ILSpy.Tests/Search/SearchProgressTests.cs @@ -44,10 +44,7 @@ public class SearchProgressTests [AvaloniaTest] public async Task IsSearching_Flips_True_When_A_Search_Starts_And_False_When_It_Finishes() { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + await TestHarness.BootAsync(); var search = AppComposition.Current.GetExport(); search.SearchTerm = string.Empty; @@ -84,10 +81,7 @@ public class SearchProgressTests [AvaloniaTest] public async Task SearchResult_Image_Properties_Are_Non_Null_So_Row_Icons_Render() { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + await TestHarness.BootAsync(); var search = AppComposition.Current.GetExport(); search.SearchTerm = string.Empty; diff --git a/ILSpy.Tests/Search/SearchResultNavigationTests.cs b/ILSpy.Tests/Search/SearchResultNavigationTests.cs index a2aa38914..6ebd86706 100644 --- a/ILSpy.Tests/Search/SearchResultNavigationTests.cs +++ b/ILSpy.Tests/Search/SearchResultNavigationTests.cs @@ -46,10 +46,7 @@ public class SearchResultNavigationTests // to activate it, and confirm the assembly tree's SelectedItem becomes the matching // tree node. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + var (_, vm) = await TestHarness.BootAsync(); var search = AppComposition.Current.GetExport(); search.Results.Clear(); diff --git a/ILSpy.Tests/Search/SearchResultSortOrderTests.cs b/ILSpy.Tests/Search/SearchResultSortOrderTests.cs index 4ad417872..e589c126c 100644 --- a/ILSpy.Tests/Search/SearchResultSortOrderTests.cs +++ b/ILSpy.Tests/Search/SearchResultSortOrderTests.cs @@ -47,10 +47,7 @@ public class SearchResultSortOrderTests // Name asc (StringComparer.Ordinal). Mirrors WPF's SearchPane.xaml.cs:288-290 // which captures the comparer at start-of-search based on DisplaySettings.SortResults. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + await TestHarness.BootAsync(); var settings = AppComposition.Current.GetExport(); var originalSortResults = settings.DisplaySettings.SortResults; diff --git a/ILSpy.Tests/Search/ShowSearchCommandTests.cs b/ILSpy.Tests/Search/ShowSearchCommandTests.cs index 2f16e16fe..5db6884e0 100644 --- a/ILSpy.Tests/Search/ShowSearchCommandTests.cs +++ b/ILSpy.Tests/Search/ShowSearchCommandTests.cs @@ -42,10 +42,7 @@ public class ShowSearchCommandTests // MainWindow.KeyBindings. Here we drive the command directly and verify the // activation reaches the SearchPaneModel. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + await TestHarness.BootAsync(); var dockWorkspace = AppComposition.Current.GetExport(); var search = AppComposition.Current.GetExport(); diff --git a/ILSpy.Tests/TestHarness.cs b/ILSpy.Tests/TestHarness.cs new file mode 100644 index 000000000..244df091d --- /dev/null +++ b/ILSpy.Tests/TestHarness.cs @@ -0,0 +1,112 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Input; + +using ICSharpCode.ILSpy.Properties; +using ICSharpCode.ILSpyX; + +using global::ILSpy; +using global::ILSpy.AppEnv; +using global::ILSpy.Commands; +using global::ILSpy.ViewModels; +using global::ILSpy.Views; + +namespace ICSharpCode.ILSpy.Tests; + +/// +/// Boots and drives the shared MEF-composed application for headless UI tests, collapsing the +/// four-line prologue (resolve , show it, cast its DataContext, wait for +/// the assembly list) and the "open an assembly via the production command and wait for it to +/// load" dance that nearly every fixture repeats. The container is rebuilt per test by +/// , so each call hands back that test's own singletons. +/// +public static class TestHarness +{ + /// + /// Resolves the shared , shows it, and waits until the default + /// assembly list has loaded at least assemblies. + /// Returns the window and its — the two handles the test + /// body almost always needs next. + /// + public static async Task<(MainWindow Window, MainWindowViewModel ViewModel)> BootAsync( + int minimumAssemblies = 1, + TimeSpan? timeout = null) + { + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumAssemblies, timeout); + return (window, vm); + } + + /// + /// Looks up a [ExportMainMenuCommand]-tagged command by its Header metadata (pass the + /// resource key, e.g. nameof(Resources._Open)) and materialises a fresh instance. + /// + public static ICommand GetCommand(this MainMenuCommandRegistry registry, string header) + { + ArgumentNullException.ThrowIfNull(registry); + return registry.Commands + .Single(c => string.Equals(c.Metadata.Header, header, StringComparison.Ordinal)) + .CreateExport().Value; + } + + /// + /// Looks up a [ExportContextMenuEntry]-tagged entry by its Header metadata. + /// + public static IContextMenuEntry GetEntry(this ContextMenuEntryRegistry registry, string header) + { + ArgumentNullException.ThrowIfNull(registry); + return registry.Entries + .Single(e => string.Equals(e.Metadata.Header, header, StringComparison.Ordinal)) + .Value; + } + + /// + /// Opens through the production Open command, waits for it to appear + /// in the assembly list, awaits its load result, and returns the resulting + /// . This is the canonical "add a test assembly to the tree" + /// sequence — registry lookup, execute, poll-for-appearance, await-load — in one call. + /// + public static async Task OpenAssemblyAsync( + this MainWindowViewModel vm, + string path, + TimeSpan? timeout = null) + { + ArgumentNullException.ThrowIfNull(vm); + ArgumentNullException.ThrowIfNull(path); + + var open = AppComposition.Current.GetExport().GetCommand(nameof(Resources._Open)); + open.Execute(path); + + await Waiters.WaitForAsync( + () => vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() + .Any(a => string.Equals(a.FileName, path, StringComparison.OrdinalIgnoreCase)), + timeout, + $"assembly '{path}' to appear in the active list"); + + var loaded = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() + .First(a => string.Equals(a.FileName, path, StringComparison.OrdinalIgnoreCase)); + await loaded.GetLoadResultAsync(); + return loaded; + } +} diff --git a/ILSpy.Tests/TreeNavigation.cs b/ILSpy.Tests/TreeNavigation.cs index 25b2c2e73..50b54f63e 100644 --- a/ILSpy.Tests/TreeNavigation.cs +++ b/ILSpy.Tests/TreeNavigation.cs @@ -22,6 +22,7 @@ using System.Linq; using ICSharpCode.ILSpyX.TreeView; using global::ILSpy.AssemblyTree; +using global::ILSpy.TreeNodes; namespace ICSharpCode.ILSpy.Tests; @@ -36,6 +37,56 @@ namespace ICSharpCode.ILSpy.Tests; /// public static class TreeNavigation { + /// + /// Short name of the runtime core library (e.g. System.Private.CoreLib) — the one + /// assembly the headless suite is guaranteed to have loaded. Use as the first path segment. + /// + public static string CoreLibName { get; } = typeof(object).Assembly.GetName().Name!; + + /// + /// Resolves the core-library , the fixture every test can rely + /// on. Replaces the repeated FindNode<AssemblyTreeNode>(typeof(object).Assembly...). + /// + internal static AssemblyTreeNode FindCoreLib(this AssemblyTreeModel atm) + => atm.FindNode(CoreLibName); + + /// + /// Materialises 's lazy children and returns the single child of type + /// . The workhorse for drilling metadata sub-trees, replacing the + /// node.EnsureLazyChildren(); node.Children.OfType<T>().Single() pair. Chains. + /// + public static T GetChild(this SharpTreeNode parent) where T : SharpTreeNode + { + ArgumentNullException.ThrowIfNull(parent); + parent.EnsureLazyChildren(); + return parent.Children.OfType().Single(); + } + + /// + /// Materialises lazy children and returns the single child of type + /// matching (e.g. a method picked by name). + /// + public static T GetChild(this SharpTreeNode parent, Func predicate) where T : SharpTreeNode + { + ArgumentNullException.ThrowIfNull(parent); + ArgumentNullException.ThrowIfNull(predicate); + parent.EnsureLazyChildren(); + return parent.Children.OfType().Single(predicate); + } + + /// + /// Materialises lazy children, flips , and returns the + /// node so callers can keep drilling. Replaces the + /// node.EnsureLazyChildren(); node.IsExpanded = true; pair. + /// + public static T Expand(this T node) where T : SharpTreeNode + { + ArgumentNullException.ThrowIfNull(node); + node.EnsureLazyChildren(); + node.IsExpanded = true; + return node; + } + public static SharpTreeNode FindNode(this AssemblyTreeModel atm, params string[] segments) { ArgumentNullException.ThrowIfNull(atm);