rask-ui 0.2.1 → 0.2.3

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.
@@ -11,11 +11,13 @@ class MockVNode extends AbstractVNode {
11
11
  constructor(key) {
12
12
  super();
13
13
  this.key = key;
14
+ // Initialize elm in constructor so getElements() always works
15
+ this.elm = document.createTextNode(`mock-${this.key || "no-key"}`);
14
16
  }
15
17
  mount(parent) {
16
18
  this.mountCalls++;
17
19
  this.parent = parent;
18
- this.elm = document.createTextNode(`mock-${this.key || "no-key"}`);
20
+ // elm already exists from constructor
19
21
  return this.elm;
20
22
  }
21
23
  patch(newNode) {
@@ -26,7 +28,8 @@ class MockVNode extends AbstractVNode {
26
28
  }
27
29
  unmount() {
28
30
  this.unmountCalls++;
29
- delete this.elm;
31
+ // Don't delete elm - it's needed for getElements() when building operations
32
+ // delete this.elm;
30
33
  delete this.parent;
31
34
  }
32
35
  rerender() {
@@ -37,6 +40,8 @@ class MockVNode extends AbstractVNode {
37
40
  class MockParentVNode extends MockVNode {
38
41
  constructor(initialChildren, key) {
39
42
  super(key);
43
+ // Override elm with an HTMLElement since parents need to contain children
44
+ this.elm = document.createElement("div");
40
45
  this.children = (initialChildren || []);
41
46
  }
42
47
  }
@@ -46,25 +51,33 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
46
51
  const newChild1 = new MockVNode("a");
47
52
  const newChild2 = new MockVNode("b");
48
53
  const parent = new MockParentVNode([]);
49
- const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
54
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2]));
50
55
  // New children should be mounted
51
56
  expect(newChild1.mountCalls).toBe(1);
52
57
  expect(newChild2.mountCalls).toBe(1);
53
58
  expect(newChild1.parent).toBe(parent);
54
59
  expect(newChild2.parent).toBe(parent);
55
60
  // Result should be the new children (since old was empty)
56
- expect(result).toEqual([newChild1, newChild2]);
61
+ expect(children).toEqual([newChild1, newChild2]);
62
+ // Should return insert operation
63
+ expect(operations).toHaveLength(1);
64
+ expect(operations[0].type).toBe("insert");
65
+ expect(operations[0]).toHaveProperty("elms");
57
66
  });
58
67
  it("should unmount all old children when new is empty", () => {
59
68
  const oldChild1 = new MockVNode("a");
60
69
  const oldChild2 = new MockVNode("b");
61
70
  const parent = new MockParentVNode([oldChild1, oldChild2]);
62
- const result = parent.patchChildren(toVNodes([]));
71
+ const { children, operations } = parent.patchChildren(toVNodes([]));
63
72
  // Old children should be unmounted
64
73
  expect(oldChild1.unmountCalls).toBe(1);
65
74
  expect(oldChild2.unmountCalls).toBe(1);
66
75
  // Result should be empty
67
- expect(result).toEqual([]);
76
+ expect(children).toEqual([]);
77
+ // Should return remove operation
78
+ expect(operations).toHaveLength(1);
79
+ expect(operations[0].type).toBe("remove");
80
+ expect(operations[0]).toHaveProperty("elms");
68
81
  });
69
82
  });
70
83
  describe("Patching with keys", () => {
@@ -76,7 +89,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
76
89
  const newChild1 = new MockVNode("a");
77
90
  const newChild2 = new MockVNode("b");
78
91
  const newChild3 = new MockVNode("c");
79
- const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
92
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
80
93
  // OLD children should be patched with new children
81
94
  expect(oldChild1.patchCalls).toBe(1);
82
95
  expect(oldChild2.patchCalls).toBe(1);
@@ -93,7 +106,9 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
93
106
  expect(oldChild2.unmountCalls).toBe(0);
94
107
  expect(oldChild3.unmountCalls).toBe(0);
95
108
  // Result should still be the OLD children (reused)
96
- expect(result).toEqual([oldChild1, oldChild2, oldChild3]);
109
+ expect(children).toEqual([oldChild1, oldChild2, oldChild3]);
110
+ // Should have no operations (just patching)
111
+ expect(operations).toHaveLength(0);
97
112
  });
