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 246 lines 8.2 kB view raw
1import { mount } from "$core/binder"; 2import { signal } from "$core/signal"; 3import { describe, expect, it } from "vitest"; 4 5describe("data-volt-for binding", () => { 6 it("renders a list from array signal", () => { 7 const container = document.createElement("div"); 8 container.innerHTML = `<ul><li data-volt-for="item in items" data-volt-text="item"></li></ul>`; 9 10 const items = signal(["apple", "banana", "cherry"]); 11 mount(container, { items }); 12 13 const ul = container.querySelector("ul")!; 14 const listItems = ul.querySelectorAll("li"); 15 16 expect(listItems.length).toBe(3); 17 expect(listItems[0]?.textContent).toBe("apple"); 18 expect(listItems[1]?.textContent).toBe("banana"); 19 expect(listItems[2]?.textContent).toBe("cherry"); 20 }); 21 22 it("updates list when signal changes", () => { 23 const container = document.createElement("div"); 24 container.innerHTML = `<ul><li data-volt-for="item in items" data-volt-text="item"></li></ul>`; 25 26 const items = signal(["one", "two"]); 27 mount(container, { items }); 28 29 const ul = container.querySelector("ul")!; 30 let listItems = ul.querySelectorAll("li"); 31 32 expect(listItems.length).toBe(2); 33 expect(listItems[0]?.textContent).toBe("one"); 34 expect(listItems[1]?.textContent).toBe("two"); 35 36 items.set(["one", "two", "three", "four"]); 37 listItems = ul.querySelectorAll("li"); 38 39 expect(listItems.length).toBe(4); 40 expect(listItems[2]?.textContent).toBe("three"); 41 expect(listItems[3]?.textContent).toBe("four"); 42 43 items.set(["solo"]); 44 listItems = ul.querySelectorAll("li"); 45 46 expect(listItems.length).toBe(1); 47 expect(listItems[0]?.textContent).toBe("solo"); 48 }); 49 50 it("renders list with object properties", () => { 51 const container = document.createElement("div"); 52 container.innerHTML = ` 53 <ul> 54 <li data-volt-for="user in users"> 55 <span data-volt-text="user.name"></span> 56 </li> 57 </ul> 58 `; 59 60 const users = signal([{ name: "Alice", age: 30 }, { name: "Bob", age: 25 }]); 61 62 mount(container, { users }); 63 64 const spans = container.querySelectorAll("span"); 65 expect(spans.length).toBe(2); 66 expect(spans[0]?.textContent).toBe("Alice"); 67 expect(spans[1]?.textContent).toBe("Bob"); 68 }); 69 70 it("supports index access with (item, index) syntax", () => { 71 const container = document.createElement("div"); 72 container.innerHTML = ` 73 <ul> 74 <li data-volt-for="(item, i) in items"> 75 <span data-volt-text="i"></span>: <span data-volt-text="item"></span> 76 </li> 77 </ul> 78 `; 79 80 const items = signal(["first", "second", "third"]); 81 mount(container, { items }); 82 83 const listItems = container.querySelectorAll("li"); 84 expect(listItems.length).toBe(3); 85 86 const firstItem = listItems[0]?.querySelectorAll("span"); 87 expect(firstItem?.[0]?.textContent).toBe("0"); 88 expect(firstItem?.[1]?.textContent).toBe("first"); 89 90 const secondItem = listItems[1]?.querySelectorAll("span"); 91 expect(secondItem?.[0]?.textContent).toBe("1"); 92 expect(secondItem?.[1]?.textContent).toBe("second"); 93 }); 94 95 it("handles empty arrays", () => { 96 const container = document.createElement("div"); 97 container.innerHTML = `<ul><li data-volt-for="item in items" data-volt-text="item"></li></ul>`; 98 99 const items = signal<string[]>([]); 100 mount(container, { items }); 101 102 const listItems = container.querySelectorAll("li"); 103 expect(listItems.length).toBe(0); 104 105 items.set(["now", "there", "are", "items"]); 106 const updatedItems = container.querySelectorAll("li"); 107 expect(updatedItems.length).toBe(4); 108 }); 109 110 it("handles static arrays (non-signal)", () => { 111 const container = document.createElement("div"); 112 container.innerHTML = `<ul><li data-volt-for="item in items" data-volt-text="item"></li></ul>`; 113 114 mount(container, { items: ["static", "array"] }); 115 116 const listItems = container.querySelectorAll("li"); 117 expect(listItems.length).toBe(2); 118 expect(listItems[0]?.textContent).toBe("static"); 119 expect(listItems[1]?.textContent).toBe("array"); 120 }); 121 122 it("supports event handlers in list items", () => { 123 const container = document.createElement("div"); 124 container.innerHTML = ` 125 <ul> 126 <li data-volt-for="item in items"> 127 <button data-volt-on-click="handleClick" data-volt-text="item.text"></button> 128 </li> 129 </ul> 130 `; 131 132 let clickedItem = ""; 133 const handleClick = (event: Event) => { 134 const button = event.target as HTMLButtonElement; 135 clickedItem = button.textContent || ""; 136 }; 137 138 const items = signal([{ text: "Click Me" }, { text: "Or Me" }]); 139 mount(container, { items, handleClick }); 140 141 const buttons = container.querySelectorAll("button"); 142 expect(buttons.length).toBe(2); 143 144 buttons[0]?.click(); 145 expect(clickedItem).toBe("Click Me"); 146 147 buttons[1]?.click(); 148 expect(clickedItem).toBe("Or Me"); 149 }); 150 151 it("supports nested loops", () => { 152 const container = document.createElement("div"); 153 container.innerHTML = ` 154 <div data-volt-for="group in groups"> 155 <ul> 156 <li data-volt-for="item in group.items" data-volt-text="item"></li> 157 </ul> 158 </div> 159 `; 160 161 const groups = signal([{ items: ["a", "b"] }, { items: ["c", "d", "e"] }]); 162 163 mount(container, { groups }); 164 165 const divs = container.querySelectorAll("div"); 166 expect(divs.length).toBe(2); 167 168 const firstGroupItems = divs[0]?.querySelectorAll("li"); 169 expect(firstGroupItems?.length).toBe(2); 170 expect(firstGroupItems?.[0]?.textContent).toBe("a"); 171 172 const secondGroupItems = divs[1]?.querySelectorAll("li"); 173 expect(secondGroupItems?.length).toBe(3); 174 expect(secondGroupItems?.[2]?.textContent).toBe("e"); 175 }); 176 177 it("properly cleans up when unmounting", () => { 178 const container = document.createElement("div"); 179 container.innerHTML = `<ul><li data-volt-for="item in items" data-volt-text="item.value"></li></ul>`; 180 181 const items = signal([{ value: signal("A") }, { value: signal("B") }]); 182 const cleanup = mount(container, { items }); 183 184 expect(container.querySelectorAll("li").length).toBe(2); 185 186 const listItemsBefore = container.querySelectorAll("li"); 187 const textBefore = [listItemsBefore[0]?.textContent, listItemsBefore[1]?.textContent]; 188 189 cleanup(); 190 191 const itemA = items.get()[0]; 192 const itemB = items.get()[1]; 193 194 itemA.value.set("Changed A"); 195 itemB.value.set("Changed B"); 196 197 const listItems = container.querySelectorAll("li"); 198 expect(listItems[0]?.textContent).toBe(textBefore[0]); 199 expect(listItems[1]?.textContent).toBe(textBefore[1]); 200 }); 201 202 it("handles non-array values gracefully", () => { 203 const container = document.createElement("div"); 204 container.innerHTML = `<ul><li data-volt-for="item in notAnArray" data-volt-text="item"></li></ul>`; 205 206 mount(container, { notAnArray: "not an array" }); 207 208 const listItems = container.querySelectorAll("li"); 209 expect(listItems.length).toBe(0); 210 }); 211 212 it("supports reactive properties within list items", () => { 213 const container = document.createElement("div"); 214 container.innerHTML = ` 215 <ul> 216 <li data-volt-for="todo in todos"> 217 <span data-volt-text="todo.title"></span> 218 <span data-volt-class="todo.completed">Done</span> 219 </li> 220 </ul> 221 `; 222 223 const todos = signal([{ title: signal("Buy milk"), completed: signal({ done: false }) }, { 224 title: signal("Walk dog"), 225 completed: signal({ done: true }), 226 }]); 227 228 mount(container, { todos }); 229 230 const listItems = container.querySelectorAll("li"); 231 expect(listItems.length).toBe(2); 232 233 const firstTodo = listItems[0]; 234 const firstTitle = firstTodo?.querySelector("span:first-child"); 235 const firstStatus = firstTodo?.querySelector("span:last-child"); 236 237 expect(firstTitle?.textContent).toBe("Buy milk"); 238 expect(firstStatus?.classList.contains("done")).toBe(false); 239 240 todos.get()[0].title.set("Buy eggs"); 241 todos.get()[0].completed.set({ done: true }); 242 243 expect(firstTitle?.textContent).toBe("Buy eggs"); 244 expect(firstStatus?.classList.contains("done")).toBe(true); 245 }); 246});