@cplieger/reactive 1.0.1 → 1.0.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.
- package/jsr.json +1 -1
- package/package.json +6 -3
- package/src/invariants.test.ts +0 -483
- package/src/reactive.test.ts +0 -1571
- package/src/redteam.test.ts +0 -552
- package/src/redteam2.test.ts +0 -711
- package/src/redteam3.test.ts +0 -771
- package/src/redteam4.test.ts +0 -410
- package/src/redteam5.test.ts +0 -692
- package/src/redteam6.test.ts +0 -650
- package/src/redteam7.test.ts +0 -658
- package/src/redteam8-adversarial.test.ts +0 -1084
package/src/redteam.test.ts
DELETED
|
@@ -1,552 +0,0 @@
|
|
|
1
|
-
// @vitest-environment happy-dom
|
|
2
|
-
// RED-TEAM adversarial tests probing edge cases
|
|
3
|
-
import { describe, it, expect, vi } from "vitest";
|
|
4
|
-
import {
|
|
5
|
-
signal,
|
|
6
|
-
effect,
|
|
7
|
-
batch,
|
|
8
|
-
computed,
|
|
9
|
-
untracked,
|
|
10
|
-
subscribe,
|
|
11
|
-
setEffectErrorHandler,
|
|
12
|
-
reconcile,
|
|
13
|
-
patch,
|
|
14
|
-
reconcileChildren,
|
|
15
|
-
createStore,
|
|
16
|
-
} from "./index.js";
|
|
17
|
-
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
// Effect disposal edge cases
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
|
|
22
|
-
describe("effect disposal edge cases", () => {
|
|
23
|
-
it("dispose during own execution does not crash", () => {
|
|
24
|
-
const s = signal(0);
|
|
25
|
-
const spy = vi.fn();
|
|
26
|
-
const disposeFn: { current: () => void } = {
|
|
27
|
-
current: () => {
|
|
28
|
-
/* placeholder */
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
disposeFn.current = effect(() => {
|
|
32
|
-
spy(s.value);
|
|
33
|
-
if (s.value === 1) {
|
|
34
|
-
disposeFn.current(); // dispose self mid-run
|
|
35
|
-
}
|
|
36
|
-
return undefined;
|
|
37
|
-
});
|
|
38
|
-
expect(spy).toHaveBeenCalledWith(0);
|
|
39
|
-
s.value = 1; // triggers re-run which disposes
|
|
40
|
-
expect(spy).toHaveBeenCalledWith(1);
|
|
41
|
-
spy.mockClear();
|
|
42
|
-
s.value = 2; // should NOT trigger since disposed
|
|
43
|
-
expect(spy).not.toHaveBeenCalled();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("double dispose does not crash or double-call cleanup", () => {
|
|
47
|
-
const s = signal(0);
|
|
48
|
-
let cleanupCount = 0;
|
|
49
|
-
const dispose = effect(() => {
|
|
50
|
-
void s.value;
|
|
51
|
-
return () => {
|
|
52
|
-
cleanupCount++;
|
|
53
|
-
};
|
|
54
|
-
});
|
|
55
|
-
dispose();
|
|
56
|
-
expect(cleanupCount).toBe(1);
|
|
57
|
-
dispose(); // second dispose
|
|
58
|
-
expect(cleanupCount).toBe(1); // should NOT call cleanup again
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("dispose mid-batch does not crash", () => {
|
|
62
|
-
const s = signal(0);
|
|
63
|
-
const spy = vi.fn();
|
|
64
|
-
const dispose = effect(() => {
|
|
65
|
-
spy(s.value);
|
|
66
|
-
return undefined;
|
|
67
|
-
});
|
|
68
|
-
spy.mockClear();
|
|
69
|
-
batch(() => {
|
|
70
|
-
s.value = 1;
|
|
71
|
-
dispose(); // dispose while batch is pending
|
|
72
|
-
s.value = 2;
|
|
73
|
-
});
|
|
74
|
-
// Effect was disposed mid-batch, should not run
|
|
75
|
-
expect(spy).not.toHaveBeenCalled();
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// ---------------------------------------------------------------------------
|
|
80
|
-
// Re-entrancy: signal write inside effect
|
|
81
|
-
// ---------------------------------------------------------------------------
|
|
82
|
-
|
|
83
|
-
describe("re-entrancy", () => {
|
|
84
|
-
it("signal write inside effect triggers re-run after current flush", () => {
|
|
85
|
-
const s = signal(0);
|
|
86
|
-
const log: number[] = [];
|
|
87
|
-
effect(() => {
|
|
88
|
-
log.push(s.value);
|
|
89
|
-
if (s.value < 3) {
|
|
90
|
-
s.value = s.value + 1; // re-entrant write
|
|
91
|
-
}
|
|
92
|
-
return undefined;
|
|
93
|
-
});
|
|
94
|
-
// Should converge: 0, 1, 2, 3
|
|
95
|
-
expect(log).toEqual([0, 1, 2, 3]);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("signal write inside effect does not cause infinite loop with guard", () => {
|
|
99
|
-
const s = signal(0);
|
|
100
|
-
let runs = 0;
|
|
101
|
-
effect(() => {
|
|
102
|
-
runs++;
|
|
103
|
-
if (s.value < 100) {
|
|
104
|
-
s.value = s.value + 1;
|
|
105
|
-
}
|
|
106
|
-
return undefined;
|
|
107
|
-
});
|
|
108
|
-
expect(runs).toBe(101); // 0..100
|
|
109
|
-
expect(s.peek()).toBe(100);
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
// ---------------------------------------------------------------------------
|
|
114
|
-
// Batch nesting + exceptions mid-batch
|
|
115
|
-
// ---------------------------------------------------------------------------
|
|
116
|
-
|
|
117
|
-
describe("batch nesting + exceptions", () => {
|
|
118
|
-
it("exception mid-batch still flushes pending effects", () => {
|
|
119
|
-
const s = signal(0);
|
|
120
|
-
const spy = vi.fn();
|
|
121
|
-
effect(() => {
|
|
122
|
-
spy(s.value);
|
|
123
|
-
return undefined;
|
|
124
|
-
});
|
|
125
|
-
spy.mockClear();
|
|
126
|
-
expect(() => {
|
|
127
|
-
batch(() => {
|
|
128
|
-
s.value = 42;
|
|
129
|
-
throw new Error("mid-batch");
|
|
130
|
-
});
|
|
131
|
-
}).toThrow("mid-batch");
|
|
132
|
-
// batchDepth should be back to 0 and pending effects flushed
|
|
133
|
-
expect(spy).toHaveBeenCalledWith(42);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("nested batch exception: outer batch still flushes", () => {
|
|
137
|
-
const s = signal(0);
|
|
138
|
-
const spy = vi.fn();
|
|
139
|
-
effect(() => {
|
|
140
|
-
spy(s.value);
|
|
141
|
-
return undefined;
|
|
142
|
-
});
|
|
143
|
-
spy.mockClear();
|
|
144
|
-
batch(() => {
|
|
145
|
-
s.value = 1;
|
|
146
|
-
try {
|
|
147
|
-
batch(() => {
|
|
148
|
-
s.value = 2;
|
|
149
|
-
throw new Error("inner");
|
|
150
|
-
});
|
|
151
|
-
} catch {
|
|
152
|
-
// swallow inner error
|
|
153
|
-
}
|
|
154
|
-
s.value = 3;
|
|
155
|
-
});
|
|
156
|
-
// Should have flushed with final value
|
|
157
|
-
expect(spy).toHaveBeenCalledWith(3);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("batchDepth is not left dirty after exception", () => {
|
|
161
|
-
const s = signal(0);
|
|
162
|
-
const spy = vi.fn();
|
|
163
|
-
effect(() => {
|
|
164
|
-
spy(s.value);
|
|
165
|
-
return undefined;
|
|
166
|
-
});
|
|
167
|
-
spy.mockClear();
|
|
168
|
-
try {
|
|
169
|
-
batch(() => {
|
|
170
|
-
throw new Error("oops");
|
|
171
|
-
});
|
|
172
|
-
} catch {
|
|
173
|
-
// swallow
|
|
174
|
-
}
|
|
175
|
-
// Subsequent writes should flush immediately (not batched)
|
|
176
|
-
s.value = 99;
|
|
177
|
-
expect(spy).toHaveBeenCalledWith(99);
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
// ---------------------------------------------------------------------------
|
|
182
|
-
// Computed error caching invalidation
|
|
183
|
-
// ---------------------------------------------------------------------------
|
|
184
|
-
|
|
185
|
-
describe("computed error caching invalidation", () => {
|
|
186
|
-
it("error is invalidated when dependency changes", () => {
|
|
187
|
-
const s = signal(0);
|
|
188
|
-
let computeCount = 0;
|
|
189
|
-
const c = computed(() => {
|
|
190
|
-
computeCount++;
|
|
191
|
-
if (s.value === 0) {
|
|
192
|
-
throw new Error("zero");
|
|
193
|
-
}
|
|
194
|
-
return s.value;
|
|
195
|
-
});
|
|
196
|
-
expect(() => c.value).toThrow("zero");
|
|
197
|
-
expect(computeCount).toBe(1);
|
|
198
|
-
// Reading again without dep change should use cached error
|
|
199
|
-
expect(() => c.value).toThrow("zero");
|
|
200
|
-
expect(computeCount).toBe(1);
|
|
201
|
-
// Change dep - should recompute
|
|
202
|
-
s.value = 5;
|
|
203
|
-
expect(c.value).toBe(5);
|
|
204
|
-
expect(computeCount).toBe(2);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it("computed error in effect: recovers after dep change", () => {
|
|
208
|
-
const s = signal(0);
|
|
209
|
-
const errors: unknown[] = [];
|
|
210
|
-
const values: unknown[] = [];
|
|
211
|
-
const prev = setEffectErrorHandler((e) => {
|
|
212
|
-
errors.push(e);
|
|
213
|
-
});
|
|
214
|
-
effect(() => {
|
|
215
|
-
values.push(
|
|
216
|
-
computed(() => {
|
|
217
|
-
if (s.value === 0) {
|
|
218
|
-
throw new Error("err");
|
|
219
|
-
}
|
|
220
|
-
return s.value;
|
|
221
|
-
}).value,
|
|
222
|
-
);
|
|
223
|
-
return undefined;
|
|
224
|
-
});
|
|
225
|
-
expect(errors.length).toBe(1);
|
|
226
|
-
s.value = 1;
|
|
227
|
-
// The effect re-runs and creates a new computed each time, so it should work
|
|
228
|
-
expect(values).toContain(1);
|
|
229
|
-
setEffectErrorHandler(prev);
|
|
230
|
-
});
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
// ---------------------------------------------------------------------------
|
|
234
|
-
// Custom equals throwing
|
|
235
|
-
// ---------------------------------------------------------------------------
|
|
236
|
-
|
|
237
|
-
describe("custom equals throwing", () => {
|
|
238
|
-
it("signal: exception in equals propagates to caller", () => {
|
|
239
|
-
const s = signal(1, {
|
|
240
|
-
equals: () => {
|
|
241
|
-
throw new Error("equals boom");
|
|
242
|
-
},
|
|
243
|
-
});
|
|
244
|
-
expect(() => {
|
|
245
|
-
s.value = 2;
|
|
246
|
-
}).toThrow("equals boom");
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
it("computed: exception in equals during execute is caught and downstream still notified", () => {
|
|
250
|
-
const s = signal(1);
|
|
251
|
-
const c = computed(() => s.value, {
|
|
252
|
-
equals: () => {
|
|
253
|
-
throw new Error("eq error");
|
|
254
|
-
},
|
|
255
|
-
});
|
|
256
|
-
// First read works (no equals comparison on first compute via ensureFresh)
|
|
257
|
-
expect(c.value).toBe(1);
|
|
258
|
-
// Subscribe an effect that reads the computed
|
|
259
|
-
const values: number[] = [];
|
|
260
|
-
effect(() => {
|
|
261
|
-
values.push(c.value);
|
|
262
|
-
return undefined;
|
|
263
|
-
});
|
|
264
|
-
expect(values).toEqual([1]);
|
|
265
|
-
// Now change dep - execute() will compare using equals, which throws
|
|
266
|
-
// The equals error is swallowed (treated as "changed"), downstream is notified
|
|
267
|
-
s.value = 2;
|
|
268
|
-
expect(values).toEqual([1, 2]);
|
|
269
|
-
// System should still work after the error
|
|
270
|
-
const s2 = signal(0);
|
|
271
|
-
const spy = vi.fn();
|
|
272
|
-
effect(() => {
|
|
273
|
-
spy(s2.value);
|
|
274
|
-
return undefined;
|
|
275
|
-
});
|
|
276
|
-
spy.mockClear();
|
|
277
|
-
s2.value = 42;
|
|
278
|
-
expect(spy).toHaveBeenCalledWith(42);
|
|
279
|
-
});
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
// ---------------------------------------------------------------------------
|
|
283
|
-
// Memory leaks: disposed effects fully unsubscribe
|
|
284
|
-
// ---------------------------------------------------------------------------
|
|
285
|
-
|
|
286
|
-
describe("memory leaks: disposed effects unsubscribe", () => {
|
|
287
|
-
it("disposed effect is removed from signal subscriber set", () => {
|
|
288
|
-
const s = signal(0);
|
|
289
|
-
const spy = vi.fn();
|
|
290
|
-
const dispose = effect(() => {
|
|
291
|
-
spy(s.value);
|
|
292
|
-
return undefined;
|
|
293
|
-
});
|
|
294
|
-
dispose();
|
|
295
|
-
spy.mockClear();
|
|
296
|
-
s.value = 1;
|
|
297
|
-
expect(spy).not.toHaveBeenCalled();
|
|
298
|
-
// Verify no lingering references by triggering GC-like behavior
|
|
299
|
-
// The signal's subs set should be empty
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
it("disposed subscribe is removed from signal subscriber set", () => {
|
|
303
|
-
const s = signal(0);
|
|
304
|
-
const spy = vi.fn();
|
|
305
|
-
const dispose = subscribe(s, spy);
|
|
306
|
-
dispose();
|
|
307
|
-
spy.mockClear();
|
|
308
|
-
s.value = 1;
|
|
309
|
-
expect(spy).not.toHaveBeenCalled();
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it("computed that loses all subscribers still recomputes on dep change (no stale)", () => {
|
|
313
|
-
const s = signal(1);
|
|
314
|
-
const c = computed(() => s.value * 2);
|
|
315
|
-
const spy = vi.fn();
|
|
316
|
-
const dispose = effect(() => {
|
|
317
|
-
spy(c.value);
|
|
318
|
-
return undefined;
|
|
319
|
-
});
|
|
320
|
-
expect(spy).toHaveBeenCalledWith(2);
|
|
321
|
-
dispose();
|
|
322
|
-
s.value = 5;
|
|
323
|
-
// Reading computed lazily should still give correct value
|
|
324
|
-
expect(c.value).toBe(10);
|
|
325
|
-
});
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
// ---------------------------------------------------------------------------
|
|
329
|
-
// Deep diamond graph
|
|
330
|
-
// ---------------------------------------------------------------------------
|
|
331
|
-
|
|
332
|
-
describe("deep diamond graph", () => {
|
|
333
|
-
it("deep diamond: effect runs once", () => {
|
|
334
|
-
const root = signal(1);
|
|
335
|
-
const a = computed(() => root.value + 1);
|
|
336
|
-
const b = computed(() => root.value + 2);
|
|
337
|
-
const c = computed(() => a.value + b.value);
|
|
338
|
-
const d = computed(() => c.value * 2);
|
|
339
|
-
const spy = vi.fn();
|
|
340
|
-
effect(() => {
|
|
341
|
-
spy(d.value);
|
|
342
|
-
return undefined;
|
|
343
|
-
});
|
|
344
|
-
expect(spy).toHaveBeenCalledWith((1 + 1 + 1 + 2) * 2); // (2+3)*2 = 10
|
|
345
|
-
spy.mockClear();
|
|
346
|
-
root.value = 10;
|
|
347
|
-
// d = (a+b)*2 = ((10+1)+(10+2))*2 = (11+12)*2 = 46
|
|
348
|
-
expect(spy).toHaveBeenCalledTimes(1);
|
|
349
|
-
expect(spy).toHaveBeenCalledWith(46);
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
it("wide diamond: many computeds from same source", () => {
|
|
353
|
-
const src = signal(0);
|
|
354
|
-
const branches = Array.from({ length: 10 }, (_, i) => computed(() => src.value + i));
|
|
355
|
-
const sum = computed(() => branches.reduce((acc, b) => acc + b.value, 0));
|
|
356
|
-
const spy = vi.fn();
|
|
357
|
-
effect(() => {
|
|
358
|
-
spy(sum.value);
|
|
359
|
-
return undefined;
|
|
360
|
-
});
|
|
361
|
-
spy.mockClear();
|
|
362
|
-
src.value = 1;
|
|
363
|
-
expect(spy).toHaveBeenCalledTimes(1);
|
|
364
|
-
// sum = (1+0)+(1+1)+...+(1+9) = 10 + 45 = 55
|
|
365
|
-
expect(spy).toHaveBeenCalledWith(55);
|
|
366
|
-
});
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
// ---------------------------------------------------------------------------
|
|
370
|
-
// Untracked nesting
|
|
371
|
-
// ---------------------------------------------------------------------------
|
|
372
|
-
|
|
373
|
-
describe("untracked nesting", () => {
|
|
374
|
-
it("nested untracked restores tracking correctly", () => {
|
|
375
|
-
const a = signal(1);
|
|
376
|
-
const b = signal(2);
|
|
377
|
-
const c = signal(3);
|
|
378
|
-
const spy = vi.fn();
|
|
379
|
-
effect(() => {
|
|
380
|
-
const av = a.value; // tracked
|
|
381
|
-
const bv = untracked(() => {
|
|
382
|
-
const cv = untracked(() => c.value); // not tracked
|
|
383
|
-
return b.value + cv; // not tracked
|
|
384
|
-
});
|
|
385
|
-
spy(av + bv);
|
|
386
|
-
return undefined;
|
|
387
|
-
});
|
|
388
|
-
spy.mockClear();
|
|
389
|
-
b.value = 20; // not tracked
|
|
390
|
-
expect(spy).not.toHaveBeenCalled();
|
|
391
|
-
c.value = 30; // not tracked
|
|
392
|
-
expect(spy).not.toHaveBeenCalled();
|
|
393
|
-
a.value = 10; // tracked
|
|
394
|
-
expect(spy).toHaveBeenCalledWith(10 + 20 + 30);
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
it("untracked inside computed does not track those reads", () => {
|
|
398
|
-
const a = signal(1);
|
|
399
|
-
const b = signal(2);
|
|
400
|
-
const c = computed(() => a.value + untracked(() => b.value));
|
|
401
|
-
const spy = vi.fn();
|
|
402
|
-
effect(() => {
|
|
403
|
-
spy(c.value);
|
|
404
|
-
return undefined;
|
|
405
|
-
});
|
|
406
|
-
expect(spy).toHaveBeenCalledWith(3);
|
|
407
|
-
spy.mockClear();
|
|
408
|
-
b.value = 99; // not tracked by computed
|
|
409
|
-
expect(spy).not.toHaveBeenCalled();
|
|
410
|
-
a.value = 10;
|
|
411
|
-
expect(spy).toHaveBeenCalledWith(109); // 10 + 99
|
|
412
|
-
});
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
// ---------------------------------------------------------------------------
|
|
416
|
-
// Reconcile edge cases
|
|
417
|
-
// ---------------------------------------------------------------------------
|
|
418
|
-
|
|
419
|
-
describe("reconcile edge cases", () => {
|
|
420
|
-
it("duplicate keys: last one wins", () => {
|
|
421
|
-
const parent = document.createElement("div");
|
|
422
|
-
// Two items with same key - should not crash
|
|
423
|
-
reconcile(
|
|
424
|
-
parent,
|
|
425
|
-
[
|
|
426
|
-
{ id: "a", v: 1 },
|
|
427
|
-
{ id: "a", v: 2 },
|
|
428
|
-
],
|
|
429
|
-
{
|
|
430
|
-
key: (i) => i.id,
|
|
431
|
-
mount: (i) => {
|
|
432
|
-
const el = document.createElement("span");
|
|
433
|
-
el.textContent = String(i.v);
|
|
434
|
-
return el;
|
|
435
|
-
},
|
|
436
|
-
},
|
|
437
|
-
);
|
|
438
|
-
// Should have 2 elements (both mounted since key collision means second is new)
|
|
439
|
-
expect(parent.children.length).toBe(2);
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
it("null/undefined children in patch are skipped", () => {
|
|
443
|
-
const parent = document.createElement("div");
|
|
444
|
-
patch(parent, null, undefined, "hello", null);
|
|
445
|
-
expect(parent.textContent).toBe("hello");
|
|
446
|
-
expect(parent.childNodes.length).toBe(1);
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
it("text vs element swap", () => {
|
|
450
|
-
const parent = document.createElement("div");
|
|
451
|
-
parent.appendChild(document.createTextNode("old text"));
|
|
452
|
-
const newEl = document.createElement("span");
|
|
453
|
-
newEl.textContent = "new element";
|
|
454
|
-
patch(parent, newEl);
|
|
455
|
-
expect(parent.children.length).toBe(1);
|
|
456
|
-
expect(parent.children[0]!.tagName).toBe("SPAN");
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
it("element vs text swap", () => {
|
|
460
|
-
const parent = document.createElement("div");
|
|
461
|
-
parent.appendChild(document.createElement("span"));
|
|
462
|
-
patch(parent, "just text");
|
|
463
|
-
expect(parent.childNodes.length).toBe(1);
|
|
464
|
-
expect(parent.childNodes[0]!.nodeType).toBe(3); // text node
|
|
465
|
-
expect(parent.textContent).toBe("just text");
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
it("keyed reorder preserves identity", () => {
|
|
469
|
-
const parent = document.createElement("div");
|
|
470
|
-
const a = document.createElement("div");
|
|
471
|
-
a.setAttribute("data-id", "a");
|
|
472
|
-
a.textContent = "A";
|
|
473
|
-
const b = document.createElement("div");
|
|
474
|
-
b.setAttribute("data-id", "b");
|
|
475
|
-
b.textContent = "B";
|
|
476
|
-
const c = document.createElement("div");
|
|
477
|
-
c.setAttribute("data-id", "c");
|
|
478
|
-
c.textContent = "C";
|
|
479
|
-
parent.appendChild(a);
|
|
480
|
-
parent.appendChild(b);
|
|
481
|
-
parent.appendChild(c);
|
|
482
|
-
|
|
483
|
-
// Reorder: c, a, b
|
|
484
|
-
const newC = document.createElement("div");
|
|
485
|
-
newC.setAttribute("data-id", "c");
|
|
486
|
-
newC.textContent = "C!";
|
|
487
|
-
const newA = document.createElement("div");
|
|
488
|
-
newA.setAttribute("data-id", "a");
|
|
489
|
-
newA.textContent = "A!";
|
|
490
|
-
const newB = document.createElement("div");
|
|
491
|
-
newB.setAttribute("data-id", "b");
|
|
492
|
-
newB.textContent = "B!";
|
|
493
|
-
reconcileChildren(parent, [newC, newA, newB]);
|
|
494
|
-
|
|
495
|
-
// Identity preserved
|
|
496
|
-
expect(parent.childNodes[0]).toBe(c);
|
|
497
|
-
expect(parent.childNodes[1]).toBe(a);
|
|
498
|
-
expect(parent.childNodes[2]).toBe(b);
|
|
499
|
-
// Content patched
|
|
500
|
-
expect(c.textContent).toBe("C!");
|
|
501
|
-
expect(a.textContent).toBe("A!");
|
|
502
|
-
expect(b.textContent).toBe("B!");
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
it("DocumentFragment children are flattened in patch", () => {
|
|
506
|
-
const parent = document.createElement("div");
|
|
507
|
-
const frag = document.createDocumentFragment();
|
|
508
|
-
frag.appendChild(document.createTextNode("one"));
|
|
509
|
-
frag.appendChild(document.createTextNode("two"));
|
|
510
|
-
patch(parent, frag);
|
|
511
|
-
expect(parent.childNodes.length).toBe(2);
|
|
512
|
-
expect(parent.textContent).toBe("onetwo");
|
|
513
|
-
});
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
// ---------------------------------------------------------------------------
|
|
517
|
-
// Store edge cases
|
|
518
|
-
// ---------------------------------------------------------------------------
|
|
519
|
-
|
|
520
|
-
describe("store edge cases", () => {
|
|
521
|
-
it("subscriber error does not prevent other subscribers", () => {
|
|
522
|
-
const { set, subscribe: sub } = createStore<{ x: number }>();
|
|
523
|
-
const spy1 = vi.fn(() => {
|
|
524
|
-
throw new Error("sub1 error");
|
|
525
|
-
});
|
|
526
|
-
const spy2 = vi.fn();
|
|
527
|
-
sub("x", spy1);
|
|
528
|
-
sub("x", spy2);
|
|
529
|
-
// Should not throw, and spy2 should still be called
|
|
530
|
-
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {
|
|
531
|
-
/* noop */
|
|
532
|
-
});
|
|
533
|
-
set("x", 1);
|
|
534
|
-
expect(spy2).toHaveBeenCalledWith(1);
|
|
535
|
-
consoleSpy.mockRestore();
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
it("batch exception does not leave batchDepth dirty", () => {
|
|
539
|
-
const store = createStore<{ x: number }>();
|
|
540
|
-
const spy = vi.fn();
|
|
541
|
-
store.subscribe("x", spy);
|
|
542
|
-
try {
|
|
543
|
-
store.batch(() => {
|
|
544
|
-
throw new Error("batch err");
|
|
545
|
-
});
|
|
546
|
-
} catch {
|
|
547
|
-
// swallow
|
|
548
|
-
}
|
|
549
|
-
store.set("x", 42);
|
|
550
|
-
expect(spy).toHaveBeenCalledWith(42);
|
|
551
|
-
});
|
|
552
|
-
});
|