zero-query 0.6.3 → 0.7.5

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.
Files changed (41) hide show
  1. package/README.md +6 -6
  2. package/cli/commands/build.js +3 -3
  3. package/cli/commands/bundle.js +286 -8
  4. package/cli/commands/dev/index.js +2 -2
  5. package/cli/commands/dev/overlay.js +51 -2
  6. package/cli/commands/dev/server.js +34 -5
  7. package/cli/commands/dev/watcher.js +33 -0
  8. package/cli/scaffold/index.html +1 -0
  9. package/cli/scaffold/scripts/app.js +15 -22
  10. package/cli/scaffold/scripts/components/contacts/contacts.css +0 -7
  11. package/cli/scaffold/scripts/components/contacts/contacts.html +3 -3
  12. package/cli/scaffold/styles/styles.css +1 -0
  13. package/cli/utils.js +111 -6
  14. package/dist/zquery.dist.zip +0 -0
  15. package/dist/zquery.js +379 -27
  16. package/dist/zquery.min.js +3 -16
  17. package/index.d.ts +127 -1290
  18. package/package.json +5 -5
  19. package/src/component.js +11 -1
  20. package/src/core.js +305 -10
  21. package/src/router.js +49 -2
  22. package/tests/component.test.js +304 -0
  23. package/tests/core.test.js +726 -0
  24. package/tests/diff.test.js +194 -0
  25. package/tests/errors.test.js +162 -0
  26. package/tests/expression.test.js +334 -0
  27. package/tests/http.test.js +181 -0
  28. package/tests/reactive.test.js +191 -0
  29. package/tests/router.test.js +332 -0
  30. package/tests/store.test.js +253 -0
  31. package/tests/utils.test.js +353 -0
  32. package/types/collection.d.ts +368 -0
  33. package/types/component.d.ts +210 -0
  34. package/types/errors.d.ts +103 -0
  35. package/types/http.d.ts +81 -0
  36. package/types/misc.d.ts +166 -0
  37. package/types/reactive.d.ts +76 -0
  38. package/types/router.d.ts +132 -0
  39. package/types/ssr.d.ts +49 -0
  40. package/types/store.d.ts +107 -0
  41. package/types/utils.d.ts +142 -0
@@ -0,0 +1,253 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { createStore, getStore } from '../src/store.js';
3
+
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Store creation
7
+ // ---------------------------------------------------------------------------
8
+
9
+ describe('Store — creation', () => {
10
+ it('creates a store with initial state', () => {
11
+ const store = createStore('test-create', {
12
+ state: { count: 0, name: 'Tony' },
13
+ });
14
+ expect(store.state.count).toBe(0);
15
+ expect(store.state.name).toBe('Tony');
16
+ });
17
+
18
+ it('supports state as a factory function', () => {
19
+ const store = createStore('test-fn', {
20
+ state: () => ({ items: [] }),
21
+ });
22
+ expect(store.state.items).toEqual([]);
23
+ });
24
+
25
+ it('getStore retrieves by name', () => {
26
+ const store = createStore('named-store', { state: { x: 1 } });
27
+ expect(getStore('named-store')).toBe(store);
28
+ });
29
+
30
+ it('getStore returns null for unknown stores', () => {
31
+ expect(getStore('nonexistent')).toBeNull();
32
+ });
33
+
34
+ it('defaults to "default" when no name is provided', () => {
35
+ const store = createStore({ state: { val: 42 } });
36
+ expect(getStore('default')).toBe(store);
37
+ });
38
+ });
39
+
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Dispatch & actions
43
+ // ---------------------------------------------------------------------------
44
+
45
+ describe('Store — dispatch', () => {
46
+ it('dispatches a named action', () => {
47
+ const store = createStore('dispatch-1', {
48
+ state: { count: 0 },
49
+ actions: {
50
+ increment(state) { state.count++; },
51
+ },
52
+ });
53
+ store.dispatch('increment');
54
+ expect(store.state.count).toBe(1);
55
+ });
56
+
57
+ it('passes payload to action', () => {
58
+ const store = createStore('dispatch-2', {
59
+ state: { count: 0 },
60
+ actions: {
61
+ add(state, amount) { state.count += amount; },
62
+ },
63
+ });
64
+ store.dispatch('add', 5);
65
+ expect(store.state.count).toBe(5);
66
+ });
67
+
68
+ it('reports error for unknown actions', () => {
69
+ const store = createStore('dispatch-unknown', {
70
+ state: {},
71
+ actions: {},
72
+ });
73
+ // Should not throw if action doesn't exist
74
+ expect(() => store.dispatch('nonexistent')).not.toThrow();
75
+ });
76
+
77
+ it('records action in history', () => {
78
+ const store = createStore('dispatch-hist', {
79
+ state: { x: 0 },
80
+ actions: { inc(state) { state.x++; } },
81
+ });
82
+ store.dispatch('inc');
83
+ store.dispatch('inc');
84
+ expect(store.history.length).toBe(2);
85
+ expect(store.history[0].action).toBe('inc');
86
+ });
87
+
88
+ it('does not crash when action throws', () => {
89
+ const store = createStore('dispatch-throw', {
90
+ state: {},
91
+ actions: {
92
+ bad() { throw new Error('action error'); },
93
+ },
94
+ });
95
+ expect(() => store.dispatch('bad')).not.toThrow();
96
+ });
97
+ });
98
+
99
+
100
+ // ---------------------------------------------------------------------------
101
+ // Subscriptions
102
+ // ---------------------------------------------------------------------------
103
+
104
+ describe('Store — subscribe', () => {
105
+ it('notifies key-specific subscribers', () => {
106
+ const store = createStore('sub-1', {
107
+ state: { count: 0 },
108
+ actions: { inc(state) { state.count++; } },
109
+ });
110
+ const fn = vi.fn();
111
+ store.subscribe('count', fn);
112
+ store.dispatch('inc');
113
+ expect(fn).toHaveBeenCalledWith(1, 0, 'count');
114
+ });
115
+
116
+ it('wildcard subscriber gets all changes', () => {
117
+ const store = createStore('sub-2', {
118
+ state: { a: 0, b: 0 },
119
+ actions: {
120
+ setA(state, v) { state.a = v; },
121
+ setB(state, v) { state.b = v; },
122
+ },
123
+ });
124
+ const fn = vi.fn();
125
+ store.subscribe(fn);
126
+ store.dispatch('setA', 1);
127
+ store.dispatch('setB', 2);
128
+ expect(fn).toHaveBeenCalledTimes(2);
129
+ });
130
+
131
+ it('unsubscribe stops notifications', () => {
132
+ const store = createStore('sub-3', {
133
+ state: { x: 0 },
134
+ actions: { inc(state) { state.x++; } },
135
+ });
136
+ const fn = vi.fn();
137
+ const unsub = store.subscribe('x', fn);
138
+ store.dispatch('inc');
139
+ expect(fn).toHaveBeenCalledOnce();
140
+ unsub();
141
+ store.dispatch('inc');
142
+ expect(fn).toHaveBeenCalledOnce();
143
+ });
144
+
145
+ it('does not crash when subscriber throws', () => {
146
+ const store = createStore('sub-throw', {
147
+ state: { x: 0 },
148
+ actions: { inc(state) { state.x++; } },
149
+ });
150
+ store.subscribe('x', () => { throw new Error('subscriber error'); });
151
+ expect(() => store.dispatch('inc')).not.toThrow();
152
+ });
153
+ });
154
+
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // Getters
158
+ // ---------------------------------------------------------------------------
159
+
160
+ describe('Store — getters', () => {
161
+ it('computes values from state', () => {
162
+ const store = createStore('getters-1', {
163
+ state: { count: 5 },
164
+ getters: {
165
+ doubled: (state) => state.count * 2,
166
+ },
167
+ });
168
+ expect(store.getters.doubled).toBe(10);
169
+ });
170
+
171
+ it('updates when state changes', () => {
172
+ const store = createStore('getters-2', {
173
+ state: { count: 1 },
174
+ actions: { inc(state) { state.count++; } },
175
+ getters: { doubled: (state) => state.count * 2 },
176
+ });
177
+ store.dispatch('inc');
178
+ expect(store.getters.doubled).toBe(4);
179
+ });
180
+ });
181
+
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // Middleware
185
+ // ---------------------------------------------------------------------------
186
+
187
+ describe('Store — middleware', () => {
188
+ it('calls middleware before action', () => {
189
+ const log = vi.fn();
190
+ const store = createStore('mw-1', {
191
+ state: { x: 0 },
192
+ actions: { inc(state) { state.x++; } },
193
+ });
194
+ store.use((name, args, state) => { log(name); });
195
+ store.dispatch('inc');
196
+ expect(log).toHaveBeenCalledWith('inc');
197
+ });
198
+
199
+ it('blocks action when middleware returns false', () => {
200
+ const store = createStore('mw-block', {
201
+ state: { x: 0 },
202
+ actions: { inc(state) { state.x++; } },
203
+ });
204
+ store.use(() => false);
205
+ store.dispatch('inc');
206
+ expect(store.state.x).toBe(0);
207
+ });
208
+
209
+ it('does not crash when middleware throws', () => {
210
+ const store = createStore('mw-throw', {
211
+ state: { x: 0 },
212
+ actions: { inc(state) { state.x++; } },
213
+ });
214
+ store.use(() => { throw new Error('middleware error'); });
215
+ expect(() => store.dispatch('inc')).not.toThrow();
216
+ // Action should NOT have run (middleware threw → dispatch returns)
217
+ expect(store.state.x).toBe(0);
218
+ });
219
+ });
220
+
221
+
222
+ // ---------------------------------------------------------------------------
223
+ // Snapshot & replaceState
224
+ // ---------------------------------------------------------------------------
225
+
226
+ describe('Store — snapshot & replaceState', () => {
227
+ it('snapshot returns plain object copy', () => {
228
+ const store = createStore('snap-1', { state: { a: 1, b: { c: 2 } } });
229
+ const snap = store.snapshot();
230
+ expect(snap).toEqual({ a: 1, b: { c: 2 } });
231
+ snap.a = 99;
232
+ expect(store.state.a).toBe(1); // original unchanged
233
+ });
234
+
235
+ it('replaceState replaces entire state', () => {
236
+ const store = createStore('replace-1', { state: { x: 1, y: 2 } });
237
+ store.replaceState({ x: 10, z: 30 });
238
+ expect(store.state.x).toBe(10);
239
+ expect(store.state.z).toBe(30);
240
+ });
241
+
242
+ it('reset replaces state and clears history', () => {
243
+ const store = createStore('reset-1', {
244
+ state: { count: 0 },
245
+ actions: { inc(state) { state.count++; } },
246
+ });
247
+ store.dispatch('inc');
248
+ store.dispatch('inc');
249
+ store.reset({ count: 0 });
250
+ expect(store.state.count).toBe(0);
251
+ expect(store.history.length).toBe(0);
252
+ });
253
+ });
@@ -0,0 +1,353 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import {
3
+ debounce, throttle, pipe, once, sleep,
4
+ escapeHtml, trust, uuid, camelCase, kebabCase,
5
+ deepClone, deepMerge, isEqual, param, parseQuery,
6
+ bus,
7
+ } from '../src/utils.js';
8
+
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Function utilities
12
+ // ---------------------------------------------------------------------------
13
+
14
+ describe('debounce', () => {
15
+ beforeEach(() => { vi.useFakeTimers(); });
16
+
17
+ it('delays execution until after ms of inactivity', () => {
18
+ const fn = vi.fn();
19
+ const debounced = debounce(fn, 100);
20
+ debounced('a');
21
+ debounced('b');
22
+ expect(fn).not.toHaveBeenCalled();
23
+ vi.advanceTimersByTime(100);
24
+ expect(fn).toHaveBeenCalledOnce();
25
+ expect(fn).toHaveBeenCalledWith('b');
26
+ });
27
+
28
+ it('uses default 250ms delay', () => {
29
+ const fn = vi.fn();
30
+ const debounced = debounce(fn);
31
+ debounced();
32
+ vi.advanceTimersByTime(249);
33
+ expect(fn).not.toHaveBeenCalled();
34
+ vi.advanceTimersByTime(1);
35
+ expect(fn).toHaveBeenCalledOnce();
36
+ });
37
+
38
+ it('cancel() stops pending execution', () => {
39
+ const fn = vi.fn();
40
+ const debounced = debounce(fn, 100);
41
+ debounced();
42
+ debounced.cancel();
43
+ vi.advanceTimersByTime(200);
44
+ expect(fn).not.toHaveBeenCalled();
45
+ });
46
+
47
+ it('resets timer on subsequent calls', () => {
48
+ const fn = vi.fn();
49
+ const debounced = debounce(fn, 100);
50
+ debounced();
51
+ vi.advanceTimersByTime(80);
52
+ debounced();
53
+ vi.advanceTimersByTime(80);
54
+ expect(fn).not.toHaveBeenCalled();
55
+ vi.advanceTimersByTime(20);
56
+ expect(fn).toHaveBeenCalledOnce();
57
+ });
58
+ });
59
+
60
+
61
+ describe('throttle', () => {
62
+ beforeEach(() => { vi.useFakeTimers(); });
63
+
64
+ it('fires immediately on first call', () => {
65
+ const fn = vi.fn();
66
+ const throttled = throttle(fn, 100);
67
+ throttled('a');
68
+ expect(fn).toHaveBeenCalledWith('a');
69
+ });
70
+
71
+ it('delays subsequent calls within the window', () => {
72
+ const fn = vi.fn();
73
+ const throttled = throttle(fn, 100);
74
+ throttled('a');
75
+ throttled('b');
76
+ expect(fn).toHaveBeenCalledTimes(1);
77
+ vi.advanceTimersByTime(100);
78
+ expect(fn).toHaveBeenCalledTimes(2);
79
+ expect(fn).toHaveBeenLastCalledWith('b');
80
+ });
81
+ });
82
+
83
+
84
+ describe('pipe', () => {
85
+ it('composes functions left-to-right', () => {
86
+ const add1 = x => x + 1;
87
+ const double = x => x * 2;
88
+ expect(pipe(add1, double)(3)).toBe(8);
89
+ });
90
+
91
+ it('handles a single function', () => {
92
+ const identity = x => x;
93
+ expect(pipe(identity)(42)).toBe(42);
94
+ });
95
+
96
+ it('handles no functions', () => {
97
+ expect(pipe()(10)).toBe(10);
98
+ });
99
+ });
100
+
101
+
102
+ describe('once', () => {
103
+ it('only calls function once', () => {
104
+ const fn = vi.fn(() => 42);
105
+ const onceFn = once(fn);
106
+ expect(onceFn()).toBe(42);
107
+ expect(onceFn()).toBe(42);
108
+ expect(fn).toHaveBeenCalledOnce();
109
+ });
110
+
111
+ it('passes arguments to the first call', () => {
112
+ const fn = vi.fn((a, b) => a + b);
113
+ const onceFn = once(fn);
114
+ expect(onceFn(1, 2)).toBe(3);
115
+ expect(onceFn(10, 20)).toBe(3);
116
+ });
117
+ });
118
+
119
+
120
+ describe('sleep', () => {
121
+ it('returns a promise that resolves after ms', async () => {
122
+ vi.useFakeTimers();
123
+ const p = sleep(100);
124
+ vi.advanceTimersByTime(100);
125
+ await expect(p).resolves.toBeUndefined();
126
+ });
127
+ });
128
+
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // String utilities
132
+ // ---------------------------------------------------------------------------
133
+
134
+ describe('escapeHtml', () => {
135
+ it('escapes &, <, >, ", \'', () => {
136
+ expect(escapeHtml('&<>"\'')).toBe('&amp;&lt;&gt;&quot;&#39;');
137
+ });
138
+
139
+ it('converts non-strings to string', () => {
140
+ expect(escapeHtml(42)).toBe('42');
141
+ expect(escapeHtml(null)).toBe('null');
142
+ });
143
+
144
+ it('handles empty string', () => {
145
+ expect(escapeHtml('')).toBe('');
146
+ });
147
+ });
148
+
149
+
150
+ describe('trust', () => {
151
+ it('returns a TrustedHTML instance', () => {
152
+ const t = trust('<b>bold</b>');
153
+ expect(t.toString()).toBe('<b>bold</b>');
154
+ });
155
+ });
156
+
157
+
158
+ describe('uuid', () => {
159
+ it('returns a string', () => {
160
+ expect(typeof uuid()).toBe('string');
161
+ });
162
+
163
+ it('returns different values on successive calls', () => {
164
+ const a = uuid();
165
+ const b = uuid();
166
+ expect(a).not.toBe(b);
167
+ });
168
+
169
+ it('has valid UUID v4 format', () => {
170
+ expect(uuid()).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
171
+ });
172
+ });
173
+
174
+
175
+ describe('camelCase', () => {
176
+ it('converts kebab-case to camelCase', () => {
177
+ expect(camelCase('my-component')).toBe('myComponent');
178
+ expect(camelCase('a-b-c')).toBe('aBC');
179
+ });
180
+
181
+ it('handles no hyphens', () => {
182
+ expect(camelCase('hello')).toBe('hello');
183
+ });
184
+ });
185
+
186
+
187
+ describe('kebabCase', () => {
188
+ it('converts camelCase to kebab-case', () => {
189
+ expect(kebabCase('myComponent')).toBe('my-component');
190
+ expect(kebabCase('fooBarBaz')).toBe('foo-bar-baz');
191
+ });
192
+
193
+ it('handles already kebab-case', () => {
194
+ expect(kebabCase('hello')).toBe('hello');
195
+ });
196
+ });
197
+
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // Object utilities
201
+ // ---------------------------------------------------------------------------
202
+
203
+ describe('deepClone', () => {
204
+ it('creates an independent copy', () => {
205
+ const obj = { a: 1, b: { c: 2 } };
206
+ const clone = deepClone(obj);
207
+ clone.b.c = 99;
208
+ expect(obj.b.c).toBe(2);
209
+ });
210
+
211
+ it('handles arrays', () => {
212
+ const arr = [1, [2, 3]];
213
+ const clone = deepClone(arr);
214
+ clone[1][0] = 99;
215
+ expect(arr[1][0]).toBe(2);
216
+ });
217
+ });
218
+
219
+
220
+ describe('deepMerge', () => {
221
+ it('deeply merges objects', () => {
222
+ const a = { x: 1, nested: { y: 2 } };
223
+ const b = { nested: { z: 3 }, w: 4 };
224
+ const result = deepMerge(a, b);
225
+ expect(result).toEqual({ x: 1, nested: { y: 2, z: 3 }, w: 4 });
226
+ });
227
+
228
+ it('overwrites non-object values', () => {
229
+ const result = deepMerge({ a: 1 }, { a: 2 });
230
+ expect(result.a).toBe(2);
231
+ });
232
+
233
+ it('handles arrays by replacing them', () => {
234
+ const result = deepMerge({ a: [1, 2] }, { a: [3, 4] });
235
+ expect(result.a).toEqual([3, 4]);
236
+ });
237
+ });
238
+
239
+
240
+ describe('isEqual', () => {
241
+ it('returns true for equal primitives', () => {
242
+ expect(isEqual(1, 1)).toBe(true);
243
+ expect(isEqual('a', 'a')).toBe(true);
244
+ expect(isEqual(null, null)).toBe(true);
245
+ });
246
+
247
+ it('returns false for different primitives', () => {
248
+ expect(isEqual(1, 2)).toBe(false);
249
+ expect(isEqual('a', 'b')).toBe(false);
250
+ });
251
+
252
+ it('returns true for deeply equal objects', () => {
253
+ expect(isEqual({ a: { b: 1 } }, { a: { b: 1 } })).toBe(true);
254
+ });
255
+
256
+ it('returns false for objects with different keys', () => {
257
+ expect(isEqual({ a: 1 }, { b: 1 })).toBe(false);
258
+ });
259
+
260
+ it('returns false for objects with different lengths', () => {
261
+ expect(isEqual({ a: 1 }, { a: 1, b: 2 })).toBe(false);
262
+ });
263
+
264
+ it('handles null comparisons', () => {
265
+ expect(isEqual(null, {})).toBe(false);
266
+ expect(isEqual({}, null)).toBe(false);
267
+ });
268
+ });
269
+
270
+
271
+ // ---------------------------------------------------------------------------
272
+ // URL utilities
273
+ // ---------------------------------------------------------------------------
274
+
275
+ describe('param', () => {
276
+ it('serializes object to query string', () => {
277
+ expect(param({ a: '1', b: '2' })).toBe('a=1&b=2');
278
+ });
279
+
280
+ it('handles empty object', () => {
281
+ expect(param({})).toBe('');
282
+ });
283
+ });
284
+
285
+
286
+ describe('parseQuery', () => {
287
+ it('parses query string to object', () => {
288
+ expect(parseQuery('a=1&b=2')).toEqual({ a: '1', b: '2' });
289
+ });
290
+
291
+ it('handles leading ?', () => {
292
+ expect(parseQuery('?foo=bar')).toEqual({ foo: 'bar' });
293
+ });
294
+
295
+ it('handles empty string', () => {
296
+ expect(parseQuery('')).toEqual({});
297
+ });
298
+ });
299
+
300
+
301
+ // ---------------------------------------------------------------------------
302
+ // Event bus
303
+ // ---------------------------------------------------------------------------
304
+
305
+ describe('bus (EventBus)', () => {
306
+ beforeEach(() => { bus.clear(); });
307
+
308
+ it('on/emit — fires handler for matching events', () => {
309
+ const fn = vi.fn();
310
+ bus.on('test', fn);
311
+ bus.emit('test', 42);
312
+ expect(fn).toHaveBeenCalledWith(42);
313
+ });
314
+
315
+ it('off — removes handler', () => {
316
+ const fn = vi.fn();
317
+ bus.on('test', fn);
318
+ bus.off('test', fn);
319
+ bus.emit('test');
320
+ expect(fn).not.toHaveBeenCalled();
321
+ });
322
+
323
+ it('on() returns unsubscribe function', () => {
324
+ const fn = vi.fn();
325
+ const unsub = bus.on('test', fn);
326
+ unsub();
327
+ bus.emit('test');
328
+ expect(fn).not.toHaveBeenCalled();
329
+ });
330
+
331
+ it('once — fires handler only once', () => {
332
+ const fn = vi.fn();
333
+ bus.once('test', fn);
334
+ bus.emit('test', 'a');
335
+ bus.emit('test', 'b');
336
+ expect(fn).toHaveBeenCalledOnce();
337
+ expect(fn).toHaveBeenCalledWith('a');
338
+ });
339
+
340
+ it('clear — removes all handlers', () => {
341
+ const fn = vi.fn();
342
+ bus.on('a', fn);
343
+ bus.on('b', fn);
344
+ bus.clear();
345
+ bus.emit('a');
346
+ bus.emit('b');
347
+ expect(fn).not.toHaveBeenCalled();
348
+ });
349
+
350
+ it('emit with no handlers does not throw', () => {
351
+ expect(() => bus.emit('nonexistent')).not.toThrow();
352
+ });
353
+ });