@pyreon/reactivity 0.1.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/LICENSE +21 -0
- package/README.md +73 -0
- package/lib/analysis/index.js.html +5406 -0
- package/lib/index.js +838 -0
- package/lib/index.js.map +1 -0
- package/lib/types/index.d.ts +725 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index2.d.ts +342 -0
- package/lib/types/index2.d.ts.map +1 -0
- package/package.json +40 -0
- package/src/batch.ts +44 -0
- package/src/cell.ts +71 -0
- package/src/computed.ts +71 -0
- package/src/createSelector.ts +56 -0
- package/src/debug.ts +134 -0
- package/src/effect.ts +152 -0
- package/src/index.ts +15 -0
- package/src/reconcile.ts +98 -0
- package/src/resource.ts +66 -0
- package/src/scope.ts +80 -0
- package/src/signal.ts +125 -0
- package/src/store.ts +139 -0
- package/src/tests/batch.test.ts +69 -0
- package/src/tests/bind.test.ts +84 -0
- package/src/tests/branches.test.ts +343 -0
- package/src/tests/cell.test.ts +111 -0
- package/src/tests/computed.test.ts +146 -0
- package/src/tests/createSelector.test.ts +119 -0
- package/src/tests/debug.test.ts +196 -0
- package/src/tests/effect.test.ts +256 -0
- package/src/tests/resource.test.ts +133 -0
- package/src/tests/scope.test.ts +202 -0
- package/src/tests/signal.test.ts +120 -0
- package/src/tests/store.test.ts +136 -0
- package/src/tests/tracking.test.ts +158 -0
- package/src/tests/watch.test.ts +146 -0
- package/src/tracking.ts +103 -0
- package/src/watch.ts +69 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Cell, cell } from "../cell"
|
|
2
|
+
|
|
3
|
+
describe("Cell", () => {
|
|
4
|
+
test("stores and reads initial value", () => {
|
|
5
|
+
const c = cell(42)
|
|
6
|
+
expect(c.peek()).toBe(42)
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
test("set() updates value", () => {
|
|
10
|
+
const c = cell("hello")
|
|
11
|
+
c.set("world")
|
|
12
|
+
expect(c.peek()).toBe("world")
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test("set() skips when value is the same (Object.is)", () => {
|
|
16
|
+
const c = cell(1)
|
|
17
|
+
let calls = 0
|
|
18
|
+
c.listen(() => calls++)
|
|
19
|
+
c.set(1)
|
|
20
|
+
expect(calls).toBe(0)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test("update() applies function to current value", () => {
|
|
24
|
+
const c = cell(10)
|
|
25
|
+
c.update((v) => v + 5)
|
|
26
|
+
expect(c.peek()).toBe(15)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test("listen() fires on set()", () => {
|
|
30
|
+
const c = cell("a")
|
|
31
|
+
let fired = false
|
|
32
|
+
c.listen(() => {
|
|
33
|
+
fired = true
|
|
34
|
+
})
|
|
35
|
+
c.set("b")
|
|
36
|
+
expect(fired).toBe(true)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test("listen() single-listener fast path (no Set allocated)", () => {
|
|
40
|
+
const c = cell(0)
|
|
41
|
+
let count = 0
|
|
42
|
+
c.listen(() => count++)
|
|
43
|
+
// Should use _l fast path, not _s Set
|
|
44
|
+
expect(c._s).toBeNull()
|
|
45
|
+
expect(c._l).not.toBeNull()
|
|
46
|
+
c.set(1)
|
|
47
|
+
expect(count).toBe(1)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test("listen() promotes to Set with multiple listeners", () => {
|
|
51
|
+
const c = cell(0)
|
|
52
|
+
let a = 0
|
|
53
|
+
let b = 0
|
|
54
|
+
c.listen(() => a++)
|
|
55
|
+
c.listen(() => b++)
|
|
56
|
+
expect(c._s).not.toBeNull()
|
|
57
|
+
expect(c._l).toBeNull()
|
|
58
|
+
c.set(1)
|
|
59
|
+
expect(a).toBe(1)
|
|
60
|
+
expect(b).toBe(1)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test("subscribe() returns working unsubscribe (single listener)", () => {
|
|
64
|
+
const c = cell(0)
|
|
65
|
+
let count = 0
|
|
66
|
+
const unsub = c.subscribe(() => count++)
|
|
67
|
+
c.set(1)
|
|
68
|
+
expect(count).toBe(1)
|
|
69
|
+
unsub()
|
|
70
|
+
c.set(2)
|
|
71
|
+
expect(count).toBe(1) // no more notifications
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test("subscribe() returns working unsubscribe (multi listener)", () => {
|
|
75
|
+
const c = cell(0)
|
|
76
|
+
let a = 0
|
|
77
|
+
let b = 0
|
|
78
|
+
c.listen(() => a++)
|
|
79
|
+
const unsub = c.subscribe(() => b++)
|
|
80
|
+
c.set(1)
|
|
81
|
+
expect(a).toBe(1)
|
|
82
|
+
expect(b).toBe(1)
|
|
83
|
+
unsub()
|
|
84
|
+
c.set(2)
|
|
85
|
+
expect(a).toBe(2)
|
|
86
|
+
expect(b).toBe(1) // unsubscribed
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test("cell() factory returns Cell instance", () => {
|
|
90
|
+
const c = cell("x")
|
|
91
|
+
expect(c).toBeInstanceOf(Cell)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test("multiple rapid updates notify correctly", () => {
|
|
95
|
+
const c = cell(0)
|
|
96
|
+
const values: number[] = []
|
|
97
|
+
c.listen(() => values.push(c.peek()))
|
|
98
|
+
c.set(1)
|
|
99
|
+
c.set(2)
|
|
100
|
+
c.set(3)
|
|
101
|
+
expect(values).toEqual([1, 2, 3])
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test("NaN equality (Object.is)", () => {
|
|
105
|
+
const c = cell(Number.NaN)
|
|
106
|
+
let calls = 0
|
|
107
|
+
c.listen(() => calls++)
|
|
108
|
+
c.set(Number.NaN)
|
|
109
|
+
expect(calls).toBe(0) // Object.is(NaN, NaN) is true
|
|
110
|
+
})
|
|
111
|
+
})
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { computed } from "../computed"
|
|
2
|
+
import { effect } from "../effect"
|
|
3
|
+
import { signal } from "../signal"
|
|
4
|
+
|
|
5
|
+
describe("computed", () => {
|
|
6
|
+
test("computes derived value", () => {
|
|
7
|
+
const s = signal(2)
|
|
8
|
+
const doubled = computed(() => s() * 2)
|
|
9
|
+
expect(doubled()).toBe(4)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test("updates when dependency changes", () => {
|
|
13
|
+
const s = signal(3)
|
|
14
|
+
const tripled = computed(() => s() * 3)
|
|
15
|
+
expect(tripled()).toBe(9)
|
|
16
|
+
s.set(4)
|
|
17
|
+
expect(tripled()).toBe(12)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test("is lazy — does not compute until read", () => {
|
|
21
|
+
let computations = 0
|
|
22
|
+
const s = signal(0)
|
|
23
|
+
const c = computed(() => {
|
|
24
|
+
computations++
|
|
25
|
+
return s() + 1
|
|
26
|
+
})
|
|
27
|
+
expect(computations).toBe(0)
|
|
28
|
+
c() // first read
|
|
29
|
+
expect(computations).toBe(1)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test("is memoized — does not recompute on repeated reads", () => {
|
|
33
|
+
let computations = 0
|
|
34
|
+
const s = signal(5)
|
|
35
|
+
const c = computed(() => {
|
|
36
|
+
computations++
|
|
37
|
+
return s() * 2
|
|
38
|
+
})
|
|
39
|
+
c()
|
|
40
|
+
c()
|
|
41
|
+
c()
|
|
42
|
+
expect(computations).toBe(1)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test("recomputes only when dirty", () => {
|
|
46
|
+
let computations = 0
|
|
47
|
+
const s = signal(1)
|
|
48
|
+
const c = computed(() => {
|
|
49
|
+
computations++
|
|
50
|
+
return s()
|
|
51
|
+
})
|
|
52
|
+
c()
|
|
53
|
+
expect(computations).toBe(1)
|
|
54
|
+
s.set(2)
|
|
55
|
+
c()
|
|
56
|
+
expect(computations).toBe(2)
|
|
57
|
+
c()
|
|
58
|
+
expect(computations).toBe(2) // still memoized
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test("chains correctly", () => {
|
|
62
|
+
const base = signal(2)
|
|
63
|
+
const doubled = computed(() => base() * 2)
|
|
64
|
+
const quadrupled = computed(() => doubled() * 2)
|
|
65
|
+
expect(quadrupled()).toBe(8)
|
|
66
|
+
base.set(3)
|
|
67
|
+
expect(quadrupled()).toBe(12)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test("dispose stops recomputation", () => {
|
|
71
|
+
const s = signal(1)
|
|
72
|
+
let computations = 0
|
|
73
|
+
const c = computed(() => {
|
|
74
|
+
computations++
|
|
75
|
+
return s() * 2
|
|
76
|
+
})
|
|
77
|
+
c() // initial
|
|
78
|
+
expect(computations).toBe(1)
|
|
79
|
+
c.dispose()
|
|
80
|
+
s.set(2)
|
|
81
|
+
// After dispose, reading returns stale value and does not recompute
|
|
82
|
+
// (the computed is no longer subscribed to s)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test("custom equals skips downstream notification when equal", () => {
|
|
86
|
+
const s = signal(3)
|
|
87
|
+
let downstream = 0
|
|
88
|
+
|
|
89
|
+
const c = computed(() => Math.floor(s() / 10), {
|
|
90
|
+
equals: (a, b) => a === b,
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
effect(() => {
|
|
94
|
+
c()
|
|
95
|
+
downstream++
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
expect(downstream).toBe(1)
|
|
99
|
+
expect(c()).toBe(0)
|
|
100
|
+
|
|
101
|
+
s.set(5) // Math.floor(5/10) = 0, same as before
|
|
102
|
+
expect(downstream).toBe(1) // no downstream update
|
|
103
|
+
|
|
104
|
+
s.set(15) // Math.floor(15/10) = 1, different
|
|
105
|
+
expect(downstream).toBe(2)
|
|
106
|
+
expect(c()).toBe(1)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test("custom equals with array comparison", () => {
|
|
110
|
+
const items = signal([1, 2, 3])
|
|
111
|
+
let downstream = 0
|
|
112
|
+
|
|
113
|
+
const sorted = computed(() => items().slice().sort(), {
|
|
114
|
+
equals: (a, b) => a.length === b.length && a.every((v, i) => v === b[i]),
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
effect(() => {
|
|
118
|
+
sorted()
|
|
119
|
+
downstream++
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
expect(downstream).toBe(1)
|
|
123
|
+
|
|
124
|
+
// Set to same content in different array — equals returns true, no notification
|
|
125
|
+
items.set([1, 2, 3])
|
|
126
|
+
expect(downstream).toBe(1)
|
|
127
|
+
|
|
128
|
+
// Actually different content
|
|
129
|
+
items.set([1, 2, 4])
|
|
130
|
+
expect(downstream).toBe(2)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test("computed used as dependency inside an effect (subscribe path)", () => {
|
|
134
|
+
const s = signal(10)
|
|
135
|
+
const c = computed(() => s() + 1)
|
|
136
|
+
let result = 0
|
|
137
|
+
|
|
138
|
+
effect(() => {
|
|
139
|
+
result = c()
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
expect(result).toBe(11)
|
|
143
|
+
s.set(20)
|
|
144
|
+
expect(result).toBe(21)
|
|
145
|
+
})
|
|
146
|
+
})
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { createSelector } from "../createSelector"
|
|
2
|
+
import { effect } from "../effect"
|
|
3
|
+
import { signal } from "../signal"
|
|
4
|
+
|
|
5
|
+
describe("createSelector", () => {
|
|
6
|
+
test("returns true for the currently selected value", () => {
|
|
7
|
+
const selected = signal(1)
|
|
8
|
+
const isSelected = createSelector(() => selected())
|
|
9
|
+
|
|
10
|
+
let result = false
|
|
11
|
+
effect(() => {
|
|
12
|
+
result = isSelected(1)
|
|
13
|
+
})
|
|
14
|
+
expect(result).toBe(true)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test("returns false for non-selected values", () => {
|
|
18
|
+
const selected = signal(1)
|
|
19
|
+
const isSelected = createSelector(() => selected())
|
|
20
|
+
|
|
21
|
+
let result = true
|
|
22
|
+
effect(() => {
|
|
23
|
+
result = isSelected(2)
|
|
24
|
+
})
|
|
25
|
+
expect(result).toBe(false)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test("only notifies affected subscribers when selection changes", () => {
|
|
29
|
+
const selected = signal(1)
|
|
30
|
+
const isSelected = createSelector(() => selected())
|
|
31
|
+
|
|
32
|
+
let runs1 = 0
|
|
33
|
+
let runs2 = 0
|
|
34
|
+
let runs3 = 0
|
|
35
|
+
|
|
36
|
+
effect(() => {
|
|
37
|
+
isSelected(1)
|
|
38
|
+
runs1++
|
|
39
|
+
})
|
|
40
|
+
effect(() => {
|
|
41
|
+
isSelected(2)
|
|
42
|
+
runs2++
|
|
43
|
+
})
|
|
44
|
+
effect(() => {
|
|
45
|
+
isSelected(3)
|
|
46
|
+
runs3++
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
expect(runs1).toBe(1)
|
|
50
|
+
expect(runs2).toBe(1)
|
|
51
|
+
expect(runs3).toBe(1)
|
|
52
|
+
|
|
53
|
+
// Change selection from 1 to 2: only buckets 1 (deselected) and 2 (newly selected) should fire
|
|
54
|
+
selected.set(2)
|
|
55
|
+
expect(runs1).toBe(2) // deselected
|
|
56
|
+
expect(runs2).toBe(2) // newly selected
|
|
57
|
+
expect(runs3).toBe(1) // unaffected
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test("does not notify when source changes to the same value", () => {
|
|
61
|
+
const selected = signal(1)
|
|
62
|
+
const isSelected = createSelector(() => selected())
|
|
63
|
+
|
|
64
|
+
let runs = 0
|
|
65
|
+
effect(() => {
|
|
66
|
+
isSelected(1)
|
|
67
|
+
runs++
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
selected.set(1) // same value
|
|
71
|
+
expect(runs).toBe(1)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test("works when changing to a value with no subscribers", () => {
|
|
75
|
+
const selected = signal(1)
|
|
76
|
+
const isSelected = createSelector(() => selected())
|
|
77
|
+
|
|
78
|
+
let runs = 0
|
|
79
|
+
effect(() => {
|
|
80
|
+
isSelected(1)
|
|
81
|
+
runs++
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// Change to value 99 which has no subscriber bucket
|
|
85
|
+
selected.set(99)
|
|
86
|
+
expect(runs).toBe(2) // old bucket (1) notified
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test("reuses host objects for the same value", () => {
|
|
90
|
+
const selected = signal(1)
|
|
91
|
+
const isSelected = createSelector(() => selected())
|
|
92
|
+
|
|
93
|
+
let result1 = false
|
|
94
|
+
let result2 = false
|
|
95
|
+
effect(() => {
|
|
96
|
+
result1 = isSelected(1)
|
|
97
|
+
})
|
|
98
|
+
// Second call with same value should reuse the host
|
|
99
|
+
effect(() => {
|
|
100
|
+
result2 = isSelected(1)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
expect(result1).toBe(true)
|
|
104
|
+
expect(result2).toBe(true)
|
|
105
|
+
|
|
106
|
+
selected.set(2)
|
|
107
|
+
expect(result1).toBe(false)
|
|
108
|
+
expect(result2).toBe(false)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test("tracks correctly outside an effect (no activeEffect)", () => {
|
|
112
|
+
const selected = signal(1)
|
|
113
|
+
const isSelected = createSelector(() => selected())
|
|
114
|
+
|
|
115
|
+
// Calling outside an effect should still return the correct boolean
|
|
116
|
+
expect(isSelected(1)).toBe(true)
|
|
117
|
+
expect(isSelected(2)).toBe(false)
|
|
118
|
+
})
|
|
119
|
+
})
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { _notifyTraceListeners, inspectSignal, isTracing, onSignalUpdate, why } from "../debug"
|
|
2
|
+
import { signal } from "../signal"
|
|
3
|
+
|
|
4
|
+
describe("debug", () => {
|
|
5
|
+
describe("onSignalUpdate / isTracing", () => {
|
|
6
|
+
test("isTracing is false by default", () => {
|
|
7
|
+
expect(isTracing()).toBe(false)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
test("registering a listener enables tracing", () => {
|
|
11
|
+
const dispose = onSignalUpdate(() => {})
|
|
12
|
+
expect(isTracing()).toBe(true)
|
|
13
|
+
dispose()
|
|
14
|
+
expect(isTracing()).toBe(false)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test("listener receives signal update events", () => {
|
|
18
|
+
const events: { name: string | undefined; prev: unknown; next: unknown }[] = []
|
|
19
|
+
const dispose = onSignalUpdate((e) => {
|
|
20
|
+
events.push({ name: e.name, prev: e.prev, next: e.next })
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const s = signal(1, { name: "count" })
|
|
24
|
+
s.set(2)
|
|
25
|
+
|
|
26
|
+
expect(events.length).toBe(1)
|
|
27
|
+
expect(events[0]).toEqual({ name: "count", prev: 1, next: 2 })
|
|
28
|
+
|
|
29
|
+
dispose()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test("dispose removes only the specific listener", () => {
|
|
33
|
+
let calls1 = 0
|
|
34
|
+
let calls2 = 0
|
|
35
|
+
const dispose1 = onSignalUpdate(() => calls1++)
|
|
36
|
+
const dispose2 = onSignalUpdate(() => calls2++)
|
|
37
|
+
|
|
38
|
+
const s = signal(0)
|
|
39
|
+
s.set(1)
|
|
40
|
+
expect(calls1).toBe(1)
|
|
41
|
+
expect(calls2).toBe(1)
|
|
42
|
+
|
|
43
|
+
dispose1()
|
|
44
|
+
|
|
45
|
+
s.set(2)
|
|
46
|
+
expect(calls1).toBe(1) // removed
|
|
47
|
+
expect(calls2).toBe(2) // still active
|
|
48
|
+
|
|
49
|
+
dispose2()
|
|
50
|
+
expect(isTracing()).toBe(false)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test("dispose is safe to call when listeners already null", () => {
|
|
54
|
+
const dispose = onSignalUpdate(() => {})
|
|
55
|
+
dispose()
|
|
56
|
+
expect(isTracing()).toBe(false)
|
|
57
|
+
dispose() // should not throw — _traceListeners is null
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test("_notifyTraceListeners does nothing when no listeners", () => {
|
|
61
|
+
const s = signal(0)
|
|
62
|
+
// Should not throw
|
|
63
|
+
_notifyTraceListeners(s, 0, 1)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test("event includes stack and timestamp", () => {
|
|
67
|
+
let event: { stack: string; timestamp: number } | undefined
|
|
68
|
+
const dispose = onSignalUpdate((e) => {
|
|
69
|
+
event = { stack: e.stack, timestamp: e.timestamp }
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const s = signal(0)
|
|
73
|
+
s.set(1)
|
|
74
|
+
|
|
75
|
+
expect(event).toBeDefined()
|
|
76
|
+
expect(typeof event?.stack).toBe("string")
|
|
77
|
+
expect(typeof event?.timestamp).toBe("number")
|
|
78
|
+
|
|
79
|
+
dispose()
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe("why", () => {
|
|
84
|
+
test("logs signal updates to console", async () => {
|
|
85
|
+
const logs: unknown[][] = []
|
|
86
|
+
const origLog = console.log
|
|
87
|
+
console.log = (...args: unknown[]) => logs.push(args)
|
|
88
|
+
|
|
89
|
+
const s = signal(1, { name: "test" })
|
|
90
|
+
why()
|
|
91
|
+
s.set(2)
|
|
92
|
+
|
|
93
|
+
// Wait for microtask (auto-dispose)
|
|
94
|
+
await new Promise((r) => queueMicrotask(() => r(undefined)))
|
|
95
|
+
|
|
96
|
+
expect(logs.length).toBeGreaterThan(0)
|
|
97
|
+
console.log = origLog
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test("logs 'no updates' when nothing changes", async () => {
|
|
101
|
+
const logs: unknown[][] = []
|
|
102
|
+
const origLog = console.log
|
|
103
|
+
console.log = (...args: unknown[]) => logs.push(args)
|
|
104
|
+
|
|
105
|
+
why()
|
|
106
|
+
// No signal updates
|
|
107
|
+
|
|
108
|
+
await new Promise((r) => queueMicrotask(() => r(undefined)))
|
|
109
|
+
|
|
110
|
+
const noUpdateLog =
|
|
111
|
+
logs.find((args) =>
|
|
112
|
+
typeof args[0] === "string" ? args[0].includes("No signal") : false,
|
|
113
|
+
) ||
|
|
114
|
+
logs.find((args) => (typeof args[1] === "string" ? args[1].includes("No signal") : false))
|
|
115
|
+
expect(noUpdateLog).toBeDefined()
|
|
116
|
+
console.log = origLog
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test("calling why() twice is ignored (already active)", async () => {
|
|
120
|
+
const logs: unknown[][] = []
|
|
121
|
+
const origLog = console.log
|
|
122
|
+
console.log = (...args: unknown[]) => logs.push(args)
|
|
123
|
+
|
|
124
|
+
why()
|
|
125
|
+
why() // should be ignored
|
|
126
|
+
const s = signal(0, { name: "x" })
|
|
127
|
+
s.set(1)
|
|
128
|
+
|
|
129
|
+
await new Promise((r) => queueMicrotask(() => r(undefined)))
|
|
130
|
+
// Should not throw or double-log
|
|
131
|
+
console.log = origLog
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test("logs anonymous signal name when no name is set", async () => {
|
|
135
|
+
const logs: unknown[][] = []
|
|
136
|
+
const origLog = console.log
|
|
137
|
+
console.log = (...args: unknown[]) => logs.push(args)
|
|
138
|
+
|
|
139
|
+
const s = signal(0) // no name
|
|
140
|
+
why()
|
|
141
|
+
s.set(1)
|
|
142
|
+
|
|
143
|
+
await new Promise((r) => queueMicrotask(() => r(undefined)))
|
|
144
|
+
|
|
145
|
+
const anonLog = logs.find((args) =>
|
|
146
|
+
args.some((a) => typeof a === "string" && a.includes("anonymous")),
|
|
147
|
+
)
|
|
148
|
+
expect(anonLog).toBeDefined()
|
|
149
|
+
console.log = origLog
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
describe("inspectSignal", () => {
|
|
154
|
+
test("prints signal info and returns debug info", () => {
|
|
155
|
+
const groupCalls: unknown[][] = []
|
|
156
|
+
const logCalls: unknown[][] = []
|
|
157
|
+
const origGroup = console.group
|
|
158
|
+
const origLog = console.log
|
|
159
|
+
const origEnd = console.groupEnd
|
|
160
|
+
console.group = (...args: unknown[]) => groupCalls.push(args)
|
|
161
|
+
console.log = (...args: unknown[]) => logCalls.push(args)
|
|
162
|
+
console.groupEnd = () => {}
|
|
163
|
+
|
|
164
|
+
const s = signal(42, { name: "count" })
|
|
165
|
+
const info = inspectSignal(s)
|
|
166
|
+
|
|
167
|
+
expect(info.name).toBe("count")
|
|
168
|
+
expect(info.value).toBe(42)
|
|
169
|
+
expect(info.subscriberCount).toBe(0)
|
|
170
|
+
expect(groupCalls.length).toBe(1)
|
|
171
|
+
expect(logCalls.length).toBe(2) // value + subscribers
|
|
172
|
+
|
|
173
|
+
console.group = origGroup
|
|
174
|
+
console.log = origLog
|
|
175
|
+
console.groupEnd = origEnd
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test("handles anonymous signal", () => {
|
|
179
|
+
const origGroup = console.group
|
|
180
|
+
const origLog = console.log
|
|
181
|
+
const origEnd = console.groupEnd
|
|
182
|
+
console.group = () => {}
|
|
183
|
+
console.log = () => {}
|
|
184
|
+
console.groupEnd = () => {}
|
|
185
|
+
|
|
186
|
+
const s = signal(0)
|
|
187
|
+
const info = inspectSignal(s)
|
|
188
|
+
|
|
189
|
+
expect(info.name).toBeUndefined()
|
|
190
|
+
|
|
191
|
+
console.group = origGroup
|
|
192
|
+
console.log = origLog
|
|
193
|
+
console.groupEnd = origEnd
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
})
|