OverviewFilesCommitsIssuesDocsReleasesPeers

ifsrenderer-linux / AvaloniaShell.Tests / ShortcutTests.cs

115 lines · csharp ref:
using System.Linq;

using Avalonia;
using Avalonia.Controls;
using Avalonia.Headless;
using Avalonia.Headless.XUnit;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.Threading;

using Xunit;
using Xunit.Abstractions;

namespace IFSRenderer.AvaloniaShell.Tests;

/// <summary>
/// "is there no undo? ctrl-z doesnt work"
/// </summary>
/// <remarks>
/// Every File-menu item advertises a shortcut — Ctrl+N, Ctrl+O, Ctrl+S, Ctrl+Z, Ctrl+Y and the
/// rest. In Avalonia, <c>MenuItem.InputGesture</c> only DISPLAYS the accelerator; it does not
/// bind it. Actually handling a key needs a KeyBinding on the window (or an explicit key
/// handler, which is how Space and Delete came to work). So the menus promise shortcuts the
/// window never listened for.
/// </remarks>
public class ShortcutTests
{
    private readonly ITestOutputHelper _out;

    public ShortcutTests(ITestOutputHelper output) => _out = output;

    private static (MainWindow Window, Fakes.FakeRenderQueue Queue) Ready()
    {
        (MainWindow window, Fakes.FakeRenderQueue queue) = MainWindowHarness.Start();
        window.Measure(new Size(1500, 900));
        window.Arrange(new Rect(0, 0, 1500, 900));
        Dispatcher.UIThread.RunJobs();
        return (window, queue);
    }

    /// <summary>Undo itself works when invoked; only the key binding was missing.</summary>
    [AvaloniaFact]
    public void Undo_from_the_menu_restores_the_previous_state()
    {
        (MainWindow window, Fakes.FakeRenderQueue queue) = Ready();
        int before = queue.Current.Iterators.Count;

        MainWindowHarness.Click(window, "BtnAddIterator");
        MainWindowHarness.Settle(queue);
        Assert.Equal(before + 1, queue.Current.Iterators.Count);

        var undo = window.FindControl<MenuItem>("MenuUndo");
        Assert.NotNull(undo);
        undo.RaiseEvent(new RoutedEventArgs(MenuItem.ClickEvent));
        MainWindowHarness.Settle(queue);

        Assert.Equal(before, queue.Current.Iterators.Count);
    }

    /// <summary>The reported defect: the advertised accelerator does nothing.</summary>
    [AvaloniaFact]
    public void Ctrl_Z_undoes()
    {
        (MainWindow window, Fakes.FakeRenderQueue queue) = Ready();
        int before = queue.Current.Iterators.Count;

        MainWindowHarness.Click(window, "BtnAddIterator");
        MainWindowHarness.Settle(queue);
        Assert.Equal(before + 1, queue.Current.Iterators.Count);

        window.KeyPressQwerty(PhysicalKey.Z, RawInputModifiers.Control);
        MainWindowHarness.Settle(queue);

        Assert.Equal(before, queue.Current.Iterators.Count);
    }

    /// <summary>
    /// Every accelerator the menus advertise must actually be bound, or the menu is lying.
    /// </summary>
    [AvaloniaFact]
    public void Every_advertised_accelerator_is_bound()
    {
        (MainWindow window, _) = Ready();

        var advertised = window.GetLogicalDescendants()
            .OfType<MenuItem>()
            .Where(m => m.InputGesture is not null)
            .Select(m => (Name: m.Name ?? m.Header?.ToString(), Gesture: m.InputGesture))
            .ToList();

        Assert.NotEmpty(advertised);

        // Bound via the window's own accelerator table, not KeyBindings: the ordinary
        // text-editing keys have to be handed back to a focused text box, which a plain
        // KeyBinding cannot do.
        var bound = window.BoundGestures.ToList();
        _out.WriteLine($"advertised: {advertised.Count}, bound: {bound.Count}");

        var missing = advertised
            // Space is handled by the navigation key handler, which also guards against
            // firing while the user is typing.
            .Where(a => a.Gesture.ToString() != "Space")
            .Where(a => !bound.Any(b => b.Equals(a.Gesture)))
            .ToList();

        foreach (var m in missing)
            _out.WriteLine($"  UNBOUND: {m.Name} -> {m.Gesture}");

        Assert.True(missing.Count == 0,
            $"{missing.Count} menu accelerators are displayed but not bound: "
            + string.Join(", ", missing.Select(m => $"{m.Name}={m.Gesture}")));
    }
}