a reactive (signals based) hypermedia web framework (wip)
stormlightlabs.github.io/volt/
hypermedia
frontend
signals
1import { mount, signal } from "$volt";
2import { describe, expect, it } from "vitest";
3
4describe("integration: mount", () => {
5 it("creates a reactive counter", () => {
6 const container = document.createElement("div");
7 container.innerHTML = `
8 <div>
9 <p data-volt-text="count">0</p>
10 <p data-volt-class="countClass">Classes</p>
11 </div>
12 `;
13
14 const count = signal(0);
15 const countClass = signal({ positive: false, zero: true });
16
17 mount(container, { count, countClass });
18
19 const textElement = container.querySelector("p:first-child");
20 const classElement = container.querySelector("p:last-child");
21
22 expect(textElement?.textContent).toBe("0");
23 expect(classElement?.classList.contains("zero")).toBe(true);
24 expect(classElement?.classList.contains("positive")).toBe(false);
25
26 count.set(5);
27 countClass.set({ positive: true, zero: false });
28
29 expect(textElement?.textContent).toBe("5");
30 expect(classElement?.classList.contains("zero")).toBe(false);
31 expect(classElement?.classList.contains("positive")).toBe(true);
32 });
33
34 it("handles complex nested structures", () => {
35 const container = document.createElement("div");
36 container.innerHTML = `
37 <div data-volt-text="title">Title</div>
38 <ul>
39 <li data-volt-text="items.first">First</li>
40 <li data-volt-text="items.second">Second</li>
41 </ul>
42 <footer data-volt-html="footer">Footer</footer>
43 `;
44
45 const title = signal("My App");
46 const items = { first: "Item 1", second: "Item 2" };
47 const footer = signal("<strong>© 2025</strong>");
48
49 mount(container, { title, items, footer });
50
51 expect(container.querySelector("[data-volt-text='title']")?.textContent).toBe("My App");
52 expect(container.querySelector("li:first-child")?.textContent).toBe("Item 1");
53 expect(container.querySelector("li:last-child")?.textContent).toBe("Item 2");
54 expect(container.querySelector("footer")?.innerHTML).toBe("<strong>© 2025</strong>");
55
56 title.set("Updated App");
57 footer.set("<em>New Footer</em>");
58
59 expect(container.querySelector("[data-volt-text='title']")?.textContent).toBe("Updated App");
60 expect(container.querySelector("footer")?.innerHTML).toBe("<em>New Footer</em>");
61 });
62
63 it("properly cleans up all bindings", () => {
64 const container = document.createElement("div");
65 container.innerHTML = `
66 <div data-volt-text="a">A</div>
67 <div data-volt-text="b">B</div>
68 <div data-volt-text="c">C</div>
69 `;
70
71 const a = signal("A");
72 const b = signal("B");
73 const c = signal("C");
74
75 const cleanup = mount(container, { a, b, c });
76
77 const divs = [...container.querySelectorAll("div")];
78 expect(divs[0]?.textContent).toBe("A");
79 expect(divs[1]?.textContent).toBe("B");
80 expect(divs[2]?.textContent).toBe("C");
81
82 a.set("A1");
83 b.set("B1");
84 c.set("C1");
85
86 expect(divs[0]?.textContent).toBe("A1");
87 expect(divs[1]?.textContent).toBe("B1");
88 expect(divs[2]?.textContent).toBe("C1");
89
90 cleanup();
91
92 a.set("A2");
93 b.set("B2");
94 c.set("C2");
95
96 expect(divs[0]?.textContent).toBe("A1");
97 expect(divs[1]?.textContent).toBe("B1");
98 expect(divs[2]?.textContent).toBe("C1");
99 });
100
101 it("supports mixed static and reactive values", () => {
102 const container = document.createElement("div");
103 container.innerHTML = `
104 <h1 data-volt-text="staticTitle">Title</h1>
105 <p data-volt-text="dynamicContent">Content</p>
106 <span data-volt-class="'always-visible'">Visible</span>
107 `;
108
109 const staticTitle = "Welcome";
110 const dynamicContent = signal("Loading...");
111
112 mount(container, { staticTitle, dynamicContent });
113
114 expect(container.querySelector("h1")?.textContent).toBe("Welcome");
115 expect(container.querySelector("p")?.textContent).toBe("Loading...");
116 expect(container.querySelector("span")?.classList.contains("always-visible")).toBe(true);
117
118 dynamicContent.set("Content loaded!");
119 expect(container.querySelector("p")?.textContent).toBe("Content loaded!");
120 });
121});