+85
-39
ROADMAP.md
+85
-39
ROADMAP.md
···
19
19
| v0.4.0 | ✓ | [Animation & Transitions](#animation--transitions) |
20
20
| v0.5.0 | ✓ | [Navigation & History API Routing](#navigation--history-api-routing) |
21
21
| | ✓ | [Refactor](#evaluator--binder-hardening) |
22
+
| v0.5.1 | ✓ | [Error Handling & Diagnostics](#error-handling--diagnostics) |
23
+
| v0.5.2 | | |
24
+
| v0.5.3 | | |
25
+
| v0.5.4 | | |
26
+
| v0.6.1 | | [Persistence & Offline](#persistence--offline) |
27
+
| v0.6.2 | | |
28
+
| v0.6.3 | | |
29
+
| v0.6.4 | | [Background Requests & Reactive Polling](#background-requests--reactive-polling) |
30
+
| v0.6.5 | | |
31
+
| v0.6.6 | | |
32
+
| v0.6.7 | | [Streaming & Patch Engine](#streaming--patch-engine) |
33
+
| v0.6.8 | | |
34
+
| v0.6.9 | | |
35
+
| v0.6.10 | | |
36
+
| v0.7.0 | | |
37
+
| v0.8.0 | | Support `voltx-` & `vx-` attributes: recommend `vx-` |
38
+
| | | Switch to `data-voltx` |
22
39
| | | Update demo to be a multi page application with routing plugin |
23
-
| v0.5.1 | | Support `voltx-` & `vx-` attributes: recommend `vx-` |
24
-
| v0.5.2 | | Switch to `data-voltx` |
25
-
| v0.5.3 | | [Background Requests & Reactive Polling](#background-requests--reactive-polling) |
26
-
| v0.5.4 | | [Streaming & Patch Engine](#streaming--patch-engine) |
27
-
| v0.5.5 | | PWA Capabilities |
28
-
| v0.5.6 | | [Persistence & Offline](#persistence--offline) |
29
-
| | | |
30
40
| v0.9.0 | | [Inspector & Developer Tools](#inspector--developer-tools) |
31
41
| v1.0.0 | | [Stable Release](#stable-release) |
32
42
···
102
112
103
113
## To-Do
104
114
115
+
### Error Handling & Diagnostics
116
+
117
+
**Goal**: Provide clear, actionable feedback when runtime or directive errors occur.
118
+
**Outcome**: VoltX.js surfaces developer-friendly diagnostics for expression evaluation,
119
+
directive parsing, and network operations, making it easier to debug apps without opaque stack traces.
120
+
**Deliverables**:
121
+
- v0.5.1
122
+
✓ Centralized error boundary system for directives and effects.
123
+
✓ Sandbox error wrapping with contextual hints (directive name, expression, element).
124
+
✓ `$volt.report(error, context)` API for plugin and app-level reporting.
125
+
- v0.5.2
126
+
- Visual in-DOM error overlays for development mode.
127
+
- Enhanced console messages with source map trace and directive path.
128
+
- Differentiated error levels: warn, error, fatal.
129
+
- v0.5.3
130
+
- Runtime health monitor tracking evaluation and subscription failures.
131
+
- v0.5.4
132
+
- Documentation: "Understanding VoltX Errors" guide.
133
+
- Configurable global error policy (silent, overlay, throw).
134
+
105
135
### Streaming & Patch Engine
106
136
107
137
**Goal:** Enable real-time updates via SSE/WebSocket streaming with intelligent DOM patching.
108
138
**Outcome:** VoltX.js can receive and apply live updates from the server
109
139
**Deliverables:**
110
-
- Server-Sent Events (SSE) integration
111
-
- `data-volt-flow` attribute for SSE endpoints
112
-
- Signal patching from backend (`data-signals-*` merge system)
113
-
- Backend action system with `$$spark()` syntax
114
-
- JSON Patch parser and DOM morphing engine
115
-
- WebSocket as alternative to SSE
116
-
- `data-volt-ignore-morph` for selective patch exclusion
140
+
- v0.5.7
141
+
- Server-Sent Events (SSE) integration
142
+
- `data-volt-flow` attribute for SSE endpoints
143
+
- v0.5.8
144
+
- Signal patching from backend (`data-signals-*` merge system)
145
+
- Backend action system with `$$spark()` syntax
146
+
- v0.5.9
147
+
- JSON Patch parser and DOM morphing engine
148
+
- `data-volt-ignore-morph` for selective patch exclusion
149
+
- v0.5.10
150
+
- WebSocket as alternative to SSE
117
151
118
152
### Persistence & Offline
119
153
···
122
156
**Deliverables:**
123
157
- ✓ Persistent signals (localStorage, sessionStorage, indexedDb)
124
158
- ✓ Storage plugin (`data-volt-persist`)
125
-
- Storage modifiers on signals:
126
-
- `.local` modifier for localStorage persistence
127
-
- `.session` modifier for sessionStorage persistence
128
-
- `.ifmissing` modifier for conditional initialization
129
-
- Offline queue for deferred stream events and HTTP requests
130
-
- Sync strategy API (merge, overwrite, patch) for conflict resolution
131
-
- Service Worker integration for offline-first apps
132
-
- Background sync for deferred requests
133
-
- Cache invalidation strategies
134
-
- Cross-tab synchronization via `BroadcastChannel`
159
+
- v0.5.1
160
+
- Storage modifiers on signals:
161
+
- `.local` modifier for localStorage persistence
162
+
- `.session` modifier for sessionStorage persistence
163
+
- `.ifmissing` modifier for conditional initialization
164
+
- v0.5.2
165
+
- Sync strategy API (merge, overwrite, patch) for conflict resolution
166
+
- Cache invalidation strategies
167
+
- v0.5.3
168
+
- Offline queue for deferred stream events and HTTP requests
169
+
- Service Worker integration for offline-first apps
170
+
- Background sync for deferred requests
171
+
- Cross-tab synchronization via `BroadcastChannel`
135
172
136
173
### Background Requests & Reactive Polling
137
174
138
175
**Goal:** Enable declarative background data fetching and periodic updates within the VoltX.js runtime.
139
176
**Outcome:** VoltX.js elements can fetch or refresh data automatically based on time, visibility, or reactive conditions.
140
177
**Deliverables:**
141
-
- `data-volt-visible` for fetching when an element enters the viewport (`IntersectionObserver`)
142
-
- `data-volt-fetch` attribute for declarative background requests
143
-
- Configurable polling intervals, delays, and signal-based triggers
144
-
- Automatic cancellation of requests when elements are unmounted
145
-
- Conditional execution tied to reactive signals
146
-
- Integration hooks for loading and pending states
147
-
- Background task scheduler with priority management
178
+
- v0.5.4
179
+
- `data-volt-visible` for fetching when an element enters the viewport (`IntersectionObserver`)
180
+
- v0.5.5
181
+
- `data-volt-fetch` attribute for declarative background requests
182
+
- Configurable polling intervals, delays, and signal-based triggers
183
+
- Automatic cancellation of requests when elements are unmounted
184
+
- Conditional execution tied to reactive signals
185
+
- Integration hooks for loading and pending states
186
+
- v0.5.6
187
+
- Background task scheduler with priority management
148
188
149
189
### Inspector & Developer Tools
150
190
151
191
**Goal:** Improve developer experience and runtime introspection.
152
192
**Outcome:** First-class developer ergonomics; VoltX.js is enjoyable to debug and extend.
153
193
**Deliverables:**
154
-
- Developer overlay for inspecting signals, subscriptions, and effects
155
-
- Dev logging toggle (`Volt.debug = true`)
156
-
- Browser console integration (`window.$volt.inspect()`)
157
-
- Signal dependency graph visualization (graph data structure implemented in [proxy](#proxy-based-reactivity-enhancements) milestone)
158
-
- Performance profiling tools
159
-
- Request/response debugging (HTTP actions, SSE streams)
160
-
- Time-travel debugging for signal history
161
-
- Browser DevTools extension
194
+
- v0.9.1
195
+
- Developer overlay for inspecting signals, subscriptions, and effects
196
+
- Time-travel debugging for signal history
197
+
- v0.9.2
198
+
- Signal dependency graph visualization (graph data structure implemented in [proxy](#proxy-based-reactivity-enhancements) milestone)
199
+
- v0.9.3
200
+
- Browser console integration (`window.$volt.inspect()`)
201
+
- Dev logging toggle (`Volt.debug = true`)
202
+
- v0.9.4
203
+
- Request/response debugging (HTTP actions, SSE streams)
204
+
- v0.9.5
205
+
- Performance profiling tools
206
+
- v0.9.6 to v0.9.10
207
+
- Browser DevTools extension
162
208
163
209
### Stable Release
164
210
+4
-3
lib/src/core/async-effect.ts
+4
-3
lib/src/core/async-effect.ts
···
4
4
5
5
import type { Optional, Timer } from "$types/helpers";
6
6
import type { AsyncEffectFunction, AsyncEffectOptions, ComputedSignal, Signal } from "$types/volt";
7
+
import { report } from "./error";
7
8
8
9
/**
9
10
* Creates an async side effect that runs when dependencies change.
···
73
74
try {
74
75
cleanup();
75
76
} catch (error) {
76
-
console.error("Error in async effect cleanup:", error);
77
+
report(error as Error, { source: "effect" });
77
78
}
78
79
cleanup = undefined;
79
80
}
···
115
116
await executeEffect(currentExecutionId);
116
117
}
117
118
} else {
118
-
console.error("Error in async effect:", err);
119
+
report(err as Error, { source: "effect" });
119
120
120
121
if (onError) {
121
122
const retry = () => {
···
198
199
try {
199
200
cleanup();
200
201
} catch (error) {
201
-
console.error("Error during async effect unmount:", error);
202
+
report(error as Error, { source: "effect" });
202
203
}
203
204
cleanup = undefined;
204
205
}
+54
-15
lib/src/core/binder.ts
+54
-15
lib/src/core/binder.ts
···
16
16
} from "$types/volt";
17
17
import { BOOLEAN_ATTRS } from "./constants";
18
18
import { getVoltAttrs, parseClassBinding, setHTML, setText, toggleClass, walkDOM } from "./dom";
19
+
import { report } from "./error";
19
20
import { evaluate } from "./evaluator";
20
21
import { execGlobalHooks, notifyBindingCreated, notifyElementMounted, notifyElementUnmounted } from "./lifecycle";
21
22
import { debounce, getModifierValue, hasModifier, parseModifiers, throttle } from "./modifiers";
···
132
133
try {
133
134
cleanup();
134
135
} catch (error) {
135
-
console.error("Error during unmount:", error);
136
+
report(error as Error, { source: "binding", element: root as HTMLElement });
136
137
}
137
138
}
138
139
···
145
146
try {
146
147
plugin(pluginCtx, val);
147
148
} catch (error) {
148
-
console.error(`Error in plugin "${base}":`, error);
149
+
report(error as Error, {
150
+
source: "plugin",
151
+
element: ctx.element as HTMLElement,
152
+
directive: `data-volt-${base}`,
153
+
pluginName: base,
154
+
});
149
155
}
150
156
}
151
157
···
227
233
break;
228
234
}
229
235
default: {
230
-
// Check directive registry first (for HTTP and other optional directives)
231
236
const directiveHandler = directiveRegistry.get(baseName);
232
237
if (directiveHandler) {
233
238
directiveHandler(ctx, value, modifiers);
234
239
return;
235
240
}
236
241
237
-
// Then check plugin registry
238
242
const plugin = getPlugin(baseName);
239
243
if (plugin) {
240
244
execPlugin(plugin, ctx, value, baseName);
···
374
378
try {
375
379
element.style.setProperty(cssKey, String(val));
376
380
} catch (error) {
377
-
console.warn(`[Volt] Failed to set style property "${cssKey}":`, error);
381
+
report(error as Error, {
382
+
source: "binding",
383
+
element: element,
384
+
directive: "data-volt-style",
385
+
expression: expr,
386
+
});
378
387
}
379
388
}
380
389
}
···
458
467
result(event);
459
468
}
460
469
} catch (error) {
461
-
console.error(`Error in event handler (${eventName}):`, error);
470
+
report(error as Error, {
471
+
source: "binding",
472
+
element: ctx.element as HTMLElement,
473
+
directive: `data-volt-on-${eventName}`,
474
+
expression: expr,
475
+
});
462
476
}
463
477
};
464
478
···
598
612
function bindModel(context: BindingContext, signalPath: string, modifiers: Modifier[] = []): void {
599
613
const result = findModelSignal(context.scope, signalPath);
600
614
if (!result) {
601
-
console.error(`Signal "${signalPath}" not found for data-volt-model`);
615
+
report(new Error(`Signal "${signalPath}" not found`), {
616
+
source: "binding",
617
+
element: context.element as HTMLElement,
618
+
directive: "data-volt-model",
619
+
expression: signalPath,
620
+
});
602
621
return;
603
622
}
604
623
···
766
785
evaluate(stmt, ctx.scope, { unwrapSignals: false });
767
786
}
768
787
} catch (error) {
769
-
console.error("Error in data-volt-init:", error);
788
+
report(error as Error, {
789
+
source: "binding",
790
+
element: ctx.element as HTMLElement,
791
+
directive: "data-volt-init",
792
+
expression: expr,
793
+
});
770
794
}
771
795
}
772
796
···
791
815
function bindFor(ctx: BindingContext, expr: string): void {
792
816
const parsed = parseForExpr(expr);
793
817
if (!parsed) {
794
-
console.error(`Invalid data-volt-for expression: "${expr}"`);
818
+
report(new Error(`Invalid data-volt-for expression: "${expr}"`), {
819
+
source: "binding",
820
+
element: ctx.element as HTMLElement,
821
+
directive: "data-volt-for",
822
+
expression: expr,
823
+
});
795
824
return;
796
825
}
797
826
···
800
829
const parent = templ.parentElement;
801
830
802
831
if (!parent) {
803
-
console.error("data-volt-for element must have a parent");
832
+
report(new Error("data-volt-for element must have a parent"), {
833
+
source: "binding",
834
+
element: ctx.element as HTMLElement,
835
+
directive: "data-volt-for",
836
+
expression: expr,
837
+
});
804
838
return;
805
839
}
806
840
···
863
897
const parent = ifTempl.parentElement;
864
898
865
899
if (!parent) {
866
-
console.error("data-volt-if element must have a parent");
900
+
report(new Error("data-volt-if element must have a parent"), {
901
+
source: "binding",
902
+
element: ctx.element as HTMLElement,
903
+
directive: "data-volt-if",
904
+
expression: expr,
905
+
});
867
906
return;
868
907
}
869
908
···
1034
1073
try {
1035
1074
cb();
1036
1075
} catch (error) {
1037
-
console.error("Error in plugin onMount hook:", error);
1076
+
report(error as Error, { source: "plugin", element: ctx.element as HTMLElement, hookName: "onMount" });
1038
1077
}
1039
1078
},
1040
1079
onUnmount: (cb: () => void) => {
···
1045
1084
try {
1046
1085
cb();
1047
1086
} catch (error) {
1048
-
console.error("Error in plugin beforeBinding hook:", error);
1087
+
report(error as Error, { source: "plugin", element: ctx.element as HTMLElement, hookName: "beforeBinding" });
1049
1088
}
1050
1089
},
1051
1090
afterBinding: (cb: () => void) => {
···
1054
1093
try {
1055
1094
cb();
1056
1095
} catch (error) {
1057
-
console.error("Error in plugin afterBinding hook:", error);
1096
+
report(error as Error, { source: "plugin", element: ctx.element as HTMLElement, hookName: "afterBinding" });
1058
1097
}
1059
1098
});
1060
1099
},
···
1065
1104
try {
1066
1105
cb();
1067
1106
} catch (error) {
1068
-
console.error("Error in plugin onUnmount hook:", error);
1107
+
report(error as Error, { source: "plugin", element: ctx.element as HTMLElement, hookName: "onUnmount" });
1069
1108
}
1070
1109
}
1071
1110
});
+28
-9
lib/src/core/charge.ts
+28
-9
lib/src/core/charge.ts
···
6
6
7
7
import type { ChargedRoot, ChargeResult, Scope } from "$types/volt";
8
8
import { mount } from "./binder";
9
+
import { report } from "./error";
9
10
import { evaluate } from "./evaluator";
10
11
import { getComputedAttributes, isNil } from "./shared";
11
12
import { computed, signal } from "./signal";
···
13
14
14
15
/**
15
16
* Discover and mount all Volt roots in the document.
17
+
*
16
18
* Parses data-volt-state for initial state and data-volt-computed for derived values.
17
19
* Also parses declarative global store from script[data-volt-store] elements.
18
20
*
···
54
56
55
57
chargedRoots.push({ element, scope, cleanup });
56
58
} catch (error) {
57
-
console.error("Error charging Volt root:", element, error);
59
+
report(error as Error, { source: "charge", element: element as HTMLElement });
58
60
}
59
61
}
60
62
···
65
67
try {
66
68
root.cleanup();
67
69
} catch (error) {
68
-
console.error("Error cleaning up Volt root:", root.element, error);
70
+
report(error as Error, { source: "charge", element: root.element as HTMLElement });
69
71
}
70
72
}
71
73
},
···
84
86
const stateData = JSON.parse(stateAttr);
85
87
86
88
if (typeof stateData !== "object" || isNil(stateData) || Array.isArray(stateData)) {
87
-
console.error(`data-volt-state must be a JSON object, got ${typeof stateData}:`, el);
89
+
report(new Error(`data-volt-state must be a JSON object, got ${typeof stateData}`), {
90
+
source: "charge",
91
+
element: el as HTMLElement,
92
+
directive: "data-volt-state",
93
+
expression: stateAttr,
94
+
});
88
95
} else {
89
96
for (const [key, value] of Object.entries(stateData)) {
90
97
scope[key] = signal(value);
91
98
}
92
99
}
93
100
} catch (error) {
94
-
console.error("Failed to parse data-volt-state JSON:", stateAttr, error);
95
-
console.error("Element:", el);
101
+
report(error as Error, {
102
+
source: "charge",
103
+
element: el as HTMLElement,
104
+
directive: "data-volt-state",
105
+
expression: stateAttr,
106
+
});
96
107
}
97
108
}
98
109
···
101
112
try {
102
113
scope[name] = computed(() => evaluate(expression, scope));
103
114
} catch (error) {
104
-
console.error(`Failed to create computed "${name}" with expression "${expression}":`, error);
115
+
report(error as Error, {
116
+
source: "charge",
117
+
element: el as HTMLElement,
118
+
directive: `data-volt-computed:${name}`,
119
+
expression: expression,
120
+
});
105
121
}
106
122
}
107
123
···
125
141
const data = JSON.parse(content);
126
142
127
143
if (typeof data !== "object" || isNil(data) || Array.isArray(data)) {
128
-
console.error("data-volt-store script must contain a JSON object, got:", typeof data);
144
+
report(new Error(`data-volt-store script must contain a JSON object, got: ${typeof data}`), {
145
+
source: "charge",
146
+
element: script as HTMLElement,
147
+
directive: "data-volt-store",
148
+
});
129
149
continue;
130
150
}
131
151
132
152
registerStore(data);
133
153
} catch (error) {
134
-
console.error("Failed to parse data-volt-store script:", error);
135
-
console.error("Script element:", script);
154
+
report(error as Error, { source: "charge", element: script as HTMLElement, directive: "data-volt-store" });
136
155
}
137
156
}
138
157
}
+246
lib/src/core/error.ts
+246
lib/src/core/error.ts
···
1
+
/**
2
+
* Core error handling and reporting system for VoltX.js
3
+
*
4
+
* Provides centralized error boundary with rich contextual information
5
+
* for debugging directives, expressions, effects, and HTTP operations.
6
+
*
7
+
* @module core/error
8
+
*/
9
+
import type { ErrorContext, ErrorHandler, ErrorSource } from "$types/volt";
10
+
11
+
/**
12
+
* Enhanced error class with VoltX context
13
+
*
14
+
* Wraps original errors with rich debugging information including
15
+
* source, element, directive, and expression details.
16
+
*/
17
+
export class VoltError extends Error {
18
+
/** Error source category */
19
+
public readonly source: ErrorSource;
20
+
/** DOM element where error occurred */
21
+
public readonly element?: HTMLElement;
22
+
/** Directive name */
23
+
public readonly directive?: string;
24
+
/** Expression that failed */
25
+
public readonly expression?: string;
26
+
/** Original error */
27
+
public readonly cause: Error;
28
+
/** When error occurred */
29
+
public readonly timestamp: number;
30
+
/** Full error context */
31
+
public readonly context: ErrorContext;
32
+
/** Whether propagation was stopped */
33
+
private _stopped: boolean = false;
34
+
35
+
constructor(cause: Error, context: ErrorContext) {
36
+
const message = VoltError.buildMessage(cause, context);
37
+
super(message);
38
+
this.name = "VoltError";
39
+
this.cause = cause;
40
+
this.source = context.source;
41
+
this.element = context.element;
42
+
this.directive = context.directive;
43
+
this.expression = context.expression;
44
+
this.context = context;
45
+
this.timestamp = Date.now();
46
+
47
+
if (Error.captureStackTrace) {
48
+
Error.captureStackTrace(this);
49
+
}
50
+
}
51
+
52
+
/**
53
+
* Stop propagation to subsequent error handlers
54
+
*/
55
+
public stopPropagation(): void {
56
+
this._stopped = true;
57
+
}
58
+
59
+
/**
60
+
* Check if propagation was stopped
61
+
*/
62
+
public get stopped(): boolean {
63
+
return this._stopped;
64
+
}
65
+
66
+
private static buildMessage(cause: Error, context: ErrorContext): string {
67
+
const parts: string[] = [];
68
+
69
+
parts.push(`[${context.source}] ${cause.message}`);
70
+
71
+
if (context.directive) {
72
+
parts.push(`Directive: ${context.directive}`);
73
+
}
74
+
75
+
if (context.expression) {
76
+
const truncated = context.expression.length > 100 ? `${context.expression.slice(0, 100)}...` : context.expression;
77
+
parts.push(`Expression: ${truncated}`);
78
+
}
79
+
80
+
if (context.pluginName) {
81
+
parts.push(`Plugin: ${context.pluginName}`);
82
+
}
83
+
84
+
if (context.httpMethod && context.httpUrl) {
85
+
parts.push(`HTTP: ${context.httpMethod} ${context.httpUrl}`);
86
+
if (context.httpStatus) {
87
+
parts.push(`Status: ${context.httpStatus}`);
88
+
}
89
+
}
90
+
91
+
if (context.hookName) {
92
+
parts.push(`Hook: ${context.hookName}`);
93
+
}
94
+
95
+
if (context.element) {
96
+
const tag = context.element.tagName.toLowerCase();
97
+
const id = context.element.id ? `#${context.element.id}` : "";
98
+
const cls = context.element.className ? `.${context.element.className.split(" ").join(".")}` : "";
99
+
parts.push(`Element: <${tag}${id}${cls}>`);
100
+
}
101
+
102
+
return parts.join(" | ");
103
+
}
104
+
105
+
/**
106
+
* Serialize error for logging/reporting
107
+
*/
108
+
public toJSON(): Record<string, unknown> {
109
+
return {
110
+
name: this.name,
111
+
message: this.message,
112
+
source: this.source,
113
+
directive: this.directive,
114
+
expression: this.expression,
115
+
timestamp: this.timestamp,
116
+
context: this.context,
117
+
cause: { name: this.cause.name, message: this.cause.message, stack: this.cause.stack },
118
+
stack: this.stack,
119
+
};
120
+
}
121
+
}
122
+
123
+
/**
124
+
* Global error handler registry
125
+
*/
126
+
let errorHandlers: ErrorHandler[] = [];
127
+
128
+
/**
129
+
* Register an error handler
130
+
*
131
+
* Multiple handlers can be registered and will be called in registration order.
132
+
* Handlers can call `error.stopPropagation()` to prevent subsequent handlers
133
+
* from being called.
134
+
*
135
+
* @param handler - Error handler function
136
+
* @returns Cleanup function to unregister the handler
137
+
*
138
+
* @example
139
+
* ```ts
140
+
* const cleanup = onError((error) => {
141
+
* console.log('Error source:', error.source);
142
+
* console.log('Element:', error.element);
143
+
* console.log('Expression:', error.expression);
144
+
*
145
+
* // Stop other handlers from running
146
+
* if (error.source === "http") {
147
+
* error.stopPropagation();
148
+
* }
149
+
* });
150
+
*
151
+
* // Later: cleanup()
152
+
* ```
153
+
*/
154
+
export function onError(handler: ErrorHandler): () => void {
155
+
errorHandlers.push(handler);
156
+
return () => {
157
+
errorHandlers = errorHandlers.filter((h) => h !== handler);
158
+
};
159
+
}
160
+
161
+
/**
162
+
* Clear all registered error handlers
163
+
*
164
+
* Useful for testing or when you want to reset error handling state.
165
+
*
166
+
* @example
167
+
* ```ts
168
+
* clearErrorHandlers();
169
+
* ```
170
+
*/
171
+
export function clearErrorHandlers(): void {
172
+
errorHandlers = [];
173
+
}
174
+
175
+
/**
176
+
* Report an error through the centralized error boundary
177
+
*
178
+
* This function is used both internally by VoltX and externally by user code.
179
+
* All errors flow through this unified system.
180
+
*
181
+
* If no error handlers are registered, errors are logged to console as fallback.
182
+
* Once handlers are registered, console logging is disabled.
183
+
*
184
+
* @param error - Error to report (can be Error, unknown, or string)
185
+
* @param context - Error context with source and additional details
186
+
*
187
+
* @example
188
+
* ```ts
189
+
* // Internal usage (by VoltX)
190
+
* try {
191
+
* evaluate(expression, scope);
192
+
* } catch (err) {
193
+
* report(err, {
194
+
* source: ErrorSource.Evaluator,
195
+
* element: ctx.element,
196
+
* directive: 'data-volt-text',
197
+
* expression: expression
198
+
* });
199
+
* }
200
+
*
201
+
* // External usage (by plugins/apps)
202
+
* try {
203
+
* myCustomLogic();
204
+
* } catch (err) {
205
+
* report(err, {
206
+
* source: ErrorSource.User,
207
+
* customContext: 'My feature failed'
208
+
* });
209
+
* }
210
+
* ```
211
+
*/
212
+
export function report(error: unknown, context: ErrorContext): void {
213
+
const errorObj = error instanceof Error ? error : new Error(String(error));
214
+
const voltError = new VoltError(errorObj, context);
215
+
216
+
if (errorHandlers.length === 0) {
217
+
console.error(voltError.message);
218
+
console.error("Caused by:", voltError.cause);
219
+
if (voltError.element) {
220
+
console.error("Element:", voltError.element);
221
+
}
222
+
return;
223
+
}
224
+
225
+
for (const handler of errorHandlers) {
226
+
try {
227
+
handler(voltError);
228
+
if (voltError.stopped) {
229
+
break;
230
+
}
231
+
} catch (handlerError) {
232
+
console.error("Error in error handler:", handlerError);
233
+
}
234
+
}
235
+
}
236
+
237
+
/**
238
+
* Get count of registered error handlers
239
+
*
240
+
* Useful for testing and debugging error handling setup.
241
+
*
242
+
* @returns Number of registered error handlers
243
+
*/
244
+
export function getErrorHandlerCount(): number {
245
+
return errorHandlers.length;
246
+
}
+17
-4
lib/src/core/http.ts
+17
-4
lib/src/core/http.ts
···
17
17
SwapStrategy,
18
18
} from "$types/volt";
19
19
import { registerDirective } from "./binder";
20
+
import { report } from "./error";
20
21
import { evaluate } from "./evaluator";
21
22
import { sleep } from "./shared";
22
23
···
228
229
break;
229
230
}
230
231
default: {
231
-
console.error(`Unknown swap strategy: ${strategy as string}`);
232
+
report(new Error(`Unknown swap strategy: ${strategy as string}`), {
233
+
source: "http",
234
+
element: target as HTMLElement,
235
+
});
232
236
}
233
237
}
234
238
}
···
308
312
headers = headersValue as Record<string, string>;
309
313
}
310
314
} catch (error) {
311
-
console.error("Failed to parse data-volt-headers:", error);
315
+
report(error as Error, {
316
+
source: "http",
317
+
element: el as HTMLElement,
318
+
directive: "data-volt-headers",
319
+
expression: dataset.voltHeaders,
320
+
});
312
321
}
313
322
}
314
323
···
499
508
500
509
const target = document.querySelector(targetConf);
501
510
if (!target) {
502
-
console.warn(`Target element not found: ${targetConf}`);
511
+
report(new Error(`Target element not found: ${targetConf}`), {
512
+
source: "http",
513
+
element: defaultEl as HTMLElement,
514
+
directive: "data-volt-target",
515
+
});
503
516
return undefined;
504
517
}
505
518
···
630
643
631
644
const errorMessage = lastError instanceof Error ? lastError.message : String(lastError);
632
645
setErrorState(target, errorMessage, conf.indicator);
633
-
console.error("HTTP request failed:", lastError);
646
+
report(lastError as Error, { source: "http", element: el as HTMLElement, httpMethod: method, httpUrl: url });
634
647
}
635
648
636
649
export function bindGet(ctx: BindingContext, url: string): void {
+4
-3
lib/src/core/lifecycle.ts
+4
-3
lib/src/core/lifecycle.ts
···
4
4
*/
5
5
6
6
import type { ElementLifecycleState, GlobalHookName, MountHookCallback, Scope, UnmountHookCallback } from "$types/volt";
7
+
import { report } from "./error";
7
8
8
9
/**
9
10
* Global lifecycle hooks registry
···
124
125
(callback as UnmountHookCallback)(root);
125
126
}
126
127
} catch (error) {
127
-
console.error(`Error in global ${hookName} hook:`, error);
128
+
report(error as Error, { source: "lifecycle", element: root as HTMLElement, hookName: hookName });
128
129
}
129
130
}
130
131
}
···
181
182
try {
182
183
callback();
183
184
} catch (error) {
184
-
console.error("Error in element onMount hook:", error);
185
+
report(error as Error, { source: "lifecycle", element: el as HTMLElement, hookName: "onMount" });
185
186
}
186
187
}
187
188
}
···
205
206
try {
206
207
callback();
207
208
} catch (error) {
208
-
console.error("Error in element onUnmount hook:", error);
209
+
report(error as Error, { source: "lifecycle", element: el as HTMLElement, hookName: "onUnmount" });
209
210
}
210
211
}
211
212
+8
-7
lib/src/core/signal.ts
+8
-7
lib/src/core/signal.ts
···
1
1
import type { ComputedSignal, Signal } from "$types/volt";
2
+
import { report } from "./error";
2
3
import { recordDep, startTracking, stopTracking } from "./tracker";
3
4
4
5
/**
···
25
26
try {
26
27
callback(value);
27
28
} catch (error) {
28
-
console.error("Error in signal subscriber:", error);
29
+
report(error as Error, { source: "effect" });
29
30
}
30
31
}
31
32
};
···
87
88
try {
88
89
cb(value);
89
90
} catch (error) {
90
-
console.error("Error in computed subscriber:", error);
91
+
report(error as Error, { source: "effect" });
91
92
}
92
93
}
93
94
};
···
116
117
shouldNotify = subs.size > 0;
117
118
}
118
119
} catch (error) {
119
-
console.error("Error in computed:", error);
120
+
report(error as Error, { source: "effect" });
120
121
throw error;
121
122
} finally {
122
123
const deps = stopTracking();
···
196
197
try {
197
198
cleanup();
198
199
} catch (error) {
199
-
console.error("Error in effect cleanup:", error);
200
+
report(error as Error, { source: "effect" });
200
201
}
201
202
cleanup = undefined;
202
203
}
···
205
206
try {
206
207
cleanup = cb();
207
208
} catch (error) {
208
-
console.error("Error in effect:", error);
209
+
report(error as Error, { source: "effect" });
209
210
} finally {
210
211
const deps = stopTracking();
211
212
···
225
226
try {
226
227
cleanup();
227
228
} catch (error) {
228
-
console.error("Error in effect cleanup:", error);
229
+
report(error as Error, { source: "effect" });
229
230
}
230
231
}
231
232
···
233
234
try {
234
235
unsubscribe();
235
236
} catch (error) {
236
-
console.error("Error unsubscribing effect:", error);
237
+
report(error as Error, { source: "effect" });
237
238
}
238
239
}
239
240
};
+14
-1
lib/src/index.ts
+14
-1
lib/src/index.ts
···
7
7
export { asyncEffect } from "$core/async-effect";
8
8
export { mount } from "$core/binder";
9
9
export { charge } from "$core/charge";
10
+
export { clearErrorHandlers, onError, report } from "$core/error";
11
+
export type { VoltError } from "$core/error";
10
12
export { parseHttpConfig, request, serializeForm, serializeFormToJSON, swap } from "$core/http";
11
13
export {
12
14
clearAllGlobalHooks,
···
53
55
supportsViewTransitions,
54
56
withViewTransition,
55
57
} from "$core/view-transitions";
56
-
export { goBack, goForward, getRouterMode, initNavigationListener, navigate, redirect, setRouterMode } from "$plugins/navigate";
58
+
export {
59
+
getRouterMode,
60
+
goBack,
61
+
goForward,
62
+
initNavigationListener,
63
+
navigate,
64
+
redirect,
65
+
setRouterMode,
66
+
} from "$plugins/navigate";
57
67
export { persistPlugin, registerStorageAdapter } from "$plugins/persist";
58
68
export { scrollPlugin } from "$plugins/scroll";
59
69
export {
···
74
84
ChargedRoot,
75
85
ChargeResult,
76
86
ComputedSignal,
87
+
ErrorContext,
88
+
ErrorHandler,
89
+
ErrorSource,
77
90
GlobalHookName,
78
91
GlobalStore,
79
92
HydrateOptions,
+34
lib/src/types/volt.d.ts
+34
lib/src/types/volt.d.ts
···
552
552
*/
553
553
forceFallback?: boolean;
554
554
};
555
+
556
+
/**
557
+
* Error source categories for identifying where errors occurred
558
+
*/
559
+
export type ErrorSource = "evaluator" | "binding" | "effect" | "http" | "plugin" | "lifecycle" | "charge" | "user";
560
+
561
+
/**
562
+
* Context information for error reporting
563
+
*/
564
+
export type ErrorContext = {
565
+
/** Error source category */
566
+
source: ErrorSource;
567
+
/** DOM element where error occurred */
568
+
element?: HTMLElement;
569
+
/** Directive name (e.g., "data-volt-text", "data-volt-on-click") */
570
+
directive?: string;
571
+
/** Expression that failed */
572
+
expression?: string;
573
+
/** Plugin name (for plugin errors) */
574
+
pluginName?: string;
575
+
/** HTTP method and URL (for HTTP errors) */
576
+
httpMethod?: string;
577
+
httpUrl?: string;
578
+
httpStatus?: number;
579
+
/** Lifecycle hook name (for lifecycle errors) */
580
+
hookName?: string;
581
+
/** Additional custom context */
582
+
[key: string]: unknown;
583
+
};
584
+
585
+
/**
586
+
* Error handler function signature
587
+
*/
588
+
export type ErrorHandler = (error: VoltError) => void;
+3
-1
lib/test/core/async-effect.test.ts
+3
-1
lib/test/core/async-effect.test.ts
···
385
385
386
386
await vi.runAllTimersAsync();
387
387
388
-
expect(consoleErrorSpy).toHaveBeenCalledWith("Error in async effect:", expect.any(Error));
388
+
expect(consoleErrorSpy).toHaveBeenCalledTimes(2);
389
+
expect(consoleErrorSpy).toHaveBeenNthCalledWith(1, expect.stringContaining("[effect]"));
390
+
expect(consoleErrorSpy).toHaveBeenNthCalledWith(2, "Caused by:", expect.any(Error));
389
391
390
392
consoleErrorSpy.mockRestore();
391
393
});
+300
lib/test/core/error.test.ts
+300
lib/test/core/error.test.ts
···
1
+
import { clearErrorHandlers, getErrorHandlerCount, onError, report, VoltError } from "$core/error";
2
+
import type { ErrorContext, ErrorSource } from "$types/volt";
3
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
+
5
+
describe("VoltError", () => {
6
+
it("creates error with basic context", () => {
7
+
const cause = new Error("Test error");
8
+
const context: ErrorContext = { source: "binding" };
9
+
10
+
const voltError = new VoltError(cause, context);
11
+
12
+
expect(voltError).toBeInstanceOf(Error);
13
+
expect(voltError).toBeInstanceOf(VoltError);
14
+
expect(voltError.name).toBe("VoltError");
15
+
expect(voltError.source).toBe("binding");
16
+
expect(voltError.cause).toBe(cause);
17
+
expect(voltError.stopped).toBe(false);
18
+
});
19
+
20
+
it("includes directive and expression in context", () => {
21
+
const cause = new Error("Evaluation failed");
22
+
const context: ErrorContext = { source: "evaluator", directive: "data-volt-text", expression: "count * 2" };
23
+
24
+
const voltError = new VoltError(cause, context);
25
+
26
+
expect(voltError.directive).toBe("data-volt-text");
27
+
expect(voltError.expression).toBe("count * 2");
28
+
expect(voltError.message).toContain("[evaluator]");
29
+
expect(voltError.message).toContain("Directive: data-volt-text");
30
+
expect(voltError.message).toContain("Expression: count * 2");
31
+
});
32
+
33
+
it("includes element information in message", () => {
34
+
const div = document.createElement("div");
35
+
div.id = "test";
36
+
div.className = "foo bar";
37
+
38
+
const cause = new Error("DOM error");
39
+
const context: ErrorContext = { source: "binding", element: div };
40
+
41
+
const voltError = new VoltError(cause, context);
42
+
43
+
expect(voltError.element).toBe(div);
44
+
expect(voltError.message).toContain("Element: <div#test.foo.bar>");
45
+
});
46
+
47
+
it("includes HTTP context in message", () => {
48
+
const cause = new Error("Request failed");
49
+
const context: ErrorContext = { source: "http", httpMethod: "POST", httpUrl: "/api/users", httpStatus: 500 };
50
+
51
+
const voltError = new VoltError(cause, context);
52
+
53
+
expect(voltError.message).toContain("HTTP: POST /api/users");
54
+
expect(voltError.message).toContain("Status: 500");
55
+
});
56
+
57
+
it("includes plugin name in message", () => {
58
+
const cause = new Error("Plugin failed");
59
+
const context: ErrorContext = { source: "plugin", pluginName: "persist" };
60
+
61
+
const voltError = new VoltError(cause, context);
62
+
63
+
expect(voltError.message).toContain("Plugin: persist");
64
+
});
65
+
66
+
it("includes lifecycle hook name in message", () => {
67
+
const cause = new Error("Hook failed");
68
+
const context: ErrorContext = { source: "lifecycle", hookName: "onMount" };
69
+
70
+
const voltError = new VoltError(cause, context);
71
+
72
+
expect(voltError.message).toContain("Hook: onMount");
73
+
});
74
+
75
+
it("stopPropagation prevents handler chain", () => {
76
+
const cause = new Error("Test");
77
+
const context: ErrorContext = { source: "binding" };
78
+
79
+
const voltError = new VoltError(cause, context);
80
+
81
+
expect(voltError.stopped).toBe(false);
82
+
voltError.stopPropagation();
83
+
expect(voltError.stopped).toBe(true);
84
+
});
85
+
86
+
it("serializes to JSON", () => {
87
+
const cause = new Error("Test error");
88
+
const context: ErrorContext = { source: "effect", directive: "data-volt-on-click", expression: "count++" };
89
+
90
+
const voltError = new VoltError(cause, context);
91
+
const json = voltError.toJSON();
92
+
93
+
expect(json.name).toBe("VoltError");
94
+
expect(json.source).toBe("effect");
95
+
expect(json.directive).toBe("data-volt-on-click");
96
+
expect(json.expression).toBe("count++");
97
+
expect(json.cause).toEqual({ name: "Error", message: "Test error", stack: cause.stack });
98
+
});
99
+
100
+
it("truncates long expressions in message", () => {
101
+
const longExpr = "a".repeat(150);
102
+
const cause = new Error("Test");
103
+
const context: ErrorContext = { source: "evaluator", expression: longExpr };
104
+
105
+
const voltError = new VoltError(cause, context);
106
+
107
+
expect(voltError.message).toContain("Expression: " + "a".repeat(100) + "...");
108
+
expect(voltError.message).not.toContain("a".repeat(101));
109
+
});
110
+
});
111
+
112
+
describe("Error Handler Registration", () => {
113
+
beforeEach(() => {
114
+
clearErrorHandlers();
115
+
});
116
+
117
+
afterEach(() => {
118
+
clearErrorHandlers();
119
+
});
120
+
121
+
it("registers error handler", () => {
122
+
expect(getErrorHandlerCount()).toBe(0);
123
+
124
+
const handler = vi.fn();
125
+
onError(handler);
126
+
127
+
expect(getErrorHandlerCount()).toBe(1);
128
+
});
129
+
130
+
it("returns cleanup function", () => {
131
+
const handler = vi.fn();
132
+
const cleanup = onError(handler);
133
+
134
+
expect(getErrorHandlerCount()).toBe(1);
135
+
136
+
cleanup();
137
+
138
+
expect(getErrorHandlerCount()).toBe(0);
139
+
});
140
+
141
+
it("registers multiple handlers", () => {
142
+
const handler1 = vi.fn();
143
+
const handler2 = vi.fn();
144
+
145
+
onError(handler1);
146
+
onError(handler2);
147
+
148
+
expect(getErrorHandlerCount()).toBe(2);
149
+
});
150
+
151
+
it("clears all handlers", () => {
152
+
onError(vi.fn());
153
+
onError(vi.fn());
154
+
onError(vi.fn());
155
+
156
+
expect(getErrorHandlerCount()).toBe(3);
157
+
158
+
clearErrorHandlers();
159
+
160
+
expect(getErrorHandlerCount()).toBe(0);
161
+
});
162
+
});
163
+
164
+
describe("Error Reporting", () => {
165
+
beforeEach(() => {
166
+
clearErrorHandlers();
167
+
vi.spyOn(console, "error").mockImplementation(() => {});
168
+
});
169
+
170
+
afterEach(() => {
171
+
clearErrorHandlers();
172
+
vi.restoreAllMocks();
173
+
});
174
+
175
+
it("calls registered handler with VoltError", () => {
176
+
const handler = vi.fn();
177
+
onError(handler);
178
+
179
+
const error = new Error("Test");
180
+
const context: ErrorContext = { source: "binding" };
181
+
182
+
report(error, context);
183
+
184
+
expect(handler).toHaveBeenCalledTimes(1);
185
+
expect(handler).toHaveBeenCalledWith(expect.any(VoltError));
186
+
187
+
const voltError = handler.mock.calls[0][0];
188
+
expect(voltError.cause).toBe(error);
189
+
expect(voltError.source).toBe("binding");
190
+
});
191
+
192
+
it("calls multiple handlers in order", () => {
193
+
const callOrder: number[] = [];
194
+
195
+
const handler1 = vi.fn(() => callOrder.push(1));
196
+
const handler2 = vi.fn(() => callOrder.push(2));
197
+
const handler3 = vi.fn(() => callOrder.push(3));
198
+
199
+
onError(handler1);
200
+
onError(handler2);
201
+
onError(handler3);
202
+
203
+
report(new Error("Test"), { source: "effect" });
204
+
205
+
expect(callOrder).toEqual([1, 2, 3]);
206
+
});
207
+
208
+
it("stops propagation when stopPropagation is called", () => {
209
+
const handler1 = vi.fn((error: VoltError) => {
210
+
error.stopPropagation();
211
+
});
212
+
const handler2 = vi.fn();
213
+
const handler3 = vi.fn();
214
+
215
+
onError(handler1);
216
+
onError(handler2);
217
+
onError(handler3);
218
+
219
+
report(new Error("Test"), { source: "effect" });
220
+
221
+
expect(handler1).toHaveBeenCalledTimes(1);
222
+
expect(handler2).not.toHaveBeenCalled();
223
+
expect(handler3).not.toHaveBeenCalled();
224
+
});
225
+
226
+
it("falls back to console.error when no handlers registered", () => {
227
+
const error = new Error("Test error");
228
+
const context: ErrorContext = { source: "http", httpMethod: "GET", httpUrl: "/api/data" };
229
+
230
+
report(error, context);
231
+
232
+
expect(console.error).toHaveBeenCalledTimes(2);
233
+
expect(console.error).toHaveBeenCalledWith(expect.stringContaining("[http]"));
234
+
expect(console.error).toHaveBeenCalledWith("Caused by:", error);
235
+
});
236
+
237
+
it("converts non-Error values to Error", () => {
238
+
const handler = vi.fn();
239
+
onError(handler);
240
+
241
+
report("string error", { source: "user" });
242
+
243
+
expect(handler).toHaveBeenCalledTimes(1);
244
+
const voltError: VoltError = handler.mock.calls[0][0];
245
+
expect(voltError.cause).toBeInstanceOf(Error);
246
+
expect(voltError.cause.message).toBe("string error");
247
+
});
248
+
249
+
it("catches errors in error handlers", () => {
250
+
const handler1 = vi.fn(() => {
251
+
throw new Error("Handler error");
252
+
});
253
+
const handler2 = vi.fn();
254
+
255
+
onError(handler1);
256
+
onError(handler2);
257
+
258
+
report(new Error("Test"), { source: "effect" });
259
+
260
+
expect(handler1).toHaveBeenCalledTimes(1);
261
+
expect(handler2).toHaveBeenCalledTimes(1);
262
+
expect(console.error).toHaveBeenCalledWith("Error in error handler:", expect.any(Error));
263
+
});
264
+
265
+
it("includes element in console fallback", () => {
266
+
const div = document.createElement("div");
267
+
div.id = "test-element";
268
+
269
+
report(new Error("Test"), { source: "binding", element: div });
270
+
271
+
expect(console.error).toHaveBeenCalledWith("Element:", div);
272
+
});
273
+
274
+
it("handles all error sources", () => {
275
+
const handler = vi.fn();
276
+
onError(handler);
277
+
278
+
const sources: Array<ErrorSource> = [
279
+
"evaluator",
280
+
"binding",
281
+
"effect",
282
+
"http",
283
+
"plugin",
284
+
"lifecycle",
285
+
"charge",
286
+
"user",
287
+
];
288
+
289
+
for (const source of sources) {
290
+
report(new Error(`Test ${source}`), { source });
291
+
}
292
+
293
+
expect(handler).toHaveBeenCalledTimes(sources.length);
294
+
295
+
for (const [i, source] of sources.entries()) {
296
+
const voltError: VoltError = handler.mock.calls[i][0];
297
+
expect(voltError.source).toBe(source);
298
+
}
299
+
});
300
+
});
+8
-2
lib/test/core/lifecycle.test.ts
+8
-2
lib/test/core/lifecycle.test.ts
···
245
245
mount(root, {});
246
246
}).not.toThrow();
247
247
248
-
expect(consoleErrorSpy).toHaveBeenCalledWith("Error in global beforeMount hook:", expect.any(Error));
248
+
expect(consoleErrorSpy).toHaveBeenCalledTimes(3);
249
+
expect(consoleErrorSpy).toHaveBeenNthCalledWith(1, expect.stringContaining("[lifecycle]"));
250
+
expect(consoleErrorSpy).toHaveBeenNthCalledWith(2, "Caused by:", expect.any(Error));
251
+
expect(consoleErrorSpy).toHaveBeenNthCalledWith(3, "Element:", root);
249
252
250
253
consoleErrorSpy.mockRestore();
251
254
});
···
331
334
});
332
335
333
336
notifyElementMounted(element);
334
-
expect(consoleErrorSpy).toHaveBeenCalledWith("Error in element onMount hook:", expect.any(Error));
337
+
expect(consoleErrorSpy).toHaveBeenCalledTimes(3);
338
+
expect(consoleErrorSpy).toHaveBeenNthCalledWith(1, expect.stringContaining("[lifecycle]"));
339
+
expect(consoleErrorSpy).toHaveBeenNthCalledWith(2, "Caused by:", expect.any(Error));
340
+
expect(consoleErrorSpy).toHaveBeenNthCalledWith(3, "Element:", element);
335
341
336
342
consoleErrorSpy.mockRestore();
337
343
});
+6
-6
lib/test/integration/global-state.test.ts
+6
-6
lib/test/integration/global-state.test.ts
···
449
449
});
450
450
451
451
it("handles errors gracefully", () => {
452
-
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
453
-
452
+
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
454
453
document.body.innerHTML = `
455
454
<div data-volt data-volt-init="nonExistentVariable.doSomething()">
456
455
<p>Content</p>
···
458
457
`;
459
458
460
459
charge();
461
-
462
-
expect(consoleError).toHaveBeenCalledWith(expect.stringContaining("Error in data-volt-init"), expect.any(Error));
463
-
464
-
consoleError.mockRestore();
460
+
expect(consoleErrorSpy).toHaveBeenCalledTimes(3);
461
+
expect(consoleErrorSpy).toHaveBeenNthCalledWith(1, expect.stringContaining("[binding]"));
462
+
expect(consoleErrorSpy).toHaveBeenNthCalledWith(2, "Caused by:", expect.any(Error));
463
+
expect(consoleErrorSpy).toHaveBeenNthCalledWith(3, "Element:", expect.any(HTMLElement));
464
+
consoleErrorSpy.mockRestore();
465
465
});
466
466
});
467
467
+9
-10
lib/test/integration/plugins.test.ts
+9
-10
lib/test/integration/plugins.test.ts
···
32
32
});
33
33
34
34
it("warns when unknown binding is used without plugin", () => {
35
-
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
35
+
const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
36
36
const element = document.createElement("div");
37
37
element.dataset.voltUnknown = "value";
38
38
39
39
mount(element, {});
40
-
41
-
expect(warnSpy).toHaveBeenCalledWith("Unknown binding: data-volt-unknown");
42
-
43
-
warnSpy.mockRestore();
40
+
expect(consoleWarnSpy).toHaveBeenCalledWith("Unknown binding: data-volt-unknown");
41
+
consoleWarnSpy.mockRestore();
44
42
});
45
43
46
44
it("provides working findSignal utility to plugin", () => {
···
124
122
});
125
123
126
124
it("handles plugin errors gracefully", () => {
127
-
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
125
+
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
128
126
const badPlugin = vi.fn(() => {
129
127
throw new Error("Plugin error");
130
128
});
···
135
133
element.dataset.voltBad = "value";
136
134
137
135
mount(element, {});
138
-
139
-
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining("Error in plugin \"bad\""), expect.any(Error));
140
-
141
-
errorSpy.mockRestore();
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();
142
141
});
143
142
144
143
it("supports reactive updates from plugins", () => {