To audit what each UI test actually exercises, every step now snapshots the
live window to <TestFixtureName>/<TestName>_<NN>_<ShortDescription>.png: a
booted frame (emitted automatically by TestHarness.BootAsync), one after each
state-changing action, and one before each assertion. Flip ILSPY_TESTS_VISIBLE=1
to render the filmstrip; it lands under %TEMP%/ilspy-test-captures (overridable
via ILSPY_TEST_CAPTURES).
The step number and fixture/test name are derived automatically so inserting a
breakpoint never renumbers the rest. The identity is recorded up front from the
real ITest in an ITestAction hook rather than read live: NUnit's
TestContext.CurrentContext does not flow onto async continuations, so a capture
after an await would otherwise collide under the ad-hoc context. And when
rendering is off the whole call is a true no-op -- not even a dispatcher pump --
so instrumenting a test can never perturb the navigation/tab timing it asserts
on. Full headless suite stays green.
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<T>().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<T>, Expand<T>),
then apply them across the suite. Net ~865 lines of boilerplate removed with
no change in behaviour; the full headless suite stays green.
The command resolves each selected assembly's references through the
per-assembly resolver, which adds the targets to the live list as
auto-loaded entries. It then finished with a full list Refresh (F5),
whose LoadList rebuilds the list from persisted state -- which never
contains on-demand auto-loaded assemblies -- so the just-resolved
dependencies were discarded the instant they were added and the command
looked like a no-op. Re-decompile the active view instead (the original
behaviour) without reloading the list. F5 still drops auto-loaded
assemblies by design; the two refresh paths must stay distinct.
Two selection-sync defects in the assembly tree. Ctrl+A only selected
the last row on the first press: SelectedItem is the last entry of
SelectedItems and every collection change re-raised it, so the grid->model
sync bounced back through SyncSelectionFromModel and assigned the singular
SelectedItem, collapsing the just-made multi-selection. The syncingSelection
guard now also covers that notification.
Clicking one row of a multi-selection left every row selected: ProDataGrid
keeps the selection on press so a row-drag can move all of them (multi-row
reorder is a real feature), but 12.0.0 has no release-side collapse for a
plain click. Mirror the usual behaviour by collapsing to the clicked row on
release when the pointer did not move far enough to be a drag.
Two first-run behaviours that the previous version had and the port had
lost. When a launch has nothing to restore (no saved tree path, no
command-line target), greet the user with the About page in the main
tab instead of an empty "(no selection)" view; the greeting is fired
after the tree is ready and only takes the main tab when it's still
empty and active, so it can't steal activation from a tab the user
opened meanwhile (e.g. Compare...). And seed the default list with the
.NET framework ILSpy itself runs on - every managed assembly in the
running runtime's shared-framework directory. Headless tests opt out of
that seed (re-opening ~150 assemblies per test is ~10x slower) and fall
back to the minimal trio, leaving their expectations unchanged.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The dialog lost the entry point for building a preconfigured list when
the view-model wasn't ported; the backing AssemblyListManager.
CreateDefaultList was already present and cross-platform. Re-add the
button and a flyout of the generated lists. The three GAC-based
framework lists resolve nothing without a GAC, so they're only offered
when a GAC directory exists (i.e. on Windows); the per-installed-runtime
entries work everywhere.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The +/- expander was a 13x13 toggle whose visible glyph (a 9x9 box) was
also the only hit-testable surface, so the real tap target was barely
9px. Grow the toggle to 16x16 but keep its laid-out width at 13 via a
negative right margin, because TreeLines hardcodes a 13px expander
column with the glyph centred at +8.5 and would otherwise misalign. A
transparent wrapper fills the 16x16 so the whole area receives input;
the visible glyph is unchanged.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The user reported that expanding an enum (System.DayOfWeek) with a search
term active showed only Base/Derived nodes — every FieldTreeNode child
hidden. This is NOT a recent regression. It traces back to commit
45461ddde, which made LanguageSettings.SearchTermMatches honour the
SearchTerm and wired SearchPaneModel to push its term into
LanguageSettings on every keystroke.
The PE TypeDef table includes a <Module> pseudo-type plus any user-declared
top-level types in the empty namespace. Until now AssemblyTreeNode skipped
the empty namespace when building NamespaceTreeNodes and instead hung the
global types directly off the assembly node, breaking the long-standing
tree shape (every type lives under a namespace folder).
The Reload/Remove/SearchMSDN/OpenContainingFolder/DecompileInNewView
tests used to call `entry.Execute(synthetic TextViewContext)` against
a hand-built context. They now select via the assembly-tree model,
build the context menu through `BuildContextMenuForCurrentState`
(the same call the live `Opening` event makes), and fire the menu
item's `Click` routed event — the same handler the user's click
invokes. This widens the coverage to the menu-build → click-handler
attachment pipeline.
Assisted-by: Claude:claude-opus-4-7:Claude Code
HandleFileDrop now resolves each opened LoadedAssembly to its
AssemblyTreeNode (via AssemblyListTreeNode.FindAssemblyNode) after
any Move and writes the set into model.SelectedItems. Lookup happens
post-Move because Move re-creates the node wrappers — a pre-Move
reference would dangle. Selecting drives the decompiler view to
render the new assembly, which is what makes the LoadAsync task
observable to the user.
Assisted-by: Claude:claude-opus-4-7:Claude Code
ProDataGrid's row-drag pipeline only sees in-grid drags, so external
file drops bypass it entirely. Wires Avalonia's standard DragDrop
pipeline alongside it: DragDrop.AllowDrop on the tree DataGrid plus
DragOver / Drop handlers that read DataFormat.File from the
IDataTransfer, resolve each IStorageItem to a local path, and route
through AssemblyListPane.HandleFileDrop.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Wires ProDataGrid's native row-drag API on the assembly tree:
CanUserReorderRows=True, RowDragHandle=Row, RowDragStarting cancels
drags whose source isn't a top-level AssemblyTreeNode (or is a
package-nested entry). AssemblyRowDropHandler validates that the
target is a sibling (not Inside, not deeper than top-level) and
routes the reorder through AssemblyList.Move so the same persistence
path the rest of the app uses (Unload / OpenAssembly) captures the
new ordering.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Audit had ExitCommand listed as Missing because it grepped for a separate
ExitCommand.cs file (the WPF layout). Avalonia bundles File-menu commands
into FileCommands.cs so the audit missed it. Command was already implemented
correctly — uses IClassicDesktopStyleApplicationLifetime.Shutdown() rather
than mainWindow.Close() (cleaner since Avalonia apps can have multiple
windows).
Assisted-by: Claude:claude-opus-4-7:Claude Code
FindMemberNode's IMethod branch only checked direct MethodTreeNode children of
the type. Accessor methods (get_/set_/add_/remove_/invoke_) live as children of
their owning PropertyTreeNode / EventTreeNode, so the lookup always returned
null — and MMB on a metadata-grid accessor row silently no-opped because
OpenNodeInNewTab couldn't resolve the IMethod to a tree node.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Mirrors WPF's mechanism: ILSpyTreeNode.OnChildrenChanged calls ApplyFilterToChild
on every newly-added child while the parent IsVisible, writing the Filter result
into SharpTreeNode.IsHidden. Because SharpTreeNode.IsVisible defaults to true,
the cascade fires while a node's children are added in its constructor — so a
PropertyTreeNode's get_/set_ accessors are stamped IsHidden=true under the
default ShowApiLevel before the property even attaches to its type. The base
SharpTreeNode.ShowExpander then naturally returns false ("Children.Any(!isHidden)"
sees no visible children), no per-node override needed.
Assisted-by: Claude:claude-opus-4-7:Claude Code
PropertyTreeNode now eagerly adds Getter / Setter as MethodTreeNode children
(when present); EventTreeNode does the same for Add / Remove / Invoke
accessors. The ShowExpander => false overrides drop so the base default
(Children.Any) lights up the chevron when accessors exist.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Each ContentTabPage now carries the SharpTreeNode it represents (SourceNode),
stamped at construction time so the activation event fires with it already
populated. DockWorkspace subscribes to factory.ActiveDockableChanged: when a
new tab becomes active — whether by MMB carve-out, the "Decompile to new tab"
context-menu entry, or the user clicking a different tab in the strip — the
assembly-tree selection is pulled across to that tab's SourceNode. A guard
prevents the change from cascading back into ShowSelectedNode and overwriting
MainTab's content. Existing single-tab reuse path stamps MainTab.SourceNode
on every selection so flipping back to it stays a no-op.
Assisted-by: Claude:claude-opus-4-7:Claude Code
The new-tab path always built a DecompilerTabPageModel, so MMB / context-menu
on a metadata-table tree node forced the node into a decompiler tab and a second
metadata view couldn't coexist with the first. Move the carve-out into a single
DockWorkspace.OpenNodeInNewTab that asks node.CreateTab for custom content
(metadata tables, resource viewers, …) — wiring the same NavigateToCellRequested
/ RowActivated subscriptions AttachCustomContent applies on the reuse path —
and only falls back to spawning a fresh decompiler tab when the node has no
custom page-type. The assembly-tree MMB handler, the metadata-grid MMB handler,
and the "Decompile to new tab" context-menu entry now all delegate to this one
method.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Wires MMB on the assembly tree and the metadata grid to the existing "open in
new tab" path, matching the WPF gesture (DecompileInNewViewCommand carries
InputGestureText = "MMB"). Plain double-click reverts to reusing the active tab;
the unreliable Shift+double-click variant is dropped.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Replaces the brittle root.GetVisualDescendants().OfType<T>().Single() / .First()
pattern with a new WaitForComponent<T>() extension that polls until the requested
control is in the visual tree, then returns it. Avalonia.Headless tests routinely
queried the visual tree before lazily-templated panes (DataGrid, dock content)
had materialised, surfacing as intermittent 'Sequence contains no elements'
failures across the suite.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Mirrors the WPF host's nested ReloadAssembly entry. Same visibility shape
as Remove (every selected node must be an AssemblyTreeNode). Execute calls
AssemblyList.ReloadAssembly on each selection's path, snapshots the
pre-reload tree paths so that after the LoadedAssembly instances are swapped
out the model can re-select an equivalent node via FindNodeByPath. The
existing SelectedItem -> ShowSelectedNode plumbing then re-decompiles into
the active tab.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Each tree-expansion, selection change, command execution, async wait and
key/mouse injection inside an [AvaloniaTest] now carries a short imperative
comment on the line above (e.g. "// expand typeNode", "// select methodA",
"// wait for assemblies to load", "// execute aboutCmd"). The comments are
the same scaffolding the manual debug-with-screenshot workflow uses to
follow what's happening at each breakpoint, surfaced into the committed
source so the tests are readable without the breakpoint markers attached.
Assisted-by: Claude:claude-opus-4-7:Claude Code
AssemblyTreeNode now overrides CanDelete / Delete / DeleteCore (matching
the WPF host: Delete unloads the assembly via AssemblyList.Unload, with
package-nested entries opting out via CanDelete=false) and exports a
nested RemoveAssembly context-menu entry tagged Header="_Remove".
Assisted-by: Claude:claude-opus-4-7:Claude Code
AssemblyListPane attaches a ContextMenu to its TreeGrid at construction
time. The menu's Opening handler builds entries from the
ContextMenuEntryRegistry against a fresh TextViewContext (the live
selection + the grid as OriginalSource), and cancels Opening when no
entry would be visible so an empty popup never flashes up.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Adds a leading paragraph describing what each test verifies and Arrange /
Act / Assert markers (split into numbered phases for multi-step tests) so
the intent of each fixture is readable from comments alone.
Assisted-by: Claude:claude-opus-4-7:Claude Code
ProDataGrid's HierarchicalModel kept its own per-wrapper expanded state and
ignored SharpTreeNode.IsExpanded — toggling the model property had no visible
effect, and clicking the chevron in the grid didn't update the model either.
Set HierarchicalOptions.IsExpandedPropertyPath = "IsExpanded" so ProDataGrid
reads/writes the property via reflection and observes INotifyPropertyChanged
on the source. Production code stays a one-liner; the imperative
NodeExpanded/PropertyChanged plumbing first attempt would have been ~80 lines.
Test exercises the model→grid direction.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Both commands have correct implementations since the WPF main-menu port; this
locks the behaviour in. Clear: with a populated list, CanExecute is true and
Execute drains the list to zero. Remove-load-errors: drops a synthetic non-PE
file into the list, waits for HasLoadError (GetLoadResultAsync rethrows the
underlying BadImageFormatException so it isn't a usable wait probe), invokes
the command, asserts the broken entry is unloaded while every previously-valid
entry is preserved. Both confirmed red by temporarily no-op'ing Execute and
green again after restoring.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Mirrors the WPF SharpTreeView's ApplicationCommands.Delete handling:
KeyDown on the tree DataGrid scoops up every selected AssemblyTreeNode and
calls AssemblyList.Unload on each. Snapshots the list before mutating because
Unload triggers CollectionChanged which clears the model selection mid-loop.
Non-assembly selections (types, methods, namespaces, …) are ignored — Del is
specifically for top-level entries today; richer per-node Delete semantics
can come with the broader IPlatformDataObject port. Test pins the dispatch.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Mirrors WPF SaveCodeContextMenuEntry.Execute: when a single tree node is
selected, call its Save() override first (resource nodes already self-handle
the dialog + raw/ResX choice). Otherwise prompt for a path with the active
language's extension and re-decompile the node with FullDecompilation = true,
EscapeInvalidIdentifiers = true, writing the output as plain text via
ICSharpCode.Decompiler.PlainTextOutput on a background task.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Clicking an already-visible row was running ScrollIntoView+CenterRowInView via
the SelectionChanged → model PropertyChanged round-trip, yanking the viewport.
Add a clickInProgress flag separate from syncingSelection: clickInProgress is
set in OnTreeGridSelectionChanged and cleared at Background priority alongside
syncingSelection, so the model's two PropertyChanged notifications (Clear+Add
on SelectedItems) both bail out — but hyperlink / Back/Forward / search-hit
navigation, which never enters OnTreeGridSelectionChanged, still scrolls the
target into view.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Five new IResourceNodeFactory implementations route .xml/.xsd/.xslt to a shared
XmlResourceEntryNode (text + XML highlighting via SyntaxExtensionOverride on the
output), .xaml to XamlResourceEntryNode (same pattern), and .png/.gif/.bmp/.jpg /
.ico / .cur to image-rendering nodes that inline the bitmap with a Save button.
SyntaxExtensionOverride is read after Decompile to switch the editor's highlighter
without changing the active language. Cursor handling flips byte 2 of the on-disk
header so Skia decodes the .cur as an icon for preview while Save preserves the
original bytes. ResourceFactoryTests pin the dispatch — including .xsd — and the
existing assembly-tree assertion is relaxed since typed entries are no longer all
ResourceTreeNodes.
Assisted-by: Claude:claude-opus-4-7:Claude Code