using XFESystem.Core; using XFESystem.Desktop; using XFESystem.Kernel; using Xunit; [assembly: CollectionBehavior(DisableTestParallelization = true)] namespace XFESystem.Desktop.Platform.Tests; public sealed class NativeInputTests { [Theory] [InlineData((byte)0x4F,"End")] [InlineData((byte)0xCF,"End")] [InlineData((byte)0x47,"Home")] [InlineData((byte)0xC7,"Home")] public void PinnedUsNavigationCodesUseDeclaredEndAndHomeMappings(byte scan,string expected) { // Cosmos publishes its pure managed layout implementation only under // linux-x64. Load that exact pinned binary without loading guest-only // hardware managers into the Windows test runner. string packages=Environment.GetEnvironmentVariable("NUGET_PACKAGES")??Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),".nuget","packages"); string path=Path.Combine(packages,"cosmos.kernel.system","3.0.94","runtimes","linux-x64","lib","net10.0","Cosmos.Kernel.System.dll"); var assembly=System.Reflection.Assembly.LoadFrom(path); var layout=Activator.CreateInstance(assembly.GetType("Cosmos.Kernel.System.Input.Layouts.USStandardLayout",true)!)!; var convert=assembly.GetType("Cosmos.Kernel.System.Input.KeyboardLayout",true)!.GetMethod("ConvertScanCode",System.Reflection.BindingFlags.Instance|System.Reflection.BindingFlags.NonPublic)!; var key=convert.Invoke(layout,[scan,false,false,false,false,false]);Assert.NotNull(key); Assert.Equal(expected,key.GetType().GetProperty("Key")!.GetValue(key)!.ToString()); Assert.Equal('\0',key.GetType().GetProperty("KeyChar")!.GetValue(key)); } private sealed class Canvas : IDesktopCanvas { public int Width => 1280; public int Height => 800; public void Fill(uint rgb, int x, int y, int width, int height) { } public void Line(uint rgb, int x1, int y1, int x2, int y2) { } public void Text(string text, uint rgb, int x, int y) { } public void Present() { } } private static DesktopSession Session() => new(new Canvas(), new MemoryWorkspace()); private static DesktopKey? Translate(NativeDesktopInput input, DesktopSession session, byte code, bool up = false, bool lost = false) { var key = input.Translate(code, up, lost, session, out var character); Assert.Equal('\0', character); return key; } private static void Route(NativeDesktopInput input, DesktopSession session, byte code, bool up = false, bool lost = false) { var key = input.Translate(code, up, lost, session, out var character); if (key is { } command) session.Key(command, character); } private static void Drain() { while (NativeKeyboardCapture.TryRead(out _, out _, out _)) { } } [Theory] [InlineData((byte)0x5B)] [InlineData((byte)0x5C)] public void BareWindowsKeyOpensStartExactlyOnceAtRelease(byte windows) { var input = new NativeDesktopInput(); var desktop = Session(); Assert.Null(Translate(input, desktop, windows)); Assert.Null(Translate(input, desktop, windows)); Assert.Equal(DesktopKey.StartMenu, Translate(input, desktop, windows, up: true)); Assert.Null(Translate(input, desktop, windows, up: true)); } [Fact] public void IsolatedWindowsReleaseDoesNotOpenStartMenu() { var input = new NativeDesktopInput(); var desktop = Session(); Assert.Null(Translate(input, desktop, 0x5B, up: true)); Assert.Null(Translate(input, desktop, 0x5C, up: true)); } [Fact] public void WinEOpensFilesWithoutLeakingCharacterOrStartMenu() { var input = new NativeDesktopInput(); var desktop = Session(); var editor = desktop.Open(DesktopApplication.Editor); Route(input, desktop, 0x5B); Route(input, desktop, 0x12); Route(input, desktop, 0x12, up: true); Route(input, desktop, 0x5B, up: true); Assert.Equal(DesktopApplication.Files, desktop.ActiveWindow!.Application); Assert.Empty(editor.Content); Assert.False(desktop.IsStartMenuOpen); Route(input, desktop, 0x5B); Route(input, desktop, 0x5B, up: true); Assert.True(desktop.IsStartMenuOpen); } [Theory] [InlineData((byte)0x20, DesktopKey.ShowDesktop)] [InlineData((byte)0x4B, DesktopKey.SnapLeft)] [InlineData((byte)0x4D, DesktopKey.SnapRight)] [InlineData((byte)0x48, DesktopKey.Maximize)] [InlineData((byte)0x50, DesktopKey.Restore)] public void WindowsChordRoutesSpecificDesktopCommand(byte scan, DesktopKey expected) { var input = new NativeDesktopInput(); var desktop = Session(); Translate(input, desktop, 0x5B); Assert.Equal(expected, Translate(input, desktop, scan)); Assert.Null(Translate(input, desktop, scan, up: true)); Assert.Null(Translate(input, desktop, 0x5B, up: true)); } [Fact] public void BothWindowsKeysReleaseProducesOneStartCommand() { var input = new NativeDesktopInput(); var desktop = Session(); Assert.Null(Translate(input, desktop, 0x5B)); Assert.Null(Translate(input, desktop, 0x5C)); Assert.Null(Translate(input, desktop, 0x5B, up: true)); Assert.Equal(DesktopKey.StartMenu, Translate(input, desktop, 0x5C, up: true)); } [Fact] public void WindowsModifierChordDoesNotTriggerBareWindowsMenu() { var input = new NativeDesktopInput(); var desktop = Session(); Translate(input, desktop, 0x5B); Translate(input, desktop, 0x2A); Assert.Null(Translate(input, desktop, 0x5B, up: true)); Translate(input, desktop, 0x2A, up: true); } [Fact] public void AltTabRemainsOneStableCycleUntilAltRelease() { var input = new NativeDesktopInput(); var desktop = Session(); var first = desktop.Open(DesktopApplication.Files); var second = desktop.Open(DesktopApplication.Editor); var third = desktop.Open(DesktopApplication.Terminal); Route(input, desktop, 0x38); Route(input, desktop, 0x0F); Assert.Same(second, desktop.ActiveWindow); Route(input, desktop, 0x0F, up: true); Assert.True(desktop.IsWindowSwitching); Route(input, desktop, 0x0F); Assert.Same(first, desktop.ActiveWindow); Route(input, desktop, 0x0F, up: true); Route(input, desktop, 0x0F); Assert.Same(third, desktop.ActiveWindow); Route(input, desktop, 0x0F, up: true); Route(input, desktop, 0x38, up: true); Assert.False(desktop.IsWindowSwitching); Assert.Empty(second.Content); } [Fact] public void ShiftAltTabReversesAndWaitsForBothAltReleases() { var input = new NativeDesktopInput(); var desktop = Session(); desktop.Open(DesktopApplication.Files); var second = desktop.Open(DesktopApplication.Editor); desktop.Open(DesktopApplication.Terminal); Route(input, desktop, 0x38); Route(input, desktop, 0x60); Route(input, desktop, 0x2A); Assert.Equal(DesktopKey.SwitchWindowBack, Translate(input, desktop, 0x0F)); Route(input, desktop, 0x0F); Route(input, desktop, 0x38, up: true); Assert.True(desktop.IsWindowSwitching); Route(input, desktop, 0x60, up: true); Assert.False(desktop.IsWindowSwitching); Assert.Empty(second.Content); } [Fact] public void CtrlSSavesGuestEditorWithoutTypingS() { var storage = new MemoryWorkspace(); var desktop = new DesktopSession(new Canvas(), storage); var editor = desktop.Open(DesktopApplication.Editor); desktop.Key(DesktopKey.Character, 'X'); var input = new NativeDesktopInput(); Route(input, desktop, 0x1D); Route(input, desktop, 0x1F); Route(input, desktop, 0x1F, up: true); Route(input, desktop, 0x1D, up: true); Assert.False(editor.IsDirty); Assert.Equal("X", storage.ReadText(editor.Location)); } [Fact] public void CtrlSInFocusedWebEditorReachesDesktopSaveInsteadOfBrowserKeyHandler() { var storage=new MemoryWorkspace();var browser=new FocusedWebEditor(); var desktop=new DesktopSession(new Canvas(),storage,browser); var editor=desktop.Open(DesktopApplication.Editor,SystemPaths.Combine(SystemPaths.Documents,"Hello.cs")); browser.Model="public class SavedByDesktopShortcut { }"; Assert.True(desktop.HasBrowserContentFocus); var input=new NativeDesktopInput(); Route(input,desktop,0x1D);Route(input,desktop,0x1F); Route(input,desktop,0x1F,up:true);Route(input,desktop,0x1D,up:true); Assert.Equal(browser.Model,storage.ReadText(editor.Location)); Assert.DoesNotContain(browser.Keys,key=>key.Key.Equals("s",StringComparison.OrdinalIgnoreCase)); Assert.Contains("Saved to RAM live",editor.Status,StringComparison.Ordinal); } private sealed class FocusedWebEditor : IBrowserService,IInteractiveBrowserFactory,IInteractiveBrowserSession { public string Model=""; public List Keys=[]; public BrowserPage CurrentPage {get;private set;}=new("about:Start","Test",""); public long Revision=>0; public double DocumentHeight=>0; public IInteractiveBrowserSession CreatePageSession()=>this; public BrowserPage Navigate(string address)=>CurrentPage=new(address,"Test",""); public BrowserPage NavigateEditor(string source,string path){Model=source;return CurrentPage=new(path,"Test",source);} public string? ReadEditorText()=>Model; public string ExecuteJavaScript(string source)=>""; public string PreviewMarkdown(string markdown)=>markdown; public void Paint(IDesktopCanvas canvas,DesktopBounds viewport,double scrollY){} public bool Pointer(BrowserPointerInput input)=>false; public bool Key(BrowserKeyboardInput input){Keys.Add(input);return false;} public bool AdvanceTime(long monotonicMilliseconds)=>false; } [Fact] public void AltF4PreservesUnsavedCloseGuard() { var input = new NativeDesktopInput(); var desktop = Session(); var editor = desktop.Open(DesktopApplication.Editor); desktop.Key(DesktopKey.Character, 'X'); Route(input, desktop, 0x38); Route(input, desktop, 0x3E); Route(input, desktop, 0x38, up: true); Assert.Single(desktop.Windows); Assert.True(editor.IsDirty); Assert.Contains("Unsaved", editor.Status); } [Fact] public void LossResetsModifiersEndsCycleAndRejectsStaleWindowsRelease() { var input = new NativeDesktopInput(); var desktop = Session(); desktop.Open(DesktopApplication.Files); desktop.Open(DesktopApplication.Editor); Route(input, desktop, 0x38); Route(input, desktop, 0x0F); Route(input, desktop, 0x5B); Assert.True(desktop.IsWindowSwitching); Route(input, desktop, 0, up: true, lost: true); Assert.False(desktop.IsWindowSwitching); Assert.Null(Translate(input, desktop, 0x5B, up: true)); Route(input, desktop, 0x5B); Route(input, desktop, 0x5B, up: true); Assert.True(desktop.IsStartMenuOpen); } [Fact] public void CapturePreservesOrderedMakeAndBreakReports() { Drain(); NativeKeyboardCapture.Observe(0x5B, false); NativeKeyboardCapture.Observe(0x12, false); NativeKeyboardCapture.Observe(0x12, true); NativeKeyboardCapture.Observe(0x5B, true); (byte Scan, bool Up)[] expected = [(0x5B, false), (0x12, false), (0x12, true), (0x5B, true)]; foreach (var item in expected) { Assert.True(NativeKeyboardCapture.TryRead(out var scan, out var up, out var lost)); Assert.Equal(item.Scan, scan); Assert.Equal(item.Up, up); Assert.False(lost); } Assert.False(NativeKeyboardCapture.TryRead(out _, out _, out _)); } [Fact] public void OverflowDiscardsOldModifierReportsAndSignalsOneReset() { Drain(); NativeKeyboardCapture.Observe(0x5B, false); for (int i = 0; i < 600; i++) NativeKeyboardCapture.Observe(0x12, false); NativeKeyboardCapture.Observe(0x5B, true); Assert.True(NativeKeyboardCapture.TryRead(out var scan, out var released, out var lost)); Assert.Equal(0, scan); Assert.True(released); Assert.True(lost); Assert.False(NativeKeyboardCapture.TryRead(out _, out _, out _)); NativeKeyboardCapture.Observe(0x5B, false); NativeKeyboardCapture.Observe(0x5B, true); Assert.True(NativeKeyboardCapture.TryRead(out scan, out released, out lost)); Assert.Equal(0x5B, scan); Assert.False(released); Assert.False(lost); Assert.True(NativeKeyboardCapture.TryRead(out scan, out released, out lost)); Assert.True(released); } [Fact] public async Task ConcurrentWritersNeverPairScancodeWithAnotherWritersReleaseFlag() { Drain(); var writers = Enumerable.Range(1, 4).Select(index => Task.Run(() => { for (int i = 0; i < 10000; i++) NativeKeyboardCapture.Observe((byte)index, (index & 1) != 0); })).ToArray(); var completed = Task.WhenAll(writers); while (!completed.IsCompleted) { while (NativeKeyboardCapture.TryRead(out var scan, out var released, out var lost)) { if (lost) { Assert.Equal(0, scan); Assert.True(released); continue; } Assert.InRange(scan, 1, 4); Assert.Equal((scan & 1) != 0, released); } await Task.Yield(); } await completed; while (NativeKeyboardCapture.TryRead(out var scan, out var released, out var lost)) if (!lost) { Assert.InRange(scan, 1, 4); Assert.Equal((scan & 1) != 0, released); } NativeKeyboardCapture.Observe(0x7E, false); Assert.True(NativeKeyboardCapture.TryRead(out var marker, out var markerReleased, out var markerLost)); Assert.Equal(0x7E, marker); Assert.False(markerReleased); Assert.False(markerLost); } }