Browse Source

Fold the metadata filter oracle into the regular test fixture

Upgrade the integration filter test to drive the live DataGrid: locate
the TextBox the column builder embedded in the Name column's header,
set its Text, and assert on DataGridCollectionView.Count — the visible
row count the user actually sees. Drop the temporary verification
fixture; the regression coverage now lives alongside the predicate /
event-forwarding unit tests.

Assisted-by: Claude:claude-opus-4-7:Claude Code
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
321f75cab6
  1. 140
      ILSpy.Tests/Metadata/MetadataFilterRowVerificationTest.cs
  2. 63
      ILSpy.Tests/Metadata/MetadataFilterTests.cs

140
ILSpy.Tests/Metadata/MetadataFilterRowVerificationTest.cs

@ -1,140 +0,0 @@ @@ -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;
/// <summary>
/// TEMPORARY verification harness for the per-column filter wiring. Run with
/// <c>ILSPY_TESTS_VISIBLE=1</c> (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.
/// </summary>
[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<MainWindow>();
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<AssemblyTreeNode>(coreLibName);
assemblyNode.EnsureLazyChildren();
var metadataNode = assemblyNode.Children.OfType<MetadataTreeNode>().Single();
metadataNode.EnsureLazyChildren();
var tablesNode = metadataNode.Children.OfType<MetadataTablesTreeNode>().Single();
tablesNode.EnsureLazyChildren();
var typeDefNode = tablesNode.Children.OfType<TypeDefTableTreeNode>().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<MetadataTablePage>();
TestContext.Out.WriteLine("[VERIFY] MetadataTablePage materialised under the main window.");
var grid = await metadataPage.WaitForComponent<DataGrid>();
TestContext.Out.WriteLine("[VERIFY] Inner DataGrid materialised.");
// Force a layout pass so DataGrid populates its column-headers panel.
Dispatcher.UIThread.RunJobs();
await grid.WaitForComponent<DataGridColumnHeader>();
// === STEP 4: Find the rendered TextBox for the Name column header ===
var headerBox = grid.GetVisualDescendants().OfType<TextBox>()
.FirstOrDefault(tb => tb.FindAncestorOfType<DataGridColumnHeader>() is { } owner
&& owner.Content is StackPanel sp
&& sp.Children.OfType<TextBlock>().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<TextBox>().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");
}
}

63
ILSpy.Tests/Metadata/MetadataFilterTests.cs

@ -19,7 +19,10 @@ @@ -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 @@ -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<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
@ -159,18 +163,47 @@ public class MetadataFilterTests @@ -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<MetadataTablePage>();
var grid = await metadataPage.WaitForComponent<DataGrid>();
await grid.WaitForComponent<DataGridColumnHeader>();
var nameFilter = tab.ColumnFilters.Single(f => f.ColumnName == "Name");
nameFilter.Text = "System";
var headerBox = grid.GetVisualDescendants().OfType<TextBox>()
.FirstOrDefault(tb => tb.FindAncestorOfType<DataGridColumnHeader>() is { } owner
&& owner.Content is StackPanel sp
&& sp.Children.OfType<TextBlock>().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<TextBox>().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");
}
}

Loading…
Cancel
Save