import { mount } from "$core/binder"; import { signal } from "$core/signal"; import { describe, expect, it } from "vitest"; describe("data-volt-model binding", () => { describe("text inputs", () => { it("binds signal to text input value", () => { const container = document.createElement("div"); container.innerHTML = ``; const name = signal("Alice"); mount(container, { name }); const input = container.querySelector("input")!; expect(input.value).toBe("Alice"); }); it("updates input when signal changes", () => { const container = document.createElement("div"); container.innerHTML = ``; const message = signal("Hello"); mount(container, { message }); const input = container.querySelector("input")!; expect(input.value).toBe("Hello"); message.set("World"); expect(input.value).toBe("World"); }); it("updates signal when input changes", () => { const container = document.createElement("div"); container.innerHTML = ``; const text = signal("initial"); mount(container, { text }); const input = container.querySelector("input")!; input.value = "changed"; input.dispatchEvent(new Event("input")); expect(text.get()).toBe("changed"); }); it("handles bidirectional updates", () => { const container = document.createElement("div"); container.innerHTML = ` `; const value = signal("test"); mount(container, { value }); const input = container.querySelector("input")!; const span = container.querySelector("span")!; expect(input.value).toBe("test"); expect(span.textContent).toBe("test"); input.value = "updated"; input.dispatchEvent(new Event("input")); expect(value.get()).toBe("updated"); expect(span.textContent).toBe("updated"); }); }); describe("number inputs", () => { it("binds signal to number input", () => { const container = document.createElement("div"); container.innerHTML = ``; const count = signal(42); mount(container, { count }); const input = container.querySelector("input")!; expect(input.value).toBe("42"); }); it("updates signal with numeric value", () => { const container = document.createElement("div"); container.innerHTML = ``; const quantity = signal(0); mount(container, { quantity }); const input = container.querySelector("input")!; input.value = "10"; input.dispatchEvent(new Event("input")); expect(quantity.get()).toBe(10); expect(typeof quantity.get()).toBe("number"); }); }); describe("checkbox inputs", () => { it("binds signal to checkbox checked state", () => { const container = document.createElement("div"); container.innerHTML = ``; const agreed = signal(true); mount(container, { agreed }); const checkbox = container.querySelector("input")!; expect(checkbox.checked).toBe(true); }); it("updates checkbox when signal changes", () => { const container = document.createElement("div"); container.innerHTML = ``; const enabled = signal(false); mount(container, { enabled }); const checkbox = container.querySelector("input")!; expect(checkbox.checked).toBe(false); enabled.set(true); expect(checkbox.checked).toBe(true); }); it("updates signal when checkbox is clicked", () => { const container = document.createElement("div"); container.innerHTML = ``; const checked = signal(false); mount(container, { checked }); const checkbox = container.querySelector("input")!; checkbox.checked = true; checkbox.dispatchEvent(new Event("change")); expect(checked.get()).toBe(true); }); }); describe("radio buttons", () => { it("binds signal to radio button selection", () => { const container = document.createElement("div"); container.innerHTML = ` `; const selected = signal("b"); mount(container, { selected }); const radios = container.querySelectorAll("input"); expect(radios[0].checked).toBe(false); expect(radios[1].checked).toBe(true); }); it("updates signal when radio is selected", () => { const container = document.createElement("div"); container.innerHTML = ` `; const choice = signal("x"); mount(container, { choice }); const radios = container.querySelectorAll("input"); radios[1].checked = true; radios[1].dispatchEvent(new Event("change")); expect(choice.get()).toBe("y"); }); }); describe("select elements", () => { it("binds signal to select value", () => { const container = document.createElement("div"); container.innerHTML = ` `; const color = signal("blue"); mount(container, { color }); const select = container.querySelector("select")!; expect(select.value).toBe("blue"); }); it("updates select when signal changes", () => { const container = document.createElement("div"); container.innerHTML = ` `; const size = signal("s"); mount(container, { size }); const select = container.querySelector("select")!; expect(select.value).toBe("s"); size.set("l"); expect(select.value).toBe("l"); }); it("updates signal when selection changes", () => { const container = document.createElement("div"); container.innerHTML = ` `; const fruit = signal("apple"); mount(container, { fruit }); const select = container.querySelector("select")!; select.value = "banana"; select.dispatchEvent(new Event("input")); expect(fruit.get()).toBe("banana"); }); }); describe("textarea elements", () => { it("binds signal to textarea value", () => { const container = document.createElement("div"); container.innerHTML = ``; const content = signal("Hello World"); mount(container, { content }); const textarea = container.querySelector("textarea")!; expect(textarea.value).toBe("Hello World"); }); it("updates textarea when signal changes", () => { const container = document.createElement("div"); container.innerHTML = ``; const notes = signal("Initial"); mount(container, { notes }); const textarea = container.querySelector("textarea")!; expect(textarea.value).toBe("Initial"); notes.set("Updated"); expect(textarea.value).toBe("Updated"); }); it("updates signal when textarea changes", () => { const container = document.createElement("div"); container.innerHTML = ``; const message = signal(""); mount(container, { message }); const textarea = container.querySelector("textarea")!; textarea.value = "New content"; textarea.dispatchEvent(new Event("input")); expect(message.get()).toBe("New content"); }); }); }); describe("data-volt-bind:attr binding", () => { describe("boolean attributes", () => { it("binds disabled attribute", () => { const container = document.createElement("div"); container.innerHTML = ``; const isDisabled = signal(true); mount(container, { isDisabled }); const button = container.querySelector("button")!; expect(button.hasAttribute("disabled")).toBe(true); isDisabled.set(false); expect(button.hasAttribute("disabled")).toBe(false); }); it("binds readonly attribute", () => { const container = document.createElement("div"); container.innerHTML = ``; const locked = signal(false); mount(container, { locked }); const input = container.querySelector("input")!; expect(input.hasAttribute("readonly")).toBe(false); locked.set(true); expect(input.hasAttribute("readonly")).toBe(true); }); it("binds checked attribute", () => { const container = document.createElement("div"); container.innerHTML = ``; const isChecked = signal(true); mount(container, { isChecked }); const input = container.querySelector("input")!; expect(input.hasAttribute("checked")).toBe(true); }); it("binds required attribute", () => { const container = document.createElement("div"); container.innerHTML = ``; const mandatory = signal(true); mount(container, { mandatory }); const input = container.querySelector("input")!; expect(input.hasAttribute("required")).toBe(true); }); }); describe("string attributes", () => { it("binds href attribute", () => { const container = document.createElement("div"); container.innerHTML = `Link`; const url = signal("https://example.com"); mount(container, { url }); const link = container.querySelector("a")!; expect(link.getAttribute("href")).toBe("https://example.com"); url.set("https://volt.js.org"); expect(link.getAttribute("href")).toBe("https://volt.js.org"); }); it("binds src attribute", () => { const container = document.createElement("div"); container.innerHTML = ``; const image = signal("/placeholder.png"); mount(container, { image }); const img = container.querySelector("img")!; expect(img.getAttribute("src")).toBe("/placeholder.png"); }); it("binds title attribute", () => { const container = document.createElement("div"); container.innerHTML = `Hover me`; const tooltip = signal("Help text"); mount(container, { tooltip }); const span = container.querySelector("span")!; expect(span.getAttribute("title")).toBe("Help text"); }); it("binds aria-label attribute", () => { const container = document.createElement("div"); container.innerHTML = ``; const label = signal("Close"); mount(container, { label }); const button = container.querySelector("button")!; expect(button.getAttribute("aria-label")).toBe("Close"); }); }); describe("dynamic values", () => { it("updates attribute when expression changes", () => { const container = document.createElement("div"); container.innerHTML = `Link`; const baseUrl = signal("/page1"); mount(container, { baseUrl }); const link = container.querySelector("a")!; expect(link.getAttribute("href")).toBe("/page1"); baseUrl.set("/page2"); expect(link.getAttribute("href")).toBe("/page2"); }); it("removes attribute when value is null/undefined/false", () => { const container = document.createElement("div"); container.innerHTML = `
Content
`; const value = signal("present"); mount(container, { value }); const div = container.children[0] as HTMLElement; expect(div.dataset.value).toBe("present"); // @ts-expect-error incorrect type is intentional value.set(null); expect(Object.hasOwn(div.dataset, "value")).toBe(false); }); it("handles expressions", () => { const container = document.createElement("div"); container.innerHTML = ``; const count = signal(3); mount(container, { count }); const button = container.querySelector("button")!; expect(button.hasAttribute("disabled")).toBe(false); count.set(10); expect(button.hasAttribute("disabled")).toBe(true); }); }); }); describe("data-volt-else binding", () => { it("shows else branch when if condition is false", () => { const container = document.createElement("div"); container.innerHTML = `

If content

Else content

`; const show = signal(false); mount(container, { show }); expect(container.textContent).toContain("Else content"); expect(container.textContent).not.toContain("If content"); }); it("shows if branch when condition is true", () => { const container = document.createElement("div"); container.innerHTML = `

If content

Else content

`; const show = signal(true); mount(container, { show }); expect(container.textContent).toContain("If content"); expect(container.textContent).not.toContain("Else content"); }); it("toggles between if and else branches", () => { const container = document.createElement("div"); container.innerHTML = `
Visible Hidden
`; const visible = signal(true); mount(container, { visible }); expect(container.textContent).toContain("Visible"); expect(container.textContent).not.toContain("Hidden"); visible.set(false); expect(container.textContent).not.toContain("Visible"); expect(container.textContent).toContain("Hidden"); visible.set(true); expect(container.textContent).toContain("Visible"); expect(container.textContent).not.toContain("Hidden"); }); it("supports bindings in else branch", () => { const container = document.createElement("div"); container.innerHTML = `

`; const show = signal(false); const ifMessage = signal("If text"); const elseMessage = signal("Else text"); mount(container, { show, ifMessage, elseMessage }); expect(container.querySelector("p")?.textContent).toBe("Else text"); show.set(true); expect(container.querySelector("p")?.textContent).toBe("If text"); }); it("maintains separate state for each branch", () => { const container = document.createElement("div"); container.innerHTML = `
`; const mode = signal(true); const ifValue = signal("If"); const elseValue = signal("Else"); mount(container, { mode, ifValue, elseValue }); expect(container.querySelector("span")?.textContent).toBe("If"); mode.set(false); expect(container.querySelector("span")?.textContent).toBe("Else"); ifValue.set("Changed If"); mode.set(true); expect(container.querySelector("span")?.textContent).toBe("Changed If"); }); it("handles else without bindings", () => { const container = document.createElement("div"); container.innerHTML = `

Condition true

No condition

`; const condition = signal(false); mount(container, { condition }); const paragraphs = container.querySelectorAll("p"); expect(paragraphs).toHaveLength(1); expect(paragraphs[0].textContent).toBe("No condition"); }); it("properly cleans up when switching branches", () => { const container = document.createElement("div"); container.innerHTML = `
If
Else
`; const branch = signal(true); const message = signal("Test"); const cleanup = mount(container, { branch, message }); expect(container.querySelector("span")?.textContent).toBe("Test"); message.set("Updated"); expect(container.querySelector("span")?.textContent).toBe("Updated"); branch.set(false); expect(container.querySelector("span")?.textContent).toBe("Updated"); cleanup(); message.set("After cleanup"); expect(container.querySelector("span")?.textContent).toBe("Updated"); }); }); describe("nested property binding in data-volt-model", () => { describe("single-level nesting", () => { it("binds to nested property in signal with object value", () => { const container = document.createElement("div"); container.innerHTML = ``; const user = signal({ name: "Alice", age: 30 }); mount(container, { user }); const input = container.querySelector("input")!; expect(input.value).toBe("Alice"); }); it("updates input when nested property in signal changes", () => { const container = document.createElement("div"); container.innerHTML = ``; const person = signal({ email: "alice@example.com", verified: false }); mount(container, { person }); const input = container.querySelector("input")!; expect(input.value).toBe("alice@example.com"); person.set({ email: "bob@example.com", verified: true }); expect(input.value).toBe("bob@example.com"); }); it("updates nested property when input changes", () => { const container = document.createElement("div"); container.innerHTML = ``; const profile = signal({ username: "alice123", bio: "Developer" }); mount(container, { profile }); const input = container.querySelector("input")!; input.value = "alice456"; input.dispatchEvent(new Event("input")); expect(profile.get().username).toBe("alice456"); expect(profile.get().bio).toBe("Developer"); }); it("maintains immutability when updating nested properties", () => { const container = document.createElement("div"); container.innerHTML = ``; const data = signal({ value: "original", other: "unchanged" }); const originalObject = data.get(); mount(container, { data }); const input = container.querySelector("input")!; input.value = "modified"; input.dispatchEvent(new Event("input")); const updatedObject = data.get(); expect(updatedObject).not.toBe(originalObject); expect(updatedObject.value).toBe("modified"); expect(updatedObject.other).toBe("unchanged"); expect(originalObject.value).toBe("original"); }); it("handles bidirectional updates with nested properties", () => { const container = document.createElement("div"); container.innerHTML = ` `; const form = signal({ email: "test@example.com", name: "Test" }); mount(container, { form }); const input = container.querySelector("input")!; const span = container.querySelector("span")!; expect(input.value).toBe("test@example.com"); expect(span.textContent).toBe("test@example.com"); input.value = "updated@example.com"; input.dispatchEvent(new Event("input")); expect(form.get().email).toBe("updated@example.com"); expect(span.textContent).toBe("updated@example.com"); }); }); describe("multiple nested properties", () => { it("binds multiple inputs to different nested properties", () => { const container = document.createElement("div"); container.innerHTML = ` `; const formData = signal({ name: "Alice", email: "alice@example.com" }); mount(container, { formData }); const nameInput = container.querySelector("#name")! as HTMLInputElement; const emailInput = container.querySelector("#email")! as HTMLInputElement; expect(nameInput.value).toBe("Alice"); expect(emailInput.value).toBe("alice@example.com"); nameInput.value = "Bob"; nameInput.dispatchEvent(new Event("input")); expect(formData.get().name).toBe("Bob"); expect(formData.get().email).toBe("alice@example.com"); emailInput.value = "bob@example.com"; emailInput.dispatchEvent(new Event("input")); expect(formData.get().name).toBe("Bob"); expect(formData.get().email).toBe("bob@example.com"); }); }); describe("different form element types", () => { it("binds checkbox to nested boolean property", () => { const container = document.createElement("div"); container.innerHTML = ``; const settings = signal({ enabled: true, theme: "dark" }); mount(container, { settings }); const checkbox = container.querySelector("input")!; expect(checkbox.checked).toBe(true); checkbox.checked = false; checkbox.dispatchEvent(new Event("change")); expect(settings.get().enabled).toBe(false); expect(settings.get().theme).toBe("dark"); }); it("binds select to nested property", () => { const container = document.createElement("div"); container.innerHTML = ` `; const preferences = signal({ color: "blue", size: "medium" }); mount(container, { preferences }); const select = container.querySelector("select")!; expect(select.value).toBe("blue"); select.value = "green"; select.dispatchEvent(new Event("input")); expect(preferences.get().color).toBe("green"); expect(preferences.get().size).toBe("medium"); }); it("binds textarea to nested property", () => { const container = document.createElement("div"); container.innerHTML = ``; const post = signal({ content: "Hello world", published: false }); mount(container, { post }); const textarea = container.querySelector("textarea")!; expect(textarea.value).toBe("Hello world"); textarea.value = "Updated content"; textarea.dispatchEvent(new Event("input")); expect(post.get().content).toBe("Updated content"); expect(post.get().published).toBe(false); }); it("binds radio buttons to nested property", () => { const container = document.createElement("div"); container.innerHTML = ` `; const subscription = signal({ plan: "pro", active: true }); mount(container, { subscription }); const radios = container.querySelectorAll("input"); expect(radios[0].checked).toBe(false); expect(radios[1].checked).toBe(true); expect(radios[2].checked).toBe(false); radios[2].checked = true; radios[2].dispatchEvent(new Event("change")); expect(subscription.get().plan).toBe("enterprise"); expect(subscription.get().active).toBe(true); }); }); describe("deep nesting", () => { it("binds to deeply nested properties", () => { const container = document.createElement("div"); container.innerHTML = ``; const app = signal({ user: { profile: { displayName: "Alice", avatar: "/avatar.png" }, settings: { theme: "dark" } }, }); mount(container, { app }); const input = container.querySelector("input")!; expect(input.value).toBe("Alice"); input.value = "Bob"; input.dispatchEvent(new Event("input")); expect(app.get().user.profile.displayName).toBe("Bob"); expect(app.get().user.profile.avatar).toBe("/avatar.png"); expect(app.get().user.settings.theme).toBe("dark"); }); it("maintains immutability with deeply nested updates", () => { const container = document.createElement("div"); container.innerHTML = ``; const state = signal({ form: { fields: { email: "test@example.com", name: "Test" }, meta: { submitted: false } }, }); const originalState = state.get(); const originalForm = originalState.form; const originalFields = originalState.form.fields; mount(container, { state }); const input = container.querySelector("input")!; input.value = "new@example.com"; input.dispatchEvent(new Event("input")); const newState = state.get(); const newForm = newState.form; const newFields = newState.form.fields; expect(newState).not.toBe(originalState); expect(newForm).not.toBe(originalForm); expect(newFields).not.toBe(originalFields); expect(newFields.email).toBe("new@example.com"); expect(newFields.name).toBe("Test"); expect(newForm.meta.submitted).toBe(false); expect(originalFields.email).toBe("test@example.com"); }); }); describe("edge cases", () => { it("handles undefined nested property gracefully", () => { const container = document.createElement("div"); container.innerHTML = ``; const obj = signal({ present: "value" }); mount(container, { obj }); const input = container.querySelector("input")!; expect(input.value).toBe(""); }); it("creates nested property path when updating undefined property", () => { const container = document.createElement("div"); container.innerHTML = ``; const data = signal({ existingProp: "exists" }); mount(container, { data }); const input = container.querySelector("input")!; input.value = "new value"; input.dispatchEvent(new Event("input")); // @ts-expect-error updating shape of data expect(data.get().newProp).toBe("new value"); expect(data.get().existingProp).toBe("exists"); }); it("works with modifiers on nested properties", () => { const container = document.createElement("div"); container.innerHTML = ``; const form = signal({ username: "", email: "" }); mount(container, { form }); const input = container.querySelector("input")!; input.value = " alice "; input.dispatchEvent(new Event("input")); expect(form.get().username).toBe("alice"); }); it("works with number modifier on nested properties", () => { const container = document.createElement("div"); container.innerHTML = ``; const config = signal({ port: 8080, host: "localhost" }); mount(container, { config }); const input = container.querySelector("input")!; input.value = "3000"; input.dispatchEvent(new Event("input")); expect(config.get().port).toBe(3000); expect(typeof config.get().port).toBe("number"); }); }); });