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