a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
at main 277 lines 8.3 kB view raw
1import { mount } from "$core/binder"; 2import { registerPlugin } from "$core/plugin"; 3import { signal } from "$core/signal"; 4import { persistPlugin, registerStorageAdapter } from "$plugins/persist"; 5import { beforeEach, describe, expect, it, vi } from "vitest"; 6 7describe("persist plugin", () => { 8 beforeEach(() => { 9 localStorage.clear(); 10 sessionStorage.clear(); 11 registerPlugin("persist", persistPlugin); 12 }); 13 14 describe("localStorage persistence", () => { 15 it("supports attribute suffix syntax with camelCase signal and storage aliases", async () => { 16 localStorage.setItem("volt:persistedCount", "7"); 17 18 const element = document.createElement("div"); 19 element.dataset["voltPersist:persistedcount"] = "localStorage"; 20 21 const persistedCount = signal(0); 22 mount(element, { persistedCount }); 23 24 await new Promise((resolve) => setTimeout(resolve, 0)); 25 expect(persistedCount.get()).toBe(7); 26 27 persistedCount.set(9); 28 await new Promise((resolve) => setTimeout(resolve, 0)); 29 expect(localStorage.getItem("volt:persistedCount")).toBe("9"); 30 }); 31 32 it("loads persisted value from localStorage on mount", () => { 33 localStorage.setItem("volt:count", "42"); 34 35 const element = document.createElement("div"); 36 element.dataset.voltPersist = "count:local"; 37 38 const count = signal(0); 39 mount(element, { count }); 40 41 expect(count.get()).toBe(42); 42 }); 43 44 it("saves signal value to localStorage on change", async () => { 45 const element = document.createElement("div"); 46 element.dataset.voltPersist = "count:local"; 47 48 const count = signal(0); 49 mount(element, { count }); 50 51 count.set(99); 52 53 await new Promise((resolve) => setTimeout(resolve, 0)); 54 55 expect(localStorage.getItem("volt:count")).toBe("99"); 56 }); 57 58 it("persists string values", async () => { 59 const element = document.createElement("div"); 60 element.dataset.voltPersist = "name:local"; 61 62 const name = signal("Alice"); 63 mount(element, { name }); 64 65 name.set("Bob"); 66 67 await new Promise((resolve) => setTimeout(resolve, 0)); 68 69 expect(localStorage.getItem("volt:name")).toBe("\"Bob\""); 70 }); 71 72 it("persists object values", async () => { 73 const element = document.createElement("div"); 74 element.dataset.voltPersist = "user:local"; 75 76 const user = signal({ name: "Alice", age: 30 }); 77 mount(element, { user }); 78 79 user.set({ name: "Bob", age: 35 }); 80 81 await new Promise((resolve) => setTimeout(resolve, 0)); 82 83 const stored = localStorage.getItem("volt:user"); 84 expect(stored).toBe("{\"name\":\"Bob\",\"age\":35}"); 85 }); 86 87 it("does not override signal if localStorage is empty", () => { 88 const element = document.createElement("div"); 89 element.dataset.voltPersist = "count:local"; 90 91 const count = signal(100); 92 mount(element, { count }); 93 94 expect(count.get()).toBe(100); 95 }); 96 }); 97 98 describe("sessionStorage persistence", () => { 99 it("loads persisted value from sessionStorage on mount", () => { 100 sessionStorage.setItem("volt:sessionData", "123"); 101 102 const element = document.createElement("div"); 103 element.dataset.voltPersist = "sessionData:session"; 104 105 const sessionData = signal(0); 106 mount(element, { sessionData }); 107 108 expect(sessionData.get()).toBe(123); 109 }); 110 111 it("saves signal value to sessionStorage on change", async () => { 112 const element = document.createElement("div"); 113 element.dataset.voltPersist = "sessionData:session"; 114 115 const sessionData = signal(0); 116 mount(element, { sessionData }); 117 118 sessionData.set(456); 119 120 await new Promise((resolve) => setTimeout(resolve, 0)); 121 122 expect(sessionStorage.getItem("volt:sessionData")).toBe("456"); 123 }); 124 }); 125 126 describe("custom storage adapters", () => { 127 it("allows registering custom storage adapter", async () => { 128 const customStore = new Map<string, unknown>(); 129 registerStorageAdapter("custom", { 130 get: (key) => customStore.get(key), 131 set: (key, value) => { 132 customStore.set(key, value); 133 }, 134 remove: (key) => { 135 customStore.delete(key); 136 }, 137 }); 138 139 customStore.set("volt:data", 999); 140 141 const element = document.createElement("div"); 142 element.dataset.voltPersist = "data:custom"; 143 144 const data = signal(0); 145 mount(element, { data }); 146 147 expect(data.get()).toBe(999); 148 149 data.set(777); 150 151 await new Promise((resolve) => setTimeout(resolve, 0)); 152 153 expect(customStore.get("volt:data")).toBe(777); 154 }); 155 156 it("supports async custom adapters", async () => { 157 const customStore = new Map<string, unknown>(); 158 registerStorageAdapter("async", { 159 get: async (key) => { 160 await new Promise((resolve) => setTimeout(resolve, 10)); 161 return customStore.get(key); 162 }, 163 set: async (key, value) => { 164 await new Promise((resolve) => setTimeout(resolve, 10)); 165 customStore.set(key, value); 166 }, 167 remove: async (key) => { 168 await new Promise((resolve) => setTimeout(resolve, 10)); 169 customStore.delete(key); 170 }, 171 }); 172 173 customStore.set("volt:asyncData", 888); 174 175 const element = document.createElement("div"); 176 element.dataset.voltPersist = "asyncData:async"; 177 178 const asyncData = signal(0); 179 mount(element, { asyncData }); 180 181 await new Promise((resolve) => setTimeout(resolve, 20)); 182 183 expect(asyncData.get()).toBe(888); 184 }); 185 }); 186 187 describe("error handling", () => { 188 it("logs error for invalid binding format", () => { 189 const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); 190 const element = document.createElement("div"); 191 element.dataset.voltPersist = "invalidformat"; 192 193 mount(element, {}); 194 195 expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining("Invalid persist binding")); 196 197 errorSpy.mockRestore(); 198 }); 199 200 it("logs error when signal not found", () => { 201 const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); 202 const element = document.createElement("div"); 203 element.dataset.voltPersist = "nonexistent:local"; 204 205 mount(element, {}); 206 207 expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining("Signal \"nonexistent\" not found")); 208 209 errorSpy.mockRestore(); 210 }); 211 212 it("logs error for unknown storage type", () => { 213 const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); 214 const element = document.createElement("div"); 215 element.dataset.voltPersist = "data:unknown"; 216 217 const data = signal(0); 218 mount(element, { data }); 219 220 expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining("Unknown storage type: \"unknown\"")); 221 222 errorSpy.mockRestore(); 223 }); 224 225 it("handles storage adapter errors gracefully", async () => { 226 const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); 227 228 registerStorageAdapter("faulty", { 229 get: () => { 230 throw new Error("Read error"); 231 }, 232 set: () => { 233 throw new Error("Write error"); 234 }, 235 remove: () => {}, 236 }); 237 238 const element = document.createElement("div"); 239 element.dataset.voltPersist = "data:faulty"; 240 241 const data = signal(0); 242 mount(element, { data }); 243 244 await new Promise((resolve) => setTimeout(resolve, 0)); 245 246 expect(errorSpy).toHaveBeenCalled(); 247 248 data.set(1); 249 250 await new Promise((resolve) => setTimeout(resolve, 0)); 251 252 expect(errorSpy).toHaveBeenCalled(); 253 254 errorSpy.mockRestore(); 255 }); 256 }); 257 258 describe("cleanup", () => { 259 it("stops persisting after unmount", async () => { 260 const element = document.createElement("div"); 261 element.dataset.voltPersist = "count:local"; 262 263 const count = signal(0); 264 const cleanup = mount(element, { count }); 265 266 count.set(10); 267 await new Promise((resolve) => setTimeout(resolve, 0)); 268 expect(localStorage.getItem("volt:count")).toBe("10"); 269 270 cleanup(); 271 272 count.set(20); 273 await new Promise((resolve) => setTimeout(resolve, 0)); 274 expect(localStorage.getItem("volt:count")).toBe("10"); 275 }); 276 }); 277});