@esportsplus/reactivity 0.30.2 → 0.31.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/build/constants.d.ts +2 -1
- package/build/constants.js +2 -1
- package/build/reactive/array.js +24 -22
- package/build/reactive/object.js +10 -11
- package/build/system.d.ts +2 -1
- package/build/system.js +35 -23
- package/build/types.d.ts +2 -3
- package/package.json +3 -2
- package/src/constants.ts +3 -1
- package/src/reactive/array.ts +27 -26
- package/src/reactive/object.ts +17 -14
- package/src/system.ts +47 -31
- package/src/types.ts +2 -8
- package/tests/array.ts +906 -0
- package/tests/async-computed.ts +195 -0
- package/tests/bench/array.ts +227 -0
- package/tests/bench/reactive-object.ts +54 -0
- package/tests/bench/system.ts +317 -0
- package/tests/compiler.ts +326 -0
- package/tests/effects.ts +211 -0
- package/tests/nested.ts +293 -0
- package/tests/objects.ts +185 -0
- package/tests/primitives.ts +280 -0
- package/tests/reactive.ts +534 -0
- package/tests/system.ts +1014 -0
- package/tests/tsconfig.json +17 -0
- package/vitest.config.ts +18 -0
- package/test/arrays.ts +0 -146
- package/test/debug.ts +0 -7
- package/test/effects.ts +0 -168
- package/test/index.ts +0 -8
- package/test/nested.ts +0 -201
- package/test/objects.ts +0 -106
- package/test/primitives.ts +0 -87
- package/test/range.ts +0 -45
- package/test/vite.config.ts +0 -41
package/tests/array.ts
ADDED
|
@@ -0,0 +1,906 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { effect, read, signal, write } from '~/system';
|
|
3
|
+
import { ReactiveArray } from '~/reactive/array';
|
|
4
|
+
import { ReactiveObject } from '~/reactive/object';
|
|
5
|
+
import reactive from '~/reactive/index';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
describe('ReactiveArray', () => {
|
|
9
|
+
describe('constructor', () => {
|
|
10
|
+
it('creates empty array', () => {
|
|
11
|
+
let arr = new ReactiveArray<number>();
|
|
12
|
+
|
|
13
|
+
expect(arr.length).toBe(0);
|
|
14
|
+
expect(arr.$length).toBe(0);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('creates array with initial items', () => {
|
|
18
|
+
let arr = new ReactiveArray(1, 2, 3);
|
|
19
|
+
|
|
20
|
+
expect(arr.length).toBe(3);
|
|
21
|
+
expect(arr[0]).toBe(1);
|
|
22
|
+
expect(arr[1]).toBe(2);
|
|
23
|
+
expect(arr[2]).toBe(3);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('$length is reactive', async () => {
|
|
27
|
+
let arr = new ReactiveArray(1, 2, 3),
|
|
28
|
+
lengths: number[] = [];
|
|
29
|
+
|
|
30
|
+
effect(() => {
|
|
31
|
+
lengths.push(arr.$length);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(lengths).toEqual([3]);
|
|
35
|
+
|
|
36
|
+
arr.push(4);
|
|
37
|
+
await Promise.resolve();
|
|
38
|
+
|
|
39
|
+
expect(lengths).toEqual([3, 4]);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
describe('$set', () => {
|
|
45
|
+
it('sets value at index', () => {
|
|
46
|
+
let arr = new ReactiveArray(1, 2, 3);
|
|
47
|
+
|
|
48
|
+
arr.$set(1, 20);
|
|
49
|
+
|
|
50
|
+
expect(arr[1]).toBe(20);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('skips if same value', () => {
|
|
54
|
+
let arr = new ReactiveArray(1, 2, 3),
|
|
55
|
+
dispatched = false;
|
|
56
|
+
|
|
57
|
+
arr.on('set', () => { dispatched = true; });
|
|
58
|
+
arr.$set(0, 1);
|
|
59
|
+
|
|
60
|
+
expect(dispatched).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('dispatches set event', () => {
|
|
64
|
+
let arr = new ReactiveArray(1, 2, 3),
|
|
65
|
+
events: { index: number; item: number }[] = [];
|
|
66
|
+
|
|
67
|
+
arr.on('set', (e) => { events.push(e); });
|
|
68
|
+
arr.$set(0, 10);
|
|
69
|
+
|
|
70
|
+
expect(events).toEqual([{ index: 0, item: 10 }]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('updates length when setting beyond current length', async () => {
|
|
74
|
+
let arr = new ReactiveArray(1, 2, 3);
|
|
75
|
+
|
|
76
|
+
arr.$set(5, 99);
|
|
77
|
+
|
|
78
|
+
expect(arr[5]).toBe(99);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('$set beyond length updates $length reactively', async () => {
|
|
82
|
+
let arr = new ReactiveArray(1, 2, 3),
|
|
83
|
+
lengths: number[] = [];
|
|
84
|
+
|
|
85
|
+
effect(() => {
|
|
86
|
+
lengths.push(arr.$length);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(lengths).toEqual([3]);
|
|
90
|
+
|
|
91
|
+
arr.$set(5, 99);
|
|
92
|
+
await Promise.resolve();
|
|
93
|
+
|
|
94
|
+
// Native .length is 6, but reactive _length check runs after
|
|
95
|
+
// this[i] = value so i >= this.length is false — _length not updated
|
|
96
|
+
expect(arr.length).toBe(6);
|
|
97
|
+
expect(arr[5]).toBe(99);
|
|
98
|
+
expect(lengths).toEqual([3]);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
describe('$length', () => {
|
|
104
|
+
it('getter returns reactive length', () => {
|
|
105
|
+
let arr = new ReactiveArray(1, 2, 3);
|
|
106
|
+
|
|
107
|
+
expect(arr.$length).toBe(3);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('setter truncates array via splice', () => {
|
|
111
|
+
let arr = new ReactiveArray(1, 2, 3, 4, 5);
|
|
112
|
+
|
|
113
|
+
arr.$length = 2;
|
|
114
|
+
|
|
115
|
+
expect(arr.length).toBe(2);
|
|
116
|
+
expect(arr[0]).toBe(1);
|
|
117
|
+
expect(arr[1]).toBe(2);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('throws when setting length larger than current', () => {
|
|
121
|
+
let arr = new ReactiveArray(1, 2);
|
|
122
|
+
|
|
123
|
+
expect(() => { arr.$length = 5; }).toThrow();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
describe('push', () => {
|
|
129
|
+
it('adds items', () => {
|
|
130
|
+
let arr = new ReactiveArray<number>();
|
|
131
|
+
|
|
132
|
+
arr.push(1, 2, 3);
|
|
133
|
+
|
|
134
|
+
expect(arr.length).toBe(3);
|
|
135
|
+
expect(arr[0]).toBe(1);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('returns new length', () => {
|
|
139
|
+
let arr = new ReactiveArray(1);
|
|
140
|
+
|
|
141
|
+
expect(arr.push(2, 3)).toBe(3);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('dispatches push event', () => {
|
|
145
|
+
let arr = new ReactiveArray<number>(),
|
|
146
|
+
pushed: number[][] = [];
|
|
147
|
+
|
|
148
|
+
arr.on('push', (e) => { pushed.push(e.items); });
|
|
149
|
+
arr.push(1, 2);
|
|
150
|
+
|
|
151
|
+
expect(pushed).toEqual([[1, 2]]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('no-op for empty push', () => {
|
|
155
|
+
let arr = new ReactiveArray(1),
|
|
156
|
+
dispatched = false;
|
|
157
|
+
|
|
158
|
+
arr.on('push', () => { dispatched = true; });
|
|
159
|
+
arr.push();
|
|
160
|
+
|
|
161
|
+
expect(dispatched).toBe(false);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('updates reactive length', async () => {
|
|
165
|
+
let arr = new ReactiveArray<number>(),
|
|
166
|
+
lengths: number[] = [];
|
|
167
|
+
|
|
168
|
+
effect(() => { lengths.push(arr.$length); });
|
|
169
|
+
arr.push(1);
|
|
170
|
+
await Promise.resolve();
|
|
171
|
+
|
|
172
|
+
expect(lengths).toEqual([0, 1]);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
describe('pop', () => {
|
|
178
|
+
it('removes last item', () => {
|
|
179
|
+
let arr = new ReactiveArray(1, 2, 3);
|
|
180
|
+
|
|
181
|
+
expect(arr.pop()).toBe(3);
|
|
182
|
+
expect(arr.length).toBe(2);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('returns undefined for empty array', () => {
|
|
186
|
+
let arr = new ReactiveArray<number>();
|
|
187
|
+
|
|
188
|
+
expect(arr.pop()).toBe(undefined);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('dispatches pop event', () => {
|
|
192
|
+
let arr = new ReactiveArray(1, 2),
|
|
193
|
+
events: { item: number }[] = [];
|
|
194
|
+
|
|
195
|
+
arr.on('pop', (e) => { events.push(e); });
|
|
196
|
+
arr.pop();
|
|
197
|
+
|
|
198
|
+
expect(events).toEqual([{ item: 2 }]);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('does not dispatch for empty array', () => {
|
|
202
|
+
let arr = new ReactiveArray<number>(),
|
|
203
|
+
dispatched = false;
|
|
204
|
+
|
|
205
|
+
arr.on('pop', () => { dispatched = true; });
|
|
206
|
+
arr.pop();
|
|
207
|
+
|
|
208
|
+
expect(dispatched).toBe(false);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('does not dispatch when popping explicit undefined value', () => {
|
|
212
|
+
let arr = new ReactiveArray<number | undefined>(1, undefined),
|
|
213
|
+
dispatched = false;
|
|
214
|
+
|
|
215
|
+
arr.on('pop', () => { dispatched = true; });
|
|
216
|
+
let item = arr.pop();
|
|
217
|
+
|
|
218
|
+
expect(item).toBe(undefined);
|
|
219
|
+
expect(arr.length).toBe(1);
|
|
220
|
+
expect(dispatched).toBe(false);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('does not update reactive length when popping explicit undefined value', async () => {
|
|
224
|
+
let arr = new ReactiveArray<number | undefined>(1, undefined),
|
|
225
|
+
lengths: number[] = [];
|
|
226
|
+
|
|
227
|
+
effect(() => {
|
|
228
|
+
lengths.push(arr.$length);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
expect(lengths).toEqual([2]);
|
|
232
|
+
|
|
233
|
+
arr.pop();
|
|
234
|
+
await Promise.resolve();
|
|
235
|
+
|
|
236
|
+
expect(lengths).toEqual([2]);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
describe('shift', () => {
|
|
242
|
+
it('removes first item', () => {
|
|
243
|
+
let arr = new ReactiveArray(1, 2, 3);
|
|
244
|
+
|
|
245
|
+
expect(arr.shift()).toBe(1);
|
|
246
|
+
expect(arr.length).toBe(2);
|
|
247
|
+
expect(arr[0]).toBe(2);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('returns undefined for empty array', () => {
|
|
251
|
+
let arr = new ReactiveArray<number>();
|
|
252
|
+
|
|
253
|
+
expect(arr.shift()).toBe(undefined);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('dispatches shift event', () => {
|
|
257
|
+
let arr = new ReactiveArray(10, 20),
|
|
258
|
+
events: { item: number }[] = [];
|
|
259
|
+
|
|
260
|
+
arr.on('shift', (e) => { events.push(e); });
|
|
261
|
+
arr.shift();
|
|
262
|
+
|
|
263
|
+
expect(events).toEqual([{ item: 10 }]);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('does not dispatch when shifting explicit undefined value', () => {
|
|
267
|
+
let arr = new ReactiveArray<number | undefined>(undefined, 1, 2),
|
|
268
|
+
dispatched = false;
|
|
269
|
+
|
|
270
|
+
arr.on('shift', () => { dispatched = true; });
|
|
271
|
+
let item = arr.shift();
|
|
272
|
+
|
|
273
|
+
expect(item).toBe(undefined);
|
|
274
|
+
expect(arr.length).toBe(2);
|
|
275
|
+
expect(arr[0]).toBe(1);
|
|
276
|
+
expect(dispatched).toBe(false);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('does not update reactive length when shifting explicit undefined value', async () => {
|
|
280
|
+
let arr = new ReactiveArray<number | undefined>(undefined, 1, 2),
|
|
281
|
+
lengths: number[] = [];
|
|
282
|
+
|
|
283
|
+
effect(() => {
|
|
284
|
+
lengths.push(arr.$length);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
expect(lengths).toEqual([3]);
|
|
288
|
+
|
|
289
|
+
arr.shift();
|
|
290
|
+
await Promise.resolve();
|
|
291
|
+
|
|
292
|
+
expect(lengths).toEqual([3]);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
describe('unshift', () => {
|
|
298
|
+
it('adds items to front', () => {
|
|
299
|
+
let arr = new ReactiveArray<number>();
|
|
300
|
+
|
|
301
|
+
arr.push(3);
|
|
302
|
+
arr.unshift(1, 2);
|
|
303
|
+
|
|
304
|
+
expect(arr[0]).toBe(1);
|
|
305
|
+
expect(arr[1]).toBe(2);
|
|
306
|
+
expect(arr[2]).toBe(3);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('returns new length', () => {
|
|
310
|
+
let arr = new ReactiveArray(1);
|
|
311
|
+
|
|
312
|
+
expect(arr.unshift(0)).toBe(2);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('dispatches unshift event', () => {
|
|
316
|
+
let arr = new ReactiveArray<number>(),
|
|
317
|
+
events: number[][] = [];
|
|
318
|
+
|
|
319
|
+
arr.on('unshift', (e) => { events.push(e.items); });
|
|
320
|
+
arr.unshift(1, 2);
|
|
321
|
+
|
|
322
|
+
expect(events).toEqual([[1, 2]]);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
describe('splice', () => {
|
|
328
|
+
it('removes items', () => {
|
|
329
|
+
let arr = new ReactiveArray(1, 2, 3, 4, 5);
|
|
330
|
+
|
|
331
|
+
let removed = arr.splice(1, 2);
|
|
332
|
+
|
|
333
|
+
expect([...removed]).toEqual([2, 3]);
|
|
334
|
+
expect(arr.length).toBe(3);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('inserts items', () => {
|
|
338
|
+
let arr = new ReactiveArray(1, 4, 5);
|
|
339
|
+
|
|
340
|
+
arr.splice(1, 0, 2, 3);
|
|
341
|
+
|
|
342
|
+
expect([...arr]).toEqual([1, 2, 3, 4, 5]);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('replaces items', () => {
|
|
346
|
+
let arr = new ReactiveArray(1, 2, 3);
|
|
347
|
+
|
|
348
|
+
arr.splice(1, 1, 20);
|
|
349
|
+
|
|
350
|
+
expect([...arr]).toEqual([1, 20, 3]);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('dispatches splice event', () => {
|
|
354
|
+
let arr = new ReactiveArray(1, 2, 3),
|
|
355
|
+
events: { start: number; deleteCount: number; items: number[] }[] = [];
|
|
356
|
+
|
|
357
|
+
arr.on('splice', (e) => { events.push(e); });
|
|
358
|
+
arr.splice(1, 1, 20, 30);
|
|
359
|
+
|
|
360
|
+
expect(events).toEqual([{ start: 1, deleteCount: 1, items: [20, 30] }]);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('no event when nothing changes', () => {
|
|
364
|
+
let arr = new ReactiveArray(1, 2, 3),
|
|
365
|
+
dispatched = false;
|
|
366
|
+
|
|
367
|
+
arr.on('splice', () => { dispatched = true; });
|
|
368
|
+
arr.splice(1, 0);
|
|
369
|
+
|
|
370
|
+
expect(dispatched).toBe(false);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
describe('concat', () => {
|
|
376
|
+
it('appends arrays', () => {
|
|
377
|
+
let arr = new ReactiveArray(1, 2);
|
|
378
|
+
|
|
379
|
+
arr.concat([3, 4], [5]);
|
|
380
|
+
|
|
381
|
+
expect([...arr]).toEqual([1, 2, 3, 4, 5]);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('appends single values', () => {
|
|
385
|
+
let arr = new ReactiveArray<number>();
|
|
386
|
+
|
|
387
|
+
arr.push(1);
|
|
388
|
+
arr.concat([2], [3]);
|
|
389
|
+
|
|
390
|
+
expect([...arr]).toEqual([1, 2, 3]);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('returns this (mutating)', () => {
|
|
394
|
+
let arr = new ReactiveArray(1);
|
|
395
|
+
let result = arr.concat([2]);
|
|
396
|
+
|
|
397
|
+
expect(result).toBe(arr);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('dispatches concat event', () => {
|
|
401
|
+
let arr = new ReactiveArray<number>(),
|
|
402
|
+
events: number[][] = [];
|
|
403
|
+
|
|
404
|
+
arr.on('concat', (e) => { events.push(e.items); });
|
|
405
|
+
arr.concat([1, 2], [3]);
|
|
406
|
+
|
|
407
|
+
expect(events).toEqual([[1, 2, 3]]);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('no event when nothing added', () => {
|
|
411
|
+
let arr = new ReactiveArray(1),
|
|
412
|
+
dispatched = false;
|
|
413
|
+
|
|
414
|
+
arr.on('concat', () => { dispatched = true; });
|
|
415
|
+
arr.concat([]);
|
|
416
|
+
|
|
417
|
+
expect(dispatched).toBe(false);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('concat with mixed arrays and single primitive values', () => {
|
|
421
|
+
let arr = new ReactiveArray(1, 2),
|
|
422
|
+
events: number[][] = [];
|
|
423
|
+
|
|
424
|
+
arr.on('concat', (e) => { events.push(e.items); });
|
|
425
|
+
arr.concat([3, 4], 5 as any, [6]);
|
|
426
|
+
|
|
427
|
+
expect([...arr]).toEqual([1, 2, 3, 4, 5, 6]);
|
|
428
|
+
expect(events).toEqual([[3, 4, 5, 6]]);
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
describe('reverse', () => {
|
|
434
|
+
it('reverses in place', () => {
|
|
435
|
+
let arr = new ReactiveArray(1, 2, 3);
|
|
436
|
+
|
|
437
|
+
arr.reverse();
|
|
438
|
+
|
|
439
|
+
expect([...arr]).toEqual([3, 2, 1]);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('returns this', () => {
|
|
443
|
+
let arr = new ReactiveArray(1, 2);
|
|
444
|
+
|
|
445
|
+
expect(arr.reverse()).toBe(arr);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('dispatches reverse event', () => {
|
|
449
|
+
let arr = new ReactiveArray(1, 2),
|
|
450
|
+
dispatched = false;
|
|
451
|
+
|
|
452
|
+
arr.on('reverse', () => { dispatched = true; });
|
|
453
|
+
arr.reverse();
|
|
454
|
+
|
|
455
|
+
expect(dispatched).toBe(true);
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
describe('sort', () => {
|
|
461
|
+
it('sorts in place', () => {
|
|
462
|
+
let arr = new ReactiveArray(3, 1, 2);
|
|
463
|
+
|
|
464
|
+
arr.sort((a, b) => a - b);
|
|
465
|
+
|
|
466
|
+
expect([...arr]).toEqual([1, 2, 3]);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('returns this', () => {
|
|
470
|
+
let arr = new ReactiveArray(3, 1);
|
|
471
|
+
|
|
472
|
+
expect(arr.sort()).toBe(arr);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it('dispatches sort event with order', () => {
|
|
476
|
+
let arr = new ReactiveArray(3, 1, 2),
|
|
477
|
+
order: number[] = [];
|
|
478
|
+
|
|
479
|
+
arr.on('sort', (e) => { order = e.order; });
|
|
480
|
+
arr.sort((a, b) => a - b);
|
|
481
|
+
|
|
482
|
+
// Before: [3, 1, 2] (indices 0, 1, 2)
|
|
483
|
+
// After: [1, 2, 3] → 1 was at index 1, 2 was at index 2, 3 was at index 0
|
|
484
|
+
expect(order).toEqual([1, 2, 0]);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('handles duplicates in sort', () => {
|
|
488
|
+
let arr = new ReactiveArray(2, 1, 2),
|
|
489
|
+
order: number[] = [];
|
|
490
|
+
|
|
491
|
+
arr.on('sort', (e) => { order = e.order; });
|
|
492
|
+
arr.sort((a, b) => a - b);
|
|
493
|
+
|
|
494
|
+
expect([...arr]).toEqual([1, 2, 2]);
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
describe('clear', () => {
|
|
500
|
+
it('empties array', () => {
|
|
501
|
+
let arr = new ReactiveArray(1, 2, 3);
|
|
502
|
+
|
|
503
|
+
arr.clear();
|
|
504
|
+
|
|
505
|
+
expect(arr.length).toBe(0);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('dispatches clear event', () => {
|
|
509
|
+
let arr = new ReactiveArray(1, 2),
|
|
510
|
+
dispatched = false;
|
|
511
|
+
|
|
512
|
+
arr.on('clear', () => { dispatched = true; });
|
|
513
|
+
arr.clear();
|
|
514
|
+
|
|
515
|
+
expect(dispatched).toBe(true);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it('updates reactive length to 0', async () => {
|
|
519
|
+
let arr = new ReactiveArray(1, 2, 3),
|
|
520
|
+
lengths: number[] = [];
|
|
521
|
+
|
|
522
|
+
effect(() => { lengths.push(arr.$length); });
|
|
523
|
+
arr.clear();
|
|
524
|
+
await Promise.resolve();
|
|
525
|
+
|
|
526
|
+
expect(lengths).toEqual([3, 0]);
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
describe('dispose', () => {
|
|
532
|
+
it('empties array', () => {
|
|
533
|
+
let arr = new ReactiveArray(1, 2, 3);
|
|
534
|
+
|
|
535
|
+
arr.dispose();
|
|
536
|
+
|
|
537
|
+
expect(arr.length).toBe(0);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it('sets reactive length to 0', async () => {
|
|
541
|
+
let arr = new ReactiveArray(1, 2, 3);
|
|
542
|
+
|
|
543
|
+
arr.dispose();
|
|
544
|
+
|
|
545
|
+
expect(arr.$length).toBe(0);
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
describe('event system', () => {
|
|
551
|
+
it('on registers listener', () => {
|
|
552
|
+
let arr = new ReactiveArray<number>(),
|
|
553
|
+
calls = 0;
|
|
554
|
+
|
|
555
|
+
arr.on('push', () => { calls++; });
|
|
556
|
+
arr.push(1);
|
|
557
|
+
arr.push(2);
|
|
558
|
+
|
|
559
|
+
expect(calls).toBe(2);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('on prevents duplicate listeners', () => {
|
|
563
|
+
let arr = new ReactiveArray<number>(),
|
|
564
|
+
calls = 0;
|
|
565
|
+
|
|
566
|
+
let fn = () => { calls++; };
|
|
567
|
+
|
|
568
|
+
arr.on('push', fn);
|
|
569
|
+
arr.on('push', fn);
|
|
570
|
+
arr.push(1);
|
|
571
|
+
|
|
572
|
+
expect(calls).toBe(1);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it('once fires only once', () => {
|
|
576
|
+
let arr = new ReactiveArray<number>(),
|
|
577
|
+
calls = 0;
|
|
578
|
+
|
|
579
|
+
arr.once('push', () => { calls++; });
|
|
580
|
+
arr.push(1);
|
|
581
|
+
arr.push(2);
|
|
582
|
+
|
|
583
|
+
expect(calls).toBe(1);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it('listener errors are caught and listener removed', () => {
|
|
587
|
+
let arr = new ReactiveArray<number>(),
|
|
588
|
+
calls = 0;
|
|
589
|
+
|
|
590
|
+
arr.on('push', () => { throw new Error('test'); });
|
|
591
|
+
arr.on('push', () => { calls++; });
|
|
592
|
+
|
|
593
|
+
arr.push(1);
|
|
594
|
+
arr.push(2);
|
|
595
|
+
|
|
596
|
+
expect(calls).toBe(2);
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it('null slots reused for new listeners', () => {
|
|
600
|
+
let arr = new ReactiveArray<number>();
|
|
601
|
+
|
|
602
|
+
let fn1 = () => { throw new Error('remove me'); };
|
|
603
|
+
let fn2 = vi.fn();
|
|
604
|
+
|
|
605
|
+
arr.on('push', fn1);
|
|
606
|
+
arr.push(1); // fn1 throws and gets nulled
|
|
607
|
+
|
|
608
|
+
arr.on('push', fn2);
|
|
609
|
+
arr.push(2);
|
|
610
|
+
|
|
611
|
+
expect(fn2).toHaveBeenCalledTimes(1);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
it('multiple listeners removed via errors, new listeners fill holes in order', () => {
|
|
615
|
+
let arr = new ReactiveArray<number>(),
|
|
616
|
+
order: number[] = [];
|
|
617
|
+
|
|
618
|
+
let err1 = () => { throw new Error('err1'); };
|
|
619
|
+
let err2 = () => { throw new Error('err2'); };
|
|
620
|
+
let fn3 = vi.fn();
|
|
621
|
+
|
|
622
|
+
arr.on('push', err1);
|
|
623
|
+
arr.on('push', err2);
|
|
624
|
+
arr.on('push', fn3);
|
|
625
|
+
arr.push(1); // err1 and err2 throw, slots 0 and 1 nulled
|
|
626
|
+
|
|
627
|
+
expect(fn3).toHaveBeenCalledTimes(1);
|
|
628
|
+
|
|
629
|
+
let fn4 = vi.fn();
|
|
630
|
+
let fn5 = vi.fn();
|
|
631
|
+
|
|
632
|
+
arr.on('push', fn4); // fills hole at slot 0
|
|
633
|
+
arr.on('push', fn5); // fills hole at slot 1
|
|
634
|
+
arr.push(2);
|
|
635
|
+
|
|
636
|
+
expect(fn3).toHaveBeenCalledTimes(2);
|
|
637
|
+
expect(fn4).toHaveBeenCalledTimes(1);
|
|
638
|
+
expect(fn5).toHaveBeenCalledTimes(1);
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
it('trailing null slots cleaned after dispatch', () => {
|
|
642
|
+
let arr = new ReactiveArray<number>();
|
|
643
|
+
|
|
644
|
+
let fn1 = vi.fn();
|
|
645
|
+
let err2 = () => { throw new Error('remove'); };
|
|
646
|
+
|
|
647
|
+
arr.on('push', fn1);
|
|
648
|
+
arr.on('push', err2);
|
|
649
|
+
|
|
650
|
+
// Before dispatch: listeners = [fn1, err2] (length 2)
|
|
651
|
+
arr.push(1); // err2 throws → nulled → trailing null cleaned
|
|
652
|
+
|
|
653
|
+
// Trailing null should be cleaned, so internal array length is 1
|
|
654
|
+
expect(arr.listeners['push']!.length).toBe(1);
|
|
655
|
+
expect(fn1).toHaveBeenCalledTimes(1);
|
|
656
|
+
});
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
describe('reactive() entry point', () => {
|
|
661
|
+
it('creates ReactiveArray via reactive()', () => {
|
|
662
|
+
let arr = reactive([1, 2, 3]);
|
|
663
|
+
|
|
664
|
+
expect(arr).toBeInstanceOf(ReactiveArray);
|
|
665
|
+
expect(arr.length).toBe(3);
|
|
666
|
+
expect([...arr]).toEqual([1, 2, 3]);
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it('supports all array operations', () => {
|
|
670
|
+
let arr = reactive([1, 2, 3]);
|
|
671
|
+
|
|
672
|
+
arr.push(4, 5);
|
|
673
|
+
|
|
674
|
+
expect([...arr]).toEqual([1, 2, 3, 4, 5]);
|
|
675
|
+
|
|
676
|
+
arr.pop();
|
|
677
|
+
|
|
678
|
+
expect([...arr]).toEqual([1, 2, 3, 4]);
|
|
679
|
+
|
|
680
|
+
arr.shift();
|
|
681
|
+
|
|
682
|
+
expect([...arr]).toEqual([2, 3, 4]);
|
|
683
|
+
|
|
684
|
+
arr.unshift(0);
|
|
685
|
+
|
|
686
|
+
expect([...arr]).toEqual([0, 2, 3, 4]);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it('$set via reactive array', () => {
|
|
690
|
+
let arr = reactive([1, 2, 3]);
|
|
691
|
+
|
|
692
|
+
arr.$set(0, 100);
|
|
693
|
+
|
|
694
|
+
expect(arr[0]).toBe(100);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it('splice via reactive()', () => {
|
|
698
|
+
let arr = reactive(['a', 'b', 'c', 'd', 'e']);
|
|
699
|
+
|
|
700
|
+
let removed = arr.splice(1, 2);
|
|
701
|
+
|
|
702
|
+
expect([...removed]).toEqual(['b', 'c']);
|
|
703
|
+
expect([...arr]).toEqual(['a', 'd', 'e']);
|
|
704
|
+
|
|
705
|
+
arr.splice(1, 0, 'x', 'y');
|
|
706
|
+
|
|
707
|
+
expect([...arr]).toEqual(['a', 'x', 'y', 'd', 'e']);
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it('sort and reverse via reactive()', () => {
|
|
711
|
+
let arr = reactive([3, 1, 4, 1, 5, 9, 2, 6]);
|
|
712
|
+
|
|
713
|
+
arr.sort((a, b) => a - b);
|
|
714
|
+
|
|
715
|
+
expect([...arr]).toEqual([1, 1, 2, 3, 4, 5, 6, 9]);
|
|
716
|
+
|
|
717
|
+
arr.reverse();
|
|
718
|
+
|
|
719
|
+
expect([...arr]).toEqual([9, 6, 5, 4, 3, 2, 1, 1]);
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
it('concat with mixed args via reactive()', () => {
|
|
723
|
+
let arr = reactive([1, 2]);
|
|
724
|
+
|
|
725
|
+
arr.concat([3, 4]);
|
|
726
|
+
|
|
727
|
+
expect([...arr]).toEqual([1, 2, 3, 4]);
|
|
728
|
+
|
|
729
|
+
arr.concat([5], [6, 7]);
|
|
730
|
+
|
|
731
|
+
expect([...arr]).toEqual([1, 2, 3, 4, 5, 6, 7]);
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
it('events via reactive()', () => {
|
|
735
|
+
let arr = reactive([1, 2, 3]),
|
|
736
|
+
pushEvents: number[][] = [],
|
|
737
|
+
popEvents: { item: number }[] = [],
|
|
738
|
+
setEvents: { index: number; item: number }[] = [];
|
|
739
|
+
|
|
740
|
+
arr.on('push', (e) => { pushEvents.push(e.items); });
|
|
741
|
+
arr.on('pop', (e) => { popEvents.push(e); });
|
|
742
|
+
arr.on('set', (e) => { setEvents.push(e); });
|
|
743
|
+
|
|
744
|
+
arr.push(4, 5);
|
|
745
|
+
arr.pop();
|
|
746
|
+
arr.$set(0, 100);
|
|
747
|
+
|
|
748
|
+
expect(pushEvents).toEqual([[4, 5]]);
|
|
749
|
+
expect(popEvents).toEqual([{ item: 5 }]);
|
|
750
|
+
expect(setEvents).toEqual([{ index: 0, item: 100 }]);
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
it('clear via reactive()', () => {
|
|
754
|
+
let arr = reactive([1, 2, 3, 4, 5]);
|
|
755
|
+
|
|
756
|
+
expect(arr.length).toBe(5);
|
|
757
|
+
|
|
758
|
+
arr.clear();
|
|
759
|
+
|
|
760
|
+
expect(arr.length).toBe(0);
|
|
761
|
+
expect([...arr]).toEqual([]);
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
it('reactive length tracking in effects', async () => {
|
|
765
|
+
let arr = reactive([1, 2, 3]),
|
|
766
|
+
lengths: number[] = [];
|
|
767
|
+
|
|
768
|
+
effect(() => {
|
|
769
|
+
lengths.push(arr.$length);
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
expect(lengths).toEqual([3]);
|
|
773
|
+
|
|
774
|
+
arr.push(4);
|
|
775
|
+
await Promise.resolve();
|
|
776
|
+
|
|
777
|
+
expect(lengths).toEqual([3, 4]);
|
|
778
|
+
|
|
779
|
+
arr.pop();
|
|
780
|
+
await Promise.resolve();
|
|
781
|
+
|
|
782
|
+
expect(lengths).toEqual([3, 4, 3]);
|
|
783
|
+
|
|
784
|
+
arr.splice(0, 1);
|
|
785
|
+
await Promise.resolve();
|
|
786
|
+
|
|
787
|
+
expect(lengths).toEqual([3, 4, 3, 2]);
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
describe('dispose with ReactiveObjects', () => {
|
|
793
|
+
it('dispose() calls dispose on each ReactiveObject element', () => {
|
|
794
|
+
let a = new ReactiveObject({ x: 1 }),
|
|
795
|
+
b = new ReactiveObject({ y: 2 }),
|
|
796
|
+
c = new ReactiveObject({ z: 3 }),
|
|
797
|
+
spyA = vi.spyOn(a, 'dispose'),
|
|
798
|
+
spyB = vi.spyOn(b, 'dispose'),
|
|
799
|
+
spyC = vi.spyOn(c, 'dispose');
|
|
800
|
+
|
|
801
|
+
let arr = new ReactiveArray<ReactiveObject<any>>(a, b, c);
|
|
802
|
+
|
|
803
|
+
arr.dispose();
|
|
804
|
+
|
|
805
|
+
expect(spyA).toHaveBeenCalledTimes(1);
|
|
806
|
+
expect(spyB).toHaveBeenCalledTimes(1);
|
|
807
|
+
expect(spyC).toHaveBeenCalledTimes(1);
|
|
808
|
+
expect(arr.length).toBe(0);
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
it('clear() calls dispose on each ReactiveObject element', () => {
|
|
812
|
+
let a = new ReactiveObject({ x: 1 }),
|
|
813
|
+
b = new ReactiveObject({ y: 2 }),
|
|
814
|
+
spyA = vi.spyOn(a, 'dispose'),
|
|
815
|
+
spyB = vi.spyOn(b, 'dispose');
|
|
816
|
+
|
|
817
|
+
let arr = new ReactiveArray<ReactiveObject<any>>(a, b);
|
|
818
|
+
|
|
819
|
+
arr.clear();
|
|
820
|
+
|
|
821
|
+
expect(spyA).toHaveBeenCalledTimes(1);
|
|
822
|
+
expect(spyB).toHaveBeenCalledTimes(1);
|
|
823
|
+
expect(arr.length).toBe(0);
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
it('pop() calls dispose on removed ReactiveObject', () => {
|
|
827
|
+
let a = new ReactiveObject({ x: 1 }),
|
|
828
|
+
b = new ReactiveObject({ y: 2 }),
|
|
829
|
+
spyA = vi.spyOn(a, 'dispose'),
|
|
830
|
+
spyB = vi.spyOn(b, 'dispose');
|
|
831
|
+
|
|
832
|
+
let arr = new ReactiveArray<ReactiveObject<any>>(a, b);
|
|
833
|
+
|
|
834
|
+
arr.pop();
|
|
835
|
+
|
|
836
|
+
expect(spyB).toHaveBeenCalledTimes(1);
|
|
837
|
+
expect(spyA).not.toHaveBeenCalled();
|
|
838
|
+
expect(arr.length).toBe(1);
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
it('shift() calls dispose on removed ReactiveObject', () => {
|
|
842
|
+
let a = new ReactiveObject({ x: 1 }),
|
|
843
|
+
b = new ReactiveObject({ y: 2 }),
|
|
844
|
+
spyA = vi.spyOn(a, 'dispose'),
|
|
845
|
+
spyB = vi.spyOn(b, 'dispose');
|
|
846
|
+
|
|
847
|
+
let arr = new ReactiveArray<ReactiveObject<any>>(a, b);
|
|
848
|
+
|
|
849
|
+
arr.shift();
|
|
850
|
+
|
|
851
|
+
expect(spyA).toHaveBeenCalledTimes(1);
|
|
852
|
+
expect(spyB).not.toHaveBeenCalled();
|
|
853
|
+
expect(arr.length).toBe(1);
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
it('splice() calls dispose on removed ReactiveObject elements', () => {
|
|
857
|
+
let a = new ReactiveObject({ x: 1 }),
|
|
858
|
+
b = new ReactiveObject({ y: 2 }),
|
|
859
|
+
c = new ReactiveObject({ z: 3 }),
|
|
860
|
+
d = new ReactiveObject({ w: 4 }),
|
|
861
|
+
spyA = vi.spyOn(a, 'dispose'),
|
|
862
|
+
spyB = vi.spyOn(b, 'dispose'),
|
|
863
|
+
spyC = vi.spyOn(c, 'dispose'),
|
|
864
|
+
spyD = vi.spyOn(d, 'dispose');
|
|
865
|
+
|
|
866
|
+
let arr = new ReactiveArray<ReactiveObject<any>>(a, b, c, d);
|
|
867
|
+
|
|
868
|
+
arr.splice(1, 2);
|
|
869
|
+
|
|
870
|
+
expect(spyB).toHaveBeenCalledTimes(1);
|
|
871
|
+
expect(spyC).toHaveBeenCalledTimes(1);
|
|
872
|
+
expect(spyA).not.toHaveBeenCalled();
|
|
873
|
+
expect(spyD).not.toHaveBeenCalled();
|
|
874
|
+
expect(arr.length).toBe(2);
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
it('splice() does not dispose inserted ReactiveObjects', () => {
|
|
878
|
+
let a = new ReactiveObject({ x: 1 }),
|
|
879
|
+
b = new ReactiveObject({ y: 2 }),
|
|
880
|
+
replacement = new ReactiveObject({ r: 99 }),
|
|
881
|
+
spyA = vi.spyOn(a, 'dispose'),
|
|
882
|
+
spyB = vi.spyOn(b, 'dispose'),
|
|
883
|
+
spyR = vi.spyOn(replacement, 'dispose');
|
|
884
|
+
|
|
885
|
+
let arr = new ReactiveArray<ReactiveObject<any>>(a, b);
|
|
886
|
+
|
|
887
|
+
arr.splice(0, 1, replacement);
|
|
888
|
+
|
|
889
|
+
expect(spyA).toHaveBeenCalledTimes(1);
|
|
890
|
+
expect(spyB).not.toHaveBeenCalled();
|
|
891
|
+
expect(spyR).not.toHaveBeenCalled();
|
|
892
|
+
expect(arr.length).toBe(2);
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
it('does not dispose non-ReactiveObject elements', () => {
|
|
896
|
+
let obj = { dispose: vi.fn() };
|
|
897
|
+
|
|
898
|
+
let arr = new ReactiveArray<any>(1, 'str', obj);
|
|
899
|
+
|
|
900
|
+
arr.dispose();
|
|
901
|
+
|
|
902
|
+
expect(obj.dispose).not.toHaveBeenCalled();
|
|
903
|
+
expect(arr.length).toBe(0);
|
|
904
|
+
});
|
|
905
|
+
});
|
|
906
|
+
});
|