zero-query 0.9.8 → 0.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { ZQueryError, ErrorCode, onError, reportError, guardCallback, validate } from '../src/errors.js';
2
+ import { ZQueryError, ErrorCode, onError, reportError, guardCallback, guardAsync, validate, formatError } from '../src/errors.js';
3
3
 
4
4
 
5
5
  // ---------------------------------------------------------------------------
@@ -26,6 +26,35 @@ describe('ZQueryError', () => {
26
26
  const err = new ZQueryError(ErrorCode.HTTP_REQUEST, 'http error', {}, cause);
27
27
  expect(err.cause).toBe(cause);
28
28
  });
29
+
30
+ it('has correct name', () => {
31
+ const err = new ZQueryError(ErrorCode.INVALID_ARGUMENT, 'test');
32
+ expect(err.name).toBe('ZQueryError');
33
+ });
34
+
35
+ it('has empty context by default', () => {
36
+ const err = new ZQueryError(ErrorCode.INVALID_ARGUMENT, 'test');
37
+ expect(err.context).toEqual({});
38
+ });
39
+
40
+ it('has no cause property when not provided', () => {
41
+ const err = new ZQueryError(ErrorCode.INVALID_ARGUMENT, 'test');
42
+ expect(err.cause).toBeUndefined();
43
+ });
44
+
45
+ it('has a stack trace', () => {
46
+ const err = new ZQueryError(ErrorCode.COMP_RENDER, 'test');
47
+ expect(err.stack).toBeDefined();
48
+ expect(typeof err.stack).toBe('string');
49
+ expect(err.stack.length).toBeGreaterThan(0);
50
+ });
51
+
52
+ it('works with JSON.stringify (context serializable)', () => {
53
+ const err = new ZQueryError(ErrorCode.COMP_RENDER, 'test', { foo: 'bar' });
54
+ const json = JSON.stringify({ code: err.code, context: err.context });
55
+ expect(json).toContain('foo');
56
+ expect(json).toContain('bar');
57
+ });
29
58
  });
30
59
 
31
60
 
@@ -46,17 +75,81 @@ describe('ErrorCode', () => {
46
75
  expect(ErrorCode.HTTP_REQUEST).toBe('ZQ_HTTP_REQUEST');
47
76
  expect(ErrorCode.ROUTER_LOAD).toBe('ZQ_ROUTER_LOAD');
48
77
  });
