a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
at main 9.3 kB view raw
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});