@storve/core 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.
Files changed (198) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +993 -26
  3. package/dist/adapters/indexedDB.cjs +0 -1
  4. package/dist/adapters/indexedDB.mjs +0 -1
  5. package/dist/adapters/localStorage.cjs +0 -1
  6. package/dist/adapters/localStorage.mjs +0 -1
  7. package/dist/adapters/memory.cjs +0 -1
  8. package/dist/adapters/memory.mjs +0 -1
  9. package/dist/adapters/sessionStorage.cjs +0 -1
  10. package/dist/adapters/sessionStorage.mjs +0 -1
  11. package/dist/async-entry.d.ts +0 -1
  12. package/dist/async.cjs +0 -1
  13. package/dist/async.d.ts +0 -1
  14. package/dist/async.mjs +0 -1
  15. package/dist/batch.d.ts +0 -1
  16. package/dist/compose.d.ts +0 -1
  17. package/dist/computed-entry.d.ts +0 -1
  18. package/dist/computed.cjs +0 -1
  19. package/dist/computed.d.ts +0 -1
  20. package/dist/computed.mjs +0 -1
  21. package/dist/devtools/history.d.ts +0 -1
  22. package/dist/devtools/index.d.ts +0 -1
  23. package/dist/devtools/redux-bridge.d.ts +0 -1
  24. package/dist/devtools/snapshots.d.ts +0 -1
  25. package/dist/devtools/withDevtools.d.ts +0 -1
  26. package/dist/devtools.cjs +0 -1
  27. package/dist/devtools.mjs +0 -1
  28. package/dist/extensions/noop.d.ts +0 -1
  29. package/dist/index.cjs +0 -1
  30. package/dist/index.d.ts +0 -1
  31. package/dist/index.mjs +0 -1
  32. package/dist/persist/adapters/indexedDB.d.ts +0 -1
  33. package/dist/persist/adapters/localStorage.d.ts +0 -1
  34. package/dist/persist/adapters/memory.d.ts +0 -1
  35. package/dist/persist/adapters/sessionStorage.d.ts +0 -1
  36. package/dist/persist/debounce.d.ts +0 -1
  37. package/dist/persist/hydrate.d.ts +0 -1
  38. package/dist/persist/index.d.ts +0 -1
  39. package/dist/persist/serialize.d.ts +0 -1
  40. package/dist/persist.cjs +0 -1
  41. package/dist/persist.mjs +0 -1
  42. package/dist/proxy.d.ts +0 -1
  43. package/dist/registry-qtr1UpFU.js +0 -1
  44. package/dist/registry-zaKZ1P-s.js +0 -1
  45. package/dist/registry.d.ts +0 -1
  46. package/dist/signals/createSignal.d.ts +0 -1
  47. package/dist/signals/index.d.ts +0 -1
  48. package/dist/signals/useSignal.d.ts +0 -1
  49. package/dist/signals.cjs +0 -1
  50. package/dist/signals.mjs +0 -1
  51. package/dist/store.d.ts +0 -1
  52. package/dist/sync/channel.d.ts +0 -1
  53. package/dist/sync/index.d.ts +0 -1
  54. package/dist/sync/protocol.d.ts +0 -1
  55. package/dist/sync/withSync.d.ts +0 -1
  56. package/dist/sync.cjs +0 -1
  57. package/dist/sync.mjs +0 -1
  58. package/dist/types.d.ts +0 -1
  59. package/package.json +9 -3
  60. package/CHANGELOG.md +0 -151
  61. package/benchmarks/run.ts +0 -102
  62. package/benchmarks/week2.md +0 -9
  63. package/benchmarks/week2.ts +0 -64
  64. package/benchmarks/week4.md +0 -13
  65. package/benchmarks/week4.ts +0 -178
  66. package/benchmarks/week5.md +0 -15
  67. package/benchmarks/week5.ts +0 -184
  68. package/coverage/coverage-summary.json +0 -31
  69. package/dist/adapters/indexedDB.cjs.map +0 -1
  70. package/dist/adapters/indexedDB.mjs.map +0 -1
  71. package/dist/adapters/localStorage.cjs.map +0 -1
  72. package/dist/adapters/localStorage.mjs.map +0 -1
  73. package/dist/adapters/memory.cjs.map +0 -1
  74. package/dist/adapters/memory.mjs.map +0 -1
  75. package/dist/adapters/sessionStorage.cjs.map +0 -1
  76. package/dist/adapters/sessionStorage.mjs.map +0 -1
  77. package/dist/async-entry.d.ts.map +0 -1
  78. package/dist/async.cjs.map +0 -1
  79. package/dist/async.d.ts.map +0 -1
  80. package/dist/async.mjs.map +0 -1
  81. package/dist/batch.d.ts.map +0 -1
  82. package/dist/compose.d.ts.map +0 -1
  83. package/dist/computed-entry.d.ts.map +0 -1
  84. package/dist/computed.cjs.map +0 -1
  85. package/dist/computed.d.ts.map +0 -1
  86. package/dist/computed.mjs.map +0 -1
  87. package/dist/devtools/history.d.ts.map +0 -1
  88. package/dist/devtools/index.d.ts.map +0 -1
  89. package/dist/devtools/redux-bridge.d.ts.map +0 -1
  90. package/dist/devtools/snapshots.d.ts.map +0 -1
  91. package/dist/devtools/withDevtools.d.ts.map +0 -1
  92. package/dist/devtools.cjs.map +0 -1
  93. package/dist/devtools.mjs.map +0 -1
  94. package/dist/extensions/noop.d.ts.map +0 -1
  95. package/dist/index.cjs.js +0 -118
  96. package/dist/index.cjs.js.map +0 -1
  97. package/dist/index.cjs.map +0 -1
  98. package/dist/index.d.ts.map +0 -1
  99. package/dist/index.esm.js +0 -116
  100. package/dist/index.esm.js.map +0 -1
  101. package/dist/index.mjs.map +0 -1
  102. package/dist/persist/adapters/indexedDB.d.ts.map +0 -1
  103. package/dist/persist/adapters/localStorage.d.ts.map +0 -1
  104. package/dist/persist/adapters/memory.d.ts.map +0 -1
  105. package/dist/persist/adapters/sessionStorage.d.ts.map +0 -1
  106. package/dist/persist/debounce.d.ts.map +0 -1
  107. package/dist/persist/hydrate.d.ts.map +0 -1
  108. package/dist/persist/index.d.ts.map +0 -1
  109. package/dist/persist/serialize.d.ts.map +0 -1
  110. package/dist/persist.cjs.map +0 -1
  111. package/dist/persist.mjs.map +0 -1
  112. package/dist/proxy.d.ts.map +0 -1
  113. package/dist/registry-D3X0HSbl.js +0 -26
  114. package/dist/registry-D3X0HSbl.js.map +0 -1
  115. package/dist/registry-RDjbeJdx.js +0 -29
  116. package/dist/registry-RDjbeJdx.js.map +0 -1
  117. package/dist/registry-qtr1UpFU.js.map +0 -1
  118. package/dist/registry-zaKZ1P-s.js.map +0 -1
  119. package/dist/registry.d.ts.map +0 -1
  120. package/dist/signals/createSignal.d.ts.map +0 -1
  121. package/dist/signals/index.d.ts.map +0 -1
  122. package/dist/signals/useSignal.d.ts.map +0 -1
  123. package/dist/signals.cjs.map +0 -1
  124. package/dist/signals.mjs.map +0 -1
  125. package/dist/stats.html +0 -4949
  126. package/dist/store.d.ts.map +0 -1
  127. package/dist/sync/channel.d.ts.map +0 -1
  128. package/dist/sync/index.d.ts.map +0 -1
  129. package/dist/sync/protocol.d.ts.map +0 -1
  130. package/dist/sync/withSync.d.ts.map +0 -1
  131. package/dist/sync.cjs.map +0 -1
  132. package/dist/sync.mjs.map +0 -1
  133. package/dist/types.d.ts.map +0 -1
  134. package/rollup.config.mjs +0 -44
  135. package/src/async-entry.ts +0 -6
  136. package/src/async.ts +0 -240
  137. package/src/batch.ts +0 -33
  138. package/src/compose.ts +0 -50
  139. package/src/computed-entry.ts +0 -6
  140. package/src/computed.ts +0 -187
  141. package/src/devtools/history.ts +0 -103
  142. package/src/devtools/index.ts +0 -5
  143. package/src/devtools/redux-bridge.ts +0 -70
  144. package/src/devtools/snapshots.ts +0 -54
  145. package/src/devtools/withDevtools.ts +0 -196
  146. package/src/extensions/noop.ts +0 -12
  147. package/src/index.ts +0 -4
  148. package/src/persist/adapters/indexedDB.ts +0 -114
  149. package/src/persist/adapters/localStorage.ts +0 -28
  150. package/src/persist/adapters/memory.ts +0 -26
  151. package/src/persist/adapters/sessionStorage.ts +0 -28
  152. package/src/persist/debounce.ts +0 -28
  153. package/src/persist/hydrate.ts +0 -60
  154. package/src/persist/index.ts +0 -141
  155. package/src/persist/serialize.ts +0 -60
  156. package/src/proxy.ts +0 -87
  157. package/src/registry.ts +0 -67
  158. package/src/signals/createSignal.ts +0 -81
  159. package/src/signals/index.ts +0 -20
  160. package/src/signals/useSignal.ts +0 -18
  161. package/src/store.ts +0 -250
  162. package/src/sync/channel.ts +0 -15
  163. package/src/sync/index.ts +0 -3
  164. package/src/sync/protocol.ts +0 -18
  165. package/src/sync/withSync.ts +0 -147
  166. package/src/types.ts +0 -159
  167. package/tests/async.test.ts +0 -1100
  168. package/tests/batch.test.ts +0 -41
  169. package/tests/compose.test.ts +0 -209
  170. package/tests/computed.test.ts +0 -867
  171. package/tests/devtools.test.ts +0 -1039
  172. package/tests/integration/persist.integration.test.ts +0 -258
  173. package/tests/integration/signals.integration.test.ts +0 -309
  174. package/tests/integration.test.ts +0 -278
  175. package/tests/persist/adapters/indexedDB.adapter.test.ts +0 -185
  176. package/tests/persist/adapters/localStorage.adapter.test.ts +0 -105
  177. package/tests/persist/adapters/memory.adapter.test.ts +0 -112
  178. package/tests/persist/adapters/sessionStorage.adapter.test.ts +0 -128
  179. package/tests/persist/debounce.test.ts +0 -121
  180. package/tests/persist/hydrate.test.ts +0 -120
  181. package/tests/persist/migrate.test.ts +0 -208
  182. package/tests/persist/persist.test.ts +0 -357
  183. package/tests/persist/serialize.test.ts +0 -128
  184. package/tests/proxy.test.ts +0 -473
  185. package/tests/registry.test.ts +0 -67
  186. package/tests/signals/derived.test.ts +0 -244
  187. package/tests/signals/inference.test.ts +0 -108
  188. package/tests/signals/signal.test.ts +0 -348
  189. package/tests/signals/useSignal.test.tsx +0 -275
  190. package/tests/store.test.ts +0 -482
  191. package/tests/stress.test.ts +0 -268
  192. package/tests/sync.test.ts +0 -576
  193. package/tests/types.test.ts +0 -32
  194. package/tests/v0.3.test.ts +0 -813
  195. package/tree-shake-test.js +0 -1
  196. package/tsconfig.json +0 -15
  197. package/vitest.config.ts +0 -22
  198. package/vitest_play.ts +0 -7
@@ -1,348 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
- import { createStore } from '../../src/store';
3
- import { signal } from '../../src/signals/createSignal';
4
- import type { Store } from '../../src/types';
5
-
6
- describe('signal() factory', () => {
7
- interface State {
8
- count: number;
9
- name: string;
10
- active: boolean;
11
- meta: { id: string };
12
- items: number[];
13
- }
14
-
15
- let store: Store<State>;
16
- let unsubscribes: (() => void)[] = [];
17
-
18
- beforeEach(() => {
19
- store = createStore<State>({
20
- count: 0,
21
- name: 'initial',
22
- active: false,
23
- meta: { id: '1' },
24
- items: [1, 2, 3],
25
- });
26
- unsubscribes = [];
27
- });
28
-
29
- afterEach(() => {
30
- unsubscribes.forEach((unsub) => unsub());
31
- });
32
-
33
- describe('get()', () => {
34
- it('returns the correct initial value for the specified key', () => {
35
- const sig = signal(store, 'count');
36
- expect(sig.get()).toBe(0);
37
- });
38
-
39
- it('returns the updated value after store.setState changes that key', () => {
40
- const sig = signal(store, 'count');
41
- store.setState({ count: 1 });
42
- expect(sig.get()).toBe(1);
43
- });
44
-
45
- it('reflects store changes immediately — no stale reads', () => {
46
- const sig = signal(store, 'count');
47
- store.setState({ count: 5 });
48
- expect(sig.get()).toBe(5);
49
- store.setState({ count: 10 });
50
- expect(sig.get()).toBe(10);
51
- });
52
-
53
- it('works correctly for string keys', () => {
54
- const sig = signal(store, 'name');
55
- expect(sig.get()).toBe('initial');
56
- store.setState({ name: 'updated' });
57
- expect(sig.get()).toBe('updated');
58
- });
59
-
60
- it('works correctly for boolean keys', () => {
61
- const sig = signal(store, 'active');
62
- expect(sig.get()).toBe(false);
63
- store.setState({ active: true });
64
- expect(sig.get()).toBe(true);
65
- });
66
-
67
- it('works correctly for object keys (returns reference)', () => {
68
- const sig = signal(store, 'meta');
69
- const initialMeta = store.getState().meta;
70
- expect(sig.get()).toBe(initialMeta);
71
- expect(sig.get()).toEqual({ id: '1' });
72
- });
73
-
74
- it('works correctly for array keys (returns reference)', () => {
75
- const sig = signal(store, 'items');
76
- const initialItems = store.getState().items;
77
- expect(sig.get()).toBe(initialItems);
78
- expect(sig.get()).toEqual([1, 2, 3]);
79
- });
80
- });
81
-
82
- describe('set() — standard signal', () => {
83
- it('updates the store state correctly', () => {
84
- const sig = signal(store, 'count');
85
- sig.set(42);
86
- expect(store.getState().count).toBe(42);
87
- });
88
-
89
- it('set(fn) updater form receives current value and updates correctly', () => {
90
- const sig = signal(store, 'count');
91
- sig.set((prev) => prev + 1);
92
- expect(store.getState().count).toBe(1);
93
- });
94
-
95
- it('set() triggers store subscribers', () => {
96
- const sig = signal(store, 'count');
97
- const listener = vi.fn();
98
- unsubscribes.push(store.subscribe(listener));
99
- sig.set(100);
100
- expect(listener).toHaveBeenCalled();
101
- });
102
-
103
- it('set() triggers signal\'s own subscribers', () => {
104
- const sig = signal(store, 'count');
105
- const listener = vi.fn();
106
- unsubscribes.push(sig.subscribe(listener));
107
- sig.set(100);
108
- expect(listener).toHaveBeenCalledWith(100);
109
- });
110
-
111
- it('set(sameValue) with Object.is equal value still updates store (store behaviour)', () => {
112
- const sig = signal(store, 'count');
113
- const listener = vi.fn();
114
- unsubscribes.push(store.subscribe(listener));
115
- sig.set(0); // initial is 0
116
- expect(listener).toHaveBeenCalled();
117
- });
118
- });
119
-
120
- describe('set() — derived signal', () => {
121
- it('throws with EXACT message when calling set() on a derived signal', () => {
122
- const derivedSig = signal(store, 'count', (v) => v * 2);
123
- expect(() => derivedSig.set(10 as unknown as number)).toThrow(
124
- 'Storve: cannot call set() on a derived signal. Derived signals are read-only.'
125
- );
126
- });
127
- });
128
-
129
- describe('subscribe() — notification correctness', () => {
130
- it('calls listener with the new value when signal\'s key changes via store.setState', () => {
131
- const sig = signal(store, 'count');
132
- const listener = vi.fn();
133
- unsubscribes.push(sig.subscribe(listener));
134
- store.setState({ count: 1 });
135
- expect(listener).toHaveBeenCalledWith(1);
136
- });
137
-
138
- it('calls listener with the new value when signal\'s key changes via signal.set()', () => {
139
- const sig = signal(store, 'count');
140
- const listener = vi.fn();
141
- unsubscribes.push(sig.subscribe(listener));
142
- sig.set(1);
143
- expect(listener).toHaveBeenCalledWith(1);
144
- });
145
-
146
- it('does NOT call listener when an unrelated key changes', () => {
147
- const sig = signal(store, 'count');
148
- const listener = vi.fn();
149
- unsubscribes.push(sig.subscribe(listener));
150
- store.setState({ name: 'updated' });
151
- expect(listener).not.toHaveBeenCalled();
152
- });
153
-
154
- it('does NOT call listener when setState sets the same primitive value (Object.is equality)', () => {
155
- const sig = signal(store, 'count');
156
- const listener = vi.fn();
157
- unsubscribes.push(sig.subscribe(listener));
158
- store.setState({ count: 0 }); // already 0
159
- expect(listener).not.toHaveBeenCalled();
160
- });
161
-
162
- it('DOES call listener when value changes from 0 to 1', () => {
163
- const sig = signal(store, 'count');
164
- const listener = vi.fn();
165
- unsubscribes.push(sig.subscribe(listener));
166
- store.setState({ count: 1 });
167
- expect(listener).toHaveBeenCalledWith(1);
168
- });
169
-
170
- it('DOES call listener when value changes from null to an object', () => {
171
- interface NullableState { data: { a: number } | null }
172
- const nStore = createStore<NullableState>({ data: null });
173
- const sig = signal(nStore, 'data');
174
- const listener = vi.fn();
175
- unsubscribes.push(sig.subscribe(listener));
176
- nStore.setState({ data: { a: 1 } });
177
- expect(listener).toHaveBeenCalledWith({ a: 1 });
178
- });
179
-
180
- it('DOES call listener when array reference changes (even if contents look same)', () => {
181
- const sig = signal(store, 'items');
182
- const listener = vi.fn();
183
- unsubscribes.push(sig.subscribe(listener));
184
- store.setState({ items: [1, 2, 3] }); // new reference
185
- expect(listener).toHaveBeenCalled();
186
- });
187
-
188
- it('calls listener exactly once per relevant setState — never multiple times', () => {
189
- const sig = signal(store, 'count');
190
- const listener = vi.fn();
191
- unsubscribes.push(sig.subscribe(listener));
192
- store.setState({ count: 1 });
193
- expect(listener).toHaveBeenCalledTimes(1);
194
- });
195
- });
196
-
197
- describe('subscribe() — multiple subscribers', () => {
198
- it('two subscribers on same signal both receive updates', () => {
199
- const sig = signal(store, 'count');
200
- const l1 = vi.fn();
201
- const l2 = vi.fn();
202
- unsubscribes.push(sig.subscribe(l1));
203
- unsubscribes.push(sig.subscribe(l2));
204
- sig.set(1);
205
- expect(l1).toHaveBeenCalledWith(1);
206
- expect(l2).toHaveBeenCalledWith(1);
207
- });
208
-
209
- it('removing one subscriber does not affect the other', () => {
210
- const sig = signal(store, 'count');
211
- const l1 = vi.fn();
212
- const l2 = vi.fn();
213
- const unsub1 = sig.subscribe(l1);
214
- unsubscribes.push(sig.subscribe(l2));
215
- unsub1();
216
- sig.set(1);
217
- expect(l1).not.toHaveBeenCalled();
218
- expect(l2).toHaveBeenCalledWith(1);
219
- });
220
-
221
- it('three subscribers all fire independently', () => {
222
- const sig = signal(store, 'count');
223
- const l1 = vi.fn();
224
- const l2 = vi.fn();
225
- const l3 = vi.fn();
226
- unsubscribes.push(sig.subscribe(l1));
227
- unsubscribes.push(sig.subscribe(l2));
228
- unsubscribes.push(sig.subscribe(l3));
229
- sig.set(5);
230
- expect(l1).toHaveBeenCalledWith(5);
231
- expect(l2).toHaveBeenCalledWith(5);
232
- expect(l3).toHaveBeenCalledWith(5);
233
- });
234
- });
235
-
236
- describe('subscribe() — unsubscribe', () => {
237
- it('returned unsubscribe function stops future notifications', () => {
238
- const sig = signal(store, 'count');
239
- const listener = vi.fn();
240
- const unsub = sig.subscribe(listener);
241
- unsub();
242
- sig.set(1);
243
- expect(listener).not.toHaveBeenCalled();
244
- });
245
-
246
- it('after unsubscribe, store changes do not call the listener', () => {
247
- const sig = signal(store, 'count');
248
- const listener = vi.fn();
249
- const unsub = sig.subscribe(listener);
250
- unsub();
251
- store.setState({ count: 1 });
252
- expect(listener).not.toHaveBeenCalled();
253
- });
254
-
255
- it('unsubscribing twice does not throw', () => {
256
- const sig = signal(store, 'count');
257
- const unsub = sig.subscribe(() => {});
258
- unsub();
259
- expect(() => unsub()).not.toThrow();
260
- });
261
-
262
- it('unsubscribing cleans up the underlying store subscription', () => {
263
- // This is hard to test directly without exposing store internals,
264
- // but we rely on the fact that signal uses store.subscribe.
265
- const sig = signal(store, 'count');
266
- const listener = vi.fn();
267
- const unsub = sig.subscribe(listener);
268
- unsub();
269
- store.setState({ count: 1 });
270
- expect(listener).not.toHaveBeenCalled();
271
- });
272
- });
273
-
274
- describe('subscribe() — listener arguments', () => {
275
- it('listener receives the new value, not the old value', () => {
276
- const sig = signal(store, 'count');
277
- const listener = vi.fn();
278
- unsubscribes.push(sig.subscribe(listener));
279
- sig.set(100);
280
- expect(listener).toHaveBeenCalledWith(100);
281
- });
282
-
283
- it('listener receives the raw value, not the store state object', () => {
284
- const sig = signal(store, 'count');
285
- const listener = vi.fn();
286
- unsubscribes.push(sig.subscribe(listener));
287
- sig.set(42);
288
- expect(listener).toHaveBeenCalledWith(42);
289
- expect(listener).not.toHaveBeenCalledWith(expect.objectContaining({ count: 42 }));
290
- });
291
- });
292
-
293
- describe('_derived flag', () => {
294
- it('is false on standard signal (no transform)', () => {
295
- const sig = signal(store, 'count');
296
- expect(sig._derived).toBe(false);
297
- });
298
-
299
- it('is true on derived signal (with transform)', () => {
300
- const sig = signal(store, 'count', (v) => v * 2);
301
- expect(sig._derived).toBe(true);
302
- });
303
-
304
- it('_derived cannot be set from outside (readonly)', () => {
305
- const sig = signal(store, 'count');
306
- // @ts-expect-error - _derived is readonly
307
- sig._derived = true;
308
- // Assignment silently fails — value remains false
309
- expect(sig._derived).toBe(false);
310
- });
311
- });
312
-
313
- describe('store integrity', () => {
314
- it('signal.set() does not break other store subscribers', () => {
315
- const sig = signal(store, 'count');
316
- const listener = vi.fn();
317
- unsubscribes.push(store.subscribe(listener));
318
- sig.set(1);
319
- expect(listener).toHaveBeenCalled();
320
- expect(store.getState().count).toBe(1);
321
- });
322
-
323
- it('multiple signals on same store do not interfere with each other', () => {
324
- const sigA = signal(store, 'count');
325
- const sigB = signal(store, 'name');
326
- sigA.set(10);
327
- sigB.set('hello');
328
- expect(store.getState().count).toBe(10);
329
- expect(store.getState().name).toBe('hello');
330
- });
331
-
332
- it('signal on key A and signal on key B are fully independent', () => {
333
- const sigA = signal(store, 'count');
334
- const sigB = signal(store, 'name');
335
- const lA = vi.fn();
336
- const lB = vi.fn();
337
- unsubscribes.push(sigA.subscribe(lA));
338
- unsubscribes.push(sigB.subscribe(lB));
339
-
340
- sigA.set(1);
341
- expect(lA).toHaveBeenCalled();
342
- expect(lB).not.toHaveBeenCalled();
343
-
344
- sigB.set('hi');
345
- expect(lB).toHaveBeenCalled();
346
- });
347
- });
348
- });
@@ -1,275 +0,0 @@
1
- /**
2
- * @vitest-environment jsdom
3
- */
4
- import React from 'react';
5
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
- import { render, screen, cleanup, act } from '@testing-library/react';
7
- import { createStore } from '../../src/store';
8
- import { signal } from '../../src/signals/createSignal';
9
- import { useSignal } from '../../src/signals/useSignal';
10
- import type { Signal } from '../../src/signals/index';
11
-
12
- function makeCounter() {
13
- let count = 0;
14
- return {
15
- increment: () => count++,
16
- get: () => count,
17
- reset: () => { count = 0; }
18
- };
19
- }
20
-
21
- function makeTestComponent<T>(sig: Signal<T>, counter: ReturnType<typeof makeCounter>) {
22
- return function TestComponent() {
23
- counter.increment();
24
- const value = useSignal(sig);
25
- return <div data-testid="value">{String(value)}</div>;
26
- };
27
- }
28
-
29
- describe('useSignal() hook', () => {
30
- let store: ReturnType<typeof createStore<{ count: number; name: string; active: boolean }>>;
31
- let counter: ReturnType<typeof makeCounter>;
32
-
33
- beforeEach(() => {
34
- store = createStore({ count: 0, name: 'alice', active: false as boolean });
35
- counter = makeCounter();
36
- counter.reset();
37
- });
38
-
39
- afterEach(() => {
40
- cleanup();
41
- });
42
-
43
- describe('Basic rendering', () => {
44
- it('renders the correct initial value from signal', () => {
45
- const sig = signal(store, 'count');
46
- const TestComp = makeTestComponent(sig, counter);
47
- render(<TestComp />);
48
- expect(screen.getByTestId('value').textContent).toBe('0');
49
- });
50
-
51
- it('renders the correct initial value for a string signal', () => {
52
- const sig = signal(store, 'name');
53
- const TestComp = makeTestComponent(sig, counter);
54
- render(<TestComp />);
55
- expect(screen.getByTestId('value').textContent).toBe('alice');
56
- });
57
-
58
- it('renders the correct initial value for a boolean signal', () => {
59
- const sig = signal(store, 'active');
60
- const TestComp = makeTestComponent(sig, counter);
61
- render(<TestComp />);
62
- expect(screen.getByTestId('value').textContent).toBe('false');
63
- });
64
-
65
- it('renders the correct initial value for a derived signal', () => {
66
- const sig = signal(store, 'count', (v) => v * 2);
67
- const TestComp = makeTestComponent(sig, counter);
68
- render(<TestComp />);
69
- expect(screen.getByTestId('value').textContent).toBe('0');
70
- });
71
-
72
- it('updates the rendered value when signal\'s store key changes via store.setState', () => {
73
- const sig = signal(store, 'count');
74
- const TestComp = makeTestComponent(sig, counter);
75
- render(<TestComp />);
76
- act(() => { store.setState({ count: 1 }); });
77
- expect(screen.getByTestId('value').textContent).toBe('1');
78
- });
79
-
80
- it('updates the rendered value when signal\'s store key changes via signal.set()', () => {
81
- const sig = signal(store, 'count');
82
- const TestComp = makeTestComponent(sig, counter);
83
- render(<TestComp />);
84
- act(() => { sig.set(42); });
85
- expect(screen.getByTestId('value').textContent).toBe('42');
86
- });
87
-
88
- it('updates the rendered value for a derived signal when base key changes', () => {
89
- const sig = signal(store, 'count', (v) => v + 10);
90
- const TestComp = makeTestComponent(sig, counter);
91
- render(<TestComp />);
92
- act(() => { store.setState({ count: 5 }); });
93
- expect(screen.getByTestId('value').textContent).toBe('15');
94
- });
95
- });
96
-
97
- describe('Re-render correctness', () => {
98
- it('component re-renders exactly once on initial mount', () => {
99
- const sig = signal(store, 'count');
100
- const TestComp = makeTestComponent(sig, counter);
101
- render(<TestComp />);
102
- expect(counter.get()).toBe(1);
103
- });
104
-
105
- it('component re-renders exactly once when signal value changes', () => {
106
- const sig = signal(store, 'count');
107
- const TestComp = makeTestComponent(sig, counter);
108
- render(<TestComp />);
109
- // Reseting counter because mount is 1 render
110
- counter.reset();
111
- act(() => { sig.set(1); });
112
- expect(counter.get()).toBe(1);
113
- });
114
-
115
- it('component does NOT re-render when an unrelated store key changes', () => {
116
- const sig = signal(store, 'count');
117
- const TestComp = makeTestComponent(sig, counter);
118
- render(<TestComp />);
119
- expect(counter.get()).toBe(1);
120
- act(() => { store.setState({ name: 'bob' }); });
121
- expect(counter.get()).toBe(1);
122
- });
123
-
124
- it('component does NOT re-render when signal is set to Object.is equal value', () => {
125
- const sig = signal(store, 'count');
126
- const TestComp = makeTestComponent(sig, counter);
127
- render(<TestComp />);
128
- expect(counter.get()).toBe(1);
129
- act(() => { sig.set(0); });
130
- expect(counter.get()).toBe(1);
131
- });
132
-
133
- it('component re-renders only once even after multiple rapid store changes', () => {
134
- const sig = signal(store, 'count');
135
- const TestComp = makeTestComponent(sig, counter);
136
- render(<TestComp />);
137
- counter.reset();
138
- act(() => {
139
- store.setState({ count: 1 });
140
- store.setState({ count: 2 });
141
- store.setState({ count: 3 });
142
- });
143
- expect(counter.get()).toBe(1);
144
- expect(screen.getByTestId('value').textContent).toBe('3');
145
- });
146
-
147
- it('two components using different signals — only the relevant one re-renders', () => {
148
- const sigA = signal(store, 'count');
149
- const sigB = signal(store, 'name');
150
- const counterA = makeCounter();
151
- const counterB = makeCounter();
152
- const CompA = makeTestComponent(sigA, counterA);
153
- const CompB = makeTestComponent(sigB, counterB);
154
-
155
- render(<>
156
- <CompA />
157
- <CompB />
158
- </>);
159
-
160
- expect(counterA.get()).toBe(1);
161
- expect(counterB.get()).toBe(1);
162
-
163
- act(() => { sigA.set(5); });
164
- expect(counterA.get()).toBe(2);
165
- expect(counterB.get()).toBe(1);
166
- });
167
-
168
- it('component using derived signal does NOT re-render when transform output is unchanged even if underlying key changed', () => {
169
- const sig = signal(store, 'count', (v) => v > 10);
170
- const TestComp = makeTestComponent(sig, counter);
171
- render(<TestComp />);
172
- expect(counter.get()).toBe(1);
173
-
174
- act(() => { store.setState({ count: 5 }); }); // false -> false
175
- expect(counter.get()).toBe(1);
176
-
177
- act(() => { store.setState({ count: 15 }); }); // false -> true
178
- expect(counter.get()).toBe(2);
179
- });
180
- });
181
-
182
- describe('Subscription lifecycle', () => {
183
- it('subscribes to signal on mount', () => {
184
- // Signal subscribe is called by useSyncExternalStore
185
- // Hard to check internal signal listener count without exposing it,
186
- // but we can check if it updates.
187
- const sig = signal(store, 'count');
188
- const TestComp = makeTestComponent(sig, counter);
189
- render(<TestComp />);
190
- act(() => { sig.set(1); });
191
- expect(screen.getByTestId('value').textContent).toBe('1');
192
- });
193
-
194
- it('unsubscribes from signal on unmount', () => {
195
- const sig = signal(store, 'count');
196
- const TestComp = makeTestComponent(sig, counter);
197
- const { unmount } = render(<TestComp />);
198
- unmount();
199
- act(() => { sig.set(1); });
200
- // No errors should occur
201
- });
202
-
203
- it('re-mounting the component re-subscribes correctly', () => {
204
- const sig = signal(store, 'count');
205
- const TestComp = makeTestComponent(sig, counter);
206
- const { unmount } = render(<TestComp />);
207
- unmount();
208
- render(<TestComp />);
209
- act(() => { sig.set(5); });
210
- expect(screen.getByTestId('value').textContent).toBe('5');
211
- });
212
- });
213
-
214
- describe('React.StrictMode safety', () => {
215
- it('component renders correctly inside React.StrictMode', () => {
216
- const sig = signal(store, 'count');
217
- const TestComp = makeTestComponent(sig, counter);
218
- render(
219
- <React.StrictMode>
220
- <TestComp />
221
- </React.StrictMode>
222
- );
223
- expect(screen.getByTestId('value').textContent).toBe('0');
224
- });
225
-
226
- it('no duplicate subscriptions leak after StrictMode double-invoke', () => {
227
- const sig = signal(store, 'count');
228
- const TestComp = makeTestComponent(sig, counter);
229
- render(
230
- <React.StrictMode>
231
- <TestComp />
232
- </React.StrictMode>
233
- );
234
- // Even if double-invoked, setting value should result in stable behavior
235
- act(() => { sig.set(1); });
236
- expect(screen.getByTestId('value').textContent).toBe('1');
237
- });
238
- });
239
-
240
- describe('Multiple components', () => {
241
- it('two components each using their own signal update independently', () => {
242
- const sigCount = signal(store, 'count');
243
- const sigName = signal(store, 'name');
244
- const counterCount = makeCounter();
245
- const counterName = makeCounter();
246
- const CompCount = makeTestComponent(sigCount, counterCount);
247
- const CompName = makeTestComponent(sigName, counterName);
248
-
249
- render(<>
250
- <CompCount />
251
- <CompName />
252
- </>);
253
-
254
- act(() => { sigCount.set(10); });
255
- expect(screen.getAllByTestId('value')[0].textContent).toBe('10');
256
- expect(screen.getAllByTestId('value')[1].textContent).toBe('alice');
257
- expect(counterCount.get()).toBe(2);
258
- expect(counterName.get()).toBe(1);
259
- });
260
-
261
- it('10 components all subscribed to same signal — all update when signal changes', () => {
262
- const sig = signal(store, 'count');
263
- const counters = Array.from({ length: 10 }, () => makeCounter());
264
- const Components = counters.map((c) => makeTestComponent(sig, c));
265
-
266
- render(<>{Components.map((Comp, i) => <Comp key={i} />)}</>);
267
-
268
- act(() => { sig.set(99); });
269
- const values = screen.getAllByTestId('value');
270
- expect(values).toHaveLength(10);
271
- values.forEach((v) => expect(v.textContent).toBe('99'));
272
- counters.forEach((c) => expect(c.get()).toBe(2));
273
- });
274
- });
275
- });