a reactive (signals based) hypermedia web framework (wip)
stormlightlabs.github.io/volt/
hypermedia
frontend
signals
1import { mount } from "$core/binder";
2import { clearPlugins, registerPlugin } from "$core/plugin";
3import { signal } from "$core/signal";
4import { beforeEach, describe, expect, it, vi } from "vitest";
5
6describe("plugin integration with binder", () => {
7 beforeEach(() => {
8 clearPlugins();
9 });
10
11 it("calls registered plugin when binding attribute", () => {
12 const pluginHandler = vi.fn();
13 registerPlugin("custom", pluginHandler);
14
15 const element = document.createElement("div");
16 element.dataset.voltCustom = "testValue";
17
18 const scope = { test: "value" };
19 mount(element, scope);
20
21 expect(pluginHandler).toHaveBeenCalledOnce();
22 expect(pluginHandler).toHaveBeenCalledWith(
23 expect.objectContaining({
24 element,
25 scope,
26 addCleanup: expect.any(Function),
27 findSignal: expect.any(Function),
28 evaluate: expect.any(Function),
29 }),
30 "testValue",
31 );
32 });
33
34 it("warns when unknown binding is used without plugin", () => {
35 const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
36 const element = document.createElement("div");
37 element.dataset.voltUnknown = "value";
38
39 mount(element, {});
40 expect(consoleWarnSpy).toHaveBeenCalledWith("Unknown binding: data-volt-unknown");
41 consoleWarnSpy.mockRestore();
42 });
43
44 it("provides working findSignal utility to plugin", () => {
45 let foundSignal: unknown;
46 registerPlugin("finder", (context) => {
47 foundSignal = context.findSignal("count");
48 });
49
50 const element = document.createElement("div");
51 element.dataset.voltFinder = "test";
52
53 const count = signal(42);
54 mount(element, { count });
55
56 expect(foundSignal).toBe(count);
57 });
58
59 it("provides working evaluate utility to plugin", () => {
60 let evaluatedValue: unknown;
61 registerPlugin("evaluator", (context, value) => {
62 evaluatedValue = context.evaluate(value);
63 });
64
65 const element = document.createElement("div");
66 element.dataset.voltEvaluator = "count";
67
68 const count = signal(100);
69 mount(element, { count });
70
71 expect(evaluatedValue).toBe(100);
72 });
73
74 it("registers and calls cleanup functions", () => {
75 const cleanup = vi.fn();
76 registerPlugin("cleaner", (context) => {
77 context.addCleanup(cleanup);
78 });
79
80 const element = document.createElement("div");
81 element.dataset.voltCleaner = "test";
82
83 const unmount = mount(element, {});
84
85 expect(cleanup).not.toHaveBeenCalled();
86
87 unmount();
88
89 expect(cleanup).toHaveBeenCalledOnce();
90 });
91
92 it("handles multiple plugins on same element", () => {
93 const plugin1 = vi.fn();
94 const plugin2 = vi.fn();
95
96 registerPlugin("plugin1", plugin1);
97 registerPlugin("plugin2", plugin2);
98
99 const element = document.createElement("div");
100 element.dataset.voltPlugin1 = "value1";
101 element.dataset.voltPlugin2 = "value2";
102
103 mount(element, {});
104
105 expect(plugin1).toHaveBeenCalledWith(expect.anything(), "value1");
106 expect(plugin2).toHaveBeenCalledWith(expect.anything(), "value2");
107 });
108
109 it("allows plugins to work alongside core bindings", () => {
110 const pluginHandler = vi.fn();
111 registerPlugin("custom", pluginHandler);
112
113 const element = document.createElement("div");
114 element.dataset.voltText = "message";
115 element.dataset.voltCustom = "customValue";
116
117 const scope = { message: "Hello" };
118 mount(element, scope);
119
120 expect(element.textContent).toBe("Hello");
121 expect(pluginHandler).toHaveBeenCalledWith(expect.anything(), "customValue");
122 });
123
124 it("handles plugin errors gracefully", () => {
125 const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
126 const badPlugin = vi.fn(() => {
127 throw new Error("Plugin error");
128 });
129
130 registerPlugin("bad", badPlugin);
131
132 const element = document.createElement("div");
133 element.dataset.voltBad = "value";
134
135 mount(element, {});
136 expect(consoleErrorSpy).toHaveBeenCalledTimes(3);
137 expect(consoleErrorSpy).toHaveBeenNthCalledWith(1, expect.stringContaining("[plugin]"));
138 expect(consoleErrorSpy).toHaveBeenNthCalledWith(2, "Caused by:", expect.any(Error));
139 expect(consoleErrorSpy).toHaveBeenNthCalledWith(3, "Element:", element);
140 consoleErrorSpy.mockRestore();
141 });
142
143 it("supports reactive updates from plugins", () => {
144 registerPlugin("reactive", (context, value) => {
145 const sig = context.findSignal(value);
146 if (sig) {
147 const update = () => {
148 (context.element as HTMLElement).dataset.testValue = String(sig.get());
149 };
150 update();
151 const unsubscribe = sig.subscribe(update);
152 context.addCleanup(unsubscribe);
153 }
154 });
155
156 const element = document.createElement("div");
157 element.dataset.voltReactive = "count";
158
159 const count = signal(1);
160 mount(element, { count });
161
162 expect(element.dataset.testValue).toBe("1");
163
164 count.set(5);
165 expect(element.dataset.testValue).toBe("5");
166
167 count.set(10);
168 expect(element.dataset.testValue).toBe("10");
169 });
170});