a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
at main 174 lines 6.7 kB view raw
1import { computed, mount, signal } from "$volt"; 2import { describe, expect, it } from "vitest"; 3 4describe("integration: list rendering", () => { 5 it("creates a reactive todo list", () => { 6 const container = document.createElement("div"); 7 container.innerHTML = ` 8 <div> 9 <input id="new-todo" type="text" /> 10 <button id="add-btn" data-volt-on-click="addTodo">Add</button> 11 12 <ul id="todo-list"> 13 <li data-volt-for="todo in todos"> 14 <input type="checkbox" data-volt-on-click="toggleTodo" /> 15 <span data-volt-text="todo.text"></span> 16 <button data-volt-on-click="deleteTodo">Delete</button> 17 </li> 18 </ul> 19 20 <div data-volt-text="remaining">0</div> 21 </div> 22 `; 23 24 type Todo = { id: number; text: string; completed: boolean }; 25 let nextId = 1; 26 27 const todos = signal<Todo[]>([{ id: nextId++, text: "Learn Volt.js", completed: false }, { 28 id: nextId++, 29 text: "Build an app", 30 completed: false, 31 }]); 32 33 const remaining = computed(() => { 34 return todos.get().filter((t) => !t.completed).length; 35 }); 36 37 const addTodo = () => { 38 const input = container.querySelector("#new-todo") as HTMLInputElement; 39 if (input.value.trim()) { 40 todos.set([...todos.get(), { id: nextId++, text: input.value, completed: false }]); 41 input.value = ""; 42 } 43 }; 44 45 const toggleTodo = (event: Event) => { 46 const checkbox = event.target as HTMLInputElement; 47 const li = checkbox.closest("li"); 48 const index = [...(li?.parentElement?.children || [])].indexOf(li!); 49 50 const updated = todos.get().map((todo, i) => (i === index ? { ...todo, completed: checkbox.checked } : todo)); 51 52 todos.set(updated); 53 }; 54 55 const deleteTodo = (event: Event) => { 56 const button = event.target as HTMLButtonElement; 57 const li = button.closest("li"); 58 const index = [...(li?.parentElement?.children || [])].indexOf(li!); 59 60 todos.set(todos.get().filter((_, i) => i !== index)); 61 }; 62 63 mount(container, { todos, remaining, addTodo, toggleTodo, deleteTodo }); 64 65 const listItems = container.querySelectorAll("#todo-list li"); 66 expect(listItems.length).toBe(2); 67 expect(listItems[0]?.querySelector("span")?.textContent).toBe("Learn Volt.js"); 68 expect(listItems[1]?.querySelector("span")?.textContent).toBe("Build an app"); 69 70 const remainingDiv = container.querySelector("div[data-volt-text='remaining']"); 71 expect(remainingDiv?.textContent).toBe("2"); 72 73 const checkboxes = container.querySelectorAll("input[type='checkbox']"); 74 (checkboxes[0] as HTMLInputElement).checked = true; 75 checkboxes[0]?.dispatchEvent(new Event("click", { bubbles: true })); 76 77 expect(remainingDiv?.textContent).toBe("1"); 78 79 const deleteButtons = container.querySelectorAll("button[data-volt-on-click='deleteTodo']"); 80 deleteButtons[1]?.dispatchEvent(new Event("click", { bubbles: true })); 81 82 const updatedListItems = container.querySelectorAll("#todo-list li"); 83 expect(updatedListItems.length).toBe(1); 84 expect(updatedListItems[0]?.querySelector("span")?.textContent).toBe("Learn Volt.js"); 85 }); 86 87 it("renders filtered lists with computed signals", () => { 88 const container = document.createElement("div"); 89 container.innerHTML = ` 90 <div> 91 <div id="all-items"> 92 <h3>All Items</h3> 93 <div data-volt-for="item in allItems" data-volt-text="item.name"></div> 94 </div> 95 96 <div id="active-items"> 97 <h3>Active Items</h3> 98 <div data-volt-for="item in activeItems" data-volt-text="item.name"></div> 99 </div> 100 </div> 101 `; 102 103 const items = signal([{ name: "Item 1", active: true }, { name: "Item 2", active: false }, { 104 name: "Item 3", 105 active: true, 106 }]); 107 108 const activeItems = computed(() => items.get().filter((item) => item.active)); 109 110 mount(container, { allItems: items, activeItems }); 111 112 const allItemDivs = container.querySelectorAll("#all-items > div[data-volt-for]"); 113 const activeItemDivs = container.querySelectorAll("#active-items > div[data-volt-for]"); 114 115 expect(allItemDivs.length).toBe(0); 116 expect(activeItemDivs.length).toBe(0); 117 118 const renderedAll = container.querySelectorAll("#all-items div[data-volt-text]"); 119 const renderedActive = container.querySelectorAll("#active-items div[data-volt-text]"); 120 121 expect(renderedAll.length).toBe(3); 122 expect(renderedActive.length).toBe(2); 123 expect(renderedActive[0]?.textContent).toBe("Item 1"); 124 expect(renderedActive[1]?.textContent).toBe("Item 3"); 125 126 items.set([{ name: "Item 1", active: false }, { name: "Item 2", active: true }, { name: "Item 3", active: true }]); 127 128 const updatedActive = container.querySelectorAll("#active-items div[data-volt-text]"); 129 expect(updatedActive.length).toBe(2); 130 expect(updatedActive[0]?.textContent).toBe("Item 2"); 131 expect(updatedActive[1]?.textContent).toBe("Item 3"); 132 }); 133 134 it("handles complex nested data structures", () => { 135 const container = document.createElement("div"); 136 container.innerHTML = ` 137 <div> 138 <div data-volt-for="category in categories"> 139 <h2 data-volt-text="category.name"></h2> 140 <ul> 141 <li data-volt-for="product in category.products"> 142 <span data-volt-text="product.name"></span>: $<span data-volt-text="product.price"></span> 143 </li> 144 </ul> 145 </div> 146 </div> 147 `; 148 149 const categories = signal([{ 150 name: "Electronics", 151 products: [{ name: "Laptop", price: 999 }, { name: "Phone", price: 699 }], 152 }, { name: "Books", products: [{ name: "JavaScript Guide", price: 29 }, { name: "CSS Mastery", price: 35 }] }]); 153 154 mount(container, { categories }); 155 156 const categoryDivs = container.querySelectorAll("div > div > h2"); 157 expect(categoryDivs.length).toBe(2); 158 expect(categoryDivs[0]?.textContent).toBe("Electronics"); 159 expect(categoryDivs[1]?.textContent).toBe("Books"); 160 161 const categoryDivElements = container.querySelectorAll("div > div > div"); 162 expect(categoryDivElements.length).toBe(2); 163 164 const electronicsProducts = categoryDivElements[0]?.querySelectorAll("ul li"); 165 expect(electronicsProducts?.length).toBe(2); 166 expect(electronicsProducts?.[0]?.textContent).toContain("Laptop"); 167 expect(electronicsProducts?.[0]?.textContent).toContain("999"); 168 169 const booksProducts = categoryDivElements[1]?.querySelectorAll("ul li"); 170 expect(booksProducts?.length).toBe(2); 171 expect(booksProducts?.[1]?.textContent).toContain("CSS Mastery"); 172 expect(booksProducts?.[1]?.textContent).toContain("35"); 173 }); 174});