a reactive (signals based) hypermedia web framework (wip)
stormlightlabs.github.io/volt/
hypermedia
frontend
signals
1import { mount } from "$core/binder";
2import { extractDeps } from "$core/shared";
3import { signal } from "$core/signal";
4import { describe, expect, it } from "vitest";
5
6describe("data-volt-if binding", () => {
7 it("shows element when condition is truthy", () => {
8 const container = document.createElement("div");
9 container.innerHTML = `
10 <div>
11 <p data-volt-if="show" data-volt-text="message">Hidden</p>
12 </div>
13 `;
14
15 const show = signal(true);
16 const message = "Visible!";
17
18 mount(container, { show, message });
19
20 const paragraph = container.querySelector("p");
21 expect(paragraph).toBeTruthy();
22 expect(paragraph?.textContent).toBe("Visible!");
23 });
24
25 it("hides element when condition is falsy", () => {
26 const container = document.createElement("div");
27 container.innerHTML = `
28 <div>
29 <p data-volt-if="show">Should not appear</p>
30 </div>
31 `;
32
33 const show = signal(false);
34 mount(container, { show });
35
36 const paragraph = container.querySelector("p");
37 expect(paragraph).toBeNull();
38 });
39
40 it("toggles element visibility when signal changes", () => {
41 const container = document.createElement("div");
42 container.innerHTML = `
43 <div>
44 <span data-volt-if="visible">Toggle Me</span>
45 </div>
46 `;
47
48 const visible = signal(false);
49 mount(container, { visible });
50
51 expect(container.querySelector("span")).toBeNull();
52
53 visible.set(true);
54 expect(container.querySelector("span")).toBeTruthy();
55 expect(container.querySelector("span")?.textContent).toBe("Toggle Me");
56
57 visible.set(false);
58 expect(container.querySelector("span")).toBeNull();
59
60 visible.set(true);
61 expect(container.querySelector("span")).toBeTruthy();
62 });
63
64 it("works with static truthy values", () => {
65 const container = document.createElement("div");
66 container.innerHTML = `
67 <div>
68 <p data-volt-if="alwaysTrue">Always visible</p>
69 </div>
70 `;
71
72 mount(container, { alwaysTrue: true });
73
74 const paragraph = container.querySelector("p");
75 expect(paragraph).toBeTruthy();
76 expect(paragraph?.textContent).toBe("Always visible");
77 });
78
79 it("works with static falsy values", () => {
80 const container = document.createElement("div");
81 container.innerHTML = `
82 <div>
83 <p data-volt-if="alwaysFalse">Never visible</p>
84 </div>
85 `;
86
87 mount(container, { alwaysFalse: false });
88
89 expect(container.querySelector("p")).toBeNull();
90 });
91
92 it("preserves element bindings when re-rendering", () => {
93 const container = document.createElement("div");
94 container.innerHTML = `
95 <div>
96 <div data-volt-if="show">
97 <span data-volt-text="message">Default</span>
98 </div>
99 </div>
100 `;
101
102 const show = signal(true);
103 const message = signal("First");
104
105 mount(container, { show, message });
106
107 expect(container.querySelector("span")?.textContent).toBe("First");
108
109 message.set("Second");
110 expect(container.querySelector("span")?.textContent).toBe("Second");
111
112 show.set(false);
113 expect(container.querySelector("div[data-volt-if]")).toBeNull();
114
115 message.set("Third");
116 show.set(true);
117
118 expect(container.querySelector("span")?.textContent).toBe("Third");
119 });
120
121 it("handles nested bindings correctly", () => {
122 const container = document.createElement("div");
123 container.innerHTML = `
124 <div>
125 <div data-volt-if="showOuter">
126 <p>Outer</p>
127 <div data-volt-if="showInner">
128 <p>Inner</p>
129 </div>
130 </div>
131 </div>
132 `;
133
134 const showOuter = signal(true);
135 const showInner = signal(true);
136
137 mount(container, { showOuter, showInner });
138
139 expect(container.textContent?.trim()).toContain("Outer");
140 expect(container.textContent?.trim()).toContain("Inner");
141
142 showInner.set(false);
143 expect(container.textContent?.trim()).toContain("Outer");
144 expect(container.textContent?.trim()).not.toContain("Inner");
145
146 showOuter.set(false);
147 expect(container.textContent?.trim()).not.toContain("Outer");
148 expect(container.textContent?.trim()).not.toContain("Inner");
149
150 showOuter.set(true);
151 expect(container.textContent?.trim()).toContain("Outer");
152 expect(container.textContent?.trim()).not.toContain("Inner");
153
154 showInner.set(true);
155 expect(container.textContent?.trim()).toContain("Outer");
156 expect(container.textContent?.trim()).toContain("Inner");
157 });
158
159 it("properly cleans up when unmounting", () => {
160 const container = document.createElement("div");
161 container.innerHTML = `
162 <div>
163 <p data-volt-if="show" data-volt-text="message">Hidden</p>
164 </div>
165 `;
166
167 const show = signal(true);
168 const message = signal("Hello");
169
170 const cleanup = mount(container, { show, message });
171
172 expect(container.querySelector("p")?.textContent).toBe("Hello");
173
174 const elementBeforeCleanup = container.querySelector("p");
175
176 cleanup();
177
178 message.set("Changed");
179 expect(elementBeforeCleanup?.textContent).toBe("Hello");
180
181 show.set(false);
182 expect(container.querySelector("p")).toBe(elementBeforeCleanup);
183 });
184
185 it("handles event handlers correctly", () => {
186 const container = document.createElement("div");
187 container.innerHTML = `
188 <div>
189 <button data-volt-if="show" data-volt-on-click="handleClick">Click Me</button>
190 </div>
191 `;
192
193 let clicked = false;
194 const handleClick = () => {
195 clicked = true;
196 };
197 const show = signal(true);
198
199 mount(container, { show, handleClick });
200
201 const button = container.querySelector("button");
202 expect(button).toBeTruthy();
203
204 button?.click();
205 expect(clicked).toBe(true);
206
207 show.set(false);
208 expect(container.querySelector("button")).toBeNull();
209
210 clicked = false;
211 show.set(true);
212 container.querySelector("button")?.click();
213 expect(clicked).toBe(true);
214 });
215
216 it("works with property paths", () => {
217 const container = document.createElement("div");
218 container.innerHTML = `
219 <div>
220 <p data-volt-if="user.isActive">User is active</p>
221 </div>
222 `;
223
224 const user = { isActive: signal(true) };
225 mount(container, { user });
226
227 expect(container.querySelector("p")).toBeTruthy();
228
229 user.isActive.set(false);
230 expect(container.querySelector("p")).toBeNull();
231
232 user.isActive.set(true);
233 expect(container.querySelector("p")).toBeTruthy();
234 });
235
236 it("evaluates truthy and falsy values correctly", () => {
237 const container = document.createElement("div");
238 container.innerHTML = `
239 <div>
240 <p id="zero" data-volt-if="zero">0</p>
241 <p id="empty" data-volt-if="empty">Empty</p>
242 <p id="one" data-volt-if="one">1</p>
243 <p id="string" data-volt-if="string">String</p>
244 </div>
245 `;
246
247 mount(container, { zero: signal(0), empty: signal(""), one: signal(1), string: signal("text") });
248
249 expect(container.querySelector("#zero")).toBeNull();
250 expect(container.querySelector("#empty")).toBeNull();
251 expect(container.querySelector("#one")).toBeTruthy();
252 expect(container.querySelector("#string")).toBeTruthy();
253 });
254
255 it("reacts to complex expressions with multiple signal dependencies", () => {
256 const container = document.createElement("div");
257 container.innerHTML = `
258 <div>
259 <p data-volt-if="value.length > 0 && !isValid">Error message</p>
260 </div>
261 `;
262
263 const value = signal("");
264 const isValid = signal(true);
265 const scope = { value, isValid };
266 const deps = extractDeps("value.length > 0 && !isValid", scope);
267 expect(deps.length).toBe(2);
268 expect(deps).toContain(value);
269 expect(deps).toContain(isValid);
270
271 mount(container, scope);
272 expect(container.querySelector("p")).toBeNull();
273
274 value.set("test");
275 expect(container.querySelector("p")).toBeNull();
276
277 isValid.set(false);
278 expect(container.querySelector("p")).toBeTruthy();
279 expect(container.querySelector("p")?.textContent).toBe("Error message");
280
281 value.set("");
282 expect(container.querySelector("p")).toBeNull();
283
284 value.set("test");
285 expect(container.querySelector("p")).toBeTruthy();
286
287 isValid.set(true);
288 expect(container.querySelector("p")).toBeNull();
289 });
290});