98
113
  it("should handle reordered children with keys", () => {
99
114
  const oldChild1 = new MockVNode("a");
@@ -104,7 +119,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
104
119
  const newChild1 = new MockVNode("c");
105
120
  const newChild2 = new MockVNode("a");
106
121
  const newChild3 = new MockVNode("b");
107
- const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
122
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
108
123
  // Old nodes should be patched with corresponding new nodes by key
109
124
  expect(oldChild1.patchedWith).toBe(newChild2); // a->a
110
125
  expect(oldChild2.patchedWith).toBe(newChild3); // b->b
@@ -114,11 +129,14 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
114
129
  expect(oldChild2.unmountCalls).toBe(0);
115
130
  expect(oldChild3.unmountCalls).toBe(0);
116
131
  // Result should be old children in NEW order (c, a, b)
117
- expect(result).toEqual([oldChild3, oldChild1, oldChild2]);
132
+ expect(children).toEqual([oldChild3, oldChild1, oldChild2]);
118
133
  // Verify correct keys
119
- expect(result[0].key).toBe("c");
120
- expect(result[1].key).toBe("a");
121
- expect(result[2].key).toBe("b");
134
+ expect(children[0].key).toBe("c");
135
+ expect(children[1].key).toBe("a");
136
+ expect(children[2].key).toBe("b");
137
+ // Should have move operations for reordered children
138
+ expect(operations.length).toBeGreaterThan(0);
139
+ expect(operations.some((op) => op.type === "move" || op.type === "insert")).toBe(true);
122
140
  });
123
141
  it("should mount new children and unmount removed children", () => {
124
142
  const oldChild1 = new MockVNode("a");
@@ -129,7 +147,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
129
147
  const newChild1 = new MockVNode("a");
130
148
  const newChild2 = new MockVNode("c");
131
149
  const newChild3 = new MockVNode("d");
132
- const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
150
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
133
151
  // a and c should be patched (reused)
134
152
  expect(oldChild1.patchedWith).toBe(newChild1);
135
153
  expect(oldChild3.patchedWith).toBe(newChild2);
@@ -138,11 +156,44 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
138
156
  // b should be unmounted
139
157
  expect(oldChild2.unmountCalls).toBe(1);
140
158
  // Result should contain old a, old c, and new d
141
- expect(result).toContain(oldChild1);
142
- expect(result).toContain(oldChild3);
143
- expect(result).toContain(newChild3);
144
- expect(result).not.toContain(oldChild2);
145
- expect(result.length).toBe(3);
159
+ expect(children).toContain(oldChild1);
160
+ expect(children).toContain(oldChild3);
161
+ expect(children).toContain(newChild3);
162
+ expect(children).not.toContain(oldChild2);
163
+ expect(children.length).toBe(3);
164
+ // Should have operations for insert and remove
165
+ expect(operations.length).toBeGreaterThan(0);
166
+ expect(operations.some((op) => op.type === "insert")).toBe(true);
167
+ expect(operations.some((op) => op.type === "remove")).toBe(true);
168
+ });
169
+ it("should generate move operations when children are reordered", () => {
170
+ const oldChild1 = new MockVNode("a");
171
+ const oldChild2 = new MockVNode("b");
172
+ const parent = new MockParentVNode([oldChild1, oldChild2]);
173
+ // Swap order: b, a
174
+ const newChild1 = new MockVNode("b");
175
+ const newChild2 = new MockVNode("a");
176
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2]));
177
+ // Both should be patched
178
+ expect(oldChild1.patchedWith).toBe(newChild2);
179
+ expect(oldChild2.patchedWith).toBe(newChild1);
180
+ // No unmounts
181
+ expect(oldChild1.unmountCalls).toBe(0);
182
+ expect(oldChild2.unmountCalls).toBe(0);
183
+ // Result should be old children in new order [b, a]
184
+ expect(children).toEqual([oldChild2, oldChild1]);
185
+ // Should have move or insert operations for repositioning
186
+ expect(operations.length).toBeGreaterThan(0);
187
+ const hasMoveOrInsert = operations.some((op) => op.type === "move" || op.type === "insert");
188
+ expect(hasMoveOrInsert).toBe(true);
189
+ // If there's a move operation, it should have the required properties
190
+ const moveOps = operations.filter((op) => op.type === "move");
191
+ moveOps.forEach((op) => {
192
+ if (op.type === "move") {
193
+ expect(op).toHaveProperty("elms");
194
+ expect(op).toHaveProperty("afterElm");
195
+ }
196
+ });
146
197
  });
