// 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.Linq; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Headless; using Avalonia.Headless.NUnit; using Avalonia.Input; using Avalonia.VisualTree; using AwesomeAssertions; using ICSharpCode.ILSpy.Metadata.Filters; using ICSharpCode.ILSpy.Views.Filters; using NUnit.Framework; namespace ICSharpCode.ILSpy.Tests.Views.Filters; /// /// Pins arrow-key navigation between chips inside . /// The popup is composed of one per axis plus an /// ; without explicit directional-navigation wiring /// the user can only Tab between every focusable element (chips + Clear button), which /// is awkward for a power-user filter. Arrow keys should hop chip-by-chip. /// [TestFixture] public class FlagsFilterPopupKeyboardTests { [System.Flags] enum SampleAttrs : uint { Public = 1, Private = 2, VisibilityMask = 0x3, Static = 0x10, Abstract = 0x20, } [AvaloniaTest] public void Right_Arrow_From_The_First_Chip_Moves_Focus_To_The_Next_Chip_In_The_Same_Group() { // Build the popup, push it onto a window so focus management works, focus the // first chip, send Right. Default Avalonia DirectionalNavigation should carry // focus through siblings inside the WrapPanel. var schema = FlagsSchemaInferer.For(typeof(SampleAttrs)); var state = new FilterState(schema); var popup = new FlagsFilterPopup(state); var window = new Window { Content = popup }; window.Show(); var chips = popup.GetVisualDescendants().OfType().ToList(); chips.Should().HaveCountGreaterThan(1, "test relies on at least two chips being present"); var first = chips[0]; first.Focus(); first.IsFocused.Should().BeTrue("setup precondition — first chip must accept focus before we exercise arrow nav"); window.KeyPress(Key.Right, RawInputModifiers.None, PhysicalKey.ArrowRight, keySymbol: null); var newlyFocused = chips.SingleOrDefault(c => c.IsFocused); newlyFocused.Should().NotBeNull("Right arrow must transfer focus to a sibling chip"); ReferenceEquals(newlyFocused, first).Should().BeFalse( "Right arrow must move focus AWAY from the first chip, not stay on it"); } [AvaloniaTest] public void Down_Arrow_From_A_Chip_In_The_First_Group_Lands_On_A_Chip_In_The_Next_Group() { // Vertical traversal: pressing Down from a chip in the first mutex group must move // into the second group (or the independent-flags group if there's only one mutex // group). Without arrow-traversal wiring the focus would stay inside the same // WrapPanel because WrapPanel is a horizontal-flow container. var schema = FlagsSchemaInferer.For(typeof(SampleAttrs)); var state = new FilterState(schema); var popup = new FlagsFilterPopup(state); var window = new Window { Content = popup }; window.Show(); var groups = popup.GetVisualDescendants() .OfType() .Where(c => c is MutexChipGroup || c is IndependentFlagGroup) .ToList(); groups.Should().HaveCountGreaterThan(1, "test relies on at least two filter groups (one mutex + one independent, or two mutex)"); // First group is a MutexChipGroup full of ToggleButton chips; second group is an // IndependentFlagGroup full of Button pills. Allow either control type as a valid // landing slot so we're testing the cross-group jump, not the chip type. var firstGroupChips = groups[0].GetVisualDescendants().OfType().ToList(); var secondGroupTargets = groups[1].GetVisualDescendants() .Where(d => d is ToggleButton || d is Button) .ToList(); var anchor = firstGroupChips[0]; anchor.Focus(); anchor.IsFocused.Should().BeTrue(); window.KeyPress(Key.Down, RawInputModifiers.None, PhysicalKey.ArrowDown, keySymbol: null); var focusedNow = popup.GetVisualDescendants() .OfType() .FirstOrDefault(c => c.IsFocused && (c is ToggleButton || c is Button)); focusedNow.Should().NotBeNull("Down arrow must keep focus on some chip in the popup"); secondGroupTargets.Contains(focusedNow!).Should().BeTrue( "Down arrow from the first group must land on a chip/pill in the next group"); } }