@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.
Files changed (104) hide show
  1. package/README.md +48 -0
  2. package/dist/__tests__/ansi.test.d.ts +1 -0
  3. package/dist/__tests__/ansi.test.js +520 -0
  4. package/dist/__tests__/chat-view.test.d.ts +4 -0
  5. package/dist/__tests__/chat-view.test.js +480 -0
  6. package/dist/__tests__/drawing.test.d.ts +4 -0
  7. package/dist/__tests__/drawing.test.js +426 -0
  8. package/dist/__tests__/input.test.d.ts +5 -0
  9. package/dist/__tests__/input.test.js +911 -0
  10. package/dist/__tests__/layout.test.d.ts +4 -0
  11. package/dist/__tests__/layout.test.js +689 -0
  12. package/dist/__tests__/pixel.test.d.ts +1 -0
  13. package/dist/__tests__/pixel.test.js +674 -0
  14. package/dist/__tests__/render.test.d.ts +1 -0
  15. package/dist/__tests__/render.test.js +400 -0
  16. package/dist/__tests__/styled.test.d.ts +4 -0
  17. package/dist/__tests__/styled.test.js +149 -0
  18. package/dist/__tests__/widgets.test.d.ts +5 -0
  19. package/dist/__tests__/widgets.test.js +924 -0
  20. package/dist/ansi/esc.d.ts +61 -0
  21. package/dist/ansi/esc.js +85 -0
  22. package/dist/ansi/output.d.ts +66 -0
  23. package/dist/ansi/output.js +192 -0
  24. package/dist/ansi/strip.d.ts +16 -0
  25. package/dist/ansi/strip.js +74 -0
  26. package/dist/app.d.ts +68 -0
  27. package/dist/app.js +297 -0
  28. package/dist/drawing/clip.d.ts +23 -0
  29. package/dist/drawing/clip.js +67 -0
  30. package/dist/drawing/context.d.ts +77 -0
  31. package/dist/drawing/context.js +275 -0
  32. package/dist/index.d.ts +48 -0
  33. package/dist/index.js +63 -0
  34. package/dist/input/escape-matcher.d.ts +27 -0
  35. package/dist/input/escape-matcher.js +253 -0
  36. package/dist/input/events.d.ts +49 -0
  37. package/dist/input/events.js +17 -0
  38. package/dist/input/index.d.ts +15 -0
  39. package/dist/input/index.js +14 -0
  40. package/dist/input/matcher.d.ts +23 -0
  41. package/dist/input/matcher.js +14 -0
  42. package/dist/input/mouse-matcher.d.ts +27 -0
  43. package/dist/input/mouse-matcher.js +142 -0
  44. package/dist/input/paste-matcher.d.ts +23 -0
  45. package/dist/input/paste-matcher.js +104 -0
  46. package/dist/input/processor.d.ts +51 -0
  47. package/dist/input/processor.js +145 -0
  48. package/dist/input/raw-mode.d.ts +13 -0
  49. package/dist/input/raw-mode.js +24 -0
  50. package/dist/input/text-matcher.d.ts +14 -0
  51. package/dist/input/text-matcher.js +32 -0
  52. package/dist/layout/box.d.ts +33 -0
  53. package/dist/layout/box.js +92 -0
  54. package/dist/layout/column.d.ts +21 -0
  55. package/dist/layout/column.js +90 -0
  56. package/dist/layout/control.d.ts +73 -0
  57. package/dist/layout/control.js +215 -0
  58. package/dist/layout/row.d.ts +21 -0
  59. package/dist/layout/row.js +95 -0
  60. package/dist/layout/stack.d.ts +18 -0
  61. package/dist/layout/stack.js +64 -0
  62. package/dist/layout/types.d.ts +27 -0
  63. package/dist/layout/types.js +4 -0
  64. package/dist/pixel/background.d.ts +16 -0
  65. package/dist/pixel/background.js +16 -0
  66. package/dist/pixel/box-pattern.d.ts +38 -0
  67. package/dist/pixel/box-pattern.js +57 -0
  68. package/dist/pixel/buffer.d.ts +25 -0
  69. package/dist/pixel/buffer.js +51 -0
  70. package/dist/pixel/color.d.ts +48 -0
  71. package/dist/pixel/color.js +92 -0
  72. package/dist/pixel/foreground.d.ts +31 -0
  73. package/dist/pixel/foreground.js +64 -0
  74. package/dist/pixel/pixel.d.ts +21 -0
  75. package/dist/pixel/pixel.js +38 -0
  76. package/dist/pixel/symbol.d.ts +38 -0
  77. package/dist/pixel/symbol.js +192 -0
  78. package/dist/render/regions.d.ts +54 -0
  79. package/dist/render/regions.js +102 -0
  80. package/dist/render/render-target.d.ts +42 -0
  81. package/dist/render/render-target.js +118 -0
  82. package/dist/styled.d.ts +113 -0
  83. package/dist/styled.js +176 -0
  84. package/dist/widgets/border.d.ts +34 -0
  85. package/dist/widgets/border.js +121 -0
  86. package/dist/widgets/chat-view.d.ts +239 -0
  87. package/dist/widgets/chat-view.js +993 -0
  88. package/dist/widgets/interview.d.ts +87 -0
  89. package/dist/widgets/interview.js +187 -0
  90. package/dist/widgets/markdown.d.ts +87 -0
  91. package/dist/widgets/markdown.js +611 -0
  92. package/dist/widgets/panel.d.ts +19 -0
  93. package/dist/widgets/panel.js +35 -0
  94. package/dist/widgets/scroll-view.d.ts +43 -0
  95. package/dist/widgets/scroll-view.js +182 -0
  96. package/dist/widgets/styled-text.d.ts +38 -0
  97. package/dist/widgets/styled-text.js +183 -0
  98. package/dist/widgets/syntax.d.ts +37 -0
  99. package/dist/widgets/syntax.js +670 -0
  100. package/dist/widgets/text-input.d.ts +121 -0
  101. package/dist/widgets/text-input.js +618 -0
  102. package/dist/widgets/text.d.ts +34 -0
  103. package/dist/widgets/text.js +168 -0
  104. package/package.json +45 -0