147
198
  it("should replace all children when all keys change", () => {
148
199
  const oldChild1 = new MockVNode("a");
@@ -150,7 +201,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
150
201
  const parent = new MockParentVNode([oldChild1, oldChild2]);
151
202
  const newChild1 = new MockVNode("x");
152
203
  const newChild2 = new MockVNode("y");
153
- const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
204
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2]));
154
205
  // All new children should be mounted
155
206
  expect(newChild1.mountCalls).toBe(1);
156
207
  expect(newChild2.mountCalls).toBe(1);
@@ -158,7 +209,11 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
158
209
  expect(oldChild1.unmountCalls).toBe(1);
159
210
  expect(oldChild2.unmountCalls).toBe(1);
160
211
  // Result should be the new children
161
- expect(result).toEqual([newChild1, newChild2]);
212
+ expect(children).toEqual([newChild1, newChild2]);
213
+ // Should have operations for both insert and remove
214
+ expect(operations.length).toBeGreaterThan(0);
215
+ expect(operations.some((op) => op.type === "insert")).toBe(true);
216
+ expect(operations.some((op) => op.type === "remove")).toBe(true);
162
217
  });
163
218
  });
164
219
  describe("Patching without keys (index-based)", () => {
@@ -170,7 +225,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
170
225
  const newChild1 = new MockVNode();
171
226
  const newChild2 = new MockVNode();
172
227
  const newChild3 = new MockVNode();
173
- const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
228
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
174
229
  // Should patch by index: 0->0, 1->1, 2->2
175
230
  expect(oldChild1.patchedWith).toBe(newChild1);
176
231
  expect(oldChild2.patchedWith).toBe(newChild2);
@@ -180,7 +235,9 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
180
235
  expect(oldChild2.unmountCalls).toBe(0);
181
236
  expect(oldChild3.unmountCalls).toBe(0);
182
237
  // Result should be old children (reused)
183
- expect(result).toEqual([oldChild1, oldChild2, oldChild3]);
238
+ expect(children).toEqual([oldChild1, oldChild2, oldChild3]);
239
+ // Should have no operations (just patching)
240
+ expect(operations).toHaveLength(0);
184
241
  });
185
242
  it("should mount new children when growing without keys", () => {
186
243
  const oldChild1 = new MockVNode();
@@ -190,7 +247,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
190
247
  const newChild2 = new MockVNode();
191
248
  const newChild3 = new MockVNode();
192
249
  const newChild4 = new MockVNode();
193
- const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3, newChild4]));
250
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3, newChild4]));
194
251
  // First two should be patched (reused)
195
252
  expect(oldChild1.patchedWith).toBe(newChild1);
196
253
  expect(oldChild2.patchedWith).toBe(newChild2);
@@ -201,7 +258,10 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
201
258
  expect(oldChild1.unmountCalls).toBe(0);
202
259
  expect(oldChild2.unmountCalls).toBe(0);
203
260
  // Result should be [oldChild1, oldChild2, newChild3, newChild4]
204
- expect(result).toEqual([oldChild1, oldChild2, newChild3, newChild4]);
261
+ expect(children).toEqual([oldChild1, oldChild2, newChild3, newChild4]);
262
+ // Should have insert operation
263
+ expect(operations.length).toBeGreaterThan(0);
264
+ expect(operations.some((op) => op.type === "insert")).toBe(true);
205
265
  });
206
266
  it("should unmount old children when shrinking without keys", () => {
207
267
  const oldChild1 = new MockVNode();
@@ -216,7 +276,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
216
276
  ]);
217
277
  const newChild1 = new MockVNode();
218
278
  const newChild2 = new MockVNode();
219
- const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
279
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2]));
220
280
  // First two should be patched
221
281
  expect(oldChild1.patchedWith).toBe(newChild1);
