-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathui_listbox_datalist.test.ts
More file actions
256 lines (213 loc) · 6.56 KB
/
Copy pathui_listbox_datalist.test.ts
File metadata and controls
256 lines (213 loc) · 6.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
import { test, expect, beforeAll } from "vitest";
import { DataAPI, DataStruct } from "../scripts/path-controller/controller/controller";
import { UIBase } from "../scripts/core/ui_base";
import { ListBox } from "../scripts/widgets/ui_listbox";
beforeAll(() => {
(globalThis as unknown as { window: unknown }).window ||= globalThis;
});
class Item {
constructor(
public id: number,
public name: string
) {}
}
function buildFixture(withActive: boolean) {
const api = new DataAPI();
const root = new DataStruct();
api.setRoot(root);
const itemDef = api.mapStruct(Item);
itemDef.int("id", "id", "id");
itemDef.string("name", "name", "name");
const arr: Item[] = [new Item(0, "a"), new Item(1, "b"), new Item(2, "c")];
let activeId: number | undefined = undefined;
let version = 1;
const funcs: any[] = [
function get(_api: any, list: Item[], key: number) {
return list.find((it) => it.id === key);
},
function getKey(_api: any, _list: Item[], obj: Item) {
return obj.id;
},
function getLength(_api: any, list: Item[]) {
return list.length;
},
function getIter(_api: any, list: Item[]) {
return list;
},
function getStruct() {
return itemDef;
},
];
if (withActive) {
funcs.push(
function getActive(_api: any, list: Item[]) {
return list.find((it) => it.id === activeId);
},
function setActive(_api: any, _list: Item[], val: Item | undefined) {
activeId = val ? val.id : undefined;
version++;
},
function getVersion() {
return version;
}
);
}
root.list("items", "items", funcs);
const toolstack = {
cur: undefined as any,
execTool(ctx: any, tool: any) {
tool.undoPre(ctx);
tool.exec(ctx);
this.cur = tool;
},
undo(ctx: any) {
this.cur?.undo(ctx);
},
};
const ctx: any = { api, toolstack, items: arr };
return {
api,
ctx,
arr,
bump : () => version++,
getActiveId: () => activeId,
setActiveId: (v: number | undefined) => {
activeId = v;
version++;
},
};
}
function makeBox(ctx: any, path = "items"): ListBox<any, number> {
const box = UIBase.createElement("listbox-x") as ListBox<any, number>;
// Value-level reference to ListBox: the TS transform elides type-only
// imports, which would skip ui_listbox's customElements.define side effect
// and leave the element un-upgraded.
if (!(box instanceof ListBox)) {
throw new Error("listbox-x failed to register");
}
box.ctx = ctx;
box._init();
box.setAttribute("datapath", path);
return box;
}
test("populates items from a bound DataList", () => {
const fx = buildFixture(true);
const box = makeBox(fx.ctx);
box._syncFromDataList();
expect(box.items.length).toBe(3);
expect(box.items.map((it) => it.listId)).toEqual([0, 1, 2]);
});
test("uses getItemName for labels when provided", () => {
const fx = buildFixture(true);
const box = makeBox(fx.ctx);
const seen: number[] = [];
box.itemNames((_obj, key) => {
seen.push(key);
return "item-" + key;
});
box._syncFromDataList();
expect(seen).toEqual([0, 1, 2]);
});
test("reflects the data list's active element", () => {
const fx = buildFixture(true);
const box = makeBox(fx.ctx);
box._syncFromDataList();
fx.setActiveId(2);
box._syncFromDataList();
expect(box.activeId).toBe(2);
expect(box.active?.is_active).toBe(true);
});
test("writes selection back via setActive (direct, no undo)", () => {
const fx = buildFixture(true);
const box = makeBox(fx.ctx);
// update() sets _dataMode, which gates the write-back path.
box.update();
box.setActive(box.idmap.get(1));
expect(fx.getActiveId()).toBe(1);
});
test("writes selection back through an undoable op", () => {
const fx = buildFixture(true);
const box = makeBox(fx.ctx);
box.update();
box.useActiveUndo = true;
fx.setActiveId(2); // start with id 2 active
box.setActive(box.idmap.get(0));
expect(fx.getActiveId()).toBe(0);
fx.ctx.toolstack.undo(fx.ctx);
expect(fx.getActiveId()).toBe(2);
});
test("version fast-path avoids rebuild when unchanged, rebuilds on mutation", () => {
const fx = buildFixture(true);
const box = makeBox(fx.ctx);
box._syncFromDataList();
let rebuilds = 0;
const origClear = box.clear.bind(box);
box.clear = () => {
rebuilds++;
return origClear();
};
box._syncFromDataList();
expect(rebuilds).toBe(0); // version unchanged → no rebuild
fx.arr.push(new Item(3, "d"));
fx.bump();
box._syncFromDataList();
expect(rebuilds).toBe(1);
expect(box.items.length).toBe(4);
});
test("fallback: list without getActive/setActive/getVersion", () => {
const fx = buildFixture(false);
const box = makeBox(fx.ctx);
box.update();
// population via O(n) key diff
expect(box.items.length).toBe(3);
// selection stays internal, no throw (no setActive callback)
expect(() => box.setActive(box.idmap.get(1))).not.toThrow();
expect(box.activeId).toBe(1);
// key-diff triggers rebuild on structural mutation
let rebuilds = 0;
const origClear = box.clear.bind(box);
box.clear = () => {
rebuilds++;
return origClear();
};
box._syncFromDataList();
expect(rebuilds).toBe(0);
fx.arr.push(new Item(9, "z"));
box._syncFromDataList();
expect(rebuilds).toBe(1);
expect(box.items.length).toBe(4);
});
test("rebuilds when the datapath resolves to a different list object", () => {
const fx = buildFixture(true);
const box = makeBox(fx.ctx);
box._syncFromDataList();
expect(box.items.length).toBe(3);
// Swap to a different array instance WITHOUT bumping the version counter, so
// the version/key fast-path matches and only the list-ref check can catch it.
fx.ctx.items = [new Item(100, "x"), new Item(101, "y")];
box._syncFromDataList();
expect(box.items.length).toBe(2);
});
test("clears items when the bound list ceases to exist", () => {
const fx = buildFixture(true);
const box = makeBox(fx.ctx);
box._syncFromDataList();
expect(box.items.length).toBe(3);
fx.ctx.items = undefined;
box._syncFromDataList();
expect(box.items.length).toBe(0);
});
test("repopulates when a vanished list reappears as the same object", () => {
const fx = buildFixture(true);
const box = makeBox(fx.ctx);
box._syncFromDataList();
expect(box.items.length).toBe(3);
const saved = fx.ctx.items;
fx.ctx.items = undefined;
box._syncFromDataList();
expect(box.items.length).toBe(0);
// Same array object reappears with an unchanged version; must still rebuild.
fx.ctx.items = saved;
box._syncFromDataList();
expect(box.items.length).toBe(3);
});