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 { recordDependencies } from "$debug/graph";
4import {
5 disableGlobalTracing,
6 enableGlobalTracing,
7 logAllReactives,
8 logAllSignals,
9 logReactive,
10 logSignal,
11 logSignalTable,
12 trace,
13 watch,
14} from "$debug/logger";
15import { clearRegistry, registerReactive, registerSignal } from "$debug/registry";
16import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
17
18describe("debug/logger", () => {
19 let consoleSpy: {
20 log: ReturnType<typeof vi.spyOn>;
21 group: ReturnType<typeof vi.spyOn>;
22 groupEnd: ReturnType<typeof vi.spyOn>;
23 table: ReturnType<typeof vi.spyOn>;
24 };
25
26 beforeEach(() => {
27 clearRegistry();
28 consoleSpy = {
29 log: vi.spyOn(console, "log").mockImplementation(() => {}),
30 group: vi.spyOn(console, "group").mockImplementation(() => {}),
31 groupEnd: vi.spyOn(console, "groupEnd").mockImplementation(() => {}),
32 table: vi.spyOn(console, "table").mockImplementation(() => {}),
33 };
34 });
35
36 afterEach(() => {
37 vi.restoreAllMocks();
38 });
39
40 describe("logSignal", () => {
41 it("logs signal information", () => {
42 const sig = signal(42);
43 registerSignal(sig, "signal", "answer");
44 logSignal(sig);
45 expect(consoleSpy.group).toHaveBeenCalledWith(expect.stringContaining("answer"));
46 expect(consoleSpy.log).toHaveBeenCalledWith("Type:", "signal");
47 expect(consoleSpy.log).toHaveBeenCalledWith("Value:", 42);
48 expect(consoleSpy.groupEnd).toHaveBeenCalled();
49 });
50
51 it("logs unnamed signal with ID", () => {
52 const sig = signal(0);
53 registerSignal(sig, "signal");
54 logSignal(sig);
55 expect(consoleSpy.group).toHaveBeenCalledWith(expect.stringMatching(/signal-\d+/));
56 });
57
58 it("logs dependencies and dependents", () => {
59 const a = signal(1);
60 const b = signal(2);
61 const sum = computed(() => a.get() + b.get());
62
63 registerSignal(a, "signal", "a");
64 registerSignal(b, "signal", "b");
65 registerSignal(sum, "computed", "sum");
66
67 recordDependencies(sum, [a, b]);
68
69 logSignal(sum);
70
71 expect(consoleSpy.log).toHaveBeenCalledWith("Dependencies:", 2);
72 expect(consoleSpy.log).toHaveBeenCalledWith("Dependents:", 0);
73 expect(consoleSpy.group).toHaveBeenCalledWith("Depends on:");
74 });
75
76 it("logs message for unregistered signal", () => {
77 const sig = signal(0);
78 logSignal(sig);
79 expect(consoleSpy.log).toHaveBeenCalledWith("[Volt Debug] Unregistered signal");
80 });
81 });
82
83 describe("logAllSignals", () => {
84 it("logs all registered signals", () => {
85 const sig1 = signal(1);
86 const sig2 = signal(2);
87
88 registerSignal(sig1, "signal", "first");
89 registerSignal(sig2, "signal", "second");
90 logAllSignals();
91
92 expect(consoleSpy.group).toHaveBeenCalledWith(expect.stringContaining("All Signals (2)"));
93 expect(consoleSpy.log).toHaveBeenCalledWith(expect.stringContaining("first"));
94 expect(consoleSpy.log).toHaveBeenCalledWith(expect.stringContaining("second"));
95 expect(consoleSpy.groupEnd).toHaveBeenCalled();
96 });
97
98 it("handles empty signal list", () => {
99 logAllSignals();
100 expect(consoleSpy.group).toHaveBeenCalledWith(expect.stringContaining("All Signals (0)"));
101 });
102 });
103
104 describe("logReactive", () => {
105 it("logs reactive object information", () => {
106 const obj = reactive({ count: 42 });
107 registerReactive(obj, "state");
108 logReactive(obj);
109 expect(consoleSpy.group).toHaveBeenCalledWith(expect.stringContaining("state"));
110 expect(consoleSpy.log).toHaveBeenCalledWith("Type:", "reactive");
111 expect(consoleSpy.log).toHaveBeenCalledWith("Value:", obj);
112 expect(consoleSpy.groupEnd).toHaveBeenCalled();
113 });
114
115 it("logs message for unregistered reactive", () => {
116 const obj = reactive({ count: 0 });
117 logReactive(obj);
118 expect(consoleSpy.log).toHaveBeenCalledWith("[Volt Debug] Unregistered reactive object");
119 });
120 });
121
122 describe("logAllReactives", () => {
123 it("logs all registered reactive objects", () => {
124 const obj1 = reactive({ a: 1 });
125 const obj2 = reactive({ b: 2 });
126
127 registerReactive(obj1, "first");
128 registerReactive(obj2, "second");
129
130 logAllReactives();
131
132 expect(consoleSpy.group).toHaveBeenCalledWith(expect.stringContaining("All Reactive Objects (2)"));
133 expect(consoleSpy.log).toHaveBeenCalledWith(expect.stringContaining("first"));
134 expect(consoleSpy.log).toHaveBeenCalledWith(expect.stringContaining("second"));
135 expect(consoleSpy.groupEnd).toHaveBeenCalled();
136 });
137 });
138
139 describe("logSignalTable", () => {
140 it("logs signals as a table", () => {
141 const sig1 = signal(1);
142 const sig2 = signal(2);
143
144 registerSignal(sig1, "signal", "first");
145 registerSignal(sig2, "signal", "second");
146
147 logSignalTable();
148
149 expect(consoleSpy.table).toHaveBeenCalledWith(
150 expect.arrayContaining([
151 expect.objectContaining({ Name: "first", Type: "signal" }),
152 expect.objectContaining({ Name: "second", Type: "signal" }),
153 ]),
154 );
155 });
156
157 it("handles empty signal list", () => {
158 logSignalTable();
159
160 expect(consoleSpy.table).toHaveBeenCalledWith([]);
161 });
162
163 it("includes dependency counts in table", () => {
164 const a = signal(1);
165 const double = computed(() => a.get() * 2);
166
167 registerSignal(a, "signal", "a");
168 registerSignal(double, "computed", "double");
169 recordDependencies(double, [a]);
170
171 logSignalTable();
172
173 expect(consoleSpy.table).toHaveBeenCalledWith(
174 expect.arrayContaining([
175 expect.objectContaining({ Name: "a", Dependencies: 0, Dependents: 1 }),
176 expect.objectContaining({ Name: "double", Dependencies: 1, Dependents: 0 }),
177 ]),
178 );
179 });
180 });
181
182 describe("trace", () => {
183 it("enables tracing for a signal", () => {
184 const sig = signal(0);
185 registerSignal(sig, "signal", "count");
186
187 trace(sig);
188
189 expect(consoleSpy.log).toHaveBeenCalledWith(expect.stringContaining("Tracing enabled for count"));
190 });
191
192 it("logs updates with new value", () => {
193 const sig = signal(0);
194 registerSignal(sig, "signal", "count");
195
196 trace(sig);
197 consoleSpy.log.mockClear();
198
199 sig.set(5);
200
201 expect(consoleSpy.log).toHaveBeenCalledWith(
202 expect.stringContaining("[Volt Trace]"),
203 expect.anything(),
204 expect.anything(),
205 );
206 });
207
208 it("does not duplicate tracing for same signal", () => {
209 const sig = signal(0);
210 registerSignal(sig, "signal", "count");
211
212 trace(sig);
213 consoleSpy.log.mockClear();
214
215 trace(sig);
216
217 expect(consoleSpy.log).not.toHaveBeenCalled();
218 });
219
220 it.skip("disables tracing when enabled is false", () => {
221 // TODO: implement tracing unsubscription
222 const sig = signal(0);
223 registerSignal(sig, "signal", "count");
224
225 trace(sig, true);
226 trace(sig, false);
227 consoleSpy.log.mockClear();
228
229 sig.set(5);
230
231 expect(consoleSpy.log).not.toHaveBeenCalled();
232 });
233 });
234
235 describe("watch", () => {
236 it("logs updates with full signal info", () => {
237 const sig = signal(0);
238 registerSignal(sig, "signal", "count");
239
240 watch(sig);
241 consoleSpy.group.mockClear();
242 consoleSpy.log.mockClear();
243
244 sig.set(5);
245
246 expect(consoleSpy.group).toHaveBeenCalledWith(expect.stringContaining("[Volt Watch]"));
247 expect(consoleSpy.log).toHaveBeenCalledWith("New value:", 5);
248 });
249
250 it("returns unsubscribe function", () => {
251 const sig = signal(0);
252 registerSignal(sig, "signal", "count");
253
254 const unsubscribe = watch(sig);
255 unsubscribe();
256
257 consoleSpy.group.mockClear();
258 consoleSpy.log.mockClear();
259
260 sig.set(5);
261
262 expect(consoleSpy.group).not.toHaveBeenCalled();
263 });
264
265 it("logs unwatch message", () => {
266 const sig = signal(0);
267 registerSignal(sig, "signal", "count");
268
269 const unsubscribe = watch(sig);
270 consoleSpy.log.mockClear();
271
272 unsubscribe();
273
274 expect(consoleSpy.log).toHaveBeenCalledWith(expect.stringContaining("Stopped watching count"));
275 });
276 });
277
278 describe("global tracing", () => {
279 it("enables tracing for all signals", () => {
280 const sig1 = signal(0);
281 const sig2 = signal(0);
282
283 registerSignal(sig1, "signal", "first");
284 registerSignal(sig2, "signal", "second");
285
286 enableGlobalTracing();
287 consoleSpy.log.mockClear();
288
289 sig1.set(1);
290 sig2.set(2);
291
292 expect(consoleSpy.log).toHaveBeenCalledWith(
293 expect.stringContaining("first"),
294 expect.anything(),
295 expect.anything(),
296 );
297 expect(consoleSpy.log).toHaveBeenCalledWith(
298 expect.stringContaining("second"),
299 expect.anything(),
300 expect.anything(),
301 );
302 });
303
304 it.skip("disables tracing for all signals", () => {
305 const sig = signal(0);
306 registerSignal(sig, "signal", "count");
307
308 enableGlobalTracing();
309 disableGlobalTracing();
310 consoleSpy.log.mockClear();
311
312 sig.set(5);
313
314 expect(consoleSpy.log).not.toHaveBeenCalled();
315 });
316 });
317});