222
282
  expect(oldChild2.patchedWith).toBe(newChild2);
@@ -227,7 +287,10 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
227
287
  expect(oldChild1.unmountCalls).toBe(0);
228
288
  expect(oldChild2.unmountCalls).toBe(0);
229
289
  // Result should be [oldChild1, oldChild2]
230
- expect(result).toEqual([oldChild1, oldChild2]);
290
+ expect(children).toEqual([oldChild1, oldChild2]);
291
+ // Should have remove operations
292
+ expect(operations.length).toBeGreaterThan(0);
293
+ expect(operations.some((op) => op.type === "remove")).toBe(true);
231
294
  });
232
295
  });
233
296
  describe("Mixed keys and indices", () => {
@@ -239,7 +302,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
239
302
  const newChild1 = new MockVNode("a"); // key: "a"
240
303
  const newChild2 = new MockVNode(); // key: undefined -> index 1
241
304
  const newChild3 = new MockVNode("c"); // key: "c"
242
- const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
305
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
243
306
  // Keyed children should patch by key
244
307
  expect(oldChild1.patchedWith).toBe(newChild1);
245
308
  expect(oldChild3.patchedWith).toBe(newChild3);
@@ -250,7 +313,9 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
250
313
  expect(oldChild2.unmountCalls).toBe(0);
251
314
  expect(oldChild3.unmountCalls).toBe(0);
252
315
  // Result should be old children (reused)
253
- expect(result).toEqual([oldChild1, oldChild2, oldChild3]);
316
+ expect(children).toEqual([oldChild1, oldChild2, oldChild3]);
317
+ // Should have no operations (just patching)
318
+ expect(operations).toHaveLength(0);
254
319
  });
255
320
  });
256
321
  describe("Real-world scenarios", () => {
@@ -259,7 +324,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
259
324
  const parent = new MockParentVNode([oldChild1]);
260
325
  const newChild1 = new MockVNode("title");
261
326
  const newChild2 = new MockVNode("details");
262
- const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
327
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2]));
263
328
  // Title should be patched (reused)
264
329
  expect(oldChild1.patchedWith).toBe(newChild1);
265
330
  // Details should be mounted
@@ -267,14 +332,17 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
267
332
  // No unmounts
268
333
  expect(oldChild1.unmountCalls).toBe(0);
269
334
  // Result should be [oldChild1, newChild2]
270
- expect(result).toEqual([oldChild1, newChild2]);
335
+ expect(children).toEqual([oldChild1, newChild2]);
336
+ // Should have insert operation
337
+ expect(operations.length).toBeGreaterThan(0);
338
+ expect(operations.some((op) => op.type === "insert")).toBe(true);
271
339
  });
272
340
  it("should handle conditional rendering (component -> null)", () => {
273
341
  const oldChild1 = new MockVNode("title");
274
342
  const oldChild2 = new MockVNode("details");
275
343
  const parent = new MockParentVNode([oldChild1, oldChild2]);
276
344
  const newChild1 = new MockVNode("title");
277
- const result = parent.patchChildren(toVNodes([newChild1]));
345
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1]));
278
346
  // Title should be patched (reused)
279
347
  expect(oldChild1.patchedWith).toBe(newChild1);
280
348
  // Details should be unmounted
@@ -282,7 +350,10 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
282
350
  // Title should not be unmounted
283
351
  expect(oldChild1.unmountCalls).toBe(0);
284
352
  // Result should be [oldChild1]
285
- expect(result).toEqual([oldChild1]);
353
+ expect(children).toEqual([oldChild1]);
354
+ // Should have remove operation
355
+ expect(operations.length).toBeGreaterThan(0);
356
+ expect(operations.some((op) => op.type === "remove")).toBe(true);
286
357
  });