78
+
79
+ it('has reactive codes', () => {
80
+ expect(ErrorCode.REACTIVE_CALLBACK).toBe('ZQ_REACTIVE_CALLBACK');
81
+ expect(ErrorCode.SIGNAL_CALLBACK).toBe('ZQ_SIGNAL_CALLBACK');
82
+ expect(ErrorCode.EFFECT_EXEC).toBe('ZQ_EFFECT_EXEC');
83
+ });
84
+
85
+ it('has expression codes', () => {
86
+ expect(ErrorCode.EXPR_PARSE).toBe('ZQ_EXPR_PARSE');
87
+ expect(ErrorCode.EXPR_EVAL).toBe('ZQ_EXPR_EVAL');
88
+ expect(ErrorCode.EXPR_UNSAFE_ACCESS).toBe('ZQ_EXPR_UNSAFE_ACCESS');
89
+ });
90
+
91
+ it('has component codes', () => {
92
+ expect(ErrorCode.COMP_INVALID_NAME).toBe('ZQ_COMP_INVALID_NAME');
93
+ expect(ErrorCode.COMP_NOT_FOUND).toBe('ZQ_COMP_NOT_FOUND');
94
+ expect(ErrorCode.COMP_MOUNT_TARGET).toBe('ZQ_COMP_MOUNT_TARGET');
95
+ expect(ErrorCode.COMP_RENDER).toBe('ZQ_COMP_RENDER');
96
+ expect(ErrorCode.COMP_LIFECYCLE).toBe('ZQ_COMP_LIFECYCLE');
97
+ expect(ErrorCode.COMP_RESOURCE).toBe('ZQ_COMP_RESOURCE');
98
+ expect(ErrorCode.COMP_DIRECTIVE).toBe('ZQ_COMP_DIRECTIVE');
99
+ });
100
+
101
+ it('has router codes', () => {
102
+ expect(ErrorCode.ROUTER_LOAD).toBe('ZQ_ROUTER_LOAD');
103
+ expect(ErrorCode.ROUTER_GUARD).toBe('ZQ_ROUTER_GUARD');
104
+ expect(ErrorCode.ROUTER_RESOLVE).toBe('ZQ_ROUTER_RESOLVE');
105
+ });
106
+
107
+ it('has store codes', () => {
108
+ expect(ErrorCode.STORE_ACTION).toBe('ZQ_STORE_ACTION');
109
+ expect(ErrorCode.STORE_MIDDLEWARE).toBe('ZQ_STORE_MIDDLEWARE');
110
+ expect(ErrorCode.STORE_SUBSCRIBE).toBe('ZQ_STORE_SUBSCRIBE');
111
+ });
112
+
113
+ it('has http codes', () => {
114
+ expect(ErrorCode.HTTP_REQUEST).toBe('ZQ_HTTP_REQUEST');
115
+ expect(ErrorCode.HTTP_TIMEOUT).toBe('ZQ_HTTP_TIMEOUT');
116
+ expect(ErrorCode.HTTP_INTERCEPTOR).toBe('ZQ_HTTP_INTERCEPTOR');
117
+ expect(ErrorCode.HTTP_PARSE).toBe('ZQ_HTTP_PARSE');
118
+ });
119
+
120
+ it('has SSR codes', () => {
121
+ expect(ErrorCode.SSR_RENDER).toBe('ZQ_SSR_RENDER');
122
+ expect(ErrorCode.SSR_COMPONENT).toBe('ZQ_SSR_COMPONENT');
123
+ expect(ErrorCode.SSR_HYDRATION).toBe('ZQ_SSR_HYDRATION');
124
+ expect(ErrorCode.SSR_PAGE).toBe('ZQ_SSR_PAGE');
125
+ });
126
+
127
+ it('has general codes', () => {
128
+ expect(ErrorCode.INVALID_ARGUMENT).toBe('ZQ_INVALID_ARGUMENT');
129
+ });
130
+
131
+ it('all codes start with ZQ_ prefix', () => {
132
+ for (const [, value] of Object.entries(ErrorCode)) {
133
+ expect(value).toMatch(/^ZQ_/);
134
+ }
135
+ });
136
+
137
+ it('all codes are unique', () => {
138
+ const values = Object.values(ErrorCode);
139
+ const unique = new Set(values);
140
+ expect(unique.size).toBe(values.length);
141
+ });
49
142
  });
50
143
 
51
144
 
52
145
  // ---------------------------------------------------------------------------
53
- // onError / reportError
146
+ // onError / reportError — multi-handler support
54
147
  // ---------------------------------------------------------------------------
55
148
 