@@ -0,0 +1,426 @@
1
+ /**
2
+ * Tests for Phase 5: Drawing Context (ClipStack + DrawingContext).
3
+ */
4
+ import { describe, expect, it } from "vitest";
5
+ import { ClipStack } from "../drawing/clip.js";
6
+ import { DrawingContext } from "../drawing/context.js";
7
+ import { BOX_CHARS, DOWN, LEFT, RIGHT, UP } from "../pixel/box-pattern.js";
8
+ import { PixelBuffer } from "../pixel/buffer.js";
9
+ import { BLUE, GREEN, RED, WHITE } from "../pixel/color.js";
10
+ // ── Helpers ──────────────────────────────────────────────────────────
11
+ /** Read the character text at (x, y) from a buffer. */
12
+ function charAt(buf, x, y) {
13
+ return buf.get(x, y).foreground.symbol.text;
14
+ }
15
+ /** Read the box pattern at (x, y) from a buffer. */
16
+ function patternAt(buf, x, y) {
17
+ return buf.get(x, y).foreground.symbol.pattern;
18
+ }
19
+ /** Read the background color at (x, y). */
20
+ function bgAt(buf, x, y) {
21
+ return buf.get(x, y).background.color;
22
+ }
23
+ /** Read the foreground color at (x, y). */
24
+ function fgAt(buf, x, y) {
25
+ return buf.get(x, y).foreground.color;
26
+ }
27
+ // ═══════════════════════════════════════════════════════════════════════
28
+ // ClipStack
29
+ // ═══════════════════════════════════════════════════════════════════════
30
+ describe("ClipStack", () => {
31
+ it("empty stack: contains() returns true for any point", () => {
32
+ const cs = new ClipStack();
33
+ expect(cs.contains(0, 0)).toBe(true);
34
+ expect(cs.contains(100, 200)).toBe(true);
35
+ expect(cs.contains(-5, -10)).toBe(true);
36
+ });
37
+ it("single push: only points inside the rect pass", () => {
38
+ const cs = new ClipStack();
39
+ cs.push({ x: 2, y: 3, width: 5, height: 4 });
40
+ // Inside
41
+ expect(cs.contains(2, 3)).toBe(true);
42
+ expect(cs.contains(6, 6)).toBe(true); // x=6 < 2+5=7, y=6 < 3+4=7
43
+ // On exclusive boundary (right/bottom edge)
44
+ expect(cs.contains(7, 3)).toBe(false);
45
+ expect(cs.contains(2, 7)).toBe(false);
46
+ // Outside
47
+ expect(cs.contains(1, 3)).toBe(false);
48
+ expect(cs.contains(2, 2)).toBe(false);
49
+ expect(cs.contains(10, 10)).toBe(false);
50
+ });
51
+ it("nested push: intersection narrows the visible area", () => {
52
+ const cs = new ClipStack();
53
+ cs.push({ x: 0, y: 0, width: 10, height: 10 });
54
+ cs.push({ x: 5, y: 5, width: 10, height: 10 });
55
+ // Intersection is { x: 5, y: 5, width: 5, height: 5 }
56
+ expect(cs.contains(5, 5)).toBe(true);
57
+ expect(cs.contains(9, 9)).toBe(true);
58
+ // In the first rect but not the intersection
59
+ expect(cs.contains(0, 0)).toBe(false);
60
+ expect(cs.contains(4, 4)).toBe(false);
61
+ // In the second rect but not the intersection
62
+ expect(cs.contains(10, 10)).toBe(false);
63
+ });
64
+ it("pop restores previous clip", () => {
65
+ const cs = new ClipStack();
66
+ cs.push({ x: 0, y: 0, width: 10, height: 10 });
67
+ cs.push({ x: 5, y: 5, width: 10, height: 10 });
68
+ // After nested push, (0,0) is clipped
69
+ expect(cs.contains(0, 0)).toBe(false);
70
+ cs.pop();
71
+ // After pop, we're back to the first clip: (0,0) is visible
72
+ expect(cs.contains(0, 0)).toBe(true);
73
+ expect(cs.contains(9, 9)).toBe(true);
74
+ cs.pop();
75
+ // After popping all, everything is visible
76
+ expect(cs.contains(0, 0)).toBe(true);
77
+ expect(cs.contains(100, 100)).toBe(true);
78
+ });
79
+ it("pop on empty stack throws", () => {
80
+ const cs = new ClipStack();
81
+ expect(() => cs.pop()).toThrow("ClipStack underflow");
82
+ });
83
+ it("degenerate (zero area) clip rejects everything", () => {
84
+ const cs = new ClipStack();
85
+ // Push a non-overlapping rect pair to get a null intersection
86
+ cs.push({ x: 0, y: 0, width: 5, height: 5 });
87
+ cs.push({ x: 10, y: 10, width: 5, height: 5 });
88
+ expect(cs.contains(0, 0)).toBe(false);
89
+ expect(cs.contains(3, 3)).toBe(false);
90
+ expect(cs.contains(12, 12)).toBe(false);
91
+ expect(cs.contains(50, 50)).toBe(false);
92
+ });
93
+ it("zero-width rect rejects everything", () => {
94
+ const cs = new ClipStack();
95
+ cs.push({ x: 5, y: 5, width: 0, height: 10 });
96
+ expect(cs.contains(5, 5)).toBe(false);
97
+ expect(cs.contains(5, 10)).toBe(false);
98
+ });
99
+ it("current() returns null when empty, rect when pushed", () => {
100
+ const cs = new ClipStack();
101
+ expect(cs.current()).toBeNull();
102
+ const rect = { x: 1, y: 2, width: 3, height: 4 };
103
+ cs.push(rect);
104
+ expect(cs.current()).toEqual(rect);
105
+ });
106
+ });
107
+ // ═══════════════════════════════════════════════════════════════════════
108
+ // DrawingContext
109
+ // ═══════════════════════════════════════════════════════════════════════
110
+ describe("DrawingContext", () => {
111
+ // ── drawChar ───────────────────────────────────────────────────────
112
+ describe("drawChar", () => {
113
+ it("writes character to buffer at correct position", () => {
114
+ const buf = new PixelBuffer(10, 10);
115
+ const ctx = new DrawingContext(buf);
116
+ ctx.drawChar(3, 4, "A", { fg: WHITE });
117
+ expect(charAt(buf, 3, 4)).toBe("A");
118
+ // Adjacent cell should still be default space
119
+ expect(charAt(buf, 2, 4)).toBe(" ");
120
+ });
121
+ it("writes wide character with continuation in next cell", () => {
122
+ const buf = new PixelBuffer(10, 10);
123
+ const ctx = new DrawingContext(buf);
124
+ // CJK character U+4E16 (world) is width 2
125
+ ctx.drawChar(2, 1, "\u4E16", { fg: WHITE });
126
+ expect(charAt(buf, 2, 1)).toBe("\u4E16");
127
+ expect(buf.get(2, 1).foreground.symbol.width).toBe(2);
128
+ // Continuation cell gets empty string
129
+ expect(charAt(buf, 3, 1)).toBe("");
130
+ });
131
+ it("applies foreground color", () => {
132
+ const buf = new PixelBuffer(10, 10);
133
+ const ctx = new DrawingContext(buf);
134
+ ctx.drawChar(0, 0, "X", { fg: RED });
135
+ expect(fgAt(buf, 0, 0)).toEqual(RED);
136
+ });
137
+ it("applies background color", () => {
138
+ const buf = new PixelBuffer(10, 10);
139
+ const ctx = new DrawingContext(buf);
140
+ ctx.drawChar(0, 0, "X", { fg: WHITE, bg: BLUE });
141
+ expect(bgAt(buf, 0, 0)).toEqual(BLUE);
142
+ });
143
+ it("out-of-bounds write is silently ignored", () => {
144
+ const buf = new PixelBuffer(5, 5);
145
+ const ctx = new DrawingContext(buf);
146
+ // Should not throw
147
+ ctx.drawChar(10, 10, "X", { fg: WHITE });
148
+ ctx.drawChar(-1, -1, "Y", { fg: WHITE });
149
+ });
150
+ });
151
+ // ── drawText ──────────────────────────────────────────────────────
152
+ describe("drawText", () => {
153
+ it("writes string advancing by charWidth for each char", () => {
154
+ const buf = new PixelBuffer(20, 5);
155
+ const ctx = new DrawingContext(buf);
156
+ ctx.drawText(1, 0, "Hello", { fg: WHITE });
157
+ expect(charAt(buf, 1, 0)).toBe("H");
158
+ expect(charAt(buf, 2, 0)).toBe("e");
159
+ expect(charAt(buf, 3, 0)).toBe("l");
160
+ expect(charAt(buf, 4, 0)).toBe("l");
161
+ expect(charAt(buf, 5, 0)).toBe("o");
162
+ // Before and after should be untouched
163
+ expect(charAt(buf, 0, 0)).toBe(" ");
164
+ expect(charAt(buf, 6, 0)).toBe(" ");
165
+ });
166
+ it("handles tabs as 4 spaces", () => {
167
+ const buf = new PixelBuffer(20, 5);
168
+ const ctx = new DrawingContext(buf);
169
+ ctx.drawText(0, 0, "A\tB", { fg: WHITE });
170
+ expect(charAt(buf, 0, 0)).toBe("A");
171
+ // Tab = 4 spaces at positions 1,2,3,4
172
+ expect(charAt(buf, 1, 0)).toBe(" ");
173
+ expect(charAt(buf, 2, 0)).toBe(" ");
174
+ expect(charAt(buf, 3, 0)).toBe(" ");
175
+ expect(charAt(buf, 4, 0)).toBe(" ");
176
+ // B at position 5
177
+ expect(charAt(buf, 5, 0)).toBe("B");
178
+ });
179
+ it("handles wide characters in text", () => {
180
+ const buf = new PixelBuffer(20, 5);
181
+ const ctx = new DrawingContext(buf);
182
+ // Mix of ASCII and CJK
183
+ ctx.drawText(0, 0, "A\u4E16B", { fg: WHITE });
184
+ expect(charAt(buf, 0, 0)).toBe("A");
185
+ expect(charAt(buf, 1, 0)).toBe("\u4E16"); // wide char at 1
186
+ expect(charAt(buf, 2, 0)).toBe(""); // continuation
187
+ expect(charAt(buf, 3, 0)).toBe("B"); // B after the 2-wide char
188
+ });
189
+ it("skips control characters", () => {
190
+ const buf = new PixelBuffer(20, 5);
191
+ const ctx = new DrawingContext(buf);
192
+ ctx.drawText(0, 0, "A\x01B", { fg: WHITE });
193
+ expect(charAt(buf, 0, 0)).toBe("A");
194
+ expect(charAt(buf, 1, 0)).toBe("B");
195
+ });
196
+ });
197
+ // ── fillRect ──────────────────────────────────────────────────────
198
+ describe("fillRect", () => {
199
+ it("fills the specified rect with background color", () => {
200
+ const buf = new PixelBuffer(10, 10);
201
+ const ctx = new DrawingContext(buf);
202
+ ctx.fillRect({ x: 2, y: 3, width: 3, height: 2 }, RED);
203
+ // Inside the rect
204
+ expect(bgAt(buf, 2, 3)).toEqual(RED);
205
+ expect(bgAt(buf, 4, 4)).toEqual(RED);
206
+ // Outside
207
+ expect(bgAt(buf, 1, 3).a).toBe(0); // transparent
208
+ expect(bgAt(buf, 5, 3).a).toBe(0);
209
+ expect(bgAt(buf, 2, 2).a).toBe(0);
210
+ expect(bgAt(buf, 2, 5).a).toBe(0);
211
+ });
212
+ it("fills all cells in the rect", () => {
213
+ const buf = new PixelBuffer(5, 5);
214
+ const ctx = new DrawingContext(buf);
215
+ ctx.fillRect({ x: 0, y: 0, width: 5, height: 5 }, GREEN);
216
+ for (let y = 0; y < 5; y++) {
217
+ for (let x = 0; x < 5; x++) {
218
+ expect(bgAt(buf, x, y)).toEqual(GREEN);
219
+ }
220
+ }
221
+ });
222
+ });
223
+ // ── drawBox ───────────────────────────────────────────────────────
224
+ describe("drawBox", () => {
225
+ it("creates box-drawing chars at edges with correct patterns", () => {
226
+ const buf = new PixelBuffer(10, 10);
227
+ const ctx = new DrawingContext(buf);
228
+ ctx.drawBox({ x: 1, y: 1, width: 4, height: 3 });
229
+ // Top-left corner: DOWN | RIGHT
230
+ expect(patternAt(buf, 1, 1)).toBe(DOWN | RIGHT);
231
+ expect(charAt(buf, 1, 1)).toBe(BOX_CHARS[DOWN | RIGHT]);
232
+ // Top-right corner: DOWN | LEFT
233
+ expect(patternAt(buf, 4, 1)).toBe(DOWN | LEFT);
234
+ expect(charAt(buf, 4, 1)).toBe(BOX_CHARS[DOWN | LEFT]);
235
+ // Bottom-left corner: UP | RIGHT
236
+ expect(patternAt(buf, 1, 3)).toBe(UP | RIGHT);
237
+ // Bottom-right corner: UP | LEFT
238
+ expect(patternAt(buf, 4, 3)).toBe(UP | LEFT);
239
+ // Top edge (horizontal): LEFT | RIGHT
240
+ expect(patternAt(buf, 2, 1)).toBe(LEFT | RIGHT);
241
+ expect(patternAt(buf, 3, 1)).toBe(LEFT | RIGHT);
242
+ // Left edge (vertical): UP | DOWN
243
+ expect(patternAt(buf, 1, 2)).toBe(UP | DOWN);
244
+ // Right edge (vertical): UP | DOWN
245
+ expect(patternAt(buf, 4, 2)).toBe(UP | DOWN);
246
+ });
247
+ it("single cell box draws a cross", () => {
248
+ const buf = new PixelBuffer(5, 5);
249
+ const ctx = new DrawingContext(buf);
250
+ ctx.drawBox({ x: 2, y: 2, width: 1, height: 1 });
251
+ expect(patternAt(buf, 2, 2)).toBe(UP | RIGHT | DOWN | LEFT);
252
+ });
253
+ it("single column box draws vertical line", () => {
254
+ const buf = new PixelBuffer(5, 5);
255
+ const ctx = new DrawingContext(buf);
256
+ ctx.drawBox({ x: 1, y: 0, width: 1, height: 3 });
257
+ expect(patternAt(buf, 1, 0)).toBe(DOWN);
258
+ expect(patternAt(buf, 1, 1)).toBe(UP | DOWN);
259
+ expect(patternAt(buf, 1, 2)).toBe(UP);
260
+ });
261
+ it("single row box draws horizontal line", () => {
262
+ const buf = new PixelBuffer(5, 5);
263
+ const ctx = new DrawingContext(buf);
264
+ ctx.drawBox({ x: 0, y: 1, width: 3, height: 1 });
265
+ expect(patternAt(buf, 0, 1)).toBe(RIGHT);
266
+ expect(patternAt(buf, 1, 1)).toBe(LEFT | RIGHT);
267
+ expect(patternAt(buf, 2, 1)).toBe(LEFT);
268
+ });
269
+ it("two adjacent boxes share a junction (T or cross)", () => {
270
+ const buf = new PixelBuffer(10, 10);
271
+ const ctx = new DrawingContext(buf);
272
+ // Two boxes sharing an edge: box1 right edge = box2 left edge
273
+ ctx.drawBox({ x: 0, y: 0, width: 4, height: 3 });
274
+ ctx.drawBox({ x: 3, y: 0, width: 4, height: 3 });
275
+ // Shared top corner (3,0): box1 has DOWN|LEFT, box2 has DOWN|RIGHT -> T junction
276
+ expect(patternAt(buf, 3, 0)).toBe(DOWN | LEFT | RIGHT);
277
+ // Shared middle edge (3,1): both contribute UP|DOWN -> merged vertical
278
+ expect(patternAt(buf, 3, 1)).toBe(UP | DOWN);
279
+ // Shared bottom corner (3,2): box1 has UP|LEFT, box2 has UP|RIGHT -> T junction
280
+ expect(patternAt(buf, 3, 2)).toBe(UP | LEFT | RIGHT);
281
+ });
282
+ it("zero-size box produces no output", () => {
283
+ const buf = new PixelBuffer(5, 5);
284
+ const ctx = new DrawingContext(buf);
285
+ ctx.drawBox({ x: 0, y: 0, width: 0, height: 3 });
286
+ // All cells should remain default
287
+ for (let y = 0; y < 5; y++) {
288
+ for (let x = 0; x < 5; x++) {
289
+ expect(patternAt(buf, x, y)).toBe(0);
290
+ }
291
+ }
292
+ });
293
+ });
294
+ // ── drawHLine / drawVLine ─────────────────────────────────────────
295
+ describe("drawHLine", () => {
296
+ it("draws horizontal line with LEFT|RIGHT pattern", () => {
297
+ const buf = new PixelBuffer(10, 5);
298
+ const ctx = new DrawingContext(buf);
299
+ ctx.drawHLine(2, 1, 5);
300
+ for (let x = 2; x < 7; x++) {
301
+ expect(patternAt(buf, x, 1)).toBe(LEFT | RIGHT);
302
+ }
303
+ // Adjacent cells untouched
304
+ expect(patternAt(buf, 1, 1)).toBe(0);
305
+ expect(patternAt(buf, 7, 1)).toBe(0);
306
+ });
307
+ });
308
+ describe("drawVLine", () => {
309
+ it("draws vertical line with UP|DOWN pattern", () => {
310
+ const buf = new PixelBuffer(5, 10);
311
+ const ctx = new DrawingContext(buf);
312
+ ctx.drawVLine(1, 2, 4);
313
+ for (let y = 2; y < 6; y++) {
314
+ expect(patternAt(buf, 1, y)).toBe(UP | DOWN);
315
+ }
316
+ // Adjacent cells untouched
317
+ expect(patternAt(buf, 1, 1)).toBe(0);
318
+ expect(patternAt(buf, 1, 6)).toBe(0);
319
+ });
320
+ });
321
+ describe("drawHLine + drawVLine crossing produces cross pattern", () => {
322
+ it("crossing lines merge into a cross", () => {
323
+ const buf = new PixelBuffer(10, 10);
324
+ const ctx = new DrawingContext(buf);
325
+ ctx.drawHLine(0, 3, 8);
326
+ ctx.drawVLine(4, 0, 8);
327
+ // Intersection at (4, 3) should be a cross
328
+ expect(patternAt(buf, 4, 3)).toBe(UP | RIGHT | DOWN | LEFT);
329
+ expect(charAt(buf, 4, 3)).toBe(BOX_CHARS[UP | RIGHT | DOWN | LEFT]);
330
+ });
331
+ });
332
+ // ── Clipping ──────────────────────────────────────────────────────
333
+ describe("pushClip + drawText", () => {
334
+ it("text outside clip is not written", () => {
335
+ const buf = new PixelBuffer(20, 5);
336
+ const ctx = new DrawingContext(buf);
337
+ // Clip to columns 2..6 (width 5)
338
+ ctx.pushClip({ x: 2, y: 0, width: 5, height: 5 });
339
+ ctx.drawText(0, 0, "ABCDEFGHIJ", { fg: WHITE });
340
+ ctx.popClip();
341
+ // Positions 0 and 1 are outside clip -> should be untouched
342
+ expect(charAt(buf, 0, 0)).toBe(" ");
343
+ expect(charAt(buf, 1, 0)).toBe(" ");
344
+ // Positions 2-6 are inside clip -> should have chars C-G
345
+ expect(charAt(buf, 2, 0)).toBe("C");
346
+ expect(charAt(buf, 3, 0)).toBe("D");
347
+ expect(charAt(buf, 4, 0)).toBe("E");
348
+ expect(charAt(buf, 5, 0)).toBe("F");
349
+ expect(charAt(buf, 6, 0)).toBe("G");
350
+ // Position 7+ is outside clip
351
+ expect(charAt(buf, 7, 0)).toBe(" ");
352
+ });
353
+ });
354
+ // ── Translate ─────────────────────────────────────────────────────
355
+ describe("pushTranslate", () => {
356
+ it("drawing at (0,0) after pushTranslate(5,3) writes to buffer at (5,3)", () => {
357
+ const buf = new PixelBuffer(20, 20);
358
+ const ctx = new DrawingContext(buf);
359
+ ctx.pushTranslate(5, 3);
360
+ ctx.drawChar(0, 0, "Z", { fg: WHITE });
361
+ ctx.popTranslate();
362
+ expect(charAt(buf, 5, 3)).toBe("Z");
363
+ expect(charAt(buf, 0, 0)).toBe(" "); // original (0,0) untouched
364
+ });
365
+ it("nested translates accumulate", () => {
366
+ const buf = new PixelBuffer(20, 20);
367
+ const ctx = new DrawingContext(buf);
368
+ ctx.pushTranslate(2, 1);
369
+ ctx.pushTranslate(3, 4);
370
+ ctx.drawChar(0, 0, "Q", { fg: WHITE });
371
+ ctx.popTranslate();
372
+ ctx.popTranslate();
373
+ // Total offset: (2+3, 1+4) = (5, 5)
374
+ expect(charAt(buf, 5, 5)).toBe("Q");
375
+ });
376
+ it("popTranslate restores previous offset", () => {
377
+ const buf = new PixelBuffer(20, 20);
378
+ const ctx = new DrawingContext(buf);
379
+ ctx.pushTranslate(5, 5);
380
+ ctx.pushTranslate(2, 2);
381
+ ctx.popTranslate();
382
+ // Back to (5, 5) offset
383
+ ctx.drawChar(0, 0, "R", { fg: WHITE });
384
+ ctx.popTranslate();
385
+ expect(charAt(buf, 5, 5)).toBe("R");
386
+ });
387
+ });
388
+ // ── Translate + Clip ──────────────────────────────────────────────
389
+ describe("pushTranslate + pushClip: both work together", () => {
390
+ it("clip operates in buffer (world) coordinates", () => {
391
+ const buf = new PixelBuffer(20, 20);
392
+ const ctx = new DrawingContext(buf);
393
+ // Translate so local (0,0) maps to buffer (5, 5)
394
+ ctx.pushTranslate(5, 5);
395
+ // Clip in world coords: only buffer (5..9, 5..9) is visible
396
+ ctx.pushClip({ x: 5, y: 5, width: 5, height: 5 });
397
+ // Draw at local (0,0) -> buffer (5,5) -> inside clip -> visible
398
+ ctx.drawChar(0, 0, "A", { fg: WHITE });
399
+ // Draw at local (4,4) -> buffer (9,9) -> inside clip -> visible
400
+ ctx.drawChar(4, 4, "B", { fg: WHITE });
401
+ // Draw at local (5,5) -> buffer (10,10) -> outside clip -> not written
402
+ ctx.drawChar(5, 5, "C", { fg: WHITE });
403
+ ctx.popClip();
404
+ ctx.popTranslate();
405
+ expect(charAt(buf, 5, 5)).toBe("A");
406
+ expect(charAt(buf, 9, 9)).toBe("B");
407
+ expect(charAt(buf, 10, 10)).toBe(" "); // clipped
408
+ });
409
+ it("fillRect respects both translate and clip", () => {
410
+ const buf = new PixelBuffer(20, 20);
411
+ const ctx = new DrawingContext(buf);
412
+ ctx.pushTranslate(3, 3);
413
+ ctx.pushClip({ x: 3, y: 3, width: 4, height: 4 }); // world 3..6
414
+ // Local rect (0,0,10,10) -> world (3,3,10,10), clipped to (3,3,4,4)
415
+ ctx.fillRect({ x: 0, y: 0, width: 10, height: 10 }, RED);
416
+ ctx.popClip();
417
+ ctx.popTranslate();
418
+ // Inside clipped area
419
+ expect(bgAt(buf, 3, 3)).toEqual(RED);
420
+ expect(bgAt(buf, 6, 6)).toEqual(RED);
421
+ // Outside clipped area
422
+ expect(bgAt(buf, 7, 7).a).toBe(0);
423
+ expect(bgAt(buf, 2, 2).a).toBe(0);
424
+ });
425
+ });
426
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Comprehensive unit tests for the raw input system (Phase 4).
3
+ * Covers EscapeMatcher, PasteMatcher, MouseMatcher, TextMatcher, and InputProcessor.
4
+ */
5
+ export {};