Files
ttui/test/TinyTUI.Tests/Input/InputInfrastructureTests.cs
chuan a93233456f feat: support key release opt-in dispatch
- keep parsed Kitty release events available to runtime listeners

- filter key release before focused component dispatch unless component opts in

- cover default filtering, opt-in dispatch, and keybinding non-match behavior
2026-06-04 09:43:42 +08:00

158 lines
5.7 KiB
C#

using TinyTUI.Input;
namespace TinyTUI.Tests.Input;
public sealed class InputInfrastructureTests
{
[Fact]
public void StdinBufferSplitsBatchedEscapeSequencesAndPaste()
{
var buffer = new StdinBuffer();
Assert.Empty(buffer.Process("\e["));
Assert.Equal([new StdinBufferEvent(StdinBufferEventKind.Data, "\e[A")], buffer.Process("A"));
Assert.Equal(
[
new StdinBufferEvent(StdinBufferEventKind.Data, "\e[97u"),
new StdinBufferEvent(StdinBufferEventKind.Data, "\e[97;1:3u"),
],
buffer.Process("\e[97u\e[97;1:3u"));
Assert.Empty(buffer.Process("\e[200~hello "));
Assert.Equal([new StdinBufferEvent(StdinBufferEventKind.Paste, "hello world")], buffer.Process("world\e[201~"));
}
[Fact]
public void StdinBufferFlushesIncompleteSequences()
{
var buffer = new StdinBuffer();
Assert.Empty(buffer.Process("\e[<35"));
Assert.Equal([new StdinBufferEvent(StdinBufferEventKind.Data, "\e[<35")], buffer.Flush());
}
[Fact]
public void DefaultInputParserParsesCommonKeysAndPaste()
{
var parser = new DefaultInputParser();
Assert.Equal([new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Up)], parser.Parse("\e[A"));
Assert.Equal([new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Shift(KeyNames.Up))], parser.Parse("\e[1;2A"));
Assert.Equal([new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Ctrl("c"))], parser.Parse("\x03"));
Assert.Equal([new TuiInputEvent(TuiInputEventKind.Text, "abc")], parser.Parse("abc"));
Assert.Equal([new TuiInputEvent(TuiInputEventKind.Paste, "pasted")], parser.Parse("\e[200~pasted\e[201~"));
}
[Fact]
public void KeybindingRegistryReportsUserConflictsAndNormalizesAliases()
{
var registry = KeybindingRegistry.CreateDefault();
registry.SetUserBindings(new Dictionary<string, IEnumerable<KeyId>>
{
[TuiKeybindings.InputSubmit] = [KeyNames.PageUp],
[TuiKeybindings.InputCancel] = ["pageUp"],
});
var conflict = Assert.Single(registry.Conflicts);
Assert.Equal(KeyNames.PageUp, conflict.Key.Value);
Assert.Contains(TuiKeybindings.InputSubmit, conflict.Actions);
Assert.Contains(TuiKeybindings.InputCancel, conflict.Actions);
}
[Fact]
public void DefaultInputParserParsesShiftTabAndAltLetters()
{
var parser = new DefaultInputParser();
Assert.Equal(
[new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Shift(KeyNames.Tab))],
parser.Parse("\e[Z"));
Assert.Equal(
[new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Alt("a"))],
parser.Parse("\ea"));
}
[Fact]
public void DefaultInputParserParsesKittyCsiUKeys()
{
var parser = new DefaultInputParser();
Assert.Equal(
[new TuiInputEvent(TuiInputEventKind.Key, "a", RawSequence: "\e[97u")],
parser.Parse("\e[97u"));
Assert.Equal(
[new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Ctrl("a"), RawSequence: "\e[97;5u")],
parser.Parse("\e[97;5u"));
Assert.Equal(
[new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Shift("e"), RawSequence: "\e[69;2u")],
parser.Parse("\e[69;2u"));
var repeat = Assert.Single(parser.Parse("\e[97;1:2u"));
Assert.Equal(TuiInputEventKind.Key, repeat.Kind);
Assert.Equal("a", repeat.Value);
Assert.Equal(TuiKeyEventType.Repeat, repeat.EventType);
Assert.Equal("\e[97;1:2u", repeat.RawSequence);
}
[Fact]
public void DefaultInputParserCarriesKittyAlternateKeyMetadata()
{
var parser = new DefaultInputParser();
var input = Assert.Single(parser.Parse("\e[1089::99;5u"));
Assert.Equal(TuiInputEventKind.Key, input.Kind);
Assert.Equal(KeyNames.Ctrl("c"), input.Value);
Assert.Null(input.AlternateKey);
Assert.Equal("\e[1089::99;5u", input.RawSequence);
var shifted = Assert.Single(parser.Parse("\e[99:67:99;2u"));
Assert.Equal(KeyNames.Shift("c"), shifted.Value);
Assert.Equal("C", shifted.AlternateKey);
}
[Fact]
public void DefaultInputParserParsesModifyOtherKeysCombinations()
{
var parser = new DefaultInputParser();
Assert.Equal(
[new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Ctrl("c"), RawSequence: "\e[27;5;99~")],
parser.Parse("\e[27;5;99~"));
Assert.Equal(
[new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Shift(KeyNames.Tab), RawSequence: "\e[27;2;9~")],
parser.Parse("\e[27;2;9~"));
Assert.Equal(
[new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Alt(KeyNames.Enter), RawSequence: "\e[27;3;13~")],
parser.Parse("\e[27;3;13~"));
}
[Fact]
public void DefaultInputParserKeepsKittyReleaseEvents()
{
var parser = new DefaultInputParser();
var release = Assert.Single(parser.Parse("\e[97;1:3u"));
Assert.Equal(TuiInputEventKind.Key, release.Kind);
Assert.Equal("a", release.Value);
Assert.Equal(TuiKeyEventType.Release, release.EventType);
Assert.Equal("\e[97;1:3u", release.RawSequence);
}
[Fact]
public void KeybindingRegistryDoesNotMatchReleaseEvents()
{
var registry = KeybindingRegistry.CreateDefault();
registry.SetUserBinding(TuiKeybindings.InputSubmit, KeyNames.Enter);
var release = new TuiInputEvent(
TuiInputEventKind.Key,
KeyNames.Enter,
TuiKeyEventType.Release,
RawSequence: "\e[13;1:3u");
Assert.False(registry.Matches(release, TuiKeybindings.InputSubmit));
}
}