287
358
  it("should handle list with items added at beginning", () => {
288
359
  const oldChild1 = new MockVNode("item-1");
@@ -291,7 +362,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
291
362
  const newChild1 = new MockVNode("item-0"); // New item at start
292
363
  const newChild2 = new MockVNode("item-1");
293
364
  const newChild3 = new MockVNode("item-2");
294
- const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
365
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
295
366
  // New item should be mounted
296
367
  expect(newChild1.mountCalls).toBe(1);
297
368
  // Existing items should be patched (reused)
@@ -301,7 +372,10 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
301
372
  expect(oldChild1.unmountCalls).toBe(0);
302
373
  expect(oldChild2.unmountCalls).toBe(0);
303
374
  // Result should be [newChild1, oldChild1, oldChild2]
304
- expect(result).toEqual([newChild1, oldChild1, oldChild2]);
375
+ expect(children).toEqual([newChild1, oldChild1, oldChild2]);
376
+ // Should have insert operation and possibly move operations for shifted items
377
+ expect(operations.length).toBeGreaterThan(0);
378
+ expect(operations.some((op) => op.type === "insert")).toBe(true);
305
379
  });
306
380
  it("should handle list with items added at end", () => {
307
381
  const oldChild1 = new MockVNode("item-1");
@@ -310,7 +384,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
310
384
  const newChild1 = new MockVNode("item-1");
311
385
  const newChild2 = new MockVNode("item-2");
312
386
  const newChild3 = new MockVNode("item-3"); // New item at end
313
- const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
387
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
314
388
  // Existing items should be patched (reused)
315
389
  expect(oldChild1.patchedWith).toBe(newChild1);
316
390
  expect(oldChild2.patchedWith).toBe(newChild2);
@@ -320,7 +394,10 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
320
394
  expect(oldChild1.unmountCalls).toBe(0);
321
395
  expect(oldChild2.unmountCalls).toBe(0);
322
396
  // Result should be [oldChild1, oldChild2, newChild3]
323
- expect(result).toEqual([oldChild1, oldChild2, newChild3]);
397
+ expect(children).toEqual([oldChild1, oldChild2, newChild3]);
398
+ // Should have insert operation
399
+ expect(operations.length).toBeGreaterThan(0);
400
+ expect(operations.some((op) => op.type === "insert")).toBe(true);
324
401
  });
325
402
  it("should handle list with item removed from middle", () => {
326
403
  const oldChild1 = new MockVNode("item-1");
@@ -329,7 +406,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
329
406
  const parent = new MockParentVNode([oldChild1, oldChild2, oldChild3]);
330
407
  const newChild1 = new MockVNode("item-1");
331
408
  const newChild2 = new MockVNode("item-3"); // item-2 removed
332
- const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
409
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2]));
333
410
  // item-1 and item-3 should be patched (reused)
334
411
  expect(oldChild1.patchedWith).toBe(newChild1);
335
412
  expect(oldChild3.patchedWith).toBe(newChild2);
@@ -339,33 +416,42 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
339
416
  expect(oldChild1.unmountCalls).toBe(0);
340
417
  expect(oldChild3.unmountCalls).toBe(0);
341
418
  // Result should be [oldChild1, oldChild3]
342
- expect(result).toEqual([oldChild1, oldChild3]);
419
+ expect(children).toEqual([oldChild1, oldChild3]);
420
+ // Should have remove operation
421
+ expect(operations.length).toBeGreaterThan(0);
422
+ expect(operations.some((op) => op.type === "remove")).toBe(true);
343
423
  });
344
424
  it("should handle empty -> multiple children", () => {
345
425
  const parent = new MockParentVNode([]);
346
426
  const newChild1 = new MockVNode("a");
347
427
  const newChild2 = new MockVNode("b");
348
428
  const newChild3 = new MockVNode("c");
349
- const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
429
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
350
430
  // All should be mounted
351
431
  expect(newChild1.mountCalls).toBe(1);
352
432
  expect(newChild2.mountCalls).toBe(1);
353
433
  expect(newChild3.mountCalls).toBe(1);
354
434
  // Result should be the new children
355
- expect(result).toEqual([newChild1, newChild2, newChild3]);
435
+ expect(children).toEqual([newChild1, newChild2, newChild3]);
436
+ // Should have insert operation
437
+ expect(operations).toHaveLength(1);
438
+ expect(operations[0].type).toBe("insert");
356
439
  });