56
149
  describe('reportError', () => {
57
150
  let errorSpy;
58
151
  beforeEach(() => {
59
- onError(null); // reset handler
152
+ onError(null); // reset handlers
60
153
  errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
61
154
  });
62
155
  afterEach(() => {
@@ -92,112 +185,124 @@ describe('reportError', () => {
92
185
  reportError(ErrorCode.HTTP_REQUEST, 'http failed', {}, cause);
93
186
  expect(handler.mock.calls[0][0].cause).toBe(cause);
94
187
  });
95
- });
96
-
97
-
98
- // ---------------------------------------------------------------------------
99
- // guardCallback
100
- // ---------------------------------------------------------------------------
101
188
 
102
- describe('guardCallback', () => {
103
- let errorSpy;
104
- beforeEach(() => {
105
- onError(null);
106
- errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
107
- });
108
- afterEach(() => {
109
- errorSpy.mockRestore();
189
+ it('supports multiple handlers', () => {
190
+ const handler1 = vi.fn();
191
+ const handler2 = vi.fn();
192
+ onError(handler1);
193
+ onError(handler2);
194
+ reportError(ErrorCode.COMP_RENDER, 'test');
195
+ expect(handler1).toHaveBeenCalledOnce();
196
+ expect(handler2).toHaveBeenCalledOnce();
110
197
  });
111
198
 
112
- it('returns the same value as original function', () => {
113
- const guarded = guardCallback((x) => x * 2, ErrorCode.COMP_RENDER);
114
- expect(guarded(5)).toBe(10);
199
+ it('both handlers receive the same error', () => {
200
+ const errors = [];
201
+ onError(err => errors.push(err));
202
+ onError(err => errors.push(err));
203
+ reportError(ErrorCode.COMP_RENDER, 'shared');
204
+ expect(errors[0]).toBe(errors[1]);
115
205
  });
116
206
 
117
- it('catches errors and reports them', () => {
207
+ it('handler can be removed via unsubscribe', () => {
118
208
  const handler = vi.fn();
119
- onError(handler);
120
- const guarded = guardCallback(() => { throw new Error('boom'); }, ErrorCode.COMP_RENDER, { component: 'test' });
121
- expect(() => guarded()).not.toThrow();
122
- expect(handler).toHaveBeenCalledOnce();
123
- });
124
-
125
- it('passes arguments through', () => {
126
- const guarded = guardCallback((a, b) => a + b, ErrorCode.COMP_RENDER);
127
- expect(guarded(1, 2)).toBe(3);
209
+ const unsub = onError(handler);
210
+ unsub();
211
+ reportError(ErrorCode.COMP_RENDER, 'test');
212
+ expect(handler).not.toHaveBeenCalled();
213
+ });
214
+
215
+ it('removing one handler does not affect others', () => {
216
+ const h1 = vi.fn();
217
+ const h2 = vi.fn();
218
+ const unsub1 = onError(h1);
219
+ onError(h2);
220
+ unsub1();
221
+ reportError(ErrorCode.COMP_RENDER, 'test');
222
+ expect(h1).not.toHaveBeenCalled();
223
+ expect(h2).toHaveBeenCalledOnce();
224
+ });
225
+
226
+ it('one handler throwing does not prevent others from running', () => {
227
+ const h1 = vi.fn(() => { throw new Error('boom'); });
228
+ const h2 = vi.fn();
229
+ onError(h1);
230
+ onError(h2);
231
+ reportError(ErrorCode.COMP_RENDER, 'test');
232
+ expect(h1).toHaveBeenCalledOnce();
233
+ expect(h2).toHaveBeenCalledOnce();
128
234
  });
129
235
  });
130
236
 
131
237
 
132
238
  // ---------------------------------------------------------------------------
133
- // validate
134
- // ---------------------------------------------------------------------------
135
-
136
- describe('validate', () => {
137
- it('passes for valid values', () => {
138
- expect(() => validate('hello', 'name', 'string')).not.toThrow();
139
- expect(() => validate(42, 'count', 'number')).not.toThrow();
140
- expect(() => validate(() => {}, 'fn', 'function')).not.toThrow();
141
- });
142
-
143
- it('throws ZQueryError for null', () => {
144
- expect(() => validate(null, 'name', 'string')).toThrow(ZQueryError);
145
- });
146
-
147
- it('throws ZQueryError for undefined', () => {
148
- expect(() => validate(undefined, 'count')).toThrow(ZQueryError);
149
- });
150
-
151
- it('throws ZQueryError for wrong type', () => {
152
- expect(() => validate(42, 'name', 'string')).toThrow(ZQueryError);
153
- });
154
-
155
- it('error has INVALID_ARGUMENT code', () => {
156
- try {
157
- validate(null, 'param');
158
- } catch (err) {
159
- expect(err.code).toBe(ErrorCode.INVALID_ARGUMENT);
160
- }
161
- });
162
- });
163
-
164
-
165
- // ===========================================================================
166
239
  // onError — clearing and edge cases
167
- // ===========================================================================
240
+ // ---------------------------------------------------------------------------
168
241
 
169
242
  describe('onError — edge cases', () => {
170
243
  afterEach(() => onError(null));
171
244
 
172
- it('passing null clears handler', () => {
245
+ it('passing null clears all handlers', () => {
173
246
  const spy = vi.fn();
174
247
  onError(spy);
175
248
  onError(null);
249
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
176
250
  reportError(ErrorCode.INVALID_ARGUMENT, 'test');
177
251
  expect(spy).not.toHaveBeenCalled();
252
+ errorSpy.mockRestore();
178
253
  });
179
254
 
180
- it('passing non-function is treated as null', () => {
255
+ it('passing non-function is ignored', () => {
181
256
  const spy = vi.fn();
182
257
  onError(spy);
183
258
  onError('not a function');
259
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
184
260
  reportError(ErrorCode.INVALID_ARGUMENT, 'test');
185
- expect(spy).not.toHaveBeenCalled();
261
+ // Original handler should still be there
262
+ expect(spy).toHaveBeenCalled();
263
+ errorSpy.mockRestore();
186
264
  });
187
265
 
188
266
  it('handler exceptions do not crash reportError', () => {
189
267
  onError(() => { throw new Error('handler crash'); });
268
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
190
269
  expect(() => reportError(ErrorCode.INVALID_ARGUMENT, 'test')).not.toThrow();
270
+ errorSpy.mockRestore();
271
+ });
272
+
273
+ it('onError returns an unsubscribe function', () => {
274
+ const unsub = onError(() => {});
275
+ expect(typeof unsub).toBe('function');
276
+ });
277
+
278
+ it('onError(null) returns a no-op unsubscribe', () => {
279
+ const unsub = onError(null);
280
+ expect(typeof unsub).toBe('function');
281
+ expect(() => unsub()).not.toThrow();
282
+ });
283
+
284
+ it('double unsubscribe is safe', () => {
285
+ const handler = vi.fn();
286
+ const unsub = onError(handler);
287
+ unsub();
288
+ expect(() => unsub()).not.toThrow();
191
289
  });
192
290
  });
193
291
 
194
292
 
195
- // ===========================================================================
196
- // reportError — cause reuse
197
- // ===========================================================================
293
+ // ---------------------------------------------------------------------------
294
+ // reportError — cause handling
295
+ // ---------------------------------------------------------------------------
198
296
 
199
297
  describe('reportError — cause handling', () => {
200
- afterEach(() => onError(null));
298
+ let errorSpy;
299
+ beforeEach(() => {
300
+ errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
301
+ });
302
+ afterEach(() => {
303
+ onError(null);
304
+ errorSpy.mockRestore();
305
+ });
201
306
 
202
307
  it('reuses ZQueryError cause directly', () => {
203
308
  const causes = [];
@@ -215,19 +320,47 @@ describe('reportError — cause handling', () => {
215
320
  expect(causes[0]).toBeInstanceOf(ZQueryError);
216
321
  expect(causes[0].cause).toBe(original);
217
322
  });
323
+
324
+ it('reports error without cause', () => {
325
+ const handler = vi.fn();
326
+ onError(handler);
327
+ reportError(ErrorCode.STORE_ACTION, 'no cause');
328
+ expect(handler.mock.calls[0][0].cause).toBeUndefined();
329
+ });
218
330
  });
219
331
 
220
332
 
221
- // ===========================================================================
333
+ // ---------------------------------------------------------------------------
222
334
  // guardCallback
223
- // ===========================================================================
335
+ // ---------------------------------------------------------------------------
224
336
 
225
- describe('guardCallback — edge cases', () => {
226
- afterEach(() => onError(null));
337
+ describe('guardCallback', () => {
338
+ let errorSpy;
339
+ beforeEach(() => {
340
+ onError(null);
341
+ errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
342
+ });
343
+ afterEach(() => {
344
+ errorSpy.mockRestore();
345
+ onError(null);
346
+ });
227
347
 
228
- it('returns function result on success', () => {
229
- const guarded = guardCallback(() => 42, ErrorCode.INVALID_ARGUMENT);
230
- expect(guarded()).toBe(42);
348
+ it('returns the same value as original function', () => {
349
+ const guarded = guardCallback((x) => x * 2, ErrorCode.COMP_RENDER);
350
+ expect(guarded(5)).toBe(10);
351
+ });
352
+
353
+ it('catches errors and reports them', () => {
354
+ const handler = vi.fn();
355
+ onError(handler);
356
+ const guarded = guardCallback(() => { throw new Error('boom'); }, ErrorCode.COMP_RENDER, { component: 'test' });
357
+ expect(() => guarded()).not.toThrow();
358
+ expect(handler).toHaveBeenCalledOnce();
359
+ });
360
+
361
+ it('passes arguments through', () => {
362
+ const guarded = guardCallback((a, b) => a + b, ErrorCode.COMP_RENDER);
363
+ expect(guarded(1, 2)).toBe(3);
231
364
  });
232
365
 
233
366
  it('returns undefined on error', () => {
@@ -235,18 +368,117 @@ describe('guardCallback — edge cases', () => {
235
368
  expect(guarded()).toBeUndefined();
236
369
  });
237
370
 
238
- it('passes arguments through', () => {
239
- const guarded = guardCallback((a, b) => a + b, ErrorCode.INVALID_ARGUMENT);
240
- expect(guarded(2, 3)).toBe(5);
371
+ it('preserves context in error report', () => {
372
+ const handler = vi.fn();
373
+ onError(handler);
374
+ const guarded = guardCallback(
375
+ () => { throw new Error('bad'); },
376
+ ErrorCode.COMP_LIFECYCLE,
377
+ { hook: 'mounted', component: 'my-app' }
378
+ );
379
+ guarded();
380
+ const err = handler.mock.calls[0][0];
381
+ expect(err.code).toBe(ErrorCode.COMP_LIFECYCLE);
382
+ });
383
+
384
+ it('works with zero arguments', () => {
385
+ const guarded = guardCallback(() => 'ok', ErrorCode.COMP_RENDER);
386
+ expect(guarded()).toBe('ok');
387
+ });
388
+
389
+ it('works with many arguments', () => {
390
+ const guarded = guardCallback((...args) => args.length, ErrorCode.COMP_RENDER);
391
+ expect(guarded(1, 2, 3, 4, 5)).toBe(5);
241
392
  });
242
393
  });
243
394
 
244
395
 
245
- // ===========================================================================
246
- // validate — edge cases
247
- // ===========================================================================
396
+ // ---------------------------------------------------------------------------
397
+ // guardAsync
398
+ // ---------------------------------------------------------------------------
399
+
400
+ describe('guardAsync', () => {
401
+ let errorSpy;
402
+ beforeEach(() => {
403
+ onError(null);
404
+ errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
405
+ });
406
+ afterEach(() => {
407
+ errorSpy.mockRestore();
408
+ onError(null);
409
+ });
410
+
411
+ it('returns the resolved value on success', async () => {
412
+ const guarded = guardAsync(async (x) => x * 2, ErrorCode.COMP_RENDER);
413
+ expect(await guarded(5)).toBe(10);
414
+ });
415
+
416
+ it('catches async errors and reports them', async () => {
417
+ const handler = vi.fn();
418
+ onError(handler);
419
+ const guarded = guardAsync(async () => { throw new Error('async boom'); }, ErrorCode.HTTP_REQUEST);
420
+ const result = await guarded();
421
+ expect(result).toBeUndefined();
422
+ expect(handler).toHaveBeenCalledOnce();
423
+ expect(handler.mock.calls[0][0].code).toBe(ErrorCode.HTTP_REQUEST);
424
+ });
425
+
426
+ it('passes arguments through', async () => {
427
+ const guarded = guardAsync(async (a, b) => a + b, ErrorCode.COMP_RENDER);
428
+ expect(await guarded(3, 7)).toBe(10);
429
+ });
430
+
431
+ it('does not throw on rejection', async () => {
432
+ const guarded = guardAsync(async () => { throw new Error('fail'); }, ErrorCode.STORE_ACTION);
433
+ await expect(guarded()).resolves.toBeUndefined();
434
+ });
435
+
436
+ it('preserves context in error report', async () => {
437
+ const handler = vi.fn();
438
+ onError(handler);
439
+ const guarded = guardAsync(
440
+ async () => { throw new Error('bad'); },
441
+ ErrorCode.ROUTER_LOAD,
442
+ { route: '/about' }
443
+ );
444
+ await guarded();
445
+ const err = handler.mock.calls[0][0];
446
+ expect(err.code).toBe(ErrorCode.ROUTER_LOAD);
447
+ });
448
+ });
449
+
450
+
451
+ // ---------------------------------------------------------------------------
452
+ // validate
453
+ // ---------------------------------------------------------------------------
454
+
455
+ describe('validate', () => {
456
+ it('passes for valid values', () => {
457
+ expect(() => validate('hello', 'name', 'string')).not.toThrow();
458
+ expect(() => validate(42, 'count', 'number')).not.toThrow();
459
+ expect(() => validate(() => {}, 'fn', 'function')).not.toThrow();
460
+ });
461
+
462
+ it('throws ZQueryError for null', () => {
463
+ expect(() => validate(null, 'name', 'string')).toThrow(ZQueryError);
464
+ });
465
+
466
+ it('throws ZQueryError for undefined', () => {
467
+ expect(() => validate(undefined, 'count')).toThrow(ZQueryError);
468
+ });
469
+
470
+ it('throws ZQueryError for wrong type', () => {
471
+ expect(() => validate(42, 'name', 'string')).toThrow(ZQueryError);
472
+ });
473
+
474
+ it('error has INVALID_ARGUMENT code', () => {
475
+ try {
476
+ validate(null, 'param');
477
+ } catch (err) {
478
+ expect(err.code).toBe(ErrorCode.INVALID_ARGUMENT);
479
+ }
480
+ });
248
481
 
249
- describe('validate — edge cases', () => {
250
482
  it('validates without expectedType (null/undefined only)', () => {
251
483
  expect(() => validate(null, 'x')).toThrow();
252
484
  expect(() => validate(undefined, 'x')).toThrow();
@@ -254,88 +486,134 @@ describe('validate — edge cases', () => {
254
486
  expect(() => validate('', 'x')).not.toThrow();
255
487
  expect(() => validate(false, 'x')).not.toThrow();
256
488
  });
257
- });
258
-
259
489
 
260
- // ===========================================================================
261
- // ZQueryError — structure
262
- // ===========================================================================
263
-
264
- describe('ZQueryError structure', () => {
265
- it('has correct name', () => {
266
- const err = new ZQueryError(ErrorCode.INVALID_ARGUMENT, 'test');
267
- expect(err.name).toBe('ZQueryError');
490
+ it('error message contains parameter name', () => {
491
+ try {
492
+ validate(null, 'myParam');
493
+ expect.unreachable();
494
+ } catch (err) {
495
+ expect(err.message).toContain('myParam');
496
+ }
268
497
  });
269
498
 
270
- it('has empty context by default', () => {
271
- const err = new ZQueryError(ErrorCode.INVALID_ARGUMENT, 'test');
272
- expect(err.context).toEqual({});
499
+ it('error message for wrong type contains expected type', () => {
500
+ try {
501
+ validate(42, 'name', 'string');
502
+ expect.unreachable();
503
+ } catch (err) {
504
+ expect(err.message).toContain('string');
505
+ expect(err.message).toContain('number');
506
+ }
273
507
  });
274
508
 
275
- it('stores cause when provided', () => {
276
- const cause = new Error('root');
277
- const err = new ZQueryError(ErrorCode.INVALID_ARGUMENT, 'test', {}, cause);
278
- expect(err.cause).toBe(cause);
509
+ it('accepts object type', () => {
510
+ expect(() => validate({}, 'cfg', 'object')).not.toThrow();
511
+ expect(() => validate('str', 'cfg', 'object')).toThrow();
279
512
  });
280
513
 
281
- it('has no cause property when not provided', () => {
282
- const err = new ZQueryError(ErrorCode.INVALID_ARGUMENT, 'test');
283
- expect(err.cause).toBeUndefined();
514
+ it('accepts boolean type', () => {
515
+ expect(() => validate(true, 'flag', 'boolean')).not.toThrow();
516
+ expect(() => validate('yes', 'flag', 'boolean')).toThrow();
284
517
  });
285
518
  });
286
519
 
287
520
 
288
- // ===========================================================================
289
- // ErrorCode — completeness
290
- // ===========================================================================
521
+ // ---------------------------------------------------------------------------
522
+ // formatError
523
+ // ---------------------------------------------------------------------------
291
524
 
292
- describe('ErrorCode — all codes defined', () => {
293
- it('has reactive codes', () => {
294
- expect(ErrorCode.REACTIVE_CALLBACK).toBe('ZQ_REACTIVE_CALLBACK');
295
- expect(ErrorCode.SIGNAL_CALLBACK).toBe('ZQ_SIGNAL_CALLBACK');
296
- expect(ErrorCode.EFFECT_EXEC).toBe('ZQ_EFFECT_EXEC');
525
+ describe('formatError', () => {
526
+ it('formats ZQueryError correctly', () => {
527
+ const err = new ZQueryError(ErrorCode.COMP_RENDER, 'render failed', { component: 'my-app' });
528
+ const formatted = formatError(err);
529
+ expect(formatted.code).toBe('ZQ_COMP_RENDER');
530
+ expect(formatted.type).toBe('ZQueryError');
531
+ expect(formatted.message).toBe('render failed');
532
+ expect(formatted.context.component).toBe('my-app');
533
+ expect(formatted.stack).toBeDefined();
534
+ expect(formatted.cause).toBeNull();
535
+ });
536
+
537
+ it('formats plain Error correctly', () => {
538
+ const err = new Error('plain error');
539
+ const formatted = formatError(err);
540
+ expect(formatted.code).toBe('');
541
+ expect(formatted.type).toBe('Error');
542
+ expect(formatted.message).toBe('plain error');
543
+ expect(formatted.context).toEqual({});
544
+ expect(formatted.cause).toBeNull();
545
+ });
546
+
547
+ it('formats nested cause chain', () => {
548
+ const root = new Error('root cause');
549
+ const err = new ZQueryError(ErrorCode.HTTP_REQUEST, 'http failed', {}, root);
550
+ const formatted = formatError(err);
551
+ expect(formatted.cause).not.toBeNull();
552
+ expect(formatted.cause.message).toBe('root cause');
553
+ expect(formatted.cause.type).toBe('Error');
554
+ });
555
+
556
+ it('handles TypeError correctly', () => {
557
+ const err = new TypeError('cannot read property');
558
+ const formatted = formatError(err);
559
+ expect(formatted.type).toBe('TypeError');
560
+ expect(formatted.code).toBe('');
561
+ });
562
+
563
+ it('handles error with no message', () => {
564
+ const err = new Error();
565
+ const formatted = formatError(err);
566
+ expect(formatted.message).toBeDefined();
567
+ });
568
+
569
+ it('returns serializable object', () => {
570
+ const err = new ZQueryError(ErrorCode.STORE_ACTION, 'test', { action: 'foo' });
571
+ const formatted = formatError(err);
572
+ const json = JSON.stringify(formatted);
573
+ expect(typeof json).toBe('string');
574
+ const parsed = JSON.parse(json);
575
+ expect(parsed.code).toBe('ZQ_STORE_ACTION');
297
576
  });
577
+ });
298
578
 
299
- it('has expression codes', () => {
300
- expect(ErrorCode.EXPR_PARSE).toBe('ZQ_EXPR_PARSE');
301
- expect(ErrorCode.EXPR_EVAL).toBe('ZQ_EXPR_EVAL');
302
- expect(ErrorCode.EXPR_UNSAFE_ACCESS).toBe('ZQ_EXPR_UNSAFE_ACCESS');
303
- });
304
579
 
305
- it('has component codes', () => {
306
- expect(ErrorCode.COMP_INVALID_NAME).toBe('ZQ_COMP_INVALID_NAME');
307
- expect(ErrorCode.COMP_NOT_FOUND).toBe('ZQ_COMP_NOT_FOUND');
308
- expect(ErrorCode.COMP_MOUNT_TARGET).toBe('ZQ_COMP_MOUNT_TARGET');
309
- expect(ErrorCode.COMP_RENDER).toBe('ZQ_COMP_RENDER');
310
- expect(ErrorCode.COMP_LIFECYCLE).toBe('ZQ_COMP_LIFECYCLE');
311
- expect(ErrorCode.COMP_RESOURCE).toBe('ZQ_COMP_RESOURCE');
312
- expect(ErrorCode.COMP_DIRECTIVE).toBe('ZQ_COMP_DIRECTIVE');
313
- });
580
+ // ---------------------------------------------------------------------------
581
+ // Integration: SSR errors use SSR_* codes
582
+ // ---------------------------------------------------------------------------
314
583
 
315
- it('has router codes', () => {
316
- expect(ErrorCode.ROUTER_LOAD).toBe('ZQ_ROUTER_LOAD');
317
- expect(ErrorCode.ROUTER_GUARD).toBe('ZQ_ROUTER_GUARD');
318
- expect(ErrorCode.ROUTER_RESOLVE).toBe('ZQ_ROUTER_RESOLVE');
319
- });
584
+ describe('SSR error integration', () => {
585
+ afterEach(() => onError(null));
320
586
 
321
- it('has store codes', () => {
322
- expect(ErrorCode.STORE_ACTION).toBe('ZQ_STORE_ACTION');
323
- expect(ErrorCode.STORE_MIDDLEWARE).toBe('ZQ_STORE_MIDDLEWARE');
324
- expect(ErrorCode.STORE_SUBSCRIBE).toBe('ZQ_STORE_SUBSCRIBE');
587
+ it('SSR error codes exist and are properly prefixed', () => {
588
+ expect(ErrorCode.SSR_RENDER).toBe('ZQ_SSR_RENDER');
589
+ expect(ErrorCode.SSR_COMPONENT).toBe('ZQ_SSR_COMPONENT');
590
+ expect(ErrorCode.SSR_HYDRATION).toBe('ZQ_SSR_HYDRATION');
591
+ expect(ErrorCode.SSR_PAGE).toBe('ZQ_SSR_PAGE');
325
592
  });
326
593
 
327
- it('has http codes', () => {
328
- expect(ErrorCode.HTTP_REQUEST).toBe('ZQ_HTTP_REQUEST');
329
- expect(ErrorCode.HTTP_TIMEOUT).toBe('ZQ_HTTP_TIMEOUT');
330
- expect(ErrorCode.HTTP_INTERCEPTOR).toBe('ZQ_HTTP_INTERCEPTOR');
331
- expect(ErrorCode.HTTP_PARSE).toBe('ZQ_HTTP_PARSE');
594
+ it('ZQueryError with SSR code works correctly', () => {
595
+ const err = new ZQueryError(ErrorCode.SSR_RENDER, 'SSR render failed', { component: 'my-page' });
596
+ expect(err.code).toBe('ZQ_SSR_RENDER');
597
+ expect(err.context.component).toBe('my-page');
598
+ expect(err).toBeInstanceOf(Error);
332
599
  });
333
600
 
334
- it('has general codes', () => {
335
- expect(ErrorCode.INVALID_ARGUMENT).toBe('ZQ_INVALID_ARGUMENT');
601
+ it('reportError with SSR codes works', () => {
602
+ const handler = vi.fn();
603
+ onError(handler);
604
+ const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
605
+
606
+ reportError(ErrorCode.SSR_RENDER, 'ssr test', { component: 'test' });
607
+ expect(handler).toHaveBeenCalledOnce();
608
+ expect(handler.mock.calls[0][0].code).toBe(ErrorCode.SSR_RENDER);
609
+
610
+ spy.mockRestore();
336
611
  });
337
612
 
338
- it('object is frozen', () => {
339
- expect(Object.isFrozen(ErrorCode)).toBe(true);
613
+ it('formatError works with SSR errors', () => {
614
+ const err = new ZQueryError(ErrorCode.SSR_COMPONENT, 'not registered', { component: 'foo' });
615
+ const formatted = formatError(err);
616
+ expect(formatted.code).toBe('ZQ_SSR_COMPONENT');
617
+ expect(formatted.context.component).toBe('foo');
340
618
  });
341
619
  });