diff --git a/ILSpy.Tests/Metadata/MetadataFilterRowVerificationTest.cs b/ILSpy.Tests/Metadata/MetadataFilterRowVerificationTest.cs
deleted file mode 100644
index 24e86b0c9..000000000
--- a/ILSpy.Tests/Metadata/MetadataFilterRowVerificationTest.cs
+++ /dev/null
@@ -1,140 +0,0 @@
-// 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.Diagnostics;
-using System.Linq;
-using System.Threading.Tasks;
-
-using Avalonia.Collections;
-using Avalonia.Controls;
-using Avalonia.Headless.NUnit;
-using Avalonia.Threading;
-using Avalonia.VisualTree;
-
-using AwesomeAssertions;
-
-using ILSpy.AppEnv;
-using ILSpy.Metadata;
-using ILSpy.Metadata.CorTables;
-using ILSpy.TreeNodes;
-using ILSpy.ViewModels;
-using ILSpy.Views;
-
-using NUnit.Framework;
-
-namespace ICSharpCode.ILSpy.Tests.Metadata;
-
-///
-/// TEMPORARY verification harness for the per-column filter wiring. Run with
-/// ILSPY_TESTS_VISIBLE=1 (or debugger attached) so each step captures a PNG and
-/// pops it open in the OS image viewer; otherwise the assertions still execute headlessly
-/// but no images are produced. Delete this file once the filter is confirmed working.
-///
-[TestFixture]
-public class MetadataFilterRowVerificationTest
-{
- [AvaloniaTest]
- public async Task Step_By_Step_Verify_TypeDef_Name_Filter_With_Screenshots()
- {
- // === STEP 1: Boot the main window ===
- var window = AppComposition.Current.GetExport();
- window.Show();
- Dispatcher.UIThread.RunJobs();
- window.CaptureAndShow(label: "step1_window_shown");
-
- var vm = (MainWindowViewModel)window.DataContext!;
- await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
- Dispatcher.UIThread.RunJobs();
- window.CaptureAndShow(label: "step2_assemblies_loaded");
-
- // === STEP 2: Drill into TypeDef table ===
- 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();
- vm.AssemblyTreeModel.SelectNode(typeDefNode);
-
- var page = await vm.DockWorkspace.WaitForMetadataTabAsync();
- Dispatcher.UIThread.RunJobs();
- window.CaptureAndShow(label: "step3_typedef_open");
-
- // === STEP 3: Wait for the live MetadataTablePage to materialise ===
- // ContentTabPage opts out of Dock's deferred-content presentation so the wrapper
- // view's children (DecompilerTextView + MetadataTablePage) are part of the visual
- // tree synchronously. Without that opt-out the headless test can't reach them.
- var metadataPage = await window.WaitForComponent();
- TestContext.Out.WriteLine("[VERIFY] MetadataTablePage materialised under the main window.");
- var grid = await metadataPage.WaitForComponent();
- TestContext.Out.WriteLine("[VERIFY] Inner DataGrid materialised.");
-
- // Force a layout pass so DataGrid populates its column-headers panel.
- Dispatcher.UIThread.RunJobs();
- await grid.WaitForComponent();
-
- // === STEP 4: Find the rendered TextBox for the Name column header ===
- var headerBox = grid.GetVisualDescendants().OfType()
- .FirstOrDefault(tb => tb.FindAncestorOfType() is { } owner
- && owner.Content is StackPanel sp
- && sp.Children.OfType().FirstOrDefault()?.Text == "Name");
- headerBox.Should().NotBeNull(
- "the column-builder bakes a TextBox into the Name column's header — it must reach the rendered visual tree");
-
- // === STEP 5: Confirm the rendered TextBox is the exact instance the model holds ===
- var nameColumn = page.Columns.Single(c => (string?)c.Tag == "Name");
- var modelHeaderBox = ((StackPanel)nameColumn.Header!).Children.OfType().Single();
- ReferenceEquals(headerBox, modelHeaderBox).Should().BeTrue(
- "DataGrid must render the StackPanel Header directly without re-templating; otherwise the TextProperty observable wired by the builder is on a different TextBox than the user types into");
-
- var nameFilter = page.ColumnFilters.Single(f => f.ColumnName == "Name");
- var totalRows = page.Items.Count;
- totalRows.Should().BeGreaterThan(0, "TypeDef must have rows for filtering to be observable");
-
- // The DataGrid's ItemsSource is the DataGridCollectionView wired up by ApplySchema.
- // Its Count reflects what the user actually sees on screen — that's the post-filter
- // row count we need to assert on, not the predicate evaluated on raw Items.
- var view = grid.ItemsSource as DataGridCollectionView;
- view.Should().NotBeNull("the metadata grid must expose a DataGridCollectionView so the per-column filter can re-evaluate");
- view!.Count.Should().Be(totalRows, "every row should be visible before any filter is set");
-
- // === STEP 6: Type "System" into the rendered TextBox ===
- headerBox!.Text = "System";
- Dispatcher.UIThread.RunJobs();
- nameFilter.Text.Should().Be("System",
- "setting the rendered TextBox.Text must propagate to ColumnFilter.Text");
-
- var expectedVisible = page.Items.Count(e => MetadataTablePageModel.MatchesFilters(e, page.ColumnFilters));
- expectedVisible.Should().BeLessThan(totalRows,
- "the predicate must hide at least one TypeDef row when Name contains 'System'");
- view.Count.Should().Be(expectedVisible,
- "the live grid view must reflect the filter — typing into the header must shrink the visible-row set, not just update ColumnFilter.Text");
- window.CaptureAndShow(label: "step6_typed_System");
-
- // === STEP 7: Clear the filter ===
- headerBox.Text = "";
- Dispatcher.UIThread.RunJobs();
- nameFilter.Text.Should().BeEmpty("clearing the TextBox must clear the filter");
- view.Count.Should().Be(totalRows,
- "clearing the filter must restore every row in the visible grid view");
- window.CaptureAndShow(label: "step7_cleared");
- }
-}
diff --git a/ILSpy.Tests/Metadata/MetadataFilterTests.cs b/ILSpy.Tests/Metadata/MetadataFilterTests.cs
index a8d40f04b..a185bead0 100644
--- a/ILSpy.Tests/Metadata/MetadataFilterTests.cs
+++ b/ILSpy.Tests/Metadata/MetadataFilterTests.cs
@@ -19,7 +19,10 @@
using System.Linq;
using System.Threading.Tasks;
+using Avalonia.Collections;
+using Avalonia.Controls;
using Avalonia.Headless.NUnit;
+using Avalonia.VisualTree;
using AwesomeAssertions;
@@ -136,12 +139,13 @@ public class MetadataFilterTests
}
[AvaloniaTest]
- public async Task ColumnFilter_Reduces_Visible_Rows_To_Those_Whose_Column_Contains_The_Substring()
+ public async Task Typing_Into_The_Rendered_Header_TextBox_Filters_The_DataGrid_View()
{
- // Integration check: navigate to the TypeDef table (guaranteed populated for any
- // loaded module) and confirm that setting a Name-column filter shrinks the visible
- // row set to entries whose Name contains the filter, and clearing it restores all.
-
+ // End-to-end: navigate to TypeDef, locate the rendered TextBox in the Name column's
+ // header, write into it, and confirm the DataGrid's effective row count drops. We
+ // 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!;
@@ -159,18 +163,47 @@ public class MetadataFilterTests
vm.AssemblyTreeModel.SelectNode(typeDefNode);
var tab = await vm.DockWorkspace.WaitForMetadataTabAsync();
- var totalCount = tab.Items.Count;
- totalCount.Should().BeGreaterThan(0);
+ var metadataPage = await window.WaitForComponent();
+ var grid = await metadataPage.WaitForComponent();
+ await grid.WaitForComponent();
- var nameFilter = tab.ColumnFilters.Single(f => f.ColumnName == "Name");
- nameFilter.Text = "System";
+ var headerBox = grid.GetVisualDescendants().OfType()
+ .FirstOrDefault(tb => tb.FindAncestorOfType() is { } owner
+ && owner.Content is StackPanel sp
+ && sp.Children.OfType().FirstOrDefault()?.Text == "Name");
+ headerBox.Should().NotBeNull(
+ "the column-builder bakes a TextBox into the Name column's header — it must reach the rendered visual tree");
+
+ var nameColumn = tab.Columns.Single(c => (string?)c.Tag == "Name");
+ var modelHeaderBox = ((StackPanel)nameColumn.Header!).Children.OfType().Single();
+ ReferenceEquals(headerBox, modelHeaderBox).Should().BeTrue(
+ "DataGrid must render the StackPanel Header directly without re-templating; otherwise the property-changed handler is on a different TextBox than the user types into");
- var visible = tab.Items.Where(e => MetadataTablePageModel.MatchesFilters(e, tab.ColumnFilters)).ToList();
- visible.Should().NotBeEmpty("at least one TypeDef row should mention 'System'");
- visible.Count.Should().BeLessThan(totalCount, "the filter must hide at least one row");
+ var view = grid.ItemsSource as DataGridCollectionView;
+ view.Should().NotBeNull("the metadata grid wires its ItemsSource to a DataGridCollectionView so the per-column filter can re-evaluate");
+ var totalRows = tab.Items.Count;
+ totalRows.Should().BeGreaterThan(0, "TypeDef must have rows for filtering to be observable");
+ view!.Count.Should().Be(totalRows, "every row should be visible before any filter is set");
+
+ var nameFilter = tab.ColumnFilters.Single(f => f.ColumnName == "Name");
- nameFilter.Text = "";
- var afterClear = tab.Items.Where(e => MetadataTablePageModel.MatchesFilters(e, tab.ColumnFilters)).Count();
- afterClear.Should().Be(totalCount);
+ // Set the rendered TextBox's Text — the same code path a user keystroke takes
+ // (TextProperty AvaloniaObject change → builder's PropertyChanged handler →
+ // ColumnFilter.Text → page.ColumnFilterChanged → view.Refresh()).
+ headerBox!.Text = "System";
+
+ nameFilter.Text.Should().Be("System",
+ "setting the rendered TextBox.Text must propagate to ColumnFilter.Text");
+ var expectedVisible = tab.Items.Count(e => MetadataTablePageModel.MatchesFilters(e, tab.ColumnFilters));
+ expectedVisible.Should().BeLessThan(totalRows,
+ "the predicate must hide at least one TypeDef row when Name contains 'System'");
+ view.Count.Should().Be(expectedVisible,
+ "the live DataGridCollectionView's count must reflect the per-column filter — typing into the header must shrink the visible-row set, not just update the model");
+
+ // Clearing through the rendered TextBox must restore every row.
+ headerBox.Text = "";
+ nameFilter.Text.Should().BeEmpty();
+ view.Count.Should().Be(totalRows,
+ "clearing the filter must restore every row in the visible grid view");
}
}