357
440
  it("should handle multiple children -> empty", () => {
358
441
  const oldChild1 = new MockVNode("a");
359
442
  const oldChild2 = new MockVNode("b");
360
443
  const oldChild3 = new MockVNode("c");
361
444
  const parent = new MockParentVNode([oldChild1, oldChild2, oldChild3]);
362
- const result = parent.patchChildren(toVNodes([]));
445
+ const { children, operations } = parent.patchChildren(toVNodes([]));
363
446
  // All should be unmounted
364
447
  expect(oldChild1.unmountCalls).toBe(1);
365
448
  expect(oldChild2.unmountCalls).toBe(1);
366
449
  expect(oldChild3.unmountCalls).toBe(1);
367
450
  // Result should be empty
368
- expect(result).toEqual([]);
451
+ expect(children).toEqual([]);
452
+ // Should have remove operation
453
+ expect(operations).toHaveLength(1);
454
+ expect(operations[0].type).toBe("remove");
369
455
  });
370
456
  });
371
457
  describe("Object reference preservation", () => {
@@ -375,13 +461,66 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
375
461
  const parent = new MockParentVNode([oldChild1, oldChild2]);
376
462
  const newChild1 = new MockVNode("a");
377
463
  const newChild2 = new MockVNode("b");
378
- const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
464
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2]));
379
465
  // The result should contain the EXACT SAME object references as the old children
380
- expect(result[0]).toBe(oldChild1); // Same object reference
381
- expect(result[1]).toBe(oldChild2); // Same object reference
466
+ expect(children[0]).toBe(oldChild1); // Same object reference
467
+ expect(children[1]).toBe(oldChild2); // Same object reference
382
468
  // NOT the new children
383
- expect(result[0]).not.toBe(newChild1);
384
- expect(result[1]).not.toBe(newChild2);
469
+ expect(children[0]).not.toBe(newChild1);
470
+ expect(children[1]).not.toBe(newChild2);
471
+ // Should have no operations (just patching)
472
+ expect(operations).toHaveLength(0);
473
+ });
474
+ });
475
+ describe("Move operations - minimal reproduction", () => {
476
+ it("should correctly reorder [A,B,C] to [C,A,B] in actual DOM", () => {
477
+ // This test uses actual DOM to verify move operations work correctly
478
+ const parent = document.createElement("div");
479
+ // Create initial DOM elements
480
+ const elmA = document.createTextNode("A");
481
+ const elmB = document.createTextNode("B");
482
+ const elmC = document.createTextNode("C");
483
+ parent.appendChild(elmA);
484
+ parent.appendChild(elmB);
485
+ parent.appendChild(elmC);
486
+ // Create VNodes that represent these elements
487
+ const oldChild1 = new MockVNode("a");
488
+ const oldChild2 = new MockVNode("b");
489
+ const oldChild3 = new MockVNode("c");
490
+ oldChild1.elm = elmA;
491
+ oldChild2.elm = elmB;
492
+ oldChild3.elm = elmC;
493
+ const parentVNode = new MockParentVNode([
494
+ oldChild1,
495
+ oldChild2,
496
+ oldChild3,
497
+ ]);
498
+ parentVNode.elm = parent;
499
+ // Verify initial DOM order
500
+ expect(Array.from(parent.childNodes).map((n) => n.textContent)).toEqual([
501
+ "A",
502
+ "B",
503
+ "C",
504
+ ]);
505
+ // Reorder to C, A, B
506
+ const newChild1 = new MockVNode("c");
507
+ const newChild2 = new MockVNode("a");
508
+ const newChild3 = new MockVNode("b");
509
+ const { children, operations } = parentVNode.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
510
+ // Result should be old children in new order
511
+ expect(children).toEqual([oldChild3, oldChild1, oldChild2]);
512
+ // Apply the operations to the actual DOM
513
+ parentVNode.applyDOMOperations(operations);
514
+ // Verify DOM is correctly reordered
515
+ expect(Array.from(parent.childNodes).map((n) => n.textContent)).toEqual([
516
+ "C",
517
+ "A",
518
+ "B",
519
+ ]);
520
+ // Verify same DOM elements were moved, not recreated
521
+ expect(parent.childNodes[0]).toBe(elmC);
522
+ expect(parent.childNodes[1]).toBe(elmA);
523
+ expect(parent.childNodes[2]).toBe(elmB);
385
524
  });
386
525
  });
387
526
  });
@@ -1,5 +1,21 @@
1
1
  import { RootVNode } from "./RootVNode";
