// 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 Avalonia.Controls; using Avalonia.Interactivity; 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); // First visual breakpoint of every UI test: the booted window with its assembly list loaded. window.Capture("booted"); 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; } /// /// Finds the whose Header equals (pass /// the resolved resource string, e.g. Resources._Reload) in a menu built by /// AssemblyListPane.BuildContextMenuForCurrentState and raises its Click — the /// same routed event the production menu fires when the user picks the item. Collapses the /// Items.OfType<MenuItem>().Single(...).RaiseEvent(...) dance every context-menu /// test repeats. /// public static void ClickItem(this ContextMenu menu, string header) { ArgumentNullException.ThrowIfNull(menu); ArgumentNullException.ThrowIfNull(header); var item = menu.Items.OfType() .Single(i => string.Equals((string?)i.Header, header, StringComparison.Ordinal)); item.RaiseEvent(new RoutedEventArgs(MenuItem.ClickEvent)); } /// /// 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; } }