a reactive (signals based) hypermedia web framework (wip)
stormlightlabs.github.io/volt/
hypermedia
frontend
signals
1import { reactive } from "$core/reactive";
2import { getAllSignals, getReactiveInfo, getSignalInfo } from "$debug/registry";
3import { attachDebugger, debugComputed, debugReactive, debugSignal, vdebugger } from "$vebug";
4import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
6describe("debug API", () => {
7 let _consoleSpy: { log: ReturnType<typeof vi.spyOn> };
8
9 beforeEach(() => {
10 vdebugger.clear();
11 _consoleSpy = { log: vi.spyOn(console, "log").mockImplementation(() => {}) };
12 });
13
14 afterEach(() => {
15 vi.restoreAllMocks();
16 });
17
18 describe("debugSignal", () => {
19 it("creates a signal and registers it", () => {
20 const sig = debugSignal(42, "answer");
21 expect(sig.get()).toBe(42);
22
23 const info = getSignalInfo(sig);
24 expect(info).toBeDefined();
25 expect(info?.name).toBe("answer");
26 expect(info?.type).toBe("signal");
27 });
28
29 it("works without a name", () => {
30 const sig = debugSignal(0);
31 expect(sig.get()).toBe(0);
32
33 const info = getSignalInfo(sig);
34 expect(info).toBeDefined();
35 expect(info?.name).toBeUndefined();
36 });
37
38 it("returns standard Signal interface", () => {
39 const sig = debugSignal(0);
40 expect(sig.get).toBeTypeOf("function");
41 expect(sig.set).toBeTypeOf("function");
42 expect(sig.subscribe).toBeTypeOf("function");
43
44 sig.set(5);
45 expect(sig.get()).toBe(5);
46
47 const callback = vi.fn();
48 const unsubscribe = sig.subscribe(callback);
49 sig.set(10);
50 expect(callback).toHaveBeenCalledWith(10);
51
52 unsubscribe();
53 });
54 });
55
56 describe("debugComputed", () => {
57 it("creates a computed signal and registers it", () => {
58 const count = debugSignal(5);
59 const doubled = debugComputed(() => count.get() * 2, "doubled");
60 expect(doubled.get()).toBe(10);
61
62 const info = getSignalInfo(doubled);
63 expect(info).toBeDefined();
64 expect(info?.name).toBe("doubled");
65 expect(info?.type).toBe("computed");
66 });
67
68 it("works without a name", () => {
69 const count = debugSignal(5);
70 const doubled = debugComputed(() => count.get() * 2);
71 expect(doubled.get()).toBe(10);
72
73 const info = getSignalInfo(doubled);
74 expect(info).toBeDefined();
75 });
76
77 it("recomputes when dependencies change", () => {
78 const count = debugSignal(5);
79 const doubled = debugComputed(() => count.get() * 2);
80 expect(doubled.get()).toBe(10);
81
82 count.set(10);
83 expect(doubled.get()).toBe(20);
84 });
85 });
86
87 describe("debugReactive", () => {
88 it("creates a reactive object and registers it", () => {
89 const state = debugReactive({ count: 42 }, "state");
90 expect(state.count).toBe(42);
91
92 const info = getReactiveInfo(state);
93 expect(info).toBeDefined();
94 expect(info?.name).toBe("state");
95 expect(info?.type).toBe("reactive");
96 });
97
98 it("works without a name", () => {
99 const state = debugReactive({ count: 0 });
100 expect(state.count).toBe(0);
101
102 const info = getReactiveInfo(state);
103 expect(info).toBeDefined();
104 });
105
106 it("maintains reactivity", () => {
107 const state = debugReactive({ count: 0 });
108 const doubled = debugComputed(() => state.count * 2);
109 expect(doubled.get()).toBe(0);
110
111 state.count = 5;
112 expect(doubled.get()).toBe(10);
113 });
114 });
115
116 describe("attachDebugger", () => {
117 it("registers existing signal", () => {
118 const sig = debugSignal(0);
119 const info = getSignalInfo(sig);
120 expect(info).toBeDefined();
121 });
122
123 it("does not re-register already registered signal", () => {
124 const sig = debugSignal(0, "original");
125 attachDebugger(sig, "signal", "new");
126
127 const info = getSignalInfo(sig);
128 expect(info?.name).toBe("original");
129 });
130 });
131
132 describe("vdebugger namespace", () => {
133 it("provides signal creation", () => {
134 const sig = vdebugger.signal(42, "answer");
135 expect(sig.get()).toBe(42);
136
137 const info = getSignalInfo(sig);
138 expect(info?.name).toBe("answer");
139 });
140
141 it("provides computed creation", () => {
142 const count = vdebugger.signal(5);
143 const doubled = vdebugger.computed(() => count.get() * 2, "doubled");
144 expect(doubled.get()).toBe(10);
145 });
146
147 it("provides reactive creation", () => {
148 const state = vdebugger.reactive({ count: 0 }, "state");
149 expect(state.count).toBe(0);
150 });
151
152 it("provides getAllSignals", () => {
153 const sig1 = vdebugger.signal(1);
154 const sig2 = vdebugger.signal(2);
155 const all = vdebugger.getAllSignals();
156 expect(all).toHaveLength(2);
157 expect(all).toContain(sig1);
158 expect(all).toContain(sig2);
159 });
160
161 it("provides getAllReactives", () => {
162 const obj1 = vdebugger.reactive({ a: 1 });
163 const obj2 = vdebugger.reactive({ b: 2 });
164 const all = vdebugger.getAllReactives();
165 expect(all).toHaveLength(2);
166 expect(all).toContain(obj1);
167 expect(all).toContain(obj2);
168 });
169
170 it("provides getSignalInfo", () => {
171 const sig = vdebugger.signal(42, "answer");
172 const info = vdebugger.getSignalInfo(sig);
173 expect(info).toBeDefined();
174 expect(info?.name).toBe("answer");
175 expect(info?.value).toBe(42);
176 });
177
178 it("provides getReactiveInfo", () => {
179 const obj = vdebugger.reactive({ count: 42 }, "state");
180 const info = vdebugger.getReactiveInfo(obj);
181 expect(info).toBeDefined();
182 expect(info?.name).toBe("state");
183 });
184
185 it("provides stats", () => {
186 vdebugger.signal(1);
187 vdebugger.signal(2);
188 vdebugger.computed(() => 3);
189 vdebugger.reactive({});
190
191 const stats = vdebugger.getStats();
192 expect(stats.totalSignals).toBe(3);
193 expect(stats.regularSignals).toBe(2);
194 expect(stats.computedSignals).toBe(1);
195 expect(stats.reactiveObjects).toBe(1);
196 });
197
198 it("provides naming functions", () => {
199 const sig = vdebugger.signal(0);
200 vdebugger.nameSignal(sig, "renamed");
201
202 const info = vdebugger.getSignalInfo(sig);
203 expect(info?.name).toBe("renamed");
204 });
205
206 it("provides graph operations", () => {
207 const a = vdebugger.signal(1, "a");
208 const b = vdebugger.signal(2, "b");
209
210 expect(vdebugger.getDependencies(a)).toEqual([]);
211 expect(vdebugger.getDependents(a)).toEqual([]);
212 expect(vdebugger.getDepth(a)).toBe(0);
213
214 const graph = vdebugger.buildGraph([a, b]);
215 expect(graph.nodes).toHaveLength(2);
216 });
217
218 it("provides logging functions", () => {
219 const sig = vdebugger.signal(42, "answer");
220 expect(() => vdebugger.log(sig)).not.toThrow();
221 expect(() => vdebugger.logAll()).not.toThrow();
222 expect(() => vdebugger.logTable()).not.toThrow();
223 });
224
225 it("provides tracing functions", () => {
226 const sig = vdebugger.signal(0, "count");
227 expect(() => vdebugger.trace(sig)).not.toThrow();
228 expect(() => vdebugger.enableTracing()).not.toThrow();
229 expect(() => vdebugger.disableTracing()).not.toThrow();
230 });
231
232 it("provides watch function", () => {
233 const sig = vdebugger.signal(0, "count");
234 const unwatch = vdebugger.watch(sig);
235 expect(unwatch).toBeTypeOf("function");
236 unwatch();
237 });
238
239 it("provides clear function", () => {
240 vdebugger.signal(1);
241 vdebugger.signal(2);
242
243 expect(getAllSignals()).toHaveLength(2);
244
245 vdebugger.clear();
246
247 expect(getAllSignals()).toHaveLength(0);
248 });
249
250 it("provides attach function", () => {
251 const sig = debugSignal(0);
252 vdebugger.attach(sig, "signal", "attached");
253
254 const info = vdebugger.getSignalInfo(sig);
255 expect(info).toBeDefined();
256 });
257 });
258
259 describe("integration", () => {
260 it("works with non-debug core primitives", () => {
261 const coreReactive = reactive({ count: 0 });
262 const debugCount = debugSignal(5);
263 const sum = debugComputed(() => coreReactive.count + debugCount.get(), "sum");
264 expect(sum.get()).toBe(5);
265
266 coreReactive.count = 10;
267 expect(sum.get()).toBe(15);
268
269 debugCount.set(20);
270 expect(sum.get()).toBe(30);
271 });
272
273 it("allows mixing debug and non-debug signals", () => {
274 const debug = debugSignal(1, "debug");
275 const regular = debugSignal(2);
276 const all = getAllSignals();
277 expect(all).toHaveLength(2);
278 expect(all).toContain(debug);
279 expect(all).toContain(regular);
280 });
281 });
282});