zero-query 0.5.2 → 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.
- package/README.md +12 -10
- package/cli/commands/build.js +7 -5
- package/cli/commands/bundle.js +286 -8
- package/cli/commands/dev/index.js +82 -0
- package/cli/commands/dev/logger.js +70 -0
- package/cli/commands/dev/overlay.js +366 -0
- package/cli/commands/dev/server.js +158 -0
- package/cli/commands/dev/validator.js +94 -0
- package/cli/commands/dev/watcher.js +147 -0
- package/cli/scaffold/favicon.ico +0 -0
- package/cli/scaffold/index.html +1 -0
- package/cli/scaffold/scripts/app.js +15 -22
- package/cli/scaffold/scripts/components/about.js +14 -2
- package/cli/scaffold/scripts/components/contacts/contacts.css +0 -7
- package/cli/scaffold/scripts/components/contacts/contacts.html +8 -7
- package/cli/scaffold/scripts/components/contacts/contacts.js +17 -1
- package/cli/scaffold/scripts/components/counter.js +30 -10
- package/cli/scaffold/scripts/components/home.js +3 -3
- package/cli/scaffold/scripts/components/todos.js +6 -5
- package/cli/scaffold/styles/styles.css +1 -0
- package/cli/utils.js +111 -6
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +2005 -216
- package/dist/zquery.min.js +3 -13
- package/index.d.ts +149 -1080
- package/index.js +18 -7
- package/package.json +9 -3
- package/src/component.js +186 -45
- package/src/core.js +327 -35
- package/src/diff.js +280 -0
- package/src/errors.js +155 -0
- package/src/expression.js +806 -0
- package/src/http.js +18 -10
- package/src/reactive.js +29 -4
- package/src/router.js +59 -6
- package/src/ssr.js +224 -0
- package/src/store.js +24 -8
- package/tests/component.test.js +304 -0
- package/tests/core.test.js +726 -0
- package/tests/diff.test.js +194 -0
- package/tests/errors.test.js +162 -0
- package/tests/expression.test.js +334 -0
- package/tests/http.test.js +181 -0
- package/tests/reactive.test.js +191 -0
- package/tests/router.test.js +332 -0
- package/tests/store.test.js +253 -0
- package/tests/utils.test.js +353 -0
- package/types/collection.d.ts +368 -0
- package/types/component.d.ts +210 -0
- package/types/errors.d.ts +103 -0
- package/types/http.d.ts +81 -0
- package/types/misc.d.ts +166 -0
- package/types/reactive.d.ts +76 -0
- package/types/router.d.ts +132 -0
- package/types/ssr.d.ts +49 -0
- package/types/store.d.ts +107 -0
- package/types/utils.d.ts +142 -0
- /package/cli/commands/{dev.js → dev.old.js} +0 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { component, mount, mountAll, getInstance, destroy, getRegistry } from '../src/component.js';
|
|
3
|
+
import { ZQueryError } from '../src/errors.js';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Setup
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
document.body.innerHTML = '';
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Component registration
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
describe('component() — registration', () => {
|
|
19
|
+
it('registers a component', () => {
|
|
20
|
+
component('test-comp', {
|
|
21
|
+
state: () => ({ count: 0 }),
|
|
22
|
+
render() { return `<p>${this.state.count}</p>`; },
|
|
23
|
+
});
|
|
24
|
+
const registry = getRegistry();
|
|
25
|
+
expect(registry['test-comp']).toBeDefined();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('throws ZQueryError if name has no hyphen', () => {
|
|
29
|
+
expect(() => component('nohyphen', {})).toThrow(ZQueryError);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('throws ZQueryError if name is empty', () => {
|
|
33
|
+
expect(() => component('', {})).toThrow(ZQueryError);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('throws ZQueryError if name is not a string', () => {
|
|
37
|
+
expect(() => component(null, {})).toThrow(ZQueryError);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Mount
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
describe('mount()', () => {
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
component('mount-test', {
|
|
49
|
+
state: () => ({ msg: 'Hello' }),
|
|
50
|
+
render() { return `<div class="inner">${this.state.msg}</div>`; },
|
|
51
|
+
});
|
|
52
|
+
document.body.innerHTML = '<mount-test id="target"></mount-test>';
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('mounts component and renders HTML', () => {
|
|
56
|
+
const instance = mount('#target', 'mount-test');
|
|
57
|
+
expect(document.querySelector('.inner').textContent).toBe('Hello');
|
|
58
|
+
expect(instance).toBeDefined();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('throws ZQueryError for missing target', () => {
|
|
62
|
+
expect(() => mount('#nonexistent', 'mount-test')).toThrow(ZQueryError);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('throws ZQueryError for unregistered component', () => {
|
|
66
|
+
expect(() => mount('#target', 'unknown-comp')).toThrow(ZQueryError);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('getInstance returns instance after mount', () => {
|
|
70
|
+
mount('#target', 'mount-test');
|
|
71
|
+
const inst = getInstance('#target');
|
|
72
|
+
expect(inst).not.toBeNull();
|
|
73
|
+
expect(inst.state.msg).toBe('Hello');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Lifecycle hooks
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
describe('component — lifecycle', () => {
|
|
83
|
+
it('calls init on creation', () => {
|
|
84
|
+
const initFn = vi.fn();
|
|
85
|
+
component('life-init', {
|
|
86
|
+
init: initFn,
|
|
87
|
+
render() { return '<div>init</div>'; },
|
|
88
|
+
});
|
|
89
|
+
document.body.innerHTML = '<life-init id="li"></life-init>';
|
|
90
|
+
mount('#li', 'life-init');
|
|
91
|
+
expect(initFn).toHaveBeenCalledOnce();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('calls mounted after first render', () => {
|
|
95
|
+
const mountedFn = vi.fn();
|
|
96
|
+
component('life-mounted', {
|
|
97
|
+
mounted: mountedFn,
|
|
98
|
+
render() { return '<div>mounted</div>'; },
|
|
99
|
+
});
|
|
100
|
+
document.body.innerHTML = '<life-mounted id="lm"></life-mounted>';
|
|
101
|
+
mount('#lm', 'life-mounted');
|
|
102
|
+
expect(mountedFn).toHaveBeenCalledOnce();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('calls destroyed on destroy', () => {
|
|
106
|
+
const destroyedFn = vi.fn();
|
|
107
|
+
component('life-destroy', {
|
|
108
|
+
destroyed: destroyedFn,
|
|
109
|
+
render() { return '<div>destroy</div>'; },
|
|
110
|
+
});
|
|
111
|
+
document.body.innerHTML = '<life-destroy id="ld"></life-destroy>';
|
|
112
|
+
mount('#ld', 'life-destroy');
|
|
113
|
+
destroy('#ld');
|
|
114
|
+
expect(destroyedFn).toHaveBeenCalledOnce();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('does not crash when lifecycle hook throws', () => {
|
|
118
|
+
component('life-throw', {
|
|
119
|
+
init() { throw new Error('init error'); },
|
|
120
|
+
render() { return '<div>throw</div>'; },
|
|
121
|
+
});
|
|
122
|
+
document.body.innerHTML = '<life-throw id="lt"></life-throw>';
|
|
123
|
+
expect(() => mount('#lt', 'life-throw')).not.toThrow();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Reactive state
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
describe('component — reactive state', () => {
|
|
133
|
+
it('re-renders on state change', async () => {
|
|
134
|
+
component('react-state', {
|
|
135
|
+
state: () => ({ count: 0 }),
|
|
136
|
+
render() { return `<span class="count">${this.state.count}</span>`; },
|
|
137
|
+
});
|
|
138
|
+
document.body.innerHTML = '<react-state id="rs"></react-state>';
|
|
139
|
+
const inst = mount('#rs', 'react-state');
|
|
140
|
+
expect(document.querySelector('.count').textContent).toBe('0');
|
|
141
|
+
|
|
142
|
+
inst.state.count = 5;
|
|
143
|
+
// State update is batched via microtask
|
|
144
|
+
await new Promise(r => queueMicrotask(r));
|
|
145
|
+
expect(document.querySelector('.count').textContent).toBe('5');
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Props
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
describe('component — props', () => {
|
|
155
|
+
it('receives props', () => {
|
|
156
|
+
component('prop-test', {
|
|
157
|
+
render() { return `<span class="prop">${this.props.label}</span>`; },
|
|
158
|
+
});
|
|
159
|
+
document.body.innerHTML = '<prop-test id="pt"></prop-test>';
|
|
160
|
+
mount('#pt', 'prop-test', { label: 'Hello' });
|
|
161
|
+
expect(document.querySelector('.prop').textContent).toBe('Hello');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('props are frozen', () => {
|
|
165
|
+
component('prop-freeze', {
|
|
166
|
+
render() { return '<div>test</div>'; },
|
|
167
|
+
});
|
|
168
|
+
document.body.innerHTML = '<prop-freeze id="pf"></prop-freeze>';
|
|
169
|
+
const inst = mount('#pf', 'prop-freeze', { x: 1 });
|
|
170
|
+
expect(() => { inst.props.x = 2; }).toThrow();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// Computed properties
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
describe('component — computed', () => {
|
|
180
|
+
it('derives values from state', () => {
|
|
181
|
+
component('comp-computed', {
|
|
182
|
+
state: () => ({ count: 5 }),
|
|
183
|
+
computed: {
|
|
184
|
+
doubled(state) { return state.count * 2; },
|
|
185
|
+
},
|
|
186
|
+
render() { return `<span class="doubled">${this.computed.doubled}</span>`; },
|
|
187
|
+
});
|
|
188
|
+
document.body.innerHTML = '<comp-computed id="cc"></comp-computed>';
|
|
189
|
+
const inst = mount('#cc', 'comp-computed');
|
|
190
|
+
expect(inst.computed.doubled).toBe(10);
|
|
191
|
+
expect(document.querySelector('.doubled').textContent).toBe('10');
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// User methods
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
describe('component — methods', () => {
|
|
201
|
+
it('binds user methods to instance', () => {
|
|
202
|
+
let captured;
|
|
203
|
+
component('method-test', {
|
|
204
|
+
state: () => ({ x: 42 }),
|
|
205
|
+
myMethod() { captured = this.state.x; },
|
|
206
|
+
render() { return '<div>methods</div>'; },
|
|
207
|
+
});
|
|
208
|
+
document.body.innerHTML = '<method-test id="mt"></method-test>';
|
|
209
|
+
const inst = mount('#mt', 'method-test');
|
|
210
|
+
inst.myMethod();
|
|
211
|
+
expect(captured).toBe(42);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// setState
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
describe('component — setState', () => {
|
|
221
|
+
it('batch updates state', async () => {
|
|
222
|
+
component('set-state', {
|
|
223
|
+
state: () => ({ a: 1, b: 2 }),
|
|
224
|
+
render() { return `<div>${this.state.a}-${this.state.b}</div>`; },
|
|
225
|
+
});
|
|
226
|
+
document.body.innerHTML = '<set-state id="ss"></set-state>';
|
|
227
|
+
const inst = mount('#ss', 'set-state');
|
|
228
|
+
inst.setState({ a: 10, b: 20 });
|
|
229
|
+
await new Promise(r => queueMicrotask(r));
|
|
230
|
+
expect(inst.state.a).toBe(10);
|
|
231
|
+
expect(inst.state.b).toBe(20);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// emit
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
describe('component — emit', () => {
|
|
241
|
+
it('dispatches custom event', () => {
|
|
242
|
+
component('emit-test', {
|
|
243
|
+
render() { return '<div>emit</div>'; },
|
|
244
|
+
});
|
|
245
|
+
document.body.innerHTML = '<emit-test id="et"></emit-test>';
|
|
246
|
+
const inst = mount('#et', 'emit-test');
|
|
247
|
+
|
|
248
|
+
let received;
|
|
249
|
+
document.querySelector('#et').addEventListener('my-event', (e) => {
|
|
250
|
+
received = e.detail;
|
|
251
|
+
});
|
|
252
|
+
inst.emit('my-event', { data: 42 });
|
|
253
|
+
expect(received).toEqual({ data: 42 });
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// destroy
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
describe('component — destroy', () => {
|
|
263
|
+
it('clears innerHTML and removes from registry', () => {
|
|
264
|
+
component('destroy-test', {
|
|
265
|
+
render() { return '<div class="will-die">alive</div>'; },
|
|
266
|
+
});
|
|
267
|
+
document.body.innerHTML = '<destroy-test id="dt"></destroy-test>';
|
|
268
|
+
mount('#dt', 'destroy-test');
|
|
269
|
+
expect(document.querySelector('.will-die')).not.toBeNull();
|
|
270
|
+
destroy('#dt');
|
|
271
|
+
expect(document.querySelector('.will-die')).toBeNull();
|
|
272
|
+
expect(getInstance('#dt')).toBeNull();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('double destroy does not throw', () => {
|
|
276
|
+
component('destroy-twice', {
|
|
277
|
+
render() { return '<div>twice</div>'; },
|
|
278
|
+
});
|
|
279
|
+
document.body.innerHTML = '<destroy-twice id="d2"></destroy-twice>';
|
|
280
|
+
mount('#d2', 'destroy-twice');
|
|
281
|
+
destroy('#d2');
|
|
282
|
+
expect(() => destroy('#d2')).not.toThrow();
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
// mountAll
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
describe('mountAll()', () => {
|
|
292
|
+
it('auto-mounts all registered component tags', () => {
|
|
293
|
+
component('auto-a', {
|
|
294
|
+
render() { return '<span class="auto-a">A</span>'; },
|
|
295
|
+
});
|
|
296
|
+
component('auto-b', {
|
|
297
|
+
render() { return '<span class="auto-b">B</span>'; },
|
|
298
|
+
});
|
|
299
|
+
document.body.innerHTML = '<auto-a></auto-a><auto-b></auto-b>';
|
|
300
|
+
mountAll();
|
|
301
|
+
expect(document.querySelector('.auto-a').textContent).toBe('A');
|
|
302
|
+
expect(document.querySelector('.auto-b').textContent).toBe('B');
|
|
303
|
+
});
|
|
304
|
+
});
|