using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using XFESystem.Browser;
using XFESystem.Core;
using XFESystem.Desktop;
using XFESystem.NativeKernel;
using XFESystem.NativeRuntime.Application;
using XFESystem.Platform;
namespace XFESystem.Platform.Tests;
public sealed class Ps2InputTests
{
[Fact]
public void TerminalReceivesInsertAndControlCWithoutBrowserInterception()
{
var actions = Drain(new Ps2InputSource(new Reports(Keys(0xE0, 0x52, 0xE0, 0xD2, 0x1D, 0x2E, 0xAE, 0x9D)),
1024, 768, 100, () => new(false, DesktopApplication.Terminal)));
Assert.Collection(actions,
item => Assert.Equal(DesktopKey.Insert, item.Key),
item => Assert.Equal(DesktopKey.Cancel, item.Key));
}
[Fact]
public void ReportAbiIs32BytesAndDecoderStateHasNoManagedReferences()
{
Assert.Equal(32, Marshal.SizeOf<NativeInputReport>());
Assert.Equal(16, Marshal.OffsetOf<NativeInputReport>(nameof(NativeInputReport.DeltaX)).ToInt32());
Assert.Equal(24, Marshal.OffsetOf<NativeInputReport>(nameof(NativeInputReport.Buttons)).ToInt32());
Assert.Equal(28, Marshal.OffsetOf<NativeInputReport>(nameof(NativeInputReport.Wheel)).ToInt32());
Assert.False(RuntimeHelpers.IsReferenceOrContainsReferences<Ps2DecodeState>());
}
[Fact]
public void ExtendedMakeBreakRetainsIdentityAndTwoControlKeysDoNotReleaseEachOther()
{
var reports = Keys(0x1D, 0xE0, 0x1D, 0x9D, 0xE0, 0x9D, 0xE0, 0x4B, 0xE0, 0xCB);
Assert.Equal(6, reports.Length);
Assert.Equal(0x11Du, reports[1].KeyCode); Assert.Equal(Ps2Decoder.Control, reports[2].Modifiers);
Assert.Equal(0u, reports[3].Modifiers); Assert.Equal(0x14Bu, reports[4].KeyCode);
Assert.Equal(Ps2Decoder.KeyUp, reports[5].Kind); Assert.Equal(0x14Bu, reports[5].KeyCode);
Assert.Equal(0u, reports[5].Unicode);
}
[Fact]
public void CompletePauseSequenceDoesNotToggleNumLockOrPressControl()
{
var reports = Keys(0xE1, 0x1D, 0x45, 0xE1, 0x9D, 0xC5, 0x1E, 0x9E);
Assert.Equal(3, reports.Length); Assert.Equal(Ps2Decoder.PauseKey, reports[0].KeyCode);
Assert.Equal(0u, reports[0].Modifiers); Assert.Equal((uint)'a', reports[1].Unicode);
}
[Fact]
public void CorruptE1AndHardwareOverrunProduceRecoveryAndClearHeldModifiers()
{
var reports = Keys(0x2A, 0xE1, 0x1D, 0x00, 0x1E, 0xFF, 0x30);
Assert.Equal(Ps2Decoder.Lost, reports[1].Kind); Assert.Equal(0u, reports[1].Modifiers);
Assert.Equal((uint)'a', reports[2].Unicode); Assert.Equal(Ps2Decoder.Lost, reports[3].Kind);
Assert.Equal((uint)'b', reports[4].Unicode);
}
[Fact]
public void PrintScreenFakeShiftDoesNotCreateAStickyModifier()
{
var reports = Keys(0xE0, 0x2A, 0xE0, 0x37, 0xE0, 0xB7, 0xE0, 0xAA, 0x1E);
Assert.Equal(3, reports.Length); Assert.Equal(0x137u, reports[0].KeyCode);
Assert.Equal(Ps2Decoder.KeyUp, reports[1].Kind); Assert.All(reports, report => Assert.Equal(0u, report.Modifiers));
Assert.Equal((uint)'a', reports[2].Unicode);
}
[Fact]
public void CapsOnlyTogglesOnFirstMakeAndCombinesWithShiftUsingXor()
{
var reports = Keys(0x3A, 0x3A, 0x1E, 0x9E, 0x2A, 0x1E, 0x9E, 0xAA, 0x1E, 0x9E, 0xBA, 0x3A, 0x1E);
Assert.Equal((uint)'A', reports[2].Unicode);
Assert.Equal((uint)'a', reports[5].Unicode);
Assert.Equal((uint)'A', reports[8].Unicode);
Assert.Equal((uint)'a', reports[^1].Unicode);
}
[Fact]
public void OverrunWhileCapsIsHeldDoesNotTurnItsNextTypematicMakeIntoAnotherToggle()
{
var reports = Keys(0x3A, 0x00, 0x3A, 0x1E, 0xBA, 0x3A, 0x1E);
Assert.Equal(Ps2Decoder.Lost, reports[1].Kind);
Assert.Equal(Ps2Decoder.CapsLock, reports[2].Modifiers);
Assert.Equal((uint)'A', reports[3].Unicode); Assert.Equal((uint)'a', reports[^1].Unicode);
}
[Fact]
public void NumLockKeypadCharactersRemainDistinctFromExtendedNavigation()
{
var actions = Actions(Keys(0x45, 0xC5, 0x4B, 0xCB, 0xE0, 0x4B, 0xE0, 0xCB));
Assert.Collection(actions,
item => { Assert.Equal(DesktopKey.Character, item.Key); Assert.Equal('4', item.Character); },
item => Assert.Equal(DesktopKey.Left, item.Key));
}
[Fact]
public void MouseResynchronizesHeaderAndDecodesNineBitSignedScreenDeltas()
{
var reports = Mouse(0x00, 0x07, 0x18, 0xFF, 0x02, 0x29, 0x01, 0xFF);
Assert.Equal(2, reports.Length);
Assert.Equal(-1, reports[0].DeltaX); Assert.Equal(-2, reports[0].DeltaY);
Assert.Equal(1, reports[1].DeltaX); Assert.Equal(1, reports[1].DeltaY); Assert.Equal(1u, reports[1].Buttons);
}
[Fact]
public void MouseOverflowKeepsRealButtonsButDiscardsMotionAndRecoversNextPacket()
{
var reports = Mouse(0xC9, 0x7F, 0x7F, 0x08, 0x02, 0x00);
Assert.Equal(Ps2Decoder.Lost, reports[0].Kind); Assert.Equal(1u, reports[0].Buttons);
Assert.Equal(0, reports[0].DeltaX); Assert.Equal(0, reports[0].DeltaY);
Assert.Equal(Ps2Decoder.Pointer, reports[1].Kind); Assert.Equal(0u, reports[1].Buttons); Assert.Equal(2, reports[1].DeltaX);
}
[Theory]
[InlineData(0x01, -1)]
[InlineData(0xFF, 1)]
[InlineData(0x7F, -127)]
[InlineData(0x80, 128)]
public unsafe void IntelliMouseWaitsForFourthByteAndUsesTheFullSignedWheelByte(byte wheelByte, int wheel)
{
Ps2DecodeState state = default; NativeInputReport report;
Assert.True(Ps2Decoder.SetMouseDeviceId(ref state, 3));
Assert.False(Ps2Decoder.DecodeMouse(ref state, 0x29, &report));
Assert.False(Ps2Decoder.DecodeMouse(ref state, 2, &report));
Assert.False(Ps2Decoder.DecodeMouse(ref state, 0xff, &report));
Assert.True(Ps2Decoder.DecodeMouse(ref state, wheelByte, &report));
Assert.Equal(Ps2Decoder.Pointer, report.Kind); Assert.Equal(2, report.DeltaX);
Assert.Equal(1, report.DeltaY); Assert.Equal(1u, report.Buttons); Assert.Equal(wheel, report.Wheel);
}
[Fact]
public unsafe void ProtocolSelectionClearsPartialPacketAndStandardFallbackConsumesOnlyThreeBytes()
{
Ps2DecodeState state = default; NativeInputReport report;
Assert.True(Ps2Decoder.SetMouseDeviceId(ref state, 3));
Assert.False(Ps2Decoder.DecodeMouse(ref state, 0x08, &report));
Assert.False(Ps2Decoder.DecodeMouse(ref state, 10, &report));
Assert.True(Ps2Decoder.SetMouseDeviceId(ref state, 0));
Assert.False(Ps2Decoder.DecodeMouse(ref state, 0x08, &report));
Assert.False(Ps2Decoder.DecodeMouse(ref state, 4, &report));
Assert.True(Ps2Decoder.DecodeMouse(ref state, 0, &report));
Assert.Equal(4, report.DeltaX); Assert.Equal(0, report.Wheel);
Assert.True(Ps2Decoder.SetMouseDeviceId(ref state, 3));
Assert.False(Ps2Decoder.DecodeMouse(ref state, 0x08, &report));
Assert.False(Ps2Decoder.DecodeMouse(ref state, 0, &report));
Assert.False(Ps2Decoder.DecodeMouse(ref state, 0, &report));
Assert.True(Ps2Decoder.DecodeMouse(ref state, 0xff, &report)); Assert.Equal(1, report.Wheel);
}
[Theory]
[InlineData(1)]
[InlineData(4)]
[InlineData(0xff)]
public void UnknownDeviceIdIsRejectedInsteadOfClaimingWheelSupport(byte id)
{
Ps2DecodeState state = default;
Assert.True(Ps2Decoder.SetMouseDeviceId(ref state, 3));
Assert.False(Ps2Decoder.SetMouseDeviceId(ref state, id));
Assert.Equal(0, state.MouseDeviceId); Assert.Equal(0, state.MouseIndex);
}
[Fact]
public unsafe void FourByteOverflowConsumesWheelThenRecoversWithoutPhantomMotion()
{
Ps2DecodeState state = default; NativeInputReport report;
Assert.True(Ps2Decoder.SetMouseDeviceId(ref state, 3));
foreach (byte value in new byte[] { 0xc9, 0x7f, 0x7f }) Assert.False(Ps2Decoder.DecodeMouse(ref state, value, &report));
Assert.True(Ps2Decoder.DecodeMouse(ref state, 0x08, &report));
Assert.Equal(Ps2Decoder.Lost, report.Kind); Assert.Equal(1u, report.Buttons);
Assert.Equal(0, report.DeltaX); Assert.Equal(0, report.DeltaY); Assert.Equal(0, report.Wheel);
foreach (byte value in new byte[] { 0x08, 2, 0 }) Assert.False(Ps2Decoder.DecodeMouse(ref state, value, &report));
Assert.True(Ps2Decoder.DecodeMouse(ref state, 1, &report));
Assert.Equal(Ps2Decoder.Pointer, report.Kind); Assert.Equal(2, report.DeltaX); Assert.Equal(-1, report.Wheel);
}
[Fact]
public unsafe void HotLossClearsPartialWheelPacketButPreservesTheNegotiatedFormat()
{
Ps2DecodeState state = default; NativeInputReport report;
Assert.True(Ps2Decoder.SetMouseDeviceId(ref state, 3));
foreach (byte value in new byte[] { 0x08, 0, 0 }) Assert.False(Ps2Decoder.DecodeMouse(ref state, value, &report));
Ps2Decoder.ResetMouse(ref state); Assert.Equal(3, state.MouseDeviceId);
foreach (byte value in new byte[] { 0x08, 3, 0 }) Assert.False(Ps2Decoder.DecodeMouse(ref state, value, &report));
Assert.True(Ps2Decoder.DecodeMouse(ref state, 0xff, &report));
Assert.Equal(3, report.DeltaX); Assert.Equal(1, report.Wheel);
}
[Fact]
public void NegotiatedWheelCapabilityAndReportsReachTheActualDesktopInputContract()
{
var source = Source(new Reports(new[] { new NativeInputReport { Kind = Ps2Decoder.Pointer, Wheel = -1 } }) { Capabilities = 7 });
Assert.Equal(7u, source.Capabilities);
Assert.True(source.TryRead(out var wheel)); Assert.Equal(NativeInputKind.Pointer, wheel.Kind); Assert.Equal(-1, wheel.Wheel);
}
[Fact]
public void AvailabilityComesFromActualBackendInitializationCapabilities()
{
var backend = new Reports(Keys(0x1E)) { Capabilities = 0 };
var source = Source(backend);
Assert.False(source.IsAvailable); Assert.False(source.TryRead(out _)); Assert.Equal(0, source.ReportsRead);
backend.Capabilities = 1; Assert.True(source.IsAvailable);
Assert.True(source.TryRead(out var report)); Assert.Equal('a', report.Character);
}
[Fact]
public void WindowsChordReleaseDoesNotOpenStartMenuAndStandaloneWindowsReleaseDoes()
{
var chord = Actions(Keys(0xE0, 0x5B, 0x12, 0x92, 0xE0, 0xDB));
Assert.Single(chord); Assert.Equal(DesktopKey.OpenFiles, chord[0].Key);
var alone = Actions(Keys(0xE0, 0x5B, 0xE0, 0x5C, 0xE0, 0xDB, 0xE0, 0xDC, 0xE0, 0xDC));
Assert.Single(alone); Assert.Equal(DesktopKey.StartMenu, alone[0].Key);
var snap = Actions(Keys(0xE0, 0x5B, 0xE0, 0x4B, 0xE0, 0xCB, 0xE0, 0xDB));
Assert.Single(snap); Assert.Equal(DesktopKey.SnapLeft, snap[0].Key);
}
[Fact]
public void AltTabSwitchEndsOnlyWhenLastAltIsReleased()
{
var actions = Actions(Keys(0x38, 0xE0, 0x38, 0x0F, 0x8F, 0xB8, 0xE0, 0xB8));
Assert.Equal(new[] { DesktopKey.SwitchWindow, DesktopKey.EndWindowSwitch }, actions.Select(report => report.Key));
var backwards = Actions(Keys(0x2A, 0x38, 0x0F, 0x8F, 0xB8, 0xAA));
Assert.Equal(new[] { DesktopKey.SwitchWindowBack, DesktopKey.EndWindowSwitch }, backwards.Select(report => report.Key));
}
[Fact]
public void BrowserReceivesPhysicalCodeRepeatReleaseAndModifierStateWithoutControlText()
{
var actions = Actions(Keys(0x2A, 0x1E, 0x1E, 0x9E, 0xAA, 0x1D, 0x1E, 0x9E, 0x9D), browser: true);
Assert.Equal(9, actions.Length);
Assert.Equal("KeyA", actions[1].BrowserKey.Code); Assert.Equal("A", actions[1].BrowserKey.Text);
Assert.False(actions[1].BrowserKey.Repeat); Assert.True(actions[2].BrowserKey.Repeat);
Assert.Equal("keyup", actions[3].BrowserKey.Type); Assert.Equal("", actions[3].BrowserKey.Text);
Assert.False(actions[4].BrowserKey.Shift); Assert.True(actions[6].BrowserKey.Control); Assert.Equal("", actions[6].BrowserKey.Text);
Assert.False(actions[8].BrowserKey.Control);
}
[Fact]
public void LostReportsReleaseBrowserKeysAndDoNotTurnAWindowsBreakIntoAStartGesture()
{
var before = Keys(0x1D, 0x1E);
var backend = new Reports(before.Concat(new[] { new NativeInputReport { Kind = Ps2Decoder.Lost, Buttons = 1 } }).ToArray());
var source = Source(backend, true); var actions = Drain(source);
Assert.Equal(5, actions.Length);
Assert.All(actions.Skip(2).Take(2), value => Assert.Equal("keyup", value.BrowserKey.Type));
Assert.Equal(NativeInputKind.ReportsLost, actions[^1].Kind); Assert.True(actions[^1].LeftButton);
Assert.Equal(1, source.LostReports);
var win = Actions(Keys(0xE0, 0x5B).Concat(new[] { new NativeInputReport { Kind = Ps2Decoder.Lost } })
.Concat(Keys(0xE0, 0xDB)).ToArray());
Assert.DoesNotContain(win, action => action.Kind == NativeInputKind.Key && action.Key == DesktopKey.StartMenu);
}
[Fact]
public void PointerAccumulationUsesNativePixelsAndClipsBeforeConvertingToLayout()
{
var backend = new Reports(new[]
{
new NativeInputReport { Kind = Ps2Decoder.Pointer, DeltaX = 10, DeltaY = 20, Buttons = 1 },
new NativeInputReport { Kind = Ps2Decoder.Pointer, DeltaX = int.MinValue, DeltaY = int.MaxValue }
});
var source = new Ps2InputSource(backend, 1000, 800, 200, () => new(false, null));
Assert.True(source.TryRead(out var move)); Assert.Equal(255, move.X); Assert.Equal(210, move.Y); Assert.True(move.LeftButton);
Assert.True(source.TryRead(out var clipped)); Assert.Equal(0, clipped.X); Assert.Equal(399, clipped.Y);
var fraction = new Ps2InputSource(new Reports(new[] { new NativeInputReport { Kind = Ps2Decoder.Pointer, DeltaX = 100, DeltaY = 100 } }), 11, 9, 125, () => new(false, null));
Assert.True(fraction.TryRead(out var edge)); Assert.Equal(7, edge.X); Assert.Equal(6, edge.Y);
}
[Fact]
public void ActualDesktopCancelsCloseCaptureOnLostPacketWithoutSynthesizingClick()
{
var desktop = new DesktopSession(new CpuFramebufferCanvas(new ArrayFramebufferPresenter(1024, 768)), new MemoryWorkspace());
var window = desktop.Open(DesktopApplication.Files);
int closeX = window.Bounds.X + window.Bounds.Width - 20, closeY = window.Bounds.Y + 12;
var backend = new Reports(new[]
{
new NativeInputReport { Kind = Ps2Decoder.Pointer, DeltaX = closeX - 512, DeltaY = closeY - 384, Buttons = 1 },
new NativeInputReport { Kind = Ps2Decoder.Lost, Buttons = 0 },
new NativeInputReport { Kind = Ps2Decoder.Pointer, Buttons = 0 }
});
var source = new Ps2InputSource(backend, 1024, 768, 100, () => new(desktop.HasBrowserContentFocus, desktop.ActiveWindow?.Application));
foreach (var input in Drain(source)) NativeInput.Dispatch(desktop, input, 1000);
Assert.Single(desktop.Windows);
backend.Add(new() { Kind = Ps2Decoder.Pointer, Buttons = 1 }); backend.Add(new() { Kind = Ps2Decoder.Pointer, Buttons = 0 });
foreach (var input in Drain(source)) NativeInput.Dispatch(desktop, input, 1100);
Assert.Empty(desktop.Windows);
}
[Fact]
public void ActualPs2ByteStreamTypesIntoTheExistingJintBrowserTextarea()
{
var canvas = new CpuFramebufferCanvas(new ArrayFramebufferPresenter(1024, 768));
var desktop = new DesktopSession(canvas, new MemoryWorkspace(), new BrowserService(new InputPage()));
var window = desktop.Open(DesktopApplication.Browser, "http://input.test/"); desktop.Render();
int x = window.Bounds.X + 12, y = window.Bounds.Y + 77;
desktop.Pointer(x, y, true, timestampMs: 100); desktop.Pointer(x, y, false, timestampMs: 101);
Assert.True(desktop.HasBrowserContentFocus);
var source = new Ps2InputSource(new Reports(Keys(0x1E, 0x9E, 0x30, 0xB0, 0xE0, 0x4B, 0xE0, 0xCB, 0x2E, 0xAE)),
1024, 768, 100, () => new(desktop.HasBrowserContentFocus, desktop.ActiveWindow?.Application));
while (source.TryRead(out var report)) NativeInput.Dispatch(desktop, report, 200);
var browser = Assert.IsType<BrowserService>(window.BrowserSession);
Assert.Equal("acb", browser.ExecuteJavaScript("document.getElementById('Entry').value")); browser.Release();
}
private sealed class InputPage : IWebTransport
{
public WebResponse Get(Uri address, BrowserLimits limits) => new(address, 200, "text/html", "<style>body{margin:0}textarea{display:block;width:300px;height:100px;margin:0;padding:0;border:0}</style><textarea id='Entry'></textarea>");
}
private sealed class Reports : IPs2ReportBackend
{
private readonly Queue<NativeInputReport> reports;
public uint Capabilities { get; set; } = 3;
internal Reports(NativeInputReport[] reports) => this.reports = new(reports);
public bool TryRead(out NativeInputReport report) => reports.TryDequeue(out report);
internal void Add(NativeInputReport report) => reports.Enqueue(report);
}
private static Ps2InputSource Source(Reports backend, bool browser = false) => new(backend, 1024, 768, 100, () => new(browser, browser ? DesktopApplication.Browser : DesktopApplication.Editor));
private static NativeInputEvent[] Actions(NativeInputReport[] reports, bool browser = false) => Drain(Source(new(reports), browser));
private static NativeInputEvent[] Drain(Ps2InputSource source)
{ var result = new List<NativeInputEvent>(); for (int i = 0; i < 1024 && source.TryRead(out var report); i++) result.Add(report); return result.ToArray(); }
private static unsafe NativeInputReport[] Keys(params byte[] bytes)
{
Ps2DecodeState state = default; var result = new List<NativeInputReport>();
foreach (byte value in bytes) { NativeInputReport report; if (Ps2Decoder.DecodeKeyboard(ref state, value, &report)) result.Add(report); }
return result.ToArray();
}
private static unsafe NativeInputReport[] Mouse(params byte[] bytes)
{
Ps2DecodeState state = default; var result = new List<NativeInputReport>();
foreach (byte value in bytes) { NativeInputReport report; if (Ps2Decoder.DecodeMouse(ref state, value, &report)) result.Add(report); }
return result.ToArray();
}
}
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using XFESystem.Browser;
using XFESystem.Core;
using XFESystem.Desktop;
using XFESystem.NativeKernel;
using XFESystem.NativeRuntime.Application;
using XFESystem.Platform;
namespace XFESystem.Platform.Tests;
public sealed class Ps2InputTests
{
[Fact]
public void TerminalReceivesInsertAndControlCWithoutBrowserInterception()
{
var actions = Drain(new Ps2InputSource(new Reports(Keys(0xE0, 0x52, 0xE0, 0xD2, 0x1D, 0x2E, 0xAE, 0x9D)),
1024, 768, 100, () => new(false, DesktopApplication.Terminal)));
Assert.Collection(actions,
item => Assert.Equal(DesktopKey.Insert, item.Key),
item => Assert.Equal(DesktopKey.Cancel, item.Key));
}
[Fact]
public void ReportAbiIs32BytesAndDecoderStateHasNoManagedReferences()
{
Assert.Equal(32, Marshal.SizeOf<NativeInputReport>());
Assert.Equal(16, Marshal.OffsetOf<NativeInputReport>(nameof(NativeInputReport.DeltaX)).ToInt32());
Assert.Equal(24, Marshal.OffsetOf<NativeInputReport>(nameof(NativeInputReport.Buttons)).ToInt32());
Assert.Equal(28, Marshal.OffsetOf<NativeInputReport>(nameof(NativeInputReport.Wheel)).ToInt32());
Assert.False(RuntimeHelpers.IsReferenceOrContainsReferences<Ps2DecodeState>());
}
[Fact]
public void ExtendedMakeBreakRetainsIdentityAndTwoControlKeysDoNotReleaseEachOther()
{
var reports = Keys(0x1D, 0xE0, 0x1D, 0x9D, 0xE0, 0x9D, 0xE0, 0x4B, 0xE0, 0xCB);
Assert.Equal(6, reports.Length);
Assert.Equal(0x11Du, reports[1].KeyCode); Assert.Equal(Ps2Decoder.Control, reports[2].Modifiers);
Assert.Equal(0u, reports[3].Modifiers); Assert.Equal(0x14Bu, reports[4].KeyCode);
Assert.Equal(Ps2Decoder.KeyUp, reports[5].Kind); Assert.Equal(0x14Bu, reports[5].KeyCode);
Assert.Equal(0u, reports[5].Unicode);
}
[Fact]
public void CompletePauseSequenceDoesNotToggleNumLockOrPressControl()
{
var reports = Keys(0xE1, 0x1D, 0x45, 0xE1, 0x9D, 0xC5, 0x1E, 0x9E);
Assert.Equal(3, reports.Length); Assert.Equal(Ps2Decoder.PauseKey, reports[0].KeyCode);
Assert.Equal(0u, reports[0].Modifiers); Assert.Equal((uint)'a', reports[1].Unicode);
}
[Fact]
public void CorruptE1AndHardwareOverrunProduceRecoveryAndClearHeldModifiers()
{
var reports = Keys(0x2A, 0xE1, 0x1D, 0x00, 0x1E, 0xFF, 0x30);
Assert.Equal(Ps2Decoder.Lost, reports[1].Kind); Assert.Equal(0u, reports[1].Modifiers);
Assert.Equal((uint)'a', reports[2].Unicode); Assert.Equal(Ps2Decoder.Lost, reports[3].Kind);
Assert.Equal((uint)'b', reports[4].Unicode);
}
[Fact]
public void PrintScreenFakeShiftDoesNotCreateAStickyModifier()
{
var reports = Keys(0xE0, 0x2A, 0xE0, 0x37, 0xE0, 0xB7, 0xE0, 0xAA, 0x1E);
Assert.Equal(3, reports.Length); Assert.Equal(0x137u, reports[0].KeyCode);
Assert.Equal(Ps2Decoder.KeyUp, reports[1].Kind); Assert.All(reports, report => Assert.Equal(0u, report.Modifiers));
Assert.Equal((uint)'a', reports[2].Unicode);
}
[Fact]
public void CapsOnlyTogglesOnFirstMakeAndCombinesWithShiftUsingXor()
{
var reports = Keys(0x3A, 0x3A, 0x1E, 0x9E, 0x2A, 0x1E, 0x9E, 0xAA, 0x1E, 0x9E, 0xBA, 0x3A, 0x1E);
Assert.Equal((uint)'A', reports[2].Unicode);
Assert.Equal((uint)'a', reports[5].Unicode);
Assert.Equal((uint)'A', reports[8].Unicode);
Assert.Equal((uint)'a', reports[^1].Unicode);
}
[Fact]
public void OverrunWhileCapsIsHeldDoesNotTurnItsNextTypematicMakeIntoAnotherToggle()
{
var reports = Keys(0x3A, 0x00, 0x3A, 0x1E, 0xBA, 0x3A, 0x1E);
Assert.Equal(Ps2Decoder.Lost, reports[1].Kind);
Assert.Equal(Ps2Decoder.CapsLock, reports[2].Modifiers);
Assert.Equal((uint)'A', reports[3].Unicode); Assert.Equal((uint)'a', reports[^1].Unicode);
}
[Fact]
public void NumLockKeypadCharactersRemainDistinctFromExtendedNavigation()
{
var actions = Actions(Keys(0x45, 0xC5, 0x4B, 0xCB, 0xE0, 0x4B, 0xE0, 0xCB));
Assert.Collection(actions,
item => { Assert.Equal(DesktopKey.Character, item.Key); Assert.Equal('4', item.Character); },
item => Assert.Equal(DesktopKey.Left, item.Key));
}
[Fact]
public void MouseResynchronizesHeaderAndDecodesNineBitSignedScreenDeltas()
{
var reports = Mouse(0x00, 0x07, 0x18, 0xFF, 0x02, 0x29, 0x01, 0xFF);
Assert.Equal(2, reports.Length);
Assert.Equal(-1, reports[0].DeltaX); Assert.Equal(-2, reports[0].DeltaY);
Assert.Equal(1, reports[1].DeltaX); Assert.Equal(1, reports[1].DeltaY); Assert.Equal(1u, reports[1].Buttons);
}
[Fact]
public void MouseOverflowKeepsRealButtonsButDiscardsMotionAndRecoversNextPacket()
{
var reports = Mouse(0xC9, 0x7F, 0x7F, 0x08, 0x02, 0x00);
Assert.Equal(Ps2Decoder.Lost, reports[0].Kind); Assert.Equal(1u, reports[0].Buttons);
Assert.Equal(0, reports[0].DeltaX); Assert.Equal(0, reports[0].DeltaY);
Assert.Equal(Ps2Decoder.Pointer, reports[1].Kind); Assert.Equal(0u, reports[1].Buttons); Assert.Equal(2, reports[1].DeltaX);
}
[Theory]
[InlineData(0x01, -1)]
[InlineData(0xFF, 1)]
[InlineData(0x7F, -127)]
[InlineData(0x80, 128)]
public unsafe void IntelliMouseWaitsForFourthByteAndUsesTheFullSignedWheelByte(byte wheelByte, int wheel)
{
Ps2DecodeState state = default; NativeInputReport report;
Assert.True(Ps2Decoder.SetMouseDeviceId(ref state, 3));
Assert.False(Ps2Decoder.DecodeMouse(ref state, 0x29, &report));
Assert.False(Ps2Decoder.DecodeMouse(ref state, 2, &report));
Assert.False(Ps2Decoder.DecodeMouse(ref state, 0xff, &report));
Assert.True(Ps2Decoder.DecodeMouse(ref state, wheelByte, &report));
Assert.Equal(Ps2Decoder.Pointer, report.Kind); Assert.Equal(2, report.DeltaX);
Assert.Equal(1, report.DeltaY); Assert.Equal(1u, report.Buttons); Assert.Equal(wheel, report.Wheel);
}
[Fact]
public unsafe void ProtocolSelectionClearsPartialPacketAndStandardFallbackConsumesOnlyThreeBytes()
{
Ps2DecodeState state = default; NativeInputReport report;
Assert.True(Ps2Decoder.SetMouseDeviceId(ref state, 3));
Assert.False(Ps2Decoder.DecodeMouse(ref state, 0x08, &report));
Assert.False(Ps2Decoder.DecodeMouse(ref state, 10, &report));
Assert.True(Ps2Decoder.SetMouseDeviceId(ref state, 0));
Assert.False(Ps2Decoder.DecodeMouse(ref state, 0x08, &report));
Assert.False(Ps2Decoder.DecodeMouse(ref state, 4, &report));
Assert.True(Ps2Decoder.DecodeMouse(ref state, 0, &report));
Assert.Equal(4, report.DeltaX); Assert.Equal(0, report.Wheel);
Assert.True(Ps2Decoder.SetMouseDeviceId(ref state, 3));
Assert.False(Ps2Decoder.DecodeMouse(ref state, 0x08, &report));
Assert.False(Ps2Decoder.DecodeMouse(ref state, 0, &report));
Assert.False(Ps2Decoder.DecodeMouse(ref state, 0, &report));
Assert.True(Ps2Decoder.DecodeMouse(ref state, 0xff, &report)); Assert.Equal(1, report.Wheel);
}
[Theory]
[InlineData(1)]
[InlineData(4)]
[InlineData(0xff)]
public void UnknownDeviceIdIsRejectedInsteadOfClaimingWheelSupport(byte id)
{
Ps2DecodeState state = default;
Assert.True(Ps2Decoder.SetMouseDeviceId(ref state, 3));
Assert.False(Ps2Decoder.SetMouseDeviceId(ref state, id));
Assert.Equal(0, state.MouseDeviceId); Assert.Equal(0, state.MouseIndex);
}
[Fact]
public unsafe void FourByteOverflowConsumesWheelThenRecoversWithoutPhantomMotion()
{
Ps2DecodeState state = default; NativeInputReport report;
Assert.True(Ps2Decoder.SetMouseDeviceId(ref state, 3));
foreach (byte value in new byte[] { 0xc9, 0x7f, 0x7f }) Assert.False(Ps2Decoder.DecodeMouse(ref state, value, &report));
Assert.True(Ps2Decoder.DecodeMouse(ref state, 0x08, &report));
Assert.Equal(Ps2Decoder.Lost, report.Kind); Assert.Equal(1u, report.Buttons);
Assert.Equal(0, report.DeltaX); Assert.Equal(0, report.DeltaY); Assert.Equal(0, report.Wheel);
foreach (byte value in new byte[] { 0x08, 2, 0 }) Assert.False(Ps2Decoder.DecodeMouse(ref state, value, &report));
Assert.True(Ps2Decoder.DecodeMouse(ref state, 1, &report));
Assert.Equal(Ps2Decoder.Pointer, report.Kind); Assert.Equal(2, report.DeltaX); Assert.Equal(-1, report.Wheel);
}
[Fact]
public unsafe void HotLossClearsPartialWheelPacketButPreservesTheNegotiatedFormat()
{
Ps2DecodeState state = default; NativeInputReport report;
Assert.True(Ps2Decoder.SetMouseDeviceId(ref state, 3));
foreach (byte value in new byte[] { 0x08, 0, 0 }) Assert.False(Ps2Decoder.DecodeMouse(ref state, value, &report));
Ps2Decoder.ResetMouse(ref state); Assert.Equal(3, state.MouseDeviceId);
foreach (byte value in new byte[] { 0x08, 3, 0 }) Assert.False(Ps2Decoder.DecodeMouse(ref state, value, &report));
Assert.True(Ps2Decoder.DecodeMouse(ref state, 0xff, &report));
Assert.Equal(3, report.DeltaX); Assert.Equal(1, report.Wheel);
}
[Fact]
public void NegotiatedWheelCapabilityAndReportsReachTheActualDesktopInputContract()
{
var source = Source(new Reports(new[] { new NativeInputReport { Kind = Ps2Decoder.Pointer, Wheel = -1 } }) { Capabilities = 7 });
Assert.Equal(7u, source.Capabilities);
Assert.True(source.TryRead(out var wheel)); Assert.Equal(NativeInputKind.Pointer, wheel.Kind); Assert.Equal(-1, wheel.Wheel);
}
[Fact]
public void AvailabilityComesFromActualBackendInitializationCapabilities()
{
var backend = new Reports(Keys(0x1E)) { Capabilities = 0 };
var source = Source(backend);
Assert.False(source.IsAvailable); Assert.False(source.TryRead(out _)); Assert.Equal(0, source.ReportsRead);
backend.Capabilities = 1; Assert.True(source.IsAvailable);
Assert.True(source.TryRead(out var report)); Assert.Equal('a', report.Character);
}
[Fact]
public void WindowsChordReleaseDoesNotOpenStartMenuAndStandaloneWindowsReleaseDoes()
{
var chord = Actions(Keys(0xE0, 0x5B, 0x12, 0x92, 0xE0, 0xDB));
Assert.Single(chord); Assert.Equal(DesktopKey.OpenFiles, chord[0].Key);
var alone = Actions(Keys(0xE0, 0x5B, 0xE0, 0x5C, 0xE0, 0xDB, 0xE0, 0xDC, 0xE0, 0xDC));
Assert.Single(alone); Assert.Equal(DesktopKey.StartMenu, alone[0].Key);
var snap = Actions(Keys(0xE0, 0x5B, 0xE0, 0x4B, 0xE0, 0xCB, 0xE0, 0xDB));
Assert.Single(snap); Assert.Equal(DesktopKey.SnapLeft, snap[0].Key);
}
[Fact]
public void AltTabSwitchEndsOnlyWhenLastAltIsReleased()
{
var actions = Actions(Keys(0x38, 0xE0, 0x38, 0x0F, 0x8F, 0xB8, 0xE0, 0xB8));
Assert.Equal(new[] { DesktopKey.SwitchWindow, DesktopKey.EndWindowSwitch }, actions.Select(report => report.Key));
var backwards = Actions(Keys(0x2A, 0x38, 0x0F, 0x8F, 0xB8, 0xAA));
Assert.Equal(new[] { DesktopKey.SwitchWindowBack, DesktopKey.EndWindowSwitch }, backwards.Select(report => report.Key));
}
[Fact]
public void BrowserReceivesPhysicalCodeRepeatReleaseAndModifierStateWithoutControlText()
{
var actions = Actions(Keys(0x2A, 0x1E, 0x1E, 0x9E, 0xAA, 0x1D, 0x1E, 0x9E, 0x9D), browser: true);
Assert.Equal(9, actions.Length);
Assert.Equal("KeyA", actions[1].BrowserKey.Code); Assert.Equal("A", actions[1].BrowserKey.Text);
Assert.False(actions[1].BrowserKey.Repeat); Assert.True(actions[2].BrowserKey.Repeat);
Assert.Equal("keyup", actions[3].BrowserKey.Type); Assert.Equal("", actions[3].BrowserKey.Text);
Assert.False(actions[4].BrowserKey.Shift); Assert.True(actions[6].BrowserKey.Control); Assert.Equal("", actions[6].BrowserKey.Text);
Assert.False(actions[8].BrowserKey.Control);
}
[Fact]
public void LostReportsReleaseBrowserKeysAndDoNotTurnAWindowsBreakIntoAStartGesture()
{
var before = Keys(0x1D, 0x1E);
var backend = new Reports(before.Concat(new[] { new NativeInputReport { Kind = Ps2Decoder.Lost, Buttons = 1 } }).ToArray());
var source = Source(backend, true); var actions = Drain(source);
Assert.Equal(5, actions.Length);
Assert.All(actions.Skip(2).Take(2), value => Assert.Equal("keyup", value.BrowserKey.Type));
Assert.Equal(NativeInputKind.ReportsLost, actions[^1].Kind); Assert.True(actions[^1].LeftButton);
Assert.Equal(1, source.LostReports);
var win = Actions(Keys(0xE0, 0x5B).Concat(new[] { new NativeInputReport { Kind = Ps2Decoder.Lost } })
.Concat(Keys(0xE0, 0xDB)).ToArray());
Assert.DoesNotContain(win, action => action.Kind == NativeInputKind.Key && action.Key == DesktopKey.StartMenu);
}
[Fact]
public void PointerAccumulationUsesNativePixelsAndClipsBeforeConvertingToLayout()
{
var backend = new Reports(new[]
{
new NativeInputReport { Kind = Ps2Decoder.Pointer, DeltaX = 10, DeltaY = 20, Buttons = 1 },
new NativeInputReport { Kind = Ps2Decoder.Pointer, DeltaX = int.MinValue, DeltaY = int.MaxValue }
});
var source = new Ps2InputSource(backend, 1000, 800, 200, () => new(false, null));
Assert.True(source.TryRead(out var move)); Assert.Equal(255, move.X); Assert.Equal(210, move.Y); Assert.True(move.LeftButton);
Assert.True(source.TryRead(out var clipped)); Assert.Equal(0, clipped.X); Assert.Equal(399, clipped.Y);
var fraction = new Ps2InputSource(new Reports(new[] { new NativeInputReport { Kind = Ps2Decoder.Pointer, DeltaX = 100, DeltaY = 100 } }), 11, 9, 125, () => new(false, null));
Assert.True(fraction.TryRead(out var edge)); Assert.Equal(7, edge.X); Assert.Equal(6, edge.Y);
}
[Fact]
public void ActualDesktopCancelsCloseCaptureOnLostPacketWithoutSynthesizingClick()
{
var desktop = new DesktopSession(new CpuFramebufferCanvas(new ArrayFramebufferPresenter(1024, 768)), new MemoryWorkspace());
var window = desktop.Open(DesktopApplication.Files);
int closeX = window.Bounds.X + window.Bounds.Width - 20, closeY = window.Bounds.Y + 12;
var backend = new Reports(new[]
{
new NativeInputReport { Kind = Ps2Decoder.Pointer, DeltaX = closeX - 512, DeltaY = closeY - 384, Buttons = 1 },
new NativeInputReport { Kind = Ps2Decoder.Lost, Buttons = 0 },
new NativeInputReport { Kind = Ps2Decoder.Pointer, Buttons = 0 }
});
var source = new Ps2InputSource(backend, 1024, 768, 100, () => new(desktop.HasBrowserContentFocus, desktop.ActiveWindow?.Application));
foreach (var input in Drain(source)) NativeInput.Dispatch(desktop, input, 1000);
Assert.Single(desktop.Windows);
backend.Add(new() { Kind = Ps2Decoder.Pointer, Buttons = 1 }); backend.Add(new() { Kind = Ps2Decoder.Pointer, Buttons = 0 });
foreach (var input in Drain(source)) NativeInput.Dispatch(desktop, input, 1100);
Assert.Empty(desktop.Windows);
}
[Fact]
public void ActualPs2ByteStreamTypesIntoTheExistingJintBrowserTextarea()
{
var canvas = new CpuFramebufferCanvas(new ArrayFramebufferPresenter(1024, 768));
var desktop = new DesktopSession(canvas, new MemoryWorkspace(), new BrowserService(new InputPage()));
var window = desktop.Open(DesktopApplication.Browser, "http://input.test/"); desktop.Render();
int x = window.Bounds.X + 12, y = window.Bounds.Y + 77;
desktop.Pointer(x, y, true, timestampMs: 100); desktop.Pointer(x, y, false, timestampMs: 101);
Assert.True(desktop.HasBrowserContentFocus);
var source = new Ps2InputSource(new Reports(Keys(0x1E, 0x9E, 0x30, 0xB0, 0xE0, 0x4B, 0xE0, 0xCB, 0x2E, 0xAE)),
1024, 768, 100, () => new(desktop.HasBrowserContentFocus, desktop.ActiveWindow?.Application));
while (source.TryRead(out var report)) NativeInput.Dispatch(desktop, report, 200);
var browser = Assert.IsType<BrowserService>(window.BrowserSession);
Assert.Equal("acb", browser.ExecuteJavaScript("document.getElementById('Entry').value")); browser.Release();
}
private sealed class InputPage : IWebTransport
{
public WebResponse Get(Uri address, BrowserLimits limits) => new(address, 200, "text/html", "<style>body{margin:0}textarea{display:block;width:300px;height:100px;margin:0;padding:0;border:0}</style><textarea id='Entry'></textarea>");
}
private sealed class Reports : IPs2ReportBackend
{
private readonly Queue<NativeInputReport> reports;
public uint Capabilities { get; set; } = 3;
internal Reports(NativeInputReport[] reports) => this.reports = new(reports);
public bool TryRead(out NativeInputReport report) => reports.TryDequeue(out report);
internal void Add(NativeInputReport report) => reports.Enqueue(report);
}
private static Ps2InputSource Source(Reports backend, bool browser = false) => new(backend, 1024, 768, 100, () => new(browser, browser ? DesktopApplication.Browser : DesktopApplication.Editor));
private static NativeInputEvent[] Actions(NativeInputReport[] reports, bool browser = false) => Drain(Source(new(reports), browser));
private static NativeInputEvent[] Drain(Ps2InputSource source)
{ var result = new List<NativeInputEvent>(); for (int i = 0; i < 1024 && source.TryRead(out var report); i++) result.Add(report); return result.ToArray(); }
private static unsafe NativeInputReport[] Keys(params byte[] bytes)
{
Ps2DecodeState state = default; var result = new List<NativeInputReport>();
foreach (byte value in bytes) { NativeInputReport report; if (Ps2Decoder.DecodeKeyboard(ref state, value, &report)) result.Add(report); }
return result.ToArray();
}
private static unsafe NativeInputReport[] Mouse(params byte[] bytes)
{
Ps2DecodeState state = default; var result = new List<NativeInputReport>();
foreach (byte value in bytes) { NativeInputReport report; if (Ps2Decoder.DecodeMouse(ref state, value, &report)) result.Add(report); }
return result.ToArray();
}
}