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