a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
at main 5.2 kB view raw
1import { findScopedSignal, getComputedAttributes, isNil, isSignal, kebabToCamel } from "$core/shared"; 2import { signal } from "$core/signal"; 3import { describe, expect, it } from "vitest"; 4 5describe("shared utilities", () => { 6 describe("isNil", () => { 7 it.each([ 8 { value: null, expected: true, label: "null" }, 9 { value: undefined, expected: true, label: "undefined" }, 10 { value: 0, expected: false, label: "0" }, 11 { value: "", expected: false, label: "empty string" }, 12 { value: false, expected: false, label: "false" }, 13 { value: {}, expected: false, label: "empty object" }, 14 { value: [], expected: false, label: "empty array" }, 15 { value: Number.NaN, expected: false, label: "NaN" }, 16 { value: "test", expected: false, label: "string" }, 17 { value: 42, expected: false, label: "number" }, 18 { value: true, expected: false, label: "true" }, 19 { value: { a: 1 }, expected: false, label: "object" }, 20 { value: [1, 2], expected: false, label: "array" }, 21 ])("returns $expected for $label", ({ value, expected }) => { 22 expect(isNil(value)).toBe(expected); 23 }); 24 }); 25 26 describe("kebabToCamel", () => { 27 it.each([ 28 { input: "hello-world", expected: "helloWorld" }, 29 { input: "font-size", expected: "fontSize" }, 30 { input: "background-color", expected: "backgroundColor" }, 31 { input: "data-volt-text", expected: "dataVoltText" }, 32 { input: "simple", expected: "simple" }, 33 { input: "a-b-c-d", expected: "aBCD" }, 34 { input: "", expected: "" }, 35 ])("converts '$input' to '$expected'", ({ input, expected }) => { 36 expect(kebabToCamel(input)).toBe(expected); 37 }); 38 }); 39 40 describe("isSignal", () => { 41 it("returns true for signals", () => { 42 const sig = signal(0); 43 expect(isSignal(sig)).toBe(true); 44 }); 45 46 it.each([ 47 { value: null, label: "null" }, 48 { value: undefined, label: "undefined" }, 49 { value: 42, label: "number" }, 50 { value: "test", label: "string" }, 51 { value: {}, label: "empty object" }, 52 { value: { get: "not a function" }, label: "object with get property" }, 53 { value: { get: () => {} }, label: "object with only get method" }, 54 { value: { subscribe: () => {} }, label: "object with only subscribe method" }, 55 ])("returns false for $label", ({ value }) => { 56 expect(isSignal(value)).toBe(false); 57 }); 58 }); 59 60 describe("findScopedSignal", () => { 61 it("finds signal at top level", () => { 62 const count = signal(5); 63 const scope = { count }; 64 65 const found = findScopedSignal(scope, "count"); 66 expect(found).toBe(count); 67 }); 68 69 it("finds signal in nested path", () => { 70 const count = signal(5); 71 const scope = { user: { stats: { count } } }; 72 73 const found = findScopedSignal(scope, "user.stats.count"); 74 expect(found).toBe(count); 75 }); 76 77 it("returns undefined for non-existent path", () => { 78 const scope = { count: signal(5) }; 79 80 const found = findScopedSignal(scope, "missing"); 81 expect(found).toBeUndefined(); 82 }); 83 84 it("returns undefined for non-signal values", () => { 85 const scope = { value: 42 }; 86 87 const found = findScopedSignal(scope, "value"); 88 expect(found).toBeUndefined(); 89 }); 90 91 it("returns undefined when traversing through null", () => { 92 const scope = { user: null }; 93 const found = findScopedSignal(scope, "user.name"); 94 expect(found).toBeUndefined(); 95 }); 96 97 it("handles whitespace in path", () => { 98 const count = signal(5); 99 const scope = { count }; 100 const found = findScopedSignal(scope, " count "); 101 expect(found).toBe(count); 102 }); 103 }); 104 105 describe("getComputedAttributes", () => { 106 it("extracts computed attributes from element", () => { 107 const element = document.createElement("div"); 108 element.dataset["voltComputed:doubled"] = "count * 2"; 109 element.dataset["voltComputed:tripled"] = "count * 3"; 110 111 const computed = getComputedAttributes(element); 112 expect(computed.size).toBe(2); 113 expect(computed.get("doubled")).toBe("count * 2"); 114 expect(computed.get("tripled")).toBe("count * 3"); 115 }); 116 117 it("converts kebab-case names to camelCase", () => { 118 const element = document.createElement("div"); 119 element.dataset["voltComputed:fullName"] = "firstName + ' ' + lastName"; 120 121 const computed = getComputedAttributes(element); 122 expect(computed.get("fullName")).toBe("firstName + ' ' + lastName"); 123 }); 124 125 it("returns empty map when no computed attributes", () => { 126 const element = document.createElement("div"); 127 element.dataset.voltText = "message"; 128 129 const computed = getComputedAttributes(element); 130 expect(computed.size).toBe(0); 131 }); 132 133 it("ignores non-computed data-volt attributes", () => { 134 const element = document.createElement("div"); 135 element.dataset.voltText = "message"; 136 element.dataset["voltComputed:value"] = "count * 2"; 137 element.dataset.voltShow = "visible"; 138 139 const computed = getComputedAttributes(element); 140 expect(computed.size).toBe(1); 141 expect(computed.get("value")).toBe("count * 2"); 142 }); 143 }); 144});