2
2
  import { VNode } from "./types";
3
+ export type DOMOperation = {
4
+ type: "replace";
5
+ oldElms: Node[];
6
+ newElms: Node[];
7
+ } | {
8
+ type: "remove";
9
+ elms: Node[];
10
+ } | {
11
+ type: "insert";
12
+ elms: Node[];
13
+ afterElm?: Node;
14
+ } | {
15
+ type: "move";
16
+ elms: Node[];
17
+ afterElm?: Node;
18
+ };
3
19
  export declare abstract class AbstractVNode {
4
20
  key?: string;
5
21
  parent?: VNode;
@@ -9,7 +25,7 @@ export declare abstract class AbstractVNode {
9
25
  abstract mount(parent?: VNode): Node | Node[];
10
26
  abstract patch(oldNode: VNode): void;
11
27
  abstract unmount(): void;
12
- abstract rerender(): void;
28
+ applyDOMOperations(operations: DOMOperation[], atVNode?: VNode): void;
13
29
  protected getHTMLElement(): HTMLElement;
14
30
  /**
15
31
  * A VNode can represent multiple elements (fragment of component)
@@ -17,6 +33,9 @@ export declare abstract class AbstractVNode {
17
33
  getElements(): Node[];
18
34
  getParentElement(): HTMLElement;
19
35
  protected canPatch(oldNode: VNode, newNode: VNode): boolean;
20
- patchChildren(newChildren: VNode[]): VNode[];
36
+ patchChildren(newChildren: VNode[]): {
37
+ children: VNode[];
38
+ operations: DOMOperation[];
39
+ };
21
40
  }
22
41
  //# sourceMappingURL=AbstractVNode.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AbstractVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/AbstractVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,8BAAsB,aAAa;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,GAAG,CAAC,EAAE,IAAI,CAAC;IACX,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAC;IACnB,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI,GAAG,IAAI,EAAE;IAC7C,QAAQ,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,GAAG,IAAI;IACpC,QAAQ,CAAC,OAAO,IAAI,IAAI;IACxB,QAAQ,CAAC,QAAQ,IAAI,IAAI;IACzB,SAAS,CAAC,cAAc;IAOxB;;OAEG;IACH,WAAW,IAAI,IAAI,EAAE;IAWrB,gBAAgB,IAAI,WAAW;IAiB/B,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,GAAG,OAAO;IAoB3D,aAAa,CAAC,WAAW,EAAE,KAAK,EAAE,GAAG,KAAK,EAAE;CA2D7C"}
1
+ {"version":3,"file":"AbstractVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/AbstractVNode.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,MAAM,MAAM,YAAY,GACpB;IACE,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,IAAI,EAAE,CAAC;IAChB,OAAO,EAAE,IAAI,EAAE,CAAC;CACjB,GACD;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,IAAI,EAAE,CAAC;CACd,GACD;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,IAAI,EAAE,CAAC;IACb,QAAQ,CAAC,EAAE,IAAI,CAAC;CACjB,GACD;IACE,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,IAAI,EAAE,CAAC;IACb,QAAQ,CAAC,EAAE,IAAI,CAAC;CACjB,CAAC;AAEN,8BAAsB,aAAa;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,GAAG,CAAC,EAAE,IAAI,CAAC;IACX,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAC;IACnB,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI,GAAG,IAAI,EAAE;IAC7C,QAAQ,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,GAAG,IAAI;IACpC,QAAQ,CAAC,OAAO,IAAI,IAAI;IACxB,kBAAkB,CAAC,UAAU,EAAE,YAAY,EAAE,EAAE,OAAO,CAAC,EAAE,KAAK,GAAG,IAAI;IAkErE,SAAS,CAAC,cAAc;IAOxB;;OAEG;IACH,WAAW,IAAI,IAAI,EAAE;IAarB,gBAAgB,IAAI,WAAW;IAiB/B,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,GAAG,OAAO;IAoB3D,aAAa,CAAC,WAAW,EAAE,KAAK,EAAE,GAAG;QACnC,QAAQ,EAAE,KAAK,EAAE,CAAC;QAClB,UAAU,EAAE,YAAY,EAAE,CAAC;KAC5B;CAuJF"}