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.
- package/README.md +26 -3
- package/cli/commands/create.js +39 -5
- package/cli/help.js +2 -0
- package/cli/scaffold/ssr/app/app.js +30 -0
- package/cli/scaffold/ssr/app/components/about.js +28 -0
- package/cli/scaffold/ssr/app/components/home.js +37 -0
- package/cli/scaffold/ssr/app/components/not-found.js +15 -0
- package/cli/scaffold/ssr/app/routes.js +6 -0
- package/cli/scaffold/ssr/global.css +113 -0
- package/cli/scaffold/ssr/index.html +31 -0
- package/cli/scaffold/ssr/package.json +8 -0
- package/cli/scaffold/ssr/server/index.js +118 -0
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +64 -8
- package/dist/zquery.min.js +2 -2
- package/index.d.ts +11 -1
- package/index.js +4 -2
- package/package.json +8 -2
- package/src/errors.js +59 -5
- package/src/package.json +1 -0
- package/src/ssr.js +116 -22
- package/tests/errors.test.js +423 -145
- package/tests/ssr.test.js +435 -3
- package/types/errors.d.ts +34 -2
- package/types/ssr.d.ts +21 -1
package/tests/errors.test.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
onError(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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('
|
|
113
|
-
const
|
|
114
|
-
|
|
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('
|
|
207
|
+
it('handler can be removed via unsubscribe', () => {
|
|
118
208
|
const handler = vi.fn();
|
|
119
|
-
onError(handler);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
expect(handler).
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('
|
|
126
|
-
const
|
|
127
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
197
|
-
//
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
// reportError — cause handling
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
198
296
|
|
|
199
297
|
describe('reportError — cause handling', () => {
|
|
200
|
-
|
|
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
|
|
226
|
-
|
|
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
|
|
229
|
-
const guarded = guardCallback(() =>
|
|
230
|
-
expect(guarded()).toBe(
|
|
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('
|
|
239
|
-
const
|
|
240
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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('
|
|
271
|
-
|
|
272
|
-
|
|
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('
|
|
276
|
-
|
|
277
|
-
|
|
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('
|
|
282
|
-
|
|
283
|
-
expect(
|
|
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
|
-
//
|
|
290
|
-
//
|
|
521
|
+
// ---------------------------------------------------------------------------
|
|
522
|
+
// formatError
|
|
523
|
+
// ---------------------------------------------------------------------------
|
|
291
524
|
|
|
292
|
-
describe('
|
|
293
|
-
it('
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
expect(
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
316
|
-
|
|
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('
|
|
322
|
-
expect(ErrorCode.
|
|
323
|
-
expect(ErrorCode.
|
|
324
|
-
expect(ErrorCode.
|
|
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('
|
|
328
|
-
|
|
329
|
-
expect(
|
|
330
|
-
expect(
|
|
331
|
-
expect(
|
|
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('
|
|
335
|
-
|
|
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('
|
|
339
|
-
|
|
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
|
});
|