@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,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 {};
|