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"); } }