@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,4 @@
1
+ /**
2
+ * Tests for Phase 6: Layout Engine (Control, Box, Row, Column, Stack).
3
+ */
4
+ export {};
@@ -0,0 +1,689 @@
1
+ /**
2
+ * Tests for Phase 6: Layout Engine (Control, Box, Row, Column, Stack).
3
+ */
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import { keyEvent } from "../input/events.js";
6
+ import { Box } from "../layout/box.js";
7
+ import { Column } from "../layout/column.js";
8
+ import { Control } from "../layout/control.js";
9
+ import { Row } from "../layout/row.js";
10
+ import { Stack } from "../layout/stack.js";
11
+ // ── Helpers ──────────────────────────────────────────────────────────
12
+ /** Unconstrained constraint for measuring. */
13
+ const UNCONSTRAINED = {
14
+ minWidth: 0,
15
+ minHeight: 0,
16
+ maxWidth: Infinity,
17
+ maxHeight: Infinity,
18
+ };
19
+ /** Create a constraint with specific max bounds. */
20
+ function maxConstraint(maxWidth, maxHeight) {
21
+ return { minWidth: 0, minHeight: 0, maxWidth, maxHeight };
22
+ }
23
+ /**
24
+ * Concrete Control subclass that returns a fixed size and tracks render calls.
25
+ */
26
+ class TestControl extends Control {
27
+ fixedWidth;
28
+ fixedHeight;
29
+ renderCalled = false;
30
+ renderCount = 0;
31
+ constructor(width = 0, height = 0) {
32
+ super();
33
+ this.fixedWidth = width;
34
+ this.fixedHeight = height;
35
+ }
36
+ measure(constraint) {
37
+ const size = {
38
+ width: Math.max(constraint.minWidth, Math.min(this.fixedWidth, constraint.maxWidth)),
39
+ height: Math.max(constraint.minHeight, Math.min(this.fixedHeight, constraint.maxHeight)),
40
+ };
41
+ this.desiredSize = size;
42
+ return size;
43
+ }
44
+ render(_ctx) {
45
+ this.renderCalled = true;
46
+ this.renderCount++;
47
+ }
48
+ }
49
+ /**
50
+ * Focusable TestControl.
51
+ */
52
+ class FocusableControl extends TestControl {
53
+ constructor(width = 0, height = 0) {
54
+ super(width, height);
55
+ this.focusable = true;
56
+ }
57
+ }
58
+ // ═══════════════════════════════════════════════════════════════════════
59
+ // Control base class
60
+ // ═══════════════════════════════════════════════════════════════════════
61
+ describe("Control", () => {
62
+ describe("addChild / removeChild", () => {
63
+ it("addChild sets parent and adds to children", () => {
64
+ const parent = new TestControl();
65
+ const child = new TestControl();
66
+ parent.addChild(child);
67
+ expect(child.parent).toBe(parent);
68
+ expect(parent.children).toContain(child);
69
+ expect(parent.children.length).toBe(1);
70
+ });
71
+ it("addChild removes child from previous parent", () => {
72
+ const parent1 = new TestControl();
73
+ const parent2 = new TestControl();
74
+ const child = new TestControl();
75
+ parent1.addChild(child);
76
+ parent2.addChild(child);
77
+ expect(child.parent).toBe(parent2);
78
+ expect(parent1.children.length).toBe(0);
79
+ expect(parent2.children.length).toBe(1);
80
+ });
81
+ it("removeChild clears parent and removes from children", () => {
82
+ const parent = new TestControl();
83
+ const child = new TestControl();
84
+ parent.addChild(child);
85
+ parent.removeChild(child);
86
+ expect(child.parent).toBeNull();
87
+ expect(parent.children.length).toBe(0);
88
+ });
89
+ it("removeChild on non-child is a no-op", () => {
90
+ const parent = new TestControl();
91
+ const other = new TestControl();
92
+ // Should not throw
93
+ parent.removeChild(other);
94
+ expect(parent.children.length).toBe(0);
95
+ });
96
+ });
97
+ describe("invalidate / dirty tracking", () => {
98
+ it("invalidate sets dirty=true", () => {
99
+ const ctrl = new TestControl();
100
+ ctrl.dirty = false;
101
+ ctrl.invalidate();
102
+ expect(ctrl.dirty).toBe(true);
103
+ });
104
+ it("invalidate propagates to parent", () => {
105
+ const parent = new TestControl();
106
+ const child = new TestControl();
107
+ parent.addChild(child);
108
+ // Reset dirty flags
109
+ parent.dirty = false;
110
+ child.dirty = false;
111
+ child.invalidate();
112
+ expect(child.dirty).toBe(true);
113
+ expect(parent.dirty).toBe(true);
114
+ });
115
+ it("invalidate propagates all the way to root", () => {
116
+ const root = new TestControl();
117
+ const mid = new TestControl();
118
+ const leaf = new TestControl();
119
+ root.addChild(mid);
120
+ mid.addChild(leaf);
121
+ root.dirty = false;
122
+ mid.dirty = false;
123
+ leaf.dirty = false;
124
+ leaf.invalidate();
125
+ expect(leaf.dirty).toBe(true);
126
+ expect(mid.dirty).toBe(true);
127
+ expect(root.dirty).toBe(true);
128
+ });
129
+ it("invalidate does not propagate if already dirty", () => {
130
+ const parent = new TestControl();
131
+ const child = new TestControl();
132
+ parent.addChild(child);
133
+ // child is already dirty from addChild
134
+ // Reset parent dirty, keep child dirty
135
+ parent.dirty = false;
136
+ child.dirty = true;
137
+ // Since child is already dirty, invalidate should not propagate
138
+ child.invalidate();
139
+ expect(parent.dirty).toBe(false);
140
+ });
141
+ });
142
+ describe("event system (on/off/emit)", () => {
143
+ it("on registers handler that receives emitted events", () => {
144
+ const ctrl = new TestControl();
145
+ const handler = vi.fn();
146
+ ctrl.on("test", handler);
147
+ ctrl.emit("test", "arg1", 42);
148
+ expect(handler).toHaveBeenCalledOnce();
149
+ expect(handler).toHaveBeenCalledWith("arg1", 42);
150
+ });
151
+ it("multiple handlers are all called", () => {
152
+ const ctrl = new TestControl();
153
+ const h1 = vi.fn();
154
+ const h2 = vi.fn();
155
+ ctrl.on("click", h1);
156
+ ctrl.on("click", h2);
157
+ ctrl.emit("click");
158
+ expect(h1).toHaveBeenCalledOnce();
159
+ expect(h2).toHaveBeenCalledOnce();
160
+ });
161
+ it("off removes handler", () => {
162
+ const ctrl = new TestControl();
163
+ const handler = vi.fn();
164
+ ctrl.on("test", handler);
165
+ ctrl.off("test", handler);
166
+ ctrl.emit("test");
167
+ expect(handler).not.toHaveBeenCalled();
168
+ });
169
+ it("off with non-registered handler is a no-op", () => {
170
+ const ctrl = new TestControl();
171
+ const handler = vi.fn();
172
+ ctrl.off("test", handler); // should not throw
173
+ });
174
+ it("emit with no listeners is a no-op", () => {
175
+ const ctrl = new TestControl();
176
+ // Should not throw
177
+ ctrl.emit("nonexistent");
178
+ });
179
+ });
180
+ describe("handleInput", () => {
181
+ it("routes to focused child", () => {
182
+ const parent = new TestControl();
183
+ const child = new FocusableControl();
184
+ parent.addChild(child);
185
+ child.onFocus();
186
+ const inputHandler = vi.fn().mockReturnValue(true);
187
+ child.handleInput = inputHandler;
188
+ const event = keyEvent("a", "a");
189
+ const consumed = parent.handleInput(event);
190
+ expect(inputHandler).toHaveBeenCalledWith(event);
191
+ expect(consumed).toBe(true);
192
+ });
193
+ it("Tab key cycles focus via focusNext", () => {
194
+ const root = new TestControl();
195
+ const a = new FocusableControl();
196
+ const b = new FocusableControl();
197
+ root.addChild(a);
198
+ root.addChild(b);
199
+ // Initially nothing is focused; Tab should focus first
200
+ const tabEvent = keyEvent("tab", "", false, false, false);
201
+ root.handleInput(tabEvent);
202
+ expect(a.focused).toBe(true);
203
+ expect(b.focused).toBe(false);
204
+ root.handleInput(tabEvent);
205
+ expect(a.focused).toBe(false);
206
+ expect(b.focused).toBe(true);
207
+ });
208
+ it("Shift+Tab cycles focus via focusPrev", () => {
209
+ const root = new TestControl();
210
+ const a = new FocusableControl();
211
+ const b = new FocusableControl();
212
+ const c = new FocusableControl();
213
+ root.addChild(a);
214
+ root.addChild(b);
215
+ root.addChild(c);
216
+ // Focus the second item first
217
+ a.onFocus();
218
+ a.onBlur();
219
+ b.onFocus();
220
+ const shiftTabEvent = keyEvent("tab", "", true, false, false);
221
+ root.handleInput(shiftTabEvent);
222
+ // Should go back to a
223
+ expect(a.focused).toBe(true);
224
+ expect(b.focused).toBe(false);
225
+ });
226
+ });
227
+ describe("focus cycling", () => {
228
+ it("focusNext cycles through focusable controls", () => {
229
+ const root = new TestControl();
230
+ const a = new FocusableControl();
231
+ const b = new FocusableControl();
232
+ const c = new FocusableControl();
233
+ root.addChild(a);
234
+ root.addChild(b);
235
+ root.addChild(c);
236
+ root.focusNext(); // -> a
237
+ expect(a.focused).toBe(true);
238
+ root.focusNext(); // -> b
239
+ expect(a.focused).toBe(false);
240
+ expect(b.focused).toBe(true);
241
+ root.focusNext(); // -> c
242
+ expect(b.focused).toBe(false);
243
+ expect(c.focused).toBe(true);
244
+ root.focusNext(); // -> wraps to a
245
+ expect(c.focused).toBe(false);
246
+ expect(a.focused).toBe(true);
247
+ });
248
+ it("focusPrev cycles in reverse", () => {
249
+ const root = new TestControl();
250
+ const a = new FocusableControl();
251
+ const b = new FocusableControl();
252
+ root.addChild(a);
253
+ root.addChild(b);
254
+ // focusPrev with nothing focused -> last item
255
+ root.focusPrev();
256
+ expect(b.focused).toBe(true);
257
+ root.focusPrev();
258
+ expect(a.focused).toBe(true);
259
+ expect(b.focused).toBe(false);
260
+ root.focusPrev(); // wrap to last
261
+ expect(b.focused).toBe(true);
262
+ expect(a.focused).toBe(false);
263
+ });
264
+ it("focusNext with no focusable controls is a no-op", () => {
265
+ const root = new TestControl();
266
+ const child = new TestControl(); // not focusable
267
+ root.addChild(child);
268
+ // Should not throw
269
+ root.focusNext();
270
+ expect(child.focused).toBe(false);
271
+ });
272
+ it("focusNext skips invisible controls", () => {
273
+ const root = new TestControl();
274
+ const a = new FocusableControl();
275
+ const b = new FocusableControl();
276
+ const c = new FocusableControl();
277
+ root.addChild(a);
278
+ root.addChild(b);
279
+ root.addChild(c);
280
+ b.visible = false;
281
+ root.focusNext(); // -> a
282
+ expect(a.focused).toBe(true);
283
+ root.focusNext(); // -> c (skips invisible b)
284
+ expect(c.focused).toBe(true);
285
+ expect(a.focused).toBe(false);
286
+ });
287
+ it("onFocus/onBlur emit focus and blur events", () => {
288
+ const ctrl = new FocusableControl();
289
+ const focusSpy = vi.fn();
290
+ const blurSpy = vi.fn();
291
+ ctrl.on("focus", focusSpy);
292
+ ctrl.on("blur", blurSpy);
293
+ ctrl.onFocus();
294
+ expect(focusSpy).toHaveBeenCalledOnce();
295
+ expect(ctrl.focused).toBe(true);
296
+ ctrl.onBlur();
297
+ expect(blurSpy).toHaveBeenCalledOnce();
298
+ expect(ctrl.focused).toBe(false);
299
+ });
300
+ });
301
+ describe("measure default", () => {
302
+ it("base Control.measure returns (0,0) clamped to constraints", () => {
303
+ // We need a non-abstract subclass that calls super.measure
304
+ class DefaultControl extends Control {
305
+ render(_ctx) { }
306
+ }
307
+ const ctrl = new DefaultControl();
308
+ const size = ctrl.measure({
309
+ minWidth: 5,
310
+ minHeight: 3,
311
+ maxWidth: 100,
312
+ maxHeight: 100,
313
+ });
314
+ expect(size).toEqual({ width: 5, height: 3 });
315
+ });
316
+ });
317
+ describe("arrange default", () => {
318
+ it("base Control.arrange sets bounds", () => {
319
+ const ctrl = new TestControl();
320
+ const rect = { x: 10, y: 20, width: 30, height: 40 };
321
+ ctrl.arrange(rect);
322
+ expect(ctrl.bounds).toEqual(rect);
323
+ });
324
+ });
325
+ });
326
+ // ═══════════════════════════════════════════════════════════════════════
327
+ // Box
328
+ // ═══════════════════════════════════════════════════════════════════════
329
+ describe("Box", () => {
330
+ describe("measure", () => {
331
+ it("adds padding to child's desired size", () => {
332
+ const child = new TestControl(10, 5);
333
+ const box = new Box({ child, padding: 2 });
334
+ const size = box.measure(UNCONSTRAINED);
335
+ // 10 + 2 + 2 = 14 width, 5 + 2 + 2 = 9 height
336
+ expect(size.width).toBe(14);
337
+ expect(size.height).toBe(9);
338
+ });
339
+ it("per-side padding", () => {
340
+ const child = new TestControl(10, 5);
341
+ const box = new Box({
342
+ child,
343
+ paddingTop: 1,
344
+ paddingRight: 2,
345
+ paddingBottom: 3,
346
+ paddingLeft: 4,
347
+ });
348
+ const size = box.measure(UNCONSTRAINED);
349
+ // width: 10 + 4 + 2 = 16
350
+ // height: 5 + 1 + 3 = 9
351
+ expect(size.width).toBe(16);
352
+ expect(size.height).toBe(9);
353
+ });
354
+ it("with no child, returns just padding size", () => {
355
+ const box = new Box({ padding: 3 });
356
+ const size = box.measure(UNCONSTRAINED);
357
+ // 0 + 3 + 3 = 6 each
358
+ expect(size.width).toBe(6);
359
+ expect(size.height).toBe(6);
360
+ });
361
+ it("respects constraints", () => {
362
+ const child = new TestControl(100, 100);
363
+ const box = new Box({ child, padding: 2 });
364
+ const size = box.measure(maxConstraint(20, 15));
365
+ expect(size.width).toBeLessThanOrEqual(20);
366
+ expect(size.height).toBeLessThanOrEqual(15);
367
+ });
368
+ });
369
+ describe("arrange", () => {
370
+ it("child gets inner rect (inset by padding)", () => {
371
+ const child = new TestControl(10, 5);
372
+ const box = new Box({ child, padding: 2 });
373
+ box.measure(UNCONSTRAINED);
374
+ box.arrange({ x: 0, y: 0, width: 14, height: 9 });
375
+ expect(child.bounds).toEqual({
376
+ x: 2,
377
+ y: 2,
378
+ width: 10,
379
+ height: 5,
380
+ });
381
+ });
382
+ it("per-side padding arrange", () => {
383
+ const child = new TestControl(10, 5);
384
+ const box = new Box({
385
+ child,
386
+ paddingTop: 1,
387
+ paddingRight: 2,
388
+ paddingBottom: 3,
389
+ paddingLeft: 4,
390
+ });
391
+ box.measure(UNCONSTRAINED);
392
+ box.arrange({ x: 0, y: 0, width: 16, height: 9 });
393
+ expect(child.bounds.x).toBe(4); // paddingLeft
394
+ expect(child.bounds.y).toBe(1); // paddingTop
395
+ expect(child.bounds.width).toBe(10); // 16 - 4 - 2
396
+ expect(child.bounds.height).toBe(5); // 9 - 1 - 3
397
+ });
398
+ it("sets own bounds", () => {
399
+ const box = new Box({ padding: 2 });
400
+ const rect = { x: 5, y: 10, width: 20, height: 15 };
401
+ box.arrange(rect);
402
+ expect(box.bounds).toEqual(rect);
403
+ });
404
+ });
405
+ describe("child property", () => {
406
+ it("get child returns first child or null", () => {
407
+ const box = new Box();
408
+ expect(box.child).toBeNull();
409
+ const child = new TestControl();
410
+ box.addChild(child);
411
+ expect(box.child).toBe(child);
412
+ });
413
+ it("set child replaces existing child", () => {
414
+ const child1 = new TestControl();
415
+ const child2 = new TestControl();
416
+ const box = new Box({ child: child1 });
417
+ box.child = child2;
418
+ expect(box.children.length).toBe(1);
419
+ expect(box.child).toBe(child2);
420
+ expect(child1.parent).toBeNull();
421
+ });
422
+ it("set child to null removes child", () => {
423
+ const child = new TestControl();
424
+ const box = new Box({ child });
425
+ box.child = null;
426
+ expect(box.children.length).toBe(0);
427
+ expect(box.child).toBeNull();
428
+ });
429
+ });
430
+ });
431
+ // ═══════════════════════════════════════════════════════════════════════
432
+ // Row
433
+ // ═══════════════════════════════════════════════════════════════════════
434
+ describe("Row", () => {
435
+ describe("measure", () => {
436
+ it("sum of children widths, max height", () => {
437
+ const a = new TestControl(10, 5);
438
+ const b = new TestControl(20, 8);
439
+ const c = new TestControl(15, 3);
440
+ const row = new Row({ children: [a, b, c] });
441
+ const size = row.measure(UNCONSTRAINED);
442
+ expect(size.width).toBe(45); // 10 + 20 + 15
443
+ expect(size.height).toBe(8); // max(5, 8, 3)
444
+ });
445
+ it("no children returns zero", () => {
446
+ const row = new Row();
447
+ const size = row.measure(UNCONSTRAINED);
448
+ expect(size.width).toBe(0);
449
+ expect(size.height).toBe(0);
450
+ });
451
+ it("gap spacing included in width", () => {
452
+ const a = new TestControl(10, 5);
453
+ const b = new TestControl(20, 5);
454
+ const c = new TestControl(15, 5);
455
+ const row = new Row({ children: [a, b, c], gap: 2 });
456
+ const size = row.measure(UNCONSTRAINED);
457
+ // 10 + 20 + 15 + 2 * 2(gaps) = 49
458
+ expect(size.width).toBe(49);
459
+ });
460
+ it("invisible children are ignored", () => {
461
+ const a = new TestControl(10, 5);
462
+ const b = new TestControl(20, 8);
463
+ b.visible = false;
464
+ const row = new Row({ children: [a, b] });
465
+ const size = row.measure(UNCONSTRAINED);
466
+ expect(size.width).toBe(10);
467
+ expect(size.height).toBe(5);
468
+ });
469
+ });
470
+ describe("arrange", () => {
471
+ it("children laid out left-to-right", () => {
472
+ const a = new TestControl(10, 5);
473
+ const b = new TestControl(20, 5);
474
+ const c = new TestControl(15, 5);
475
+ const row = new Row({ children: [a, b, c] });
476
+ row.measure(UNCONSTRAINED);
477
+ row.arrange({ x: 0, y: 0, width: 45, height: 10 });
478
+ expect(a.bounds.x).toBe(0);
479
+ expect(a.bounds.width).toBe(10);
480
+ expect(b.bounds.x).toBe(10);
481
+ expect(b.bounds.width).toBe(20);
482
+ expect(c.bounds.x).toBe(30);
483
+ expect(c.bounds.width).toBe(15);
484
+ });
485
+ it("gap spacing between children", () => {
486
+ const a = new TestControl(10, 5);
487
+ const b = new TestControl(20, 5);
488
+ const row = new Row({ children: [a, b], gap: 3 });
489
+ row.measure(UNCONSTRAINED);
490
+ row.arrange({ x: 0, y: 0, width: 33, height: 10 });
491
+ expect(a.bounds.x).toBe(0);
492
+ expect(a.bounds.width).toBe(10);
493
+ expect(b.bounds.x).toBe(13); // 10 + 3 gap
494
+ expect(b.bounds.width).toBe(20);
495
+ });
496
+ it("children get full height of the row", () => {
497
+ const a = new TestControl(10, 5);
498
+ const b = new TestControl(20, 3);
499
+ const row = new Row({ children: [a, b] });
500
+ row.measure(UNCONSTRAINED);
501
+ row.arrange({ x: 0, y: 0, width: 30, height: 12 });
502
+ expect(a.bounds.height).toBe(12);
503
+ expect(b.bounds.height).toBe(12);
504
+ });
505
+ it("sets own bounds", () => {
506
+ const row = new Row();
507
+ const rect = { x: 5, y: 10, width: 100, height: 50 };
508
+ row.arrange(rect);
509
+ expect(row.bounds).toEqual(rect);
510
+ });
511
+ it("proportionally scales when overflowing", () => {
512
+ const a = new TestControl(60, 5);
513
+ const b = new TestControl(40, 5);
514
+ const row = new Row({ children: [a, b] });
515
+ row.measure(UNCONSTRAINED);
516
+ // Only 50 available, but children want 100
517
+ row.arrange({ x: 0, y: 0, width: 50, height: 10 });
518
+ // a wants 60% of 100, gets 60% of 50 = 30
519
+ expect(a.bounds.width).toBe(30);
520
+ // b wants 40% of 100, gets 40% of 50 = 20
521
+ expect(b.bounds.width).toBe(20);
522
+ });
523
+ });
524
+ });
525
+ // ═══════════════════════════════════════════════════════════════════════
526
+ // Column
527
+ // ═══════════════════════════════════════════════════════════════════════
528
+ describe("Column", () => {
529
+ describe("measure", () => {
530
+ it("max width, sum of children heights", () => {
531
+ const a = new TestControl(10, 5);
532
+ const b = new TestControl(20, 8);
533
+ const c = new TestControl(15, 3);
534
+ const col = new Column({ children: [a, b, c] });
535
+ const size = col.measure(UNCONSTRAINED);
536
+ expect(size.width).toBe(20); // max(10, 20, 15)
537
+ expect(size.height).toBe(16); // 5 + 8 + 3
538
+ });
539
+ it("no children returns zero", () => {
540
+ const col = new Column();
541
+ const size = col.measure(UNCONSTRAINED);
542
+ expect(size.width).toBe(0);
543
+ expect(size.height).toBe(0);
544
+ });
545
+ it("gap spacing included in height", () => {
546
+ const a = new TestControl(10, 5);
547
+ const b = new TestControl(10, 8);
548
+ const c = new TestControl(10, 3);
549
+ const col = new Column({ children: [a, b, c], gap: 2 });
550
+ const size = col.measure(UNCONSTRAINED);
551
+ // 5 + 8 + 3 + 2 * 2(gaps) = 20
552
+ expect(size.height).toBe(20);
553
+ });
554
+ it("invisible children are ignored", () => {
555
+ const a = new TestControl(10, 5);
556
+ const b = new TestControl(20, 8);
557
+ b.visible = false;
558
+ const col = new Column({ children: [a, b] });
559
+ const size = col.measure(UNCONSTRAINED);
560
+ expect(size.width).toBe(10);
561
+ expect(size.height).toBe(5);
562
+ });
563
+ });
564
+ describe("arrange", () => {
565
+ it("children laid out top-to-bottom", () => {
566
+ const a = new TestControl(10, 5);
567
+ const b = new TestControl(10, 8);
568
+ const c = new TestControl(10, 3);
569
+ const col = new Column({ children: [a, b, c] });
570
+ col.measure(UNCONSTRAINED);
571
+ col.arrange({ x: 0, y: 0, width: 20, height: 16 });
572
+ expect(a.bounds.y).toBe(0);
573
+ expect(a.bounds.height).toBe(5);
574
+ expect(b.bounds.y).toBe(5);
575
+ expect(b.bounds.height).toBe(8);
576
+ expect(c.bounds.y).toBe(13);
577
+ expect(c.bounds.height).toBe(3);
578
+ });
579
+ it("gap spacing between children", () => {
580
+ const a = new TestControl(10, 5);
581
+ const b = new TestControl(10, 8);
582
+ const col = new Column({ children: [a, b], gap: 3 });
583
+ col.measure(UNCONSTRAINED);
584
+ col.arrange({ x: 0, y: 0, width: 20, height: 16 });
585
+ expect(a.bounds.y).toBe(0);
586
+ expect(a.bounds.height).toBe(5);
587
+ expect(b.bounds.y).toBe(8); // 5 + 3 gap
588
+ expect(b.bounds.height).toBe(8);
589
+ });
590
+ it("children get full width of the column", () => {
591
+ const a = new TestControl(5, 10);
592
+ const b = new TestControl(3, 10);
593
+ const col = new Column({ children: [a, b] });
594
+ col.measure(UNCONSTRAINED);
595
+ col.arrange({ x: 0, y: 0, width: 30, height: 20 });
596
+ expect(a.bounds.width).toBe(30);
597
+ expect(b.bounds.width).toBe(30);
598
+ });
599
+ it("sets own bounds", () => {
600
+ const col = new Column();
601
+ const rect = { x: 5, y: 10, width: 100, height: 50 };
602
+ col.arrange(rect);
603
+ expect(col.bounds).toEqual(rect);
604
+ });
605
+ it("proportionally scales when overflowing", () => {
606
+ const a = new TestControl(10, 60);
607
+ const b = new TestControl(10, 40);
608
+ const col = new Column({ children: [a, b] });
609
+ col.measure(UNCONSTRAINED);
610
+ // Only 50 available, but children want 100
611
+ col.arrange({ x: 0, y: 0, width: 20, height: 50 });
612
+ expect(a.bounds.height).toBe(30); // 60/100 * 50
613
+ expect(b.bounds.height).toBe(20); // 40/100 * 50
614
+ });
615
+ });
616
+ });
617
+ // ═══════════════════════════════════════════════════════════════════════
618
+ // Stack
619
+ // ═══════════════════════════════════════════════════════════════════════
620
+ describe("Stack", () => {
621
+ describe("measure", () => {
622
+ it("max width, max height of all children", () => {
623
+ const a = new TestControl(10, 5);
624
+ const b = new TestControl(20, 8);
625
+ const c = new TestControl(15, 3);
626
+ const stack = new Stack({ children: [a, b, c] });
627
+ const size = stack.measure(UNCONSTRAINED);
628
+ expect(size.width).toBe(20); // max(10, 20, 15)
629
+ expect(size.height).toBe(8); // max(5, 8, 3)
630
+ });
631
+ it("no children returns zero", () => {
632
+ const stack = new Stack();
633
+ const size = stack.measure(UNCONSTRAINED);
634
+ expect(size.width).toBe(0);
635
+ expect(size.height).toBe(0);
636
+ });
637
+ it("invisible children are ignored", () => {
638
+ const a = new TestControl(10, 5);
639
+ const b = new TestControl(20, 8);
640
+ b.visible = false;
641
+ const stack = new Stack({ children: [a, b] });
642
+ const size = stack.measure(UNCONSTRAINED);
643
+ expect(size.width).toBe(10);
644
+ expect(size.height).toBe(5);
645
+ });
646
+ it("respects constraints", () => {
647
+ const a = new TestControl(100, 100);
648
+ const stack = new Stack({ children: [a] });
649
+ const size = stack.measure(maxConstraint(30, 20));
650
+ expect(size.width).toBeLessThanOrEqual(30);
651
+ expect(size.height).toBeLessThanOrEqual(20);
652
+ });
653
+ });
654
+ describe("arrange", () => {
655
+ it("all children get full rect", () => {
656
+ const a = new TestControl(10, 5);
657
+ const b = new TestControl(20, 8);
658
+ const c = new TestControl(15, 3);
659
+ const stack = new Stack({ children: [a, b, c] });
660
+ stack.measure(UNCONSTRAINED);
661
+ stack.arrange({ x: 0, y: 0, width: 50, height: 40 });
662
+ for (const child of [a, b, c]) {
663
+ expect(child.bounds).toEqual({
664
+ x: 0,
665
+ y: 0,
666
+ width: 50,
667
+ height: 40,
668
+ });
669
+ }
670
+ });
671
+ it("invisible children are skipped in arrange", () => {
672
+ const a = new TestControl(10, 5);
673
+ const b = new TestControl(20, 8);
674
+ b.visible = false;
675
+ const stack = new Stack({ children: [a, b] });
676
+ stack.measure(UNCONSTRAINED);
677
+ stack.arrange({ x: 0, y: 0, width: 50, height: 40 });
678
+ expect(a.bounds).toEqual({ x: 0, y: 0, width: 50, height: 40 });
679
+ // b is invisible, so its bounds should remain at default
680
+ expect(b.bounds).toEqual({ x: 0, y: 0, width: 0, height: 0 });
681
+ });
682
+ it("sets own bounds", () => {
683
+ const stack = new Stack();
684
+ const rect = { x: 5, y: 10, width: 100, height: 50 };
685
+ stack.arrange(rect);
686
+ expect(stack.bounds).toEqual(rect);
687
+ });
688
+ });
689
+ });
@@ -0,0 +1 @@
1
+ export {};