@teammates/consolonia 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -0
- package/dist/__tests__/ansi.test.d.ts +1 -0
- package/dist/__tests__/ansi.test.js +520 -0
- package/dist/__tests__/chat-view.test.d.ts +4 -0
- package/dist/__tests__/chat-view.test.js +480 -0
- package/dist/__tests__/drawing.test.d.ts +4 -0
- package/dist/__tests__/drawing.test.js +426 -0
- package/dist/__tests__/input.test.d.ts +5 -0
- package/dist/__tests__/input.test.js +911 -0
- package/dist/__tests__/layout.test.d.ts +4 -0
- package/dist/__tests__/layout.test.js +689 -0
- package/dist/__tests__/pixel.test.d.ts +1 -0
- package/dist/__tests__/pixel.test.js +674 -0
- package/dist/__tests__/render.test.d.ts +1 -0
- package/dist/__tests__/render.test.js +400 -0
- package/dist/__tests__/styled.test.d.ts +4 -0
- package/dist/__tests__/styled.test.js +149 -0
- package/dist/__tests__/widgets.test.d.ts +5 -0
- package/dist/__tests__/widgets.test.js +924 -0
- package/dist/ansi/esc.d.ts +61 -0
- package/dist/ansi/esc.js +85 -0
- package/dist/ansi/output.d.ts +66 -0
- package/dist/ansi/output.js +192 -0
- package/dist/ansi/strip.d.ts +16 -0
- package/dist/ansi/strip.js +74 -0
- package/dist/app.d.ts +68 -0
- package/dist/app.js +297 -0
- package/dist/drawing/clip.d.ts +23 -0
- package/dist/drawing/clip.js +67 -0
- package/dist/drawing/context.d.ts +77 -0
- package/dist/drawing/context.js +275 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +63 -0
- package/dist/input/escape-matcher.d.ts +27 -0
- package/dist/input/escape-matcher.js +253 -0
- package/dist/input/events.d.ts +49 -0
- package/dist/input/events.js +17 -0
- package/dist/input/index.d.ts +15 -0
- package/dist/input/index.js +14 -0
- package/dist/input/matcher.d.ts +23 -0
- package/dist/input/matcher.js +14 -0
- package/dist/input/mouse-matcher.d.ts +27 -0
- package/dist/input/mouse-matcher.js +142 -0
- package/dist/input/paste-matcher.d.ts +23 -0
- package/dist/input/paste-matcher.js +104 -0
- package/dist/input/processor.d.ts +51 -0
- package/dist/input/processor.js +145 -0
- package/dist/input/raw-mode.d.ts +13 -0
- package/dist/input/raw-mode.js +24 -0
- package/dist/input/text-matcher.d.ts +14 -0
- package/dist/input/text-matcher.js +32 -0
- package/dist/layout/box.d.ts +33 -0
- package/dist/layout/box.js +92 -0
- package/dist/layout/column.d.ts +21 -0
- package/dist/layout/column.js +90 -0
- package/dist/layout/control.d.ts +73 -0
- package/dist/layout/control.js +215 -0
- package/dist/layout/row.d.ts +21 -0
- package/dist/layout/row.js +95 -0
- package/dist/layout/stack.d.ts +18 -0
- package/dist/layout/stack.js +64 -0
- package/dist/layout/types.d.ts +27 -0
- package/dist/layout/types.js +4 -0
- package/dist/pixel/background.d.ts +16 -0
- package/dist/pixel/background.js +16 -0
- package/dist/pixel/box-pattern.d.ts +38 -0
- package/dist/pixel/box-pattern.js +57 -0
- package/dist/pixel/buffer.d.ts +25 -0
- package/dist/pixel/buffer.js +51 -0
- package/dist/pixel/color.d.ts +48 -0
- package/dist/pixel/color.js +92 -0
- package/dist/pixel/foreground.d.ts +31 -0
- package/dist/pixel/foreground.js +64 -0
- package/dist/pixel/pixel.d.ts +21 -0
- package/dist/pixel/pixel.js +38 -0
- package/dist/pixel/symbol.d.ts +38 -0
- package/dist/pixel/symbol.js +192 -0
- package/dist/render/regions.d.ts +54 -0
- package/dist/render/regions.js +102 -0
- package/dist/render/render-target.d.ts +42 -0
- package/dist/render/render-target.js +118 -0
- package/dist/styled.d.ts +113 -0
- package/dist/styled.js +176 -0
- package/dist/widgets/border.d.ts +34 -0
- package/dist/widgets/border.js +121 -0
- package/dist/widgets/chat-view.d.ts +239 -0
- package/dist/widgets/chat-view.js +993 -0
- package/dist/widgets/interview.d.ts +87 -0
- package/dist/widgets/interview.js +187 -0
- package/dist/widgets/markdown.d.ts +87 -0
- package/dist/widgets/markdown.js +611 -0
- package/dist/widgets/panel.d.ts +19 -0
- package/dist/widgets/panel.js +35 -0
- package/dist/widgets/scroll-view.d.ts +43 -0
- package/dist/widgets/scroll-view.js +182 -0
- package/dist/widgets/styled-text.d.ts +38 -0
- package/dist/widgets/styled-text.js +183 -0
- package/dist/widgets/syntax.d.ts +37 -0
- package/dist/widgets/syntax.js +670 -0
- package/dist/widgets/text-input.d.ts +121 -0
- package/dist/widgets/text-input.js +618 -0
- package/dist/widgets/text.d.ts +34 -0
- package/dist/widgets/text.js +168 -0
- package/package.json +45 -0
|
@@ -0,0 +1,911 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comprehensive unit tests for the raw input system (Phase 4).
|
|
3
|
+
* Covers EscapeMatcher, PasteMatcher, MouseMatcher, TextMatcher, and InputProcessor.
|
|
4
|
+
*/
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { EscapeMatcher } from "../input/escape-matcher.js";
|
|
7
|
+
import { MatchResult } from "../input/matcher.js";
|
|
8
|
+
import { MouseMatcher } from "../input/mouse-matcher.js";
|
|
9
|
+
import { PasteMatcher } from "../input/paste-matcher.js";
|
|
10
|
+
import { createInputProcessor } from "../input/processor.js";
|
|
11
|
+
import { TextMatcher } from "../input/text-matcher.js";
|
|
12
|
+
// ── Helpers ───────────────────────────────────────────────────────────
|
|
13
|
+
/** Feed a full string char-by-char into a matcher, returning each result. */
|
|
14
|
+
function feedAll(matcher, data) {
|
|
15
|
+
return [...data].map((c) => matcher.append(c));
|
|
16
|
+
}
|
|
17
|
+
/** Feed a string and return the final MatchResult. */
|
|
18
|
+
function feedString(matcher, data) {
|
|
19
|
+
const results = feedAll(matcher, data);
|
|
20
|
+
return results[results.length - 1];
|
|
21
|
+
}
|
|
22
|
+
/** Collect all InputEvents emitted by an InputProcessor when feeding data. */
|
|
23
|
+
function collectEvents(data) {
|
|
24
|
+
const { processor, events } = createInputProcessor();
|
|
25
|
+
const collected = [];
|
|
26
|
+
events.on("input", (ev) => collected.push(ev));
|
|
27
|
+
processor.feed(data);
|
|
28
|
+
processor.destroy();
|
|
29
|
+
return collected;
|
|
30
|
+
}
|
|
31
|
+
// =====================================================================
|
|
32
|
+
// EscapeMatcher
|
|
33
|
+
// =====================================================================
|
|
34
|
+
describe("EscapeMatcher", () => {
|
|
35
|
+
let matcher;
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
matcher?.reset();
|
|
38
|
+
});
|
|
39
|
+
// ── Arrow keys ────────────────────────────────────────────────────
|
|
40
|
+
describe("arrow keys", () => {
|
|
41
|
+
it.each([
|
|
42
|
+
["\x1b[A", "up"],
|
|
43
|
+
["\x1b[B", "down"],
|
|
44
|
+
["\x1b[C", "right"],
|
|
45
|
+
["\x1b[D", "left"],
|
|
46
|
+
])("parses %j as key=%s", (seq, expectedKey) => {
|
|
47
|
+
matcher = new EscapeMatcher();
|
|
48
|
+
const result = feedString(matcher, seq);
|
|
49
|
+
expect(result).toBe(MatchResult.Complete);
|
|
50
|
+
const ev = matcher.flush();
|
|
51
|
+
expect(ev).not.toBeNull();
|
|
52
|
+
expect(ev.type).toBe("key");
|
|
53
|
+
if (ev.type === "key") {
|
|
54
|
+
expect(ev.event.key).toBe(expectedKey);
|
|
55
|
+
expect(ev.event.shift).toBe(false);
|
|
56
|
+
expect(ev.event.ctrl).toBe(false);
|
|
57
|
+
expect(ev.event.alt).toBe(false);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
// ── Arrow keys with modifiers ─────────────────────────────────────
|
|
62
|
+
describe("arrow keys with modifiers", () => {
|
|
63
|
+
it("parses shift+up (\\x1b[1;2A)", () => {
|
|
64
|
+
matcher = new EscapeMatcher();
|
|
65
|
+
expect(feedString(matcher, "\x1b[1;2A")).toBe(MatchResult.Complete);
|
|
66
|
+
const ev = matcher.flush();
|
|
67
|
+
expect(ev.type).toBe("key");
|
|
68
|
+
if (ev.type === "key") {
|
|
69
|
+
expect(ev.event.key).toBe("up");
|
|
70
|
+
expect(ev.event.shift).toBe(true);
|
|
71
|
+
expect(ev.event.ctrl).toBe(false);
|
|
72
|
+
expect(ev.event.alt).toBe(false);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
it("parses ctrl+up (\\x1b[1;5A)", () => {
|
|
76
|
+
matcher = new EscapeMatcher();
|
|
77
|
+
expect(feedString(matcher, "\x1b[1;5A")).toBe(MatchResult.Complete);
|
|
78
|
+
const ev = matcher.flush();
|
|
79
|
+
expect(ev.type).toBe("key");
|
|
80
|
+
if (ev.type === "key") {
|
|
81
|
+
expect(ev.event.key).toBe("up");
|
|
82
|
+
expect(ev.event.shift).toBe(false);
|
|
83
|
+
expect(ev.event.ctrl).toBe(true);
|
|
84
|
+
expect(ev.event.alt).toBe(false);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
it("parses alt+up (\\x1b[1;3A)", () => {
|
|
88
|
+
matcher = new EscapeMatcher();
|
|
89
|
+
expect(feedString(matcher, "\x1b[1;3A")).toBe(MatchResult.Complete);
|
|
90
|
+
const ev = matcher.flush();
|
|
91
|
+
expect(ev.type).toBe("key");
|
|
92
|
+
if (ev.type === "key") {
|
|
93
|
+
expect(ev.event.key).toBe("up");
|
|
94
|
+
expect(ev.event.shift).toBe(false);
|
|
95
|
+
expect(ev.event.ctrl).toBe(false);
|
|
96
|
+
expect(ev.event.alt).toBe(true);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
it("parses shift+ctrl+alt+down (\\x1b[1;8B)", () => {
|
|
100
|
+
matcher = new EscapeMatcher();
|
|
101
|
+
// modifier 8 = 1 + shift(1) + alt(2) + ctrl(4)
|
|
102
|
+
expect(feedString(matcher, "\x1b[1;8B")).toBe(MatchResult.Complete);
|
|
103
|
+
const ev = matcher.flush();
|
|
104
|
+
expect(ev.type).toBe("key");
|
|
105
|
+
if (ev.type === "key") {
|
|
106
|
+
expect(ev.event.key).toBe("down");
|
|
107
|
+
expect(ev.event.shift).toBe(true);
|
|
108
|
+
expect(ev.event.ctrl).toBe(true);
|
|
109
|
+
expect(ev.event.alt).toBe(true);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
// ── Home/End ──────────────────────────────────────────────────────
|
|
114
|
+
describe("home and end", () => {
|
|
115
|
+
it("parses home (\\x1b[H)", () => {
|
|
116
|
+
matcher = new EscapeMatcher();
|
|
117
|
+
expect(feedString(matcher, "\x1b[H")).toBe(MatchResult.Complete);
|
|
118
|
+
const ev = matcher.flush();
|
|
119
|
+
expect(ev.type).toBe("key");
|
|
120
|
+
if (ev.type === "key") {
|
|
121
|
+
expect(ev.event.key).toBe("home");
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
it("parses end (\\x1b[F)", () => {
|
|
125
|
+
matcher = new EscapeMatcher();
|
|
126
|
+
expect(feedString(matcher, "\x1b[F")).toBe(MatchResult.Complete);
|
|
127
|
+
const ev = matcher.flush();
|
|
128
|
+
expect(ev.type).toBe("key");
|
|
129
|
+
if (ev.type === "key") {
|
|
130
|
+
expect(ev.event.key).toBe("end");
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
// ── Delete ────────────────────────────────────────────────────────
|
|
135
|
+
describe("delete key", () => {
|
|
136
|
+
it("parses delete (\\x1b[3~)", () => {
|
|
137
|
+
matcher = new EscapeMatcher();
|
|
138
|
+
expect(feedString(matcher, "\x1b[3~")).toBe(MatchResult.Complete);
|
|
139
|
+
const ev = matcher.flush();
|
|
140
|
+
expect(ev.type).toBe("key");
|
|
141
|
+
if (ev.type === "key") {
|
|
142
|
+
expect(ev.event.key).toBe("delete");
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
// ── Tilde keys (insert, pageup, pagedown) ────────────────────────
|
|
147
|
+
describe("tilde sequences", () => {
|
|
148
|
+
it.each([
|
|
149
|
+
["\x1b[2~", "insert"],
|
|
150
|
+
["\x1b[5~", "pageup"],
|
|
151
|
+
["\x1b[6~", "pagedown"],
|
|
152
|
+
])("parses %j as key=%s", (seq, expectedKey) => {
|
|
153
|
+
matcher = new EscapeMatcher();
|
|
154
|
+
expect(feedString(matcher, seq)).toBe(MatchResult.Complete);
|
|
155
|
+
const ev = matcher.flush();
|
|
156
|
+
expect(ev.type).toBe("key");
|
|
157
|
+
if (ev.type === "key") {
|
|
158
|
+
expect(ev.event.key).toBe(expectedKey);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
// ── Function keys ─────────────────────────────────────────────────
|
|
163
|
+
describe("function keys", () => {
|
|
164
|
+
it("parses F1 (\\x1bOP)", () => {
|
|
165
|
+
matcher = new EscapeMatcher();
|
|
166
|
+
expect(feedString(matcher, "\x1bOP")).toBe(MatchResult.Complete);
|
|
167
|
+
const ev = matcher.flush();
|
|
168
|
+
expect(ev.type).toBe("key");
|
|
169
|
+
if (ev.type === "key") {
|
|
170
|
+
expect(ev.event.key).toBe("f1");
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
it("parses F2 (\\x1bOQ)", () => {
|
|
174
|
+
matcher = new EscapeMatcher();
|
|
175
|
+
expect(feedString(matcher, "\x1bOQ")).toBe(MatchResult.Complete);
|
|
176
|
+
const ev = matcher.flush();
|
|
177
|
+
expect(ev.type).toBe("key");
|
|
178
|
+
if (ev.type === "key") {
|
|
179
|
+
expect(ev.event.key).toBe("f2");
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
it("parses F3 (\\x1bOR)", () => {
|
|
183
|
+
matcher = new EscapeMatcher();
|
|
184
|
+
expect(feedString(matcher, "\x1bOR")).toBe(MatchResult.Complete);
|
|
185
|
+
const ev = matcher.flush();
|
|
186
|
+
expect(ev.type).toBe("key");
|
|
187
|
+
if (ev.type === "key") {
|
|
188
|
+
expect(ev.event.key).toBe("f3");
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
it("parses F4 (\\x1bOS)", () => {
|
|
192
|
+
matcher = new EscapeMatcher();
|
|
193
|
+
expect(feedString(matcher, "\x1bOS")).toBe(MatchResult.Complete);
|
|
194
|
+
const ev = matcher.flush();
|
|
195
|
+
expect(ev.type).toBe("key");
|
|
196
|
+
if (ev.type === "key") {
|
|
197
|
+
expect(ev.event.key).toBe("f4");
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
it("parses F5 (\\x1b[15~)", () => {
|
|
201
|
+
matcher = new EscapeMatcher();
|
|
202
|
+
expect(feedString(matcher, "\x1b[15~")).toBe(MatchResult.Complete);
|
|
203
|
+
const ev = matcher.flush();
|
|
204
|
+
expect(ev.type).toBe("key");
|
|
205
|
+
if (ev.type === "key") {
|
|
206
|
+
expect(ev.event.key).toBe("f5");
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
it.each([
|
|
210
|
+
["\x1b[17~", "f6"],
|
|
211
|
+
["\x1b[18~", "f7"],
|
|
212
|
+
["\x1b[19~", "f8"],
|
|
213
|
+
["\x1b[20~", "f9"],
|
|
214
|
+
["\x1b[21~", "f10"],
|
|
215
|
+
["\x1b[23~", "f11"],
|
|
216
|
+
["\x1b[24~", "f12"],
|
|
217
|
+
])("parses %j as key=%s", (seq, expectedKey) => {
|
|
218
|
+
matcher = new EscapeMatcher();
|
|
219
|
+
expect(feedString(matcher, seq)).toBe(MatchResult.Complete);
|
|
220
|
+
const ev = matcher.flush();
|
|
221
|
+
expect(ev.type).toBe("key");
|
|
222
|
+
if (ev.type === "key") {
|
|
223
|
+
expect(ev.event.key).toBe(expectedKey);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
// ── Control characters ────────────────────────────────────────────
|
|
228
|
+
describe("control characters", () => {
|
|
229
|
+
it("parses ctrl+a (\\x01)", () => {
|
|
230
|
+
matcher = new EscapeMatcher();
|
|
231
|
+
expect(matcher.append("\x01")).toBe(MatchResult.Complete);
|
|
232
|
+
const ev = matcher.flush();
|
|
233
|
+
expect(ev.type).toBe("key");
|
|
234
|
+
if (ev.type === "key") {
|
|
235
|
+
expect(ev.event.key).toBe("a");
|
|
236
|
+
expect(ev.event.ctrl).toBe(true);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
it("parses ctrl+c (\\x03)", () => {
|
|
240
|
+
matcher = new EscapeMatcher();
|
|
241
|
+
expect(matcher.append("\x03")).toBe(MatchResult.Complete);
|
|
242
|
+
const ev = matcher.flush();
|
|
243
|
+
expect(ev.type).toBe("key");
|
|
244
|
+
if (ev.type === "key") {
|
|
245
|
+
expect(ev.event.key).toBe("c");
|
|
246
|
+
expect(ev.event.ctrl).toBe(true);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
it("parses ctrl+d (\\x04)", () => {
|
|
250
|
+
matcher = new EscapeMatcher();
|
|
251
|
+
expect(matcher.append("\x04")).toBe(MatchResult.Complete);
|
|
252
|
+
const ev = matcher.flush();
|
|
253
|
+
expect(ev.type).toBe("key");
|
|
254
|
+
if (ev.type === "key") {
|
|
255
|
+
expect(ev.event.key).toBe("d");
|
|
256
|
+
expect(ev.event.ctrl).toBe(true);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
it("parses enter (\\r)", () => {
|
|
260
|
+
matcher = new EscapeMatcher();
|
|
261
|
+
expect(matcher.append("\r")).toBe(MatchResult.Complete);
|
|
262
|
+
const ev = matcher.flush();
|
|
263
|
+
expect(ev.type).toBe("key");
|
|
264
|
+
if (ev.type === "key") {
|
|
265
|
+
expect(ev.event.key).toBe("enter");
|
|
266
|
+
expect(ev.event.ctrl).toBe(false);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
it("parses backspace (\\x7f)", () => {
|
|
270
|
+
matcher = new EscapeMatcher();
|
|
271
|
+
expect(matcher.append("\x7f")).toBe(MatchResult.Complete);
|
|
272
|
+
const ev = matcher.flush();
|
|
273
|
+
expect(ev.type).toBe("key");
|
|
274
|
+
if (ev.type === "key") {
|
|
275
|
+
expect(ev.event.key).toBe("backspace");
|
|
276
|
+
expect(ev.event.ctrl).toBe(false);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
it("parses tab (\\t)", () => {
|
|
280
|
+
matcher = new EscapeMatcher();
|
|
281
|
+
expect(matcher.append("\t")).toBe(MatchResult.Complete);
|
|
282
|
+
const ev = matcher.flush();
|
|
283
|
+
expect(ev.type).toBe("key");
|
|
284
|
+
if (ev.type === "key") {
|
|
285
|
+
expect(ev.event.key).toBe("tab");
|
|
286
|
+
expect(ev.event.char).toBe("\t");
|
|
287
|
+
expect(ev.event.ctrl).toBe(false);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
it("parses ctrl+z (\\x1a)", () => {
|
|
291
|
+
matcher = new EscapeMatcher();
|
|
292
|
+
expect(matcher.append("\x1a")).toBe(MatchResult.Complete);
|
|
293
|
+
const ev = matcher.flush();
|
|
294
|
+
expect(ev.type).toBe("key");
|
|
295
|
+
if (ev.type === "key") {
|
|
296
|
+
expect(ev.event.key).toBe("z");
|
|
297
|
+
expect(ev.event.ctrl).toBe(true);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
// ── Partial sequence feeding ──────────────────────────────────────
|
|
302
|
+
describe("partial sequence feeding", () => {
|
|
303
|
+
it("returns Partial for each char of \\x1b[A until the final char", () => {
|
|
304
|
+
matcher = new EscapeMatcher();
|
|
305
|
+
expect(matcher.append("\x1b")).toBe(MatchResult.Partial);
|
|
306
|
+
expect(matcher.append("[")).toBe(MatchResult.Partial);
|
|
307
|
+
expect(matcher.append("A")).toBe(MatchResult.Complete);
|
|
308
|
+
const ev = matcher.flush();
|
|
309
|
+
expect(ev.type).toBe("key");
|
|
310
|
+
if (ev.type === "key") {
|
|
311
|
+
expect(ev.event.key).toBe("up");
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
it("returns Partial while reading modifier params", () => {
|
|
315
|
+
matcher = new EscapeMatcher();
|
|
316
|
+
expect(matcher.append("\x1b")).toBe(MatchResult.Partial);
|
|
317
|
+
expect(matcher.append("[")).toBe(MatchResult.Partial);
|
|
318
|
+
expect(matcher.append("1")).toBe(MatchResult.Partial);
|
|
319
|
+
expect(matcher.append(";")).toBe(MatchResult.Partial);
|
|
320
|
+
expect(matcher.append("5")).toBe(MatchResult.Partial);
|
|
321
|
+
expect(matcher.append("A")).toBe(MatchResult.Complete);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
// ── Rejects mouse sequences ───────────────────────────────────────
|
|
325
|
+
describe("mouse sequence rejection", () => {
|
|
326
|
+
it("rejects CSI < (mouse) sequences with NoMatch", () => {
|
|
327
|
+
matcher = new EscapeMatcher();
|
|
328
|
+
expect(matcher.append("\x1b")).toBe(MatchResult.Partial);
|
|
329
|
+
expect(matcher.append("[")).toBe(MatchResult.Partial);
|
|
330
|
+
expect(matcher.append("<")).toBe(MatchResult.NoMatch);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
// ── Shift+Tab (backtab) ───────────────────────────────────────────
|
|
334
|
+
describe("shift+tab", () => {
|
|
335
|
+
it("parses CSI Z as shift+tab", () => {
|
|
336
|
+
matcher = new EscapeMatcher();
|
|
337
|
+
expect(feedString(matcher, "\x1b[Z")).toBe(MatchResult.Complete);
|
|
338
|
+
const ev = matcher.flush();
|
|
339
|
+
expect(ev.type).toBe("key");
|
|
340
|
+
if (ev.type === "key") {
|
|
341
|
+
expect(ev.event.key).toBe("tab");
|
|
342
|
+
expect(ev.event.shift).toBe(true);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
// ── Alt+char combinations ─────────────────────────────────────────
|
|
347
|
+
describe("alt+char", () => {
|
|
348
|
+
it("parses ESC followed by printable char as alt+char", () => {
|
|
349
|
+
matcher = new EscapeMatcher();
|
|
350
|
+
expect(matcher.append("\x1b")).toBe(MatchResult.Partial);
|
|
351
|
+
expect(matcher.append("a")).toBe(MatchResult.Complete);
|
|
352
|
+
const ev = matcher.flush();
|
|
353
|
+
expect(ev.type).toBe("key");
|
|
354
|
+
if (ev.type === "key") {
|
|
355
|
+
expect(ev.event.key).toBe("a");
|
|
356
|
+
expect(ev.event.alt).toBe(true);
|
|
357
|
+
expect(ev.event.shift).toBe(false);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
it("parses alt+uppercase letter with shift flag", () => {
|
|
361
|
+
matcher = new EscapeMatcher();
|
|
362
|
+
expect(feedString(matcher, "\x1bA")).toBe(MatchResult.Complete);
|
|
363
|
+
const ev = matcher.flush();
|
|
364
|
+
expect(ev.type).toBe("key");
|
|
365
|
+
if (ev.type === "key") {
|
|
366
|
+
expect(ev.event.key).toBe("a");
|
|
367
|
+
expect(ev.event.alt).toBe(true);
|
|
368
|
+
expect(ev.event.shift).toBe(true);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
// ── flushEscapeTimeout ────────────────────────────────────────────
|
|
373
|
+
describe("flushEscapeTimeout", () => {
|
|
374
|
+
it("emits escape key when called in GotEsc state", () => {
|
|
375
|
+
matcher = new EscapeMatcher();
|
|
376
|
+
expect(matcher.append("\x1b")).toBe(MatchResult.Partial);
|
|
377
|
+
const ev = matcher.flushEscapeTimeout();
|
|
378
|
+
expect(ev).not.toBeNull();
|
|
379
|
+
expect(ev.type).toBe("key");
|
|
380
|
+
if (ev.type === "key") {
|
|
381
|
+
expect(ev.event.key).toBe("escape");
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
it("returns null when not in GotEsc state", () => {
|
|
385
|
+
matcher = new EscapeMatcher();
|
|
386
|
+
expect(matcher.flushEscapeTimeout()).toBeNull();
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
// ── NoMatch for printable chars ───────────────────────────────────
|
|
390
|
+
describe("NoMatch for non-escape input", () => {
|
|
391
|
+
it("returns NoMatch for printable characters", () => {
|
|
392
|
+
matcher = new EscapeMatcher();
|
|
393
|
+
expect(matcher.append("a")).toBe(MatchResult.NoMatch);
|
|
394
|
+
expect(matcher.append("Z")).toBe(MatchResult.NoMatch);
|
|
395
|
+
expect(matcher.append(" ")).toBe(MatchResult.NoMatch);
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
// ── Reset ─────────────────────────────────────────────────────────
|
|
399
|
+
describe("reset", () => {
|
|
400
|
+
it("clears partial state", () => {
|
|
401
|
+
matcher = new EscapeMatcher();
|
|
402
|
+
matcher.append("\x1b");
|
|
403
|
+
matcher.reset();
|
|
404
|
+
// After reset, should be back to idle
|
|
405
|
+
expect(matcher.append("a")).toBe(MatchResult.NoMatch);
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
// =====================================================================
|
|
410
|
+
// PasteMatcher
|
|
411
|
+
// =====================================================================
|
|
412
|
+
describe("PasteMatcher", () => {
|
|
413
|
+
let matcher;
|
|
414
|
+
afterEach(() => {
|
|
415
|
+
matcher?.reset();
|
|
416
|
+
});
|
|
417
|
+
it("parses a complete paste sequence fed char by char", () => {
|
|
418
|
+
matcher = new PasteMatcher();
|
|
419
|
+
const data = "\x1b[200~hello world\x1b[201~";
|
|
420
|
+
const results = feedAll(matcher, data);
|
|
421
|
+
// Final char should produce Complete
|
|
422
|
+
expect(results[results.length - 1]).toBe(MatchResult.Complete);
|
|
423
|
+
// All preceding chars should be Partial
|
|
424
|
+
for (let i = 0; i < results.length - 1; i++) {
|
|
425
|
+
expect(results[i]).toBe(MatchResult.Partial);
|
|
426
|
+
}
|
|
427
|
+
const ev = matcher.flush();
|
|
428
|
+
expect(ev).not.toBeNull();
|
|
429
|
+
expect(ev.type).toBe("paste");
|
|
430
|
+
if (ev.type === "paste") {
|
|
431
|
+
expect(ev.event.text).toBe("hello world");
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
it("handles empty paste content", () => {
|
|
435
|
+
matcher = new PasteMatcher();
|
|
436
|
+
const data = "\x1b[200~\x1b[201~";
|
|
437
|
+
expect(feedString(matcher, data)).toBe(MatchResult.Complete);
|
|
438
|
+
const ev = matcher.flush();
|
|
439
|
+
expect(ev.type).toBe("paste");
|
|
440
|
+
if (ev.type === "paste") {
|
|
441
|
+
expect(ev.event.text).toBe("");
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
it("handles paste with special characters inside", () => {
|
|
445
|
+
matcher = new PasteMatcher();
|
|
446
|
+
const content = "line1\nline2\ttab\r\nend";
|
|
447
|
+
const data = `\x1b[200~${content}\x1b[201~`;
|
|
448
|
+
expect(feedString(matcher, data)).toBe(MatchResult.Complete);
|
|
449
|
+
const ev = matcher.flush();
|
|
450
|
+
expect(ev.type).toBe("paste");
|
|
451
|
+
if (ev.type === "paste") {
|
|
452
|
+
expect(ev.event.text).toBe(content);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
it("returns NoMatch for non-paste sequences", () => {
|
|
456
|
+
matcher = new PasteMatcher();
|
|
457
|
+
expect(matcher.append("a")).toBe(MatchResult.NoMatch);
|
|
458
|
+
expect(matcher.append("Z")).toBe(MatchResult.NoMatch);
|
|
459
|
+
expect(matcher.append("\r")).toBe(MatchResult.NoMatch);
|
|
460
|
+
});
|
|
461
|
+
it("returns NoMatch after start marker mismatch", () => {
|
|
462
|
+
matcher = new PasteMatcher();
|
|
463
|
+
// ESC starts partial, but next char does not continue start marker correctly
|
|
464
|
+
expect(matcher.append("\x1b")).toBe(MatchResult.Partial);
|
|
465
|
+
expect(matcher.append("X")).toBe(MatchResult.NoMatch);
|
|
466
|
+
});
|
|
467
|
+
it("handles partial end marker mismatch (text contains \\x1b but not end marker)", () => {
|
|
468
|
+
matcher = new PasteMatcher();
|
|
469
|
+
// Paste that contains an ESC char in the text (not forming the end marker)
|
|
470
|
+
const data = "\x1b[200~has\x1bXinside\x1b[201~";
|
|
471
|
+
expect(feedString(matcher, data)).toBe(MatchResult.Complete);
|
|
472
|
+
const ev = matcher.flush();
|
|
473
|
+
expect(ev.type).toBe("paste");
|
|
474
|
+
if (ev.type === "paste") {
|
|
475
|
+
expect(ev.event.text).toBe("has\x1bXinside");
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
it("resets properly and can match again", () => {
|
|
479
|
+
matcher = new PasteMatcher();
|
|
480
|
+
feedString(matcher, "\x1b[200~first\x1b[201~");
|
|
481
|
+
matcher.flush();
|
|
482
|
+
// Second paste
|
|
483
|
+
expect(feedString(matcher, "\x1b[200~second\x1b[201~")).toBe(MatchResult.Complete);
|
|
484
|
+
const ev = matcher.flush();
|
|
485
|
+
expect(ev.type).toBe("paste");
|
|
486
|
+
if (ev.type === "paste") {
|
|
487
|
+
expect(ev.event.text).toBe("second");
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
// =====================================================================
|
|
492
|
+
// MouseMatcher
|
|
493
|
+
// =====================================================================
|
|
494
|
+
describe("MouseMatcher", () => {
|
|
495
|
+
let matcher;
|
|
496
|
+
afterEach(() => {
|
|
497
|
+
matcher?.reset();
|
|
498
|
+
});
|
|
499
|
+
// ── Press events ──────────────────────────────────────────────────
|
|
500
|
+
describe("press events", () => {
|
|
501
|
+
it("parses left press at (9,19) from \\x1b[<0;10;20M", () => {
|
|
502
|
+
matcher = new MouseMatcher();
|
|
503
|
+
expect(feedString(matcher, "\x1b[<0;10;20M")).toBe(MatchResult.Complete);
|
|
504
|
+
const ev = matcher.flush();
|
|
505
|
+
expect(ev).not.toBeNull();
|
|
506
|
+
expect(ev.type).toBe("mouse");
|
|
507
|
+
if (ev.type === "mouse") {
|
|
508
|
+
expect(ev.event.button).toBe("left");
|
|
509
|
+
expect(ev.event.type).toBe("press");
|
|
510
|
+
expect(ev.event.x).toBe(9); // 10 - 1 = 9 (1-based to 0-based)
|
|
511
|
+
expect(ev.event.y).toBe(19); // 20 - 1 = 19
|
|
512
|
+
expect(ev.event.shift).toBe(false);
|
|
513
|
+
expect(ev.event.ctrl).toBe(false);
|
|
514
|
+
expect(ev.event.alt).toBe(false);
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
it("parses middle press", () => {
|
|
518
|
+
matcher = new MouseMatcher();
|
|
519
|
+
expect(feedString(matcher, "\x1b[<1;5;5M")).toBe(MatchResult.Complete);
|
|
520
|
+
const ev = matcher.flush();
|
|
521
|
+
if (ev.type === "mouse") {
|
|
522
|
+
expect(ev.event.button).toBe("middle");
|
|
523
|
+
expect(ev.event.type).toBe("press");
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
it("parses right press from \\x1b[<2;5;5M", () => {
|
|
527
|
+
matcher = new MouseMatcher();
|
|
528
|
+
expect(feedString(matcher, "\x1b[<2;5;5M")).toBe(MatchResult.Complete);
|
|
529
|
+
const ev = matcher.flush();
|
|
530
|
+
if (ev.type === "mouse") {
|
|
531
|
+
expect(ev.event.button).toBe("right");
|
|
532
|
+
expect(ev.event.type).toBe("press");
|
|
533
|
+
expect(ev.event.x).toBe(4);
|
|
534
|
+
expect(ev.event.y).toBe(4);
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
// ── Release events ────────────────────────────────────────────────
|
|
539
|
+
describe("release events", () => {
|
|
540
|
+
it("parses left release from \\x1b[<0;10;20m (lowercase m)", () => {
|
|
541
|
+
matcher = new MouseMatcher();
|
|
542
|
+
expect(feedString(matcher, "\x1b[<0;10;20m")).toBe(MatchResult.Complete);
|
|
543
|
+
const ev = matcher.flush();
|
|
544
|
+
if (ev.type === "mouse") {
|
|
545
|
+
expect(ev.event.button).toBe("left");
|
|
546
|
+
expect(ev.event.type).toBe("release");
|
|
547
|
+
expect(ev.event.x).toBe(9);
|
|
548
|
+
expect(ev.event.y).toBe(19);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
// ── Wheel events ──────────────────────────────────────────────────
|
|
553
|
+
describe("wheel events", () => {
|
|
554
|
+
it("parses wheel up from \\x1b[<64;5;5M", () => {
|
|
555
|
+
matcher = new MouseMatcher();
|
|
556
|
+
expect(feedString(matcher, "\x1b[<64;5;5M")).toBe(MatchResult.Complete);
|
|
557
|
+
const ev = matcher.flush();
|
|
558
|
+
if (ev.type === "mouse") {
|
|
559
|
+
expect(ev.event.button).toBe("none");
|
|
560
|
+
expect(ev.event.type).toBe("wheelup");
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
it("parses wheel down from \\x1b[<65;5;5M", () => {
|
|
564
|
+
matcher = new MouseMatcher();
|
|
565
|
+
expect(feedString(matcher, "\x1b[<65;5;5M")).toBe(MatchResult.Complete);
|
|
566
|
+
const ev = matcher.flush();
|
|
567
|
+
if (ev.type === "mouse") {
|
|
568
|
+
expect(ev.event.button).toBe("none");
|
|
569
|
+
expect(ev.event.type).toBe("wheeldown");
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
// ── Modifier keys ────────────────────────────────────────────────
|
|
574
|
+
describe("modifier keys in mouse events", () => {
|
|
575
|
+
it("parses shift+left press (cb=4)", () => {
|
|
576
|
+
matcher = new MouseMatcher();
|
|
577
|
+
// 4 = shift(4) + left(0)
|
|
578
|
+
expect(feedString(matcher, "\x1b[<4;1;1M")).toBe(MatchResult.Complete);
|
|
579
|
+
const ev = matcher.flush();
|
|
580
|
+
if (ev.type === "mouse") {
|
|
581
|
+
expect(ev.event.button).toBe("left");
|
|
582
|
+
expect(ev.event.type).toBe("press");
|
|
583
|
+
expect(ev.event.shift).toBe(true);
|
|
584
|
+
expect(ev.event.alt).toBe(false);
|
|
585
|
+
expect(ev.event.ctrl).toBe(false);
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
it("parses alt+left press (cb=8)", () => {
|
|
589
|
+
matcher = new MouseMatcher();
|
|
590
|
+
expect(feedString(matcher, "\x1b[<8;1;1M")).toBe(MatchResult.Complete);
|
|
591
|
+
const ev = matcher.flush();
|
|
592
|
+
if (ev.type === "mouse") {
|
|
593
|
+
expect(ev.event.button).toBe("left");
|
|
594
|
+
expect(ev.event.shift).toBe(false);
|
|
595
|
+
expect(ev.event.alt).toBe(true);
|
|
596
|
+
expect(ev.event.ctrl).toBe(false);
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
it("parses ctrl+left press (cb=16)", () => {
|
|
600
|
+
matcher = new MouseMatcher();
|
|
601
|
+
expect(feedString(matcher, "\x1b[<16;1;1M")).toBe(MatchResult.Complete);
|
|
602
|
+
const ev = matcher.flush();
|
|
603
|
+
if (ev.type === "mouse") {
|
|
604
|
+
expect(ev.event.button).toBe("left");
|
|
605
|
+
expect(ev.event.shift).toBe(false);
|
|
606
|
+
expect(ev.event.alt).toBe(false);
|
|
607
|
+
expect(ev.event.ctrl).toBe(true);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
it("parses shift+ctrl+alt+right press (cb=30)", () => {
|
|
611
|
+
matcher = new MouseMatcher();
|
|
612
|
+
// 30 = right(2) + shift(4) + alt(8) + ctrl(16)
|
|
613
|
+
expect(feedString(matcher, "\x1b[<30;3;7M")).toBe(MatchResult.Complete);
|
|
614
|
+
const ev = matcher.flush();
|
|
615
|
+
if (ev.type === "mouse") {
|
|
616
|
+
expect(ev.event.button).toBe("right");
|
|
617
|
+
expect(ev.event.type).toBe("press");
|
|
618
|
+
expect(ev.event.shift).toBe(true);
|
|
619
|
+
expect(ev.event.alt).toBe(true);
|
|
620
|
+
expect(ev.event.ctrl).toBe(true);
|
|
621
|
+
expect(ev.event.x).toBe(2);
|
|
622
|
+
expect(ev.event.y).toBe(6);
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
// ── Motion events ─────────────────────────────────────────────────
|
|
627
|
+
describe("motion events", () => {
|
|
628
|
+
it("parses mouse move with left button held (cb=32)", () => {
|
|
629
|
+
matcher = new MouseMatcher();
|
|
630
|
+
// 32 = motion(32) + left(0)
|
|
631
|
+
expect(feedString(matcher, "\x1b[<32;10;10M")).toBe(MatchResult.Complete);
|
|
632
|
+
const ev = matcher.flush();
|
|
633
|
+
if (ev.type === "mouse") {
|
|
634
|
+
expect(ev.event.type).toBe("move");
|
|
635
|
+
expect(ev.event.button).toBe("left");
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
it("parses mouse move with no button (cb=35)", () => {
|
|
639
|
+
matcher = new MouseMatcher();
|
|
640
|
+
// 35 = motion(32) + 3 (no button)
|
|
641
|
+
expect(feedString(matcher, "\x1b[<35;10;10M")).toBe(MatchResult.Complete);
|
|
642
|
+
const ev = matcher.flush();
|
|
643
|
+
if (ev.type === "mouse") {
|
|
644
|
+
expect(ev.event.type).toBe("move");
|
|
645
|
+
expect(ev.event.button).toBe("none");
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
// ── NoMatch cases ─────────────────────────────────────────────────
|
|
650
|
+
describe("NoMatch cases", () => {
|
|
651
|
+
it("returns NoMatch for non-ESC characters", () => {
|
|
652
|
+
matcher = new MouseMatcher();
|
|
653
|
+
expect(matcher.append("a")).toBe(MatchResult.NoMatch);
|
|
654
|
+
});
|
|
655
|
+
it("returns NoMatch if ESC not followed by [", () => {
|
|
656
|
+
matcher = new MouseMatcher();
|
|
657
|
+
expect(matcher.append("\x1b")).toBe(MatchResult.Partial);
|
|
658
|
+
expect(matcher.append("O")).toBe(MatchResult.NoMatch);
|
|
659
|
+
});
|
|
660
|
+
it("returns NoMatch if \\x1b[ not followed by <", () => {
|
|
661
|
+
matcher = new MouseMatcher();
|
|
662
|
+
expect(matcher.append("\x1b")).toBe(MatchResult.Partial);
|
|
663
|
+
expect(matcher.append("[")).toBe(MatchResult.Partial);
|
|
664
|
+
expect(matcher.append("A")).toBe(MatchResult.NoMatch);
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
// ── Partial feeding ───────────────────────────────────────────────
|
|
668
|
+
describe("partial feeding", () => {
|
|
669
|
+
it("returns Partial for each char until the final M or m", () => {
|
|
670
|
+
matcher = new MouseMatcher();
|
|
671
|
+
const results = feedAll(matcher, "\x1b[<0;1;1M");
|
|
672
|
+
for (let i = 0; i < results.length - 1; i++) {
|
|
673
|
+
expect(results[i]).toBe(MatchResult.Partial);
|
|
674
|
+
}
|
|
675
|
+
expect(results[results.length - 1]).toBe(MatchResult.Complete);
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
// =====================================================================
|
|
680
|
+
// TextMatcher
|
|
681
|
+
// =====================================================================
|
|
682
|
+
describe("TextMatcher", () => {
|
|
683
|
+
let matcher;
|
|
684
|
+
afterEach(() => {
|
|
685
|
+
matcher?.reset();
|
|
686
|
+
});
|
|
687
|
+
describe("printable characters", () => {
|
|
688
|
+
it.each([
|
|
689
|
+
["a", "a", false],
|
|
690
|
+
["z", "z", false],
|
|
691
|
+
["A", "A", true],
|
|
692
|
+
["Z", "Z", true],
|
|
693
|
+
["0", "0", false],
|
|
694
|
+
["9", "9", false],
|
|
695
|
+
["!", "!", false],
|
|
696
|
+
["@", "@", false],
|
|
697
|
+
[".", ".", false],
|
|
698
|
+
[",", ",", false],
|
|
699
|
+
])("produces KeyEvent for %j", (char, expectedChar, expectedShift) => {
|
|
700
|
+
matcher = new TextMatcher();
|
|
701
|
+
expect(matcher.append(char)).toBe(MatchResult.Complete);
|
|
702
|
+
const ev = matcher.flush();
|
|
703
|
+
expect(ev).not.toBeNull();
|
|
704
|
+
expect(ev.type).toBe("key");
|
|
705
|
+
if (ev.type === "key") {
|
|
706
|
+
expect(ev.event.char).toBe(expectedChar);
|
|
707
|
+
expect(ev.event.shift).toBe(expectedShift);
|
|
708
|
+
expect(ev.event.ctrl).toBe(false);
|
|
709
|
+
expect(ev.event.alt).toBe(false);
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
it('maps space to key="space"', () => {
|
|
713
|
+
matcher = new TextMatcher();
|
|
714
|
+
expect(matcher.append(" ")).toBe(MatchResult.Complete);
|
|
715
|
+
const ev = matcher.flush();
|
|
716
|
+
if (ev.type === "key") {
|
|
717
|
+
expect(ev.event.key).toBe("space");
|
|
718
|
+
expect(ev.event.char).toBe(" ");
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
it("uses the char itself as key for non-space printable chars", () => {
|
|
722
|
+
matcher = new TextMatcher();
|
|
723
|
+
matcher.append("x");
|
|
724
|
+
const ev = matcher.flush();
|
|
725
|
+
if (ev.type === "key") {
|
|
726
|
+
expect(ev.event.key).toBe("x");
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
});
|
|
730
|
+
describe("NoMatch for non-printable characters", () => {
|
|
731
|
+
it("returns NoMatch for control characters (code < 32)", () => {
|
|
732
|
+
matcher = new TextMatcher();
|
|
733
|
+
// Try a few control chars
|
|
734
|
+
expect(matcher.append("\x00")).toBe(MatchResult.NoMatch);
|
|
735
|
+
expect(matcher.append("\x01")).toBe(MatchResult.NoMatch);
|
|
736
|
+
expect(matcher.append("\x0a")).toBe(MatchResult.NoMatch);
|
|
737
|
+
expect(matcher.append("\r")).toBe(MatchResult.NoMatch);
|
|
738
|
+
expect(matcher.append("\t")).toBe(MatchResult.NoMatch);
|
|
739
|
+
});
|
|
740
|
+
it("returns NoMatch for DEL (0x7f)", () => {
|
|
741
|
+
matcher = new TextMatcher();
|
|
742
|
+
expect(matcher.append("\x7f")).toBe(MatchResult.NoMatch);
|
|
743
|
+
});
|
|
744
|
+
it("returns NoMatch for ESC (0x1b)", () => {
|
|
745
|
+
matcher = new TextMatcher();
|
|
746
|
+
expect(matcher.append("\x1b")).toBe(MatchResult.NoMatch);
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
describe("high unicode characters", () => {
|
|
750
|
+
it("matches unicode characters above ASCII", () => {
|
|
751
|
+
matcher = new TextMatcher();
|
|
752
|
+
expect(matcher.append("\u00e9")).toBe(MatchResult.Complete); // e-acute
|
|
753
|
+
const ev = matcher.flush();
|
|
754
|
+
expect(ev.type).toBe("key");
|
|
755
|
+
if (ev.type === "key") {
|
|
756
|
+
expect(ev.event.char).toBe("\u00e9");
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
// =====================================================================
|
|
762
|
+
// InputProcessor
|
|
763
|
+
// =====================================================================
|
|
764
|
+
describe("InputProcessor", () => {
|
|
765
|
+
it('produces 5 key events from "hello"', () => {
|
|
766
|
+
const events = collectEvents("hello");
|
|
767
|
+
expect(events).toHaveLength(5);
|
|
768
|
+
const keys = events.map((e) => {
|
|
769
|
+
expect(e.type).toBe("key");
|
|
770
|
+
return e.type === "key" ? e.event.key : "";
|
|
771
|
+
});
|
|
772
|
+
expect(keys).toEqual(["h", "e", "l", "l", "o"]);
|
|
773
|
+
});
|
|
774
|
+
it("parses arrow key \\x1b[A as up through parallel matching", () => {
|
|
775
|
+
const events = collectEvents("\x1b[A");
|
|
776
|
+
expect(events).toHaveLength(1);
|
|
777
|
+
expect(events[0].type).toBe("key");
|
|
778
|
+
if (events[0].type === "key") {
|
|
779
|
+
expect(events[0].event.key).toBe("up");
|
|
780
|
+
expect(events[0].event.shift).toBe(false);
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
it("produces one paste event from a bracketed paste sequence", () => {
|
|
784
|
+
const events = collectEvents("\x1b[200~pasted text\x1b[201~");
|
|
785
|
+
expect(events).toHaveLength(1);
|
|
786
|
+
expect(events[0].type).toBe("paste");
|
|
787
|
+
if (events[0].type === "paste") {
|
|
788
|
+
expect(events[0].event.text).toBe("pasted text");
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
it("parses SGR mouse sequence through parallel matching", () => {
|
|
792
|
+
const events = collectEvents("\x1b[<0;5;10M");
|
|
793
|
+
expect(events).toHaveLength(1);
|
|
794
|
+
expect(events[0].type).toBe("mouse");
|
|
795
|
+
if (events[0].type === "mouse") {
|
|
796
|
+
expect(events[0].event.button).toBe("left");
|
|
797
|
+
expect(events[0].event.type).toBe("press");
|
|
798
|
+
expect(events[0].event.x).toBe(4);
|
|
799
|
+
expect(events[0].event.y).toBe(9);
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
it('handles mixed text and escape sequences: "ab\\x1b[Acd"', () => {
|
|
803
|
+
const events = collectEvents("ab\x1b[Acd");
|
|
804
|
+
expect(events).toHaveLength(5);
|
|
805
|
+
// 'a', 'b', up-arrow, 'c', 'd'
|
|
806
|
+
if (events[0].type === "key")
|
|
807
|
+
expect(events[0].event.key).toBe("a");
|
|
808
|
+
if (events[1].type === "key")
|
|
809
|
+
expect(events[1].event.key).toBe("b");
|
|
810
|
+
if (events[2].type === "key")
|
|
811
|
+
expect(events[2].event.key).toBe("up");
|
|
812
|
+
if (events[3].type === "key")
|
|
813
|
+
expect(events[3].event.key).toBe("c");
|
|
814
|
+
if (events[4].type === "key")
|
|
815
|
+
expect(events[4].event.key).toBe("d");
|
|
816
|
+
});
|
|
817
|
+
it("handles multiple escape sequences in a row", () => {
|
|
818
|
+
const events = collectEvents("\x1b[A\x1b[B\x1b[C");
|
|
819
|
+
expect(events).toHaveLength(3);
|
|
820
|
+
if (events[0].type === "key")
|
|
821
|
+
expect(events[0].event.key).toBe("up");
|
|
822
|
+
if (events[1].type === "key")
|
|
823
|
+
expect(events[1].event.key).toBe("down");
|
|
824
|
+
if (events[2].type === "key")
|
|
825
|
+
expect(events[2].event.key).toBe("right");
|
|
826
|
+
});
|
|
827
|
+
it("handles control characters mixed with text", () => {
|
|
828
|
+
const events = collectEvents("a\rb");
|
|
829
|
+
expect(events).toHaveLength(3);
|
|
830
|
+
if (events[0].type === "key")
|
|
831
|
+
expect(events[0].event.key).toBe("a");
|
|
832
|
+
if (events[1].type === "key")
|
|
833
|
+
expect(events[1].event.key).toBe("enter");
|
|
834
|
+
if (events[2].type === "key")
|
|
835
|
+
expect(events[2].event.key).toBe("b");
|
|
836
|
+
});
|
|
837
|
+
it("paste matcher takes priority over escape matcher for paste sequences", () => {
|
|
838
|
+
const events = collectEvents("\x1b[200~xyz\x1b[201~");
|
|
839
|
+
expect(events).toHaveLength(1);
|
|
840
|
+
expect(events[0].type).toBe("paste");
|
|
841
|
+
if (events[0].type === "paste") {
|
|
842
|
+
expect(events[0].event.text).toBe("xyz");
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
it("mouse sequences are recognized through parallel matching", () => {
|
|
846
|
+
const events = collectEvents("\x1b[<0;1;1M");
|
|
847
|
+
expect(events).toHaveLength(1);
|
|
848
|
+
expect(events[0].type).toBe("mouse");
|
|
849
|
+
if (events[0].type === "mouse") {
|
|
850
|
+
expect(events[0].event.button).toBe("left");
|
|
851
|
+
expect(events[0].event.type).toBe("press");
|
|
852
|
+
expect(events[0].event.x).toBe(0);
|
|
853
|
+
expect(events[0].event.y).toBe(0);
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
it("handles text after a paste sequence", () => {
|
|
857
|
+
const events = collectEvents("\x1b[200~pasted\x1b[201~after");
|
|
858
|
+
expect(events).toHaveLength(6); // 1 paste + 5 chars
|
|
859
|
+
expect(events[0].type).toBe("paste");
|
|
860
|
+
if (events[0].type === "paste") {
|
|
861
|
+
expect(events[0].event.text).toBe("pasted");
|
|
862
|
+
}
|
|
863
|
+
// Next 5 events should be key events for 'a', 'f', 't', 'e', 'r'
|
|
864
|
+
for (let i = 1; i <= 5; i++) {
|
|
865
|
+
expect(events[i].type).toBe("key");
|
|
866
|
+
}
|
|
867
|
+
if (events[1].type === "key")
|
|
868
|
+
expect(events[1].event.key).toBe("a");
|
|
869
|
+
if (events[5].type === "key")
|
|
870
|
+
expect(events[5].event.key).toBe("r");
|
|
871
|
+
});
|
|
872
|
+
it("SS3 F-keys: \\x1bOP recognized as F1 through parallel matching", () => {
|
|
873
|
+
const events = collectEvents("\x1bOP");
|
|
874
|
+
expect(events).toHaveLength(1);
|
|
875
|
+
expect(events[0].type).toBe("key");
|
|
876
|
+
if (events[0].type === "key") {
|
|
877
|
+
expect(events[0].event.key).toBe("f1");
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
it("delete key \\x1b[3~ recognized through parallel matching", () => {
|
|
881
|
+
const events = collectEvents("\x1b[3~");
|
|
882
|
+
expect(events).toHaveLength(1);
|
|
883
|
+
expect(events[0].type).toBe("key");
|
|
884
|
+
if (events[0].type === "key") {
|
|
885
|
+
expect(events[0].event.key).toBe("delete");
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
it("control characters are handled even with paste matcher priority", () => {
|
|
889
|
+
// Control chars (code < 32) are NOT ESC, so PasteMatcher returns NoMatch.
|
|
890
|
+
// EscapeMatcher handles them directly.
|
|
891
|
+
const events = collectEvents("\x01\x03");
|
|
892
|
+
expect(events).toHaveLength(2);
|
|
893
|
+
if (events[0].type === "key") {
|
|
894
|
+
expect(events[0].event.key).toBe("a");
|
|
895
|
+
expect(events[0].event.ctrl).toBe(true);
|
|
896
|
+
}
|
|
897
|
+
if (events[1].type === "key") {
|
|
898
|
+
expect(events[1].event.key).toBe("c");
|
|
899
|
+
expect(events[1].event.ctrl).toBe(true);
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
it("createInputProcessor returns processor and events emitter", () => {
|
|
903
|
+
const { processor, events } = createInputProcessor();
|
|
904
|
+
expect(processor).toBeDefined();
|
|
905
|
+
expect(events).toBeDefined();
|
|
906
|
+
expect(typeof processor.feed).toBe("function");
|
|
907
|
+
expect(typeof processor.destroy).toBe("function");
|
|
908
|
+
expect(typeof events.on).toBe("function");
|
|
909
|
+
processor.destroy();
|
|
910
|
+
});
|
|
911
|
+
});
|