a reactive (signals based) hypermedia web framework (wip)
stormlightlabs.github.io/volt/
hypermedia
frontend
signals
1import { reactive } from "$core/reactive";
2import { computed, signal } from "$core/signal";
3import {
4 clearRegistry,
5 getAllReactives,
6 getAllSignals,
7 getReactiveInfo,
8 getReactiveMetadata,
9 getRegistryStats,
10 getSignalInfo,
11 getSignalMetadata,
12 nameReactive,
13 nameSignal,
14 registerReactive,
15 registerSignal,
16} from "$debug/registry";
17import { beforeEach, describe, expect, it } from "vitest";
18
19describe("debug/registry", () => {
20 beforeEach(() => {
21 clearRegistry();
22 });
23
24 describe("signal registration", () => {
25 it("registers a signal with metadata", () => {
26 const sig = signal(0);
27 registerSignal(sig, "signal", "count");
28
29 const metadata = getSignalMetadata(sig);
30 expect(metadata).toBeDefined();
31 expect(metadata?.type).toBe("signal");
32 expect(metadata?.name).toBe("count");
33 expect(metadata?.id).toMatch(/^signal-\d+$/);
34 expect(metadata?.createdAt).toBeTypeOf("number");
35 });
36
37 it("registers a signal without a name", () => {
38 const sig = signal(0);
39 registerSignal(sig, "signal");
40
41 const metadata = getSignalMetadata(sig);
42 expect(metadata).toBeDefined();
43 expect(metadata?.type).toBe("signal");
44 expect(metadata?.name).toBeUndefined();
45 });
46
47 it("does not re-register an already registered signal", () => {
48 const sig = signal(0);
49 registerSignal(sig, "signal", "first");
50 const firstMetadata = getSignalMetadata(sig);
51
52 registerSignal(sig, "signal", "second");
53 const secondMetadata = getSignalMetadata(sig);
54
55 expect(firstMetadata).toBe(secondMetadata);
56 expect(secondMetadata?.name).toBe("first");
57 });
58
59 it("registers computed signals with correct type", () => {
60 const comp = computed(() => 5);
61 registerSignal(comp, "computed", "doubled");
62
63 const metadata = getSignalMetadata(comp);
64 expect(metadata?.type).toBe("computed");
65 expect(metadata?.name).toBe("doubled");
66 });
67
68 it("assigns incremental IDs", () => {
69 const sig1 = signal(0);
70 const sig2 = signal(0);
71 const comp = computed(() => 0);
72
73 registerSignal(sig1, "signal");
74 registerSignal(sig2, "signal");
75 registerSignal(comp, "computed");
76
77 const meta1 = getSignalMetadata(sig1);
78 const meta2 = getSignalMetadata(sig2);
79 const meta3 = getSignalMetadata(comp);
80
81 expect(meta1?.id).toBe("signal-1");
82 expect(meta2?.id).toBe("signal-2");
83 expect(meta3?.id).toBe("computed-3");
84 });
85 });
86
87 describe("signal info", () => {
88 it("returns signal info with current value", () => {
89 const sig = signal(42);
90 registerSignal(sig, "signal", "answer");
91
92 const info = getSignalInfo(sig);
93 expect(info).toBeDefined();
94 expect(info?.id).toMatch(/^signal-\d+$/);
95 expect(info?.type).toBe("signal");
96 expect(info?.name).toBe("answer");
97 expect(info?.value).toBe(42);
98 expect(info?.createdAt).toBeTypeOf("number");
99 expect(info?.age).toBeTypeOf("number");
100 expect(info!.age).toBeGreaterThanOrEqual(0);
101 });
102
103 it("returns undefined for unregistered signal", () => {
104 const sig = signal(0);
105 const info = getSignalInfo(sig);
106 expect(info).toBeUndefined();
107 });
108
109 it("reflects updated values", () => {
110 const sig = signal(0);
111 registerSignal(sig, "signal");
112
113 const info1 = getSignalInfo(sig);
114 expect(info1?.value).toBe(0);
115
116 sig.set(10);
117
118 const info2 = getSignalInfo(sig);
119 expect(info2?.value).toBe(10);
120 });
121 });
122
123 describe("signal naming", () => {
124 it("sets name on a registered signal", () => {
125 const sig = signal(0);
126 registerSignal(sig, "signal");
127
128 nameSignal(sig, "mySignal");
129
130 const metadata = getSignalMetadata(sig);
131 expect(metadata?.name).toBe("mySignal");
132 });
133
134 it("updates existing name", () => {
135 const sig = signal(0);
136 registerSignal(sig, "signal", "oldName");
137
138 nameSignal(sig, "newName");
139
140 const metadata = getSignalMetadata(sig);
141 expect(metadata?.name).toBe("newName");
142 });
143
144 it("does nothing for unregistered signal", () => {
145 const sig = signal(0);
146 nameSignal(sig, "test");
147
148 const metadata = getSignalMetadata(sig);
149 expect(metadata).toBeUndefined();
150 });
151 });
152
153 describe("getAllSignals", () => {
154 it("returns all registered signals", () => {
155 const sig1 = signal(1);
156 const sig2 = signal(2);
157 const comp = computed(() => 3);
158
159 registerSignal(sig1, "signal");
160 registerSignal(sig2, "signal");
161 registerSignal(comp, "computed");
162
163 const all = getAllSignals();
164 expect(all).toHaveLength(3);
165 expect(all).toContain(sig1);
166 expect(all).toContain(sig2);
167 expect(all).toContain(comp);
168 });
169
170 it("returns empty array when no signals registered", () => {
171 const all = getAllSignals();
172 expect(all).toEqual([]);
173 });
174
175 it.skip("cleans up garbage collected signals", () => {
176 // NOTE: GC is non-deterministic in test environments
177 // We document expected behavior but can't reliably test it
178 let sig: ReturnType<typeof signal> | null = signal(0);
179 registerSignal(sig, "signal");
180 expect(getAllSignals()).toHaveLength(1);
181
182 sig = null;
183
184 const all = getAllSignals();
185 expect(all).toHaveLength(0);
186 });
187 });
188
189 describe("reactive registration", () => {
190 it("registers a reactive object with metadata", () => {
191 const obj = reactive({ count: 0 });
192 registerReactive(obj, "state");
193
194 const metadata = getReactiveMetadata(obj);
195 expect(metadata).toBeDefined();
196 expect(metadata?.type).toBe("reactive");
197 expect(metadata?.name).toBe("state");
198 expect(metadata?.id).toMatch(/^reactive-\d+$/);
199 expect(metadata?.createdAt).toBeTypeOf("number");
200 });
201
202 it("does not re-register an already registered reactive", () => {
203 const obj = reactive({ count: 0 });
204 registerReactive(obj, "first");
205 const firstMetadata = getReactiveMetadata(obj);
206
207 registerReactive(obj, "second");
208 const secondMetadata = getReactiveMetadata(obj);
209
210 expect(firstMetadata).toBe(secondMetadata);
211 expect(secondMetadata?.name).toBe("first");
212 });
213 });
214
215 describe("reactive info", () => {
216 it("returns reactive info with current value", () => {
217 const obj = reactive({ count: 42 });
218 registerReactive(obj, "state");
219
220 const info = getReactiveInfo(obj);
221 expect(info).toBeDefined();
222 expect(info?.id).toMatch(/^reactive-\d+$/);
223 expect(info?.type).toBe("reactive");
224 expect(info?.name).toBe("state");
225 expect(info?.value).toBe(obj);
226 expect(info?.createdAt).toBeTypeOf("number");
227 expect(info?.age).toBeTypeOf("number");
228 });
229
230 it("returns undefined for unregistered reactive", () => {
231 const obj = reactive({ count: 0 });
232 const info = getReactiveInfo(obj);
233 expect(info).toBeUndefined();
234 });
235 });
236
237 describe("reactive naming", () => {
238 it("sets name on a registered reactive", () => {
239 const obj = reactive({ count: 0 });
240 registerReactive(obj);
241
242 nameReactive(obj, "myState");
243
244 const metadata = getReactiveMetadata(obj);
245 expect(metadata?.name).toBe("myState");
246 });
247
248 it("does nothing for unregistered reactive", () => {
249 const obj = reactive({ count: 0 });
250 nameReactive(obj, "test");
251
252 const metadata = getReactiveMetadata(obj);
253 expect(metadata).toBeUndefined();
254 });
255 });
256
257 describe("getAllReactives", () => {
258 it("returns all registered reactive objects", () => {
259 const obj1 = reactive({ a: 1 });
260 const obj2 = reactive({ b: 2 });
261
262 registerReactive(obj1);
263 registerReactive(obj2);
264
265 const all = getAllReactives();
266 expect(all).toHaveLength(2);
267 expect(all).toContain(obj1);
268 expect(all).toContain(obj2);
269 });
270
271 it("returns empty array when no reactives registered", () => {
272 const all = getAllReactives();
273 expect(all).toEqual([]);
274 });
275
276 it.skip("cleans up garbage collected reactives", () => {
277 let obj: ReturnType<typeof reactive> | null = reactive({ count: 0 });
278 registerReactive(obj);
279 expect(getAllReactives()).toHaveLength(1);
280
281 obj = null;
282
283 const all = getAllReactives();
284 expect(all).toHaveLength(0);
285 });
286 });
287
288 describe("registry stats", () => {
289 it("returns correct counts", () => {
290 const sig1 = signal(1);
291 const sig2 = signal(2);
292 const comp = computed(() => 3);
293 const obj = reactive({ count: 0 });
294
295 registerSignal(sig1, "signal");
296 registerSignal(sig2, "signal");
297 registerSignal(comp, "computed");
298 registerReactive(obj);
299
300 const stats = getRegistryStats();
301 expect(stats.totalSignals).toBe(3);
302 expect(stats.regularSignals).toBe(2);
303 expect(stats.computedSignals).toBe(1);
304 expect(stats.reactiveObjects).toBe(1);
305 });
306
307 it("returns zeros when registry is empty", () => {
308 const stats = getRegistryStats();
309 expect(stats.totalSignals).toBe(0);
310 expect(stats.regularSignals).toBe(0);
311 expect(stats.computedSignals).toBe(0);
312 expect(stats.reactiveObjects).toBe(0);
313 });
314 });
315
316 describe("clearRegistry", () => {
317 it("clears all registered signals and reactives", () => {
318 const sig = signal(0);
319 const obj = reactive({ count: 0 });
320
321 registerSignal(sig, "signal");
322 registerReactive(obj);
323
324 expect(getAllSignals()).toHaveLength(1);
325 expect(getAllReactives()).toHaveLength(1);
326
327 clearRegistry();
328
329 expect(getAllSignals()).toHaveLength(0);
330 expect(getAllReactives()).toHaveLength(0);
331 });
332
333 it("resets ID counter", () => {
334 const sig1 = signal(0);
335 registerSignal(sig1, "signal");
336 const meta1 = getSignalMetadata(sig1);
337
338 clearRegistry();
339
340 const sig2 = signal(0);
341 registerSignal(sig2, "signal");
342 const meta2 = getSignalMetadata(sig2);
343
344 expect(meta1?.id).toBe("signal-1");
345 expect(meta2?.id).toBe("signal-1");
346 });
347 });
348});