a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 407 lines 11 kB view raw
1/* eslint-disable @typescript-eslint/no-unused-expressions */ 2import { isReactive, reactive, toRaw } from "$core/reactive"; 3import { computed, effect, signal } from "$core/signal"; 4import { describe, expect, it, vi } from "vitest"; 5 6describe("reactive", () => { 7 describe("basic reactivity", () => { 8 it("creates a reactive object", () => { 9 const obj = reactive({ count: 0 }); 10 expect(obj.count).toBe(0); 11 }); 12 13 it("allows setting properties", () => { 14 const obj = reactive({ count: 0 }); 15 obj.count = 5; 16 expect(obj.count).toBe(5); 17 }); 18 19 it("triggers effects when properties change", () => { 20 const obj = reactive({ count: 0 }); 21 const effectFn = vi.fn(() => { 22 obj.count; 23 }); 24 25 effect(effectFn); 26 expect(effectFn).toHaveBeenCalledTimes(1); 27 28 obj.count = 5; 29 expect(effectFn).toHaveBeenCalledTimes(2); 30 }); 31 32 it("works with computed signals", () => { 33 const obj = reactive({ count: 5 }); 34 const doubled = computed(() => obj.count * 2); 35 36 expect(doubled.get()).toBe(10); 37 38 obj.count = 10; 39 expect(doubled.get()).toBe(20); 40 }); 41 42 it("supports multiple properties", () => { 43 const obj = reactive({ a: 1, b: 2 }); 44 const sum = computed(() => obj.a + obj.b); 45 46 expect(sum.get()).toBe(3); 47 48 obj.a = 5; 49 expect(sum.get()).toBe(7); 50 51 obj.b = 10; 52 expect(sum.get()).toBe(15); 53 }); 54 55 it("does not trigger effects when value is the same", () => { 56 const obj = reactive({ count: 0 }); 57 const effectFn = vi.fn(() => { 58 obj.count; 59 }); 60 61 effect(effectFn); 62 expect(effectFn).toHaveBeenCalledTimes(1); 63 64 obj.count = 0; 65 expect(effectFn).toHaveBeenCalledTimes(1); 66 }); 67 68 it("handles property deletion", () => { 69 const obj = reactive({ count: 5 as number | undefined, name: "test" }); 70 const effectFn = vi.fn(() => { 71 obj.count; 72 }); 73 74 effect(effectFn); 75 expect(effectFn).toHaveBeenCalledTimes(1); 76 77 delete obj.count; 78 expect(effectFn).toHaveBeenCalledTimes(2); 79 expect(obj.count).toBeUndefined(); 80 }); 81 82 it("handles 'in' operator", () => { 83 const obj = reactive({ count: 5 }); 84 expect("count" in obj).toBe(true); 85 expect("missing" in obj).toBe(false); 86 }); 87 }); 88 89 describe("nested reactivity", () => { 90 it("makes nested objects reactive", () => { 91 const obj = reactive({ nested: { count: 0 } }); 92 const effectFn = vi.fn(() => { 93 obj.nested.count; 94 }); 95 96 effect(effectFn); 97 expect(effectFn).toHaveBeenCalledTimes(1); 98 99 obj.nested.count = 5; 100 expect(effectFn).toHaveBeenCalledTimes(2); 101 }); 102 103 it("handles deeply nested objects", () => { 104 const obj = reactive({ a: { b: { c: 0 } } }); 105 const effectFn = vi.fn(() => { 106 obj.a.b.c; 107 }); 108 109 effect(effectFn); 110 expect(effectFn).toHaveBeenCalledTimes(1); 111 112 obj.a.b.c = 5; 113 expect(effectFn).toHaveBeenCalledTimes(2); 114 }); 115 116 it("replaces nested objects reactively", () => { 117 const obj = reactive({ nested: { count: 0 } }); 118 const effectFn = vi.fn(() => { 119 obj.nested.count; 120 }); 121 122 effect(effectFn); 123 expect(effectFn).toHaveBeenCalledTimes(1); 124 125 obj.nested = { count: 10 }; 126 expect(effectFn).toHaveBeenCalledTimes(2); 127 expect(obj.nested.count).toBe(10); 128 }); 129 }); 130 131 describe("array reactivity", () => { 132 it("creates a reactive array", () => { 133 const arr = reactive([1, 2, 3]); 134 expect(arr[0]).toBe(1); 135 expect(arr.length).toBe(3); 136 }); 137 138 it("triggers effects on array index changes", () => { 139 const arr = reactive([1, 2, 3]); 140 const effectFn = vi.fn(() => { 141 arr[0]; 142 }); 143 144 effect(effectFn); 145 expect(effectFn).toHaveBeenCalledTimes(1); 146 147 arr[0] = 10; 148 expect(effectFn).toHaveBeenCalledTimes(2); 149 }); 150 151 it("triggers effects on push", () => { 152 const arr = reactive([1, 2, 3]); 153 const effectFn = vi.fn(() => { 154 arr.length; 155 }); 156 157 effect(effectFn); 158 expect(effectFn).toHaveBeenCalledTimes(1); 159 160 arr.push(4); 161 expect(effectFn).toHaveBeenCalledTimes(2); 162 expect(arr.length).toBe(4); 163 expect(arr[3]).toBe(4); 164 }); 165 166 it("triggers effects on pop", () => { 167 const arr = reactive([1, 2, 3]); 168 const effectFn = vi.fn(() => { 169 arr.length; 170 }); 171 172 effect(effectFn); 173 expect(effectFn).toHaveBeenCalledTimes(1); 174 175 const popped = arr.pop(); 176 expect(popped).toBe(3); 177 expect(effectFn).toHaveBeenCalledTimes(2); 178 expect(arr.length).toBe(2); 179 }); 180 181 it("triggers effects on shift", () => { 182 const arr = reactive([1, 2, 3]); 183 const effectFn = vi.fn(() => { 184 arr.length; 185 }); 186 187 effect(effectFn); 188 expect(effectFn).toHaveBeenCalledTimes(1); 189 190 const shifted = arr.shift(); 191 expect(shifted).toBe(1); 192 expect(effectFn).toHaveBeenCalledTimes(2); 193 expect(arr.length).toBe(2); 194 }); 195 196 it("triggers effects on unshift", () => { 197 const arr = reactive([1, 2, 3]); 198 const effectFn = vi.fn(() => { 199 arr.length; 200 }); 201 202 effect(effectFn); 203 expect(effectFn).toHaveBeenCalledTimes(1); 204 205 arr.unshift(0); 206 expect(effectFn).toHaveBeenCalledTimes(2); 207 expect(arr.length).toBe(4); 208 expect(arr[0]).toBe(0); 209 }); 210 211 it("triggers effects on splice", () => { 212 const arr = reactive([1, 2, 3, 4, 5]); 213 const effectFn = vi.fn(() => { 214 arr[2]; 215 }); 216 217 effect(effectFn); 218 expect(effectFn).toHaveBeenCalledTimes(1); 219 220 arr.splice(2, 1, 10); 221 expect(effectFn).toHaveBeenCalledTimes(2); 222 expect(arr).toEqual([1, 2, 10, 4, 5]); 223 }); 224 225 it("triggers effects on sort", () => { 226 const arr = reactive([3, 1, 2]); 227 const effectFn = vi.fn(() => { 228 arr[0]; 229 }); 230 231 effect(effectFn); 232 expect(effectFn).toHaveBeenCalledTimes(1); 233 234 arr.sort(); 235 expect(effectFn).toHaveBeenCalledTimes(2); 236 expect(arr).toEqual([1, 2, 3]); 237 }); 238 239 it("triggers effects on reverse", () => { 240 const arr = reactive([1, 2, 3]); 241 const effectFn = vi.fn(() => { 242 arr[0]; 243 }); 244 245 effect(effectFn); 246 expect(effectFn).toHaveBeenCalledTimes(1); 247 248 arr.reverse(); 249 expect(effectFn).toHaveBeenCalledTimes(2); 250 expect(arr).toEqual([3, 2, 1]); 251 }); 252 253 it("handles nested arrays", () => { 254 const arr = reactive([[1, 2], [3, 4]]); 255 const effectFn = vi.fn(() => { 256 arr[0][0]; 257 }); 258 259 effect(effectFn); 260 expect(effectFn).toHaveBeenCalledTimes(1); 261 262 arr[0][0] = 10; 263 expect(effectFn).toHaveBeenCalledTimes(2); 264 }); 265 }); 266 267 describe("isReactive and toRaw", () => { 268 it("identifies reactive objects", () => { 269 const obj = reactive({ count: 0 }); 270 expect(isReactive(obj)).toBe(true); 271 }); 272 273 it("returns false for non-reactive objects", () => { 274 const obj = { count: 0 }; 275 expect(isReactive(obj)).toBe(false); 276 }); 277 278 it("returns false for primitives", () => { 279 expect(isReactive(5)).toBe(false); 280 expect(isReactive("test")).toBe(false); 281 expect(isReactive(null)).toBe(false); 282 expect(isReactive(void 0)).toBe(false); 283 }); 284 285 it("returns the raw object from a reactive proxy", () => { 286 const original = { count: 0 }; 287 const obj = reactive(original); 288 const raw = toRaw(obj); 289 290 expect(raw).toBe(original); 291 expect(isReactive(raw)).toBe(false); 292 }); 293 294 it("returns the value as-is for non-reactive values", () => { 295 const obj = { count: 0 }; 296 expect(toRaw(obj)).toBe(obj); 297 expect(toRaw(5)).toBe(5); 298 }); 299 }); 300 301 describe("edge cases", () => { 302 it("returns the same proxy for the same object", () => { 303 const original = { count: 0 }; 304 const proxy1 = reactive(original); 305 const proxy2 = reactive(original); 306 307 expect(proxy1).toBe(proxy2); 308 }); 309 310 it("does not double-wrap reactive objects", () => { 311 const obj = reactive({ count: 0 }); 312 const obj2 = reactive(obj); 313 314 expect(obj).toBe(obj2); 315 }); 316 317 it("handles null and undefined gracefully", () => { 318 expect(reactive(null as unknown as object)).toBe(null); 319 expect(reactive(undefined as unknown as object)).toBe(undefined); 320 }); 321 322 it("handles primitive values gracefully", () => { 323 expect(reactive(5 as unknown as object)).toBe(5); 324 expect(reactive("test" as unknown as object)).toBe("test"); 325 }); 326 327 it("works with mixed signal and reactive", () => { 328 const sig = signal(5); 329 const obj = reactive({ count: 0 }); 330 331 const total = computed(() => sig.get() + obj.count); 332 333 expect(total.get()).toBe(5); 334 335 sig.set(10); 336 expect(total.get()).toBe(10); 337 338 obj.count = 5; 339 expect(total.get()).toBe(15); 340 }); 341 342 it("handles circular references", () => { 343 const obj: { self?: unknown } = reactive({ self: undefined }); 344 obj.self = obj; 345 346 expect(obj.self).toBe(obj); 347 }); 348 349 it("supports non-enumerable properties", () => { 350 const original: { hidden?: number } = {}; 351 Object.defineProperty(original, "hidden", { value: 42, enumerable: false }); 352 353 const obj = reactive(original); 354 expect(obj.hidden).toBe(42); 355 }); 356 }); 357 358 describe("integration with existing reactivity", () => { 359 it("works in computed chains", () => { 360 const obj = reactive({ a: 1, b: 2 }); 361 const sum = computed(() => obj.a + obj.b); 362 const doubled = computed(() => sum.get() * 2); 363 364 expect(doubled.get()).toBe(6); 365 366 obj.a = 5; 367 expect(sum.get()).toBe(7); 368 expect(doubled.get()).toBe(14); 369 }); 370 371 it("works with multiple effects", () => { 372 const obj = reactive({ count: 0 }); 373 const effect1 = vi.fn(() => { 374 obj.count; 375 }); 376 const effect2 = vi.fn(() => { 377 obj.count; 378 }); 379 380 effect(effect1); 381 effect(effect2); 382 383 expect(effect1).toHaveBeenCalledTimes(1); 384 expect(effect2).toHaveBeenCalledTimes(1); 385 386 obj.count = 5; 387 388 expect(effect1).toHaveBeenCalledTimes(2); 389 expect(effect2).toHaveBeenCalledTimes(2); 390 }); 391 392 it("allows unsubscribing from effects", () => { 393 const obj = reactive({ count: 0 }); 394 const effectFn = vi.fn(() => { 395 obj.count; 396 }); 397 398 const cleanup = effect(effectFn); 399 expect(effectFn).toHaveBeenCalledTimes(1); 400 401 cleanup(); 402 403 obj.count = 5; 404 expect(effectFn).toHaveBeenCalledTimes(1); 405 }); 406 }); 407});