a reactive (signals based) hypermedia web framework (wip)
stormlightlabs.github.io/volt/
hypermedia
frontend
signals
1import { mount } from "$core/binder";
2import { signal } from "$core/signal";
3import { describe, expect, it } from "vitest";
4
5describe("binder", () => {
6 describe("mount", () => {
7 it("returns a cleanup function", () => {
8 const element = document.createElement("div");
9 const cleanup = mount(element, {});
10
11 expect(typeof cleanup).toBe("function");
12 cleanup();
13 });
14
15 it("binds data-volt-text to element text content", () => {
16 const element = document.createElement("div");
17 element.dataset.voltText = "message";
18
19 const scope = { message: "Hello, World!" };
20 mount(element, scope);
21
22 expect(element.textContent).toBe("Hello, World!");
23 });
24
25 it("updates text content when signal changes", () => {
26 const element = document.createElement("div");
27 element.dataset.voltText = "count";
28
29 const count = signal(0);
30 const scope = { count };
31 mount(element, scope);
32
33 expect(element.textContent).toBe("0");
34
35 count.set(5);
36 expect(element.textContent).toBe("5");
37
38 count.set(10);
39 expect(element.textContent).toBe("10");
40 });
41
42 it("binds data-volt-html to element HTML content", () => {
43 const element = document.createElement("div");
44 element.dataset.voltHtml = "content";
45
46 const scope = { content: "<strong>Bold</strong>" };
47 mount(element, scope);
48
49 expect(element.innerHTML).toBe("<strong>Bold</strong>");
50 });
51
52 it("updates HTML content when signal changes", () => {
53 const element = document.createElement("div");
54 element.dataset.voltHtml = "html";
55
56 const html = signal("<em>Italic</em>");
57 const scope = { html };
58 mount(element, scope);
59
60 expect(element.innerHTML).toBe("<em>Italic</em>");
61
62 html.set("<strong>Bold</strong>");
63 expect(element.innerHTML).toBe("<strong>Bold</strong>");
64 });
65
66 it("binds data-volt-class with string value", () => {
67 const element = document.createElement("div");
68 element.dataset.voltClass = "classes";
69
70 const scope = { classes: "active highlight" };
71 mount(element, scope);
72
73 expect(element.classList.contains("active")).toBe(true);
74 expect(element.classList.contains("highlight")).toBe(true);
75 });
76
77 it("binds data-volt-class with object value", () => {
78 const element = document.createElement("div");
79 element.dataset.voltClass = "classes";
80
81 const scope = { classes: { active: true, disabled: false } };
82 mount(element, scope);
83
84 expect(element.classList.contains("active")).toBe(true);
85 expect(element.classList.contains("disabled")).toBe(false);
86 });
87
88 it("updates classes when signal changes", () => {
89 const element = document.createElement("div");
90 element.dataset.voltClass = "classes";
91
92 const classes = signal({ active: false, disabled: false });
93 const scope = { classes };
94 mount(element, scope);
95
96 expect(element.classList.contains("active")).toBe(false);
97
98 classes.set({ active: true, disabled: false });
99 expect(element.classList.contains("active")).toBe(true);
100 expect(element.classList.contains("disabled")).toBe(false);
101
102 classes.set({ active: false, disabled: true });
103 expect(element.classList.contains("active")).toBe(false);
104 expect(element.classList.contains("disabled")).toBe(true);
105 });
106
107 it("removes old classes when signal changes", () => {
108 const element = document.createElement("div");
109 element.dataset.voltClass = "classes";
110
111 const classes = signal("foo bar");
112 const scope = { classes };
113 mount(element, scope);
114
115 expect(element.classList.contains("foo")).toBe(true);
116 expect(element.classList.contains("bar")).toBe(true);
117
118 classes.set("baz");
119 expect(element.classList.contains("foo")).toBe(false);
120 expect(element.classList.contains("bar")).toBe(false);
121 expect(element.classList.contains("baz")).toBe(true);
122 });
123
124 it("binds data-volt-class with multiple signal dependencies", () => {
125 const element = document.createElement("div");
126 element.dataset.voltClass = "{active: isActive, disabled: isDisabled}";
127
128 const isActive = signal(true);
129 const isDisabled = signal(false);
130 const scope = { isActive, isDisabled };
131 mount(element, scope);
132
133 expect(element.classList.contains("active")).toBe(true);
134 expect(element.classList.contains("disabled")).toBe(false);
135
136 isActive.set(false);
137 expect(element.classList.contains("active")).toBe(false);
138 expect(element.classList.contains("disabled")).toBe(false);
139
140 isDisabled.set(true);
141 expect(element.classList.contains("active")).toBe(false);
142 expect(element.classList.contains("disabled")).toBe(true);
143
144 isActive.set(true);
145 isDisabled.set(false);
146 expect(element.classList.contains("active")).toBe(true);
147 expect(element.classList.contains("disabled")).toBe(false);
148 });
149
150 it("binds nested elements", () => {
151 const parent = document.createElement("div");
152 const child1 = document.createElement("span");
153 const child2 = document.createElement("span");
154 parent.append(child1, child2);
155
156 child1.dataset.voltText = "first";
157 child2.dataset.voltText = "second";
158
159 const scope = { first: "First", second: "Second" };
160 mount(parent, scope);
161
162 expect(child1.textContent).toBe("First");
163 expect(child2.textContent).toBe("Second");
164 expect(parent.textContent).toBe("FirstSecond");
165 });
166
167 it("cleans up subscriptions on unmount", () => {
168 const element = document.createElement("div");
169 element.dataset.voltText = "count";
170
171 const count = signal(0);
172 const scope = { count };
173 const cleanup = mount(element, scope);
174
175 count.set(5);
176 expect(element.textContent).toBe("5");
177
178 cleanup();
179
180 count.set(10);
181 expect(element.textContent).toBe("5");
182 });
183
184 it("handles multiple bindings on the same element", () => {
185 const element = document.createElement("div");
186 element.dataset.voltText = "message";
187 element.dataset.voltClass = "classes";
188
189 const message = signal("Hello");
190 const classes = signal("active");
191 const scope = { message, classes };
192 mount(element, scope);
193
194 expect(element.textContent).toBe("Hello");
195 expect(element.classList.contains("active")).toBe(true);
196
197 message.set("Goodbye");
198 classes.set("inactive");
199
200 expect(element.textContent).toBe("Goodbye");
201 expect(element.classList.contains("inactive")).toBe(true);
202 });
203
204 it("evaluates nested property paths", () => {
205 const element = document.createElement("div");
206 element.dataset.voltText = "user.name";
207
208 const scope = { user: { name: "Alice" } };
209 mount(element, scope);
210
211 expect(element.textContent).toBe("Alice");
212 });
213
214 it("handles static values (no signals)", () => {
215 const element = document.createElement("div");
216 element.dataset.voltText = "message";
217
218 const scope = { message: "Static" };
219 mount(element, scope);
220
221 expect(element.textContent).toBe("Static");
222 });
223
224 it("handles literal expressions", () => {
225 const element = document.createElement("div");
226 element.dataset.voltText = "'Hello'";
227
228 mount(element, {});
229
230 expect(element.textContent).toBe("Hello");
231 });
232 });
233});