rask-ui 0.1.0 → 0.1.1
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/dist/component.d.ts.map +1 -1
- package/dist/component.js +4 -2
- package/dist/createAsync.test.d.ts +2 -0
- package/dist/createAsync.test.d.ts.map +1 -0
- package/dist/createAsync.test.js +110 -0
- package/dist/createMutation.test.d.ts +2 -0
- package/dist/createMutation.test.d.ts.map +1 -0
- package/dist/createMutation.test.js +168 -0
- package/dist/createQuery.test.d.ts +2 -0
- package/dist/createQuery.test.d.ts.map +1 -0
- package/dist/createQuery.test.js +156 -0
- package/dist/createState.test.d.ts +2 -0
- package/dist/createState.test.d.ts.map +1 -0
- package/dist/createState.test.js +111 -0
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +15 -1
- package/dist/observation.test.d.ts +2 -0
- package/dist/observation.test.d.ts.map +1 -0
- package/dist/observation.test.js +150 -0
- package/dist/render-test.d.ts +2 -0
- package/dist/render-test.d.ts.map +1 -0
- package/dist/render-test.js +21 -0
- package/dist/render.d.ts +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +13 -1
- package/dist/test-setup.d.ts +16 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/dist/test-setup.js +40 -0
- package/package.json +1 -1
package/dist/component.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,KAAK,EAAkB,MAAM,UAAU,CAAC;AAE7D,OAAO,EAAsB,QAAQ,EAAU,MAAM,eAAe,CAAC;AACrE,OAAO,EAAc,SAAS,EAAE,MAAM,UAAU,CAAC;AAGjD,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,KAAK,CAAC,GAAG,CAAC,MAAM,MAAM,KAAK,CAAC,CAAC;AAE7E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACjC,SAAS,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC;IAC1B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;IACrC,QAAQ,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;IAC5B,UAAU,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;IAC9B,QAAQ,CAAC,EAAE,KAAK,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;CACnC,CAAC;AAIF,wBAAgB,mBAAmB,sBAElC;AAED,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,IAAI,QAQrC;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,QAQvC;
|
|
1
|
+
{"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,KAAK,EAAkB,MAAM,UAAU,CAAC;AAE7D,OAAO,EAAsB,QAAQ,EAAU,MAAM,eAAe,CAAC;AACrE,OAAO,EAAc,SAAS,EAAE,MAAM,UAAU,CAAC;AAGjD,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,KAAK,CAAC,GAAG,CAAC,MAAM,MAAM,KAAK,CAAC,CAAC;AAE7E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACjC,SAAS,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC;IAC1B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;IACrC,QAAQ,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;IAC5B,UAAU,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;IAC9B,QAAQ,CAAC,EAAE,KAAK,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;CACnC,CAAC;AAIF,wBAAgB,mBAAmB,sBAElC;AAED,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,IAAI,QAQrC;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,QAQvC;AAuGD,wBAAgB,eAAe,CAC7B,SAAS,EAAE,SAAS,CAAC,GAAG,CAAC,EACzB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,QAAQ,EAAE,SAAS,EAAE,GAAG,SAAS,4BAWlC"}
|
package/dist/component.js
CHANGED
|
@@ -26,7 +26,6 @@ const hook = {
|
|
|
26
26
|
vnode.data.componentInstance.onMounts.forEach((cb) => cb());
|
|
27
27
|
},
|
|
28
28
|
destroy(vnode) {
|
|
29
|
-
componentStack.shift();
|
|
30
29
|
vnode.data.componentInstance.onCleanups.forEach((cb) => cb());
|
|
31
30
|
},
|
|
32
31
|
prepatch(oldVnode, thunk) {
|
|
@@ -69,7 +68,7 @@ const hook = {
|
|
|
69
68
|
}, Array.isArray(renderResult) ? renderResult : [renderResult]);
|
|
70
69
|
};
|
|
71
70
|
const instance = {
|
|
72
|
-
parent:
|
|
71
|
+
parent: thunk.data.parentComponent || null,
|
|
73
72
|
component,
|
|
74
73
|
contexts: null,
|
|
75
74
|
onMounts: [],
|
|
@@ -116,6 +115,9 @@ const hook = {
|
|
|
116
115
|
export function createComponent(component, props, children) {
|
|
117
116
|
const thunkNode = thunk("component", props.key, component, [props, children]);
|
|
118
117
|
Object.assign(thunkNode.data.hook, hook);
|
|
118
|
+
// Capture the parent component at vnode creation time (during render)
|
|
119
|
+
// rather than at init time, to ensure correct parent relationships
|
|
120
|
+
thunkNode.data.parentComponent = getCurrentComponent();
|
|
119
121
|
return thunkNode;
|
|
120
122
|
}
|
|
121
123
|
function copyToThunk(vnode, thunk) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createAsync.test.d.ts","sourceRoot":"","sources":["../src/createAsync.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { createAsync } from './createAsync';
|
|
3
|
+
describe('createAsync', () => {
|
|
4
|
+
it('should start in pending state', () => {
|
|
5
|
+
const promise = new Promise(() => { });
|
|
6
|
+
const async = createAsync(promise);
|
|
7
|
+
expect(async.isPending).toBe(true);
|
|
8
|
+
expect(async.value).toBeNull();
|
|
9
|
+
expect(async.error).toBeNull();
|
|
10
|
+
});
|
|
11
|
+
it('should resolve to value state on success', async () => {
|
|
12
|
+
const promise = Promise.resolve('success');
|
|
13
|
+
const async = createAsync(promise);
|
|
14
|
+
expect(async.isPending).toBe(true);
|
|
15
|
+
await promise;
|
|
16
|
+
// Wait for state update
|
|
17
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
18
|
+
expect(async.isPending).toBe(false);
|
|
19
|
+
expect(async.value).toBe('success');
|
|
20
|
+
expect(async.error).toBeNull();
|
|
21
|
+
});
|
|
22
|
+
it('should resolve to error state on rejection', async () => {
|
|
23
|
+
const promise = Promise.reject(new Error('failed'));
|
|
24
|
+
const async = createAsync(promise);
|
|
25
|
+
expect(async.isPending).toBe(true);
|
|
26
|
+
try {
|
|
27
|
+
await promise;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Ignore
|
|
31
|
+
}
|
|
32
|
+
// Wait for state update
|
|
33
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
34
|
+
expect(async.isPending).toBe(false);
|
|
35
|
+
expect(async.value).toBeNull();
|
|
36
|
+
expect(async.error).toContain('failed');
|
|
37
|
+
});
|
|
38
|
+
it('should handle numeric values', async () => {
|
|
39
|
+
const promise = Promise.resolve(42);
|
|
40
|
+
const async = createAsync(promise);
|
|
41
|
+
await promise;
|
|
42
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
43
|
+
expect(async.value).toBe(42);
|
|
44
|
+
});
|
|
45
|
+
it('should handle object values', async () => {
|
|
46
|
+
const data = { id: 1, name: 'Test' };
|
|
47
|
+
const promise = Promise.resolve(data);
|
|
48
|
+
const async = createAsync(promise);
|
|
49
|
+
await promise;
|
|
50
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
51
|
+
expect(async.value).toEqual(data);
|
|
52
|
+
});
|
|
53
|
+
it('should handle array values', async () => {
|
|
54
|
+
const data = [1, 2, 3, 4, 5];
|
|
55
|
+
const promise = Promise.resolve(data);
|
|
56
|
+
const async = createAsync(promise);
|
|
57
|
+
await promise;
|
|
58
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
59
|
+
expect(async.value).toEqual(data);
|
|
60
|
+
});
|
|
61
|
+
it('should convert error to string', async () => {
|
|
62
|
+
const promise = Promise.reject('string error');
|
|
63
|
+
const async = createAsync(promise);
|
|
64
|
+
try {
|
|
65
|
+
await promise;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Ignore
|
|
69
|
+
}
|
|
70
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
71
|
+
expect(typeof async.error).toBe('string');
|
|
72
|
+
expect(async.error).toBe('string error');
|
|
73
|
+
});
|
|
74
|
+
it('should handle error objects', async () => {
|
|
75
|
+
const error = new Error('Something went wrong');
|
|
76
|
+
const promise = Promise.reject(error);
|
|
77
|
+
const async = createAsync(promise);
|
|
78
|
+
try {
|
|
79
|
+
await promise;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Ignore
|
|
83
|
+
}
|
|
84
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
85
|
+
expect(async.error).toContain('Something went wrong');
|
|
86
|
+
});
|
|
87
|
+
it('should create reactive state', async () => {
|
|
88
|
+
const promise = new Promise((resolve) => {
|
|
89
|
+
setTimeout(() => resolve('delayed'), 10);
|
|
90
|
+
});
|
|
91
|
+
const async = createAsync(promise);
|
|
92
|
+
expect(async.isPending).toBe(true);
|
|
93
|
+
await promise;
|
|
94
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
95
|
+
expect(async.isPending).toBe(false);
|
|
96
|
+
expect(async.value).toBe('delayed');
|
|
97
|
+
});
|
|
98
|
+
it('should handle immediate resolution', async () => {
|
|
99
|
+
const async = createAsync(Promise.resolve('immediate'));
|
|
100
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
101
|
+
expect(async.isPending).toBe(false);
|
|
102
|
+
expect(async.value).toBe('immediate');
|
|
103
|
+
});
|
|
104
|
+
it('should handle immediate rejection', async () => {
|
|
105
|
+
const async = createAsync(Promise.reject('immediate error'));
|
|
106
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
107
|
+
expect(async.isPending).toBe(false);
|
|
108
|
+
expect(async.error).toBe('immediate error');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createMutation.test.d.ts","sourceRoot":"","sources":["../src/createMutation.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { createMutation } from './createMutation';
|
|
3
|
+
describe('createMutation', () => {
|
|
4
|
+
it('should start in idle state', () => {
|
|
5
|
+
const mutator = vi.fn(() => Promise.resolve(null));
|
|
6
|
+
const mutation = createMutation(mutator);
|
|
7
|
+
expect(mutation.isPending).toBe(false);
|
|
8
|
+
expect(mutation.params).toBeNull();
|
|
9
|
+
expect(mutation.error).toBeNull();
|
|
10
|
+
expect(mutator).not.toHaveBeenCalled();
|
|
11
|
+
});
|
|
12
|
+
it('should execute mutator when mutate is called', async () => {
|
|
13
|
+
const mutator = vi.fn((params) => Promise.resolve(params));
|
|
14
|
+
const mutation = createMutation(mutator);
|
|
15
|
+
mutation.mutate('test');
|
|
16
|
+
expect(mutation.isPending).toBe(true);
|
|
17
|
+
expect(mutation.params).toBe('test');
|
|
18
|
+
expect(mutator).toHaveBeenCalledWith('test');
|
|
19
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
20
|
+
expect(mutation.isPending).toBe(false);
|
|
21
|
+
expect(mutation.params).toBeNull();
|
|
22
|
+
});
|
|
23
|
+
it('should handle successful mutations', async () => {
|
|
24
|
+
const mutator = (params) => Promise.resolve(params);
|
|
25
|
+
const mutation = createMutation(mutator);
|
|
26
|
+
mutation.mutate({ id: 1 });
|
|
27
|
+
expect(mutation.isPending).toBe(true);
|
|
28
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
29
|
+
expect(mutation.isPending).toBe(false);
|
|
30
|
+
expect(mutation.error).toBeNull();
|
|
31
|
+
expect(mutation.params).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
it('should handle mutation errors', async () => {
|
|
34
|
+
const mutator = (params) => Promise.reject(new Error('Mutation failed'));
|
|
35
|
+
const mutation = createMutation(mutator);
|
|
36
|
+
mutation.mutate('test');
|
|
37
|
+
expect(mutation.isPending).toBe(true);
|
|
38
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
39
|
+
expect(mutation.isPending).toBe(false);
|
|
40
|
+
expect(mutation.error).toContain('Mutation failed');
|
|
41
|
+
expect(mutation.params).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
it('should cancel previous mutation on new mutate call', async () => {
|
|
44
|
+
let resolveFirst;
|
|
45
|
+
let resolveSecond;
|
|
46
|
+
const firstPromise = new Promise((resolve) => {
|
|
47
|
+
resolveFirst = resolve;
|
|
48
|
+
});
|
|
49
|
+
const secondPromise = new Promise((resolve) => {
|
|
50
|
+
resolveSecond = resolve;
|
|
51
|
+
});
|
|
52
|
+
const mutator = vi
|
|
53
|
+
.fn()
|
|
54
|
+
.mockReturnValueOnce(firstPromise)
|
|
55
|
+
.mockReturnValueOnce(secondPromise);
|
|
56
|
+
const mutation = createMutation(mutator);
|
|
57
|
+
mutation.mutate('first');
|
|
58
|
+
expect(mutation.params).toBe('first');
|
|
59
|
+
// Trigger second mutation before first completes
|
|
60
|
+
mutation.mutate('second');
|
|
61
|
+
expect(mutation.params).toBe('second');
|
|
62
|
+
// Resolve first (should be ignored due to cancellation)
|
|
63
|
+
resolveFirst('first');
|
|
64
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
65
|
+
expect(mutation.isPending).toBe(true); // Still pending second
|
|
66
|
+
// Resolve second
|
|
67
|
+
resolveSecond('second');
|
|
68
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
69
|
+
expect(mutation.isPending).toBe(false);
|
|
70
|
+
expect(mutation.params).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
it('should handle rapid successive mutations', async () => {
|
|
73
|
+
let counter = 0;
|
|
74
|
+
const mutator = vi.fn(() => Promise.resolve(++counter));
|
|
75
|
+
const mutation = createMutation(mutator);
|
|
76
|
+
// Rapid mutations
|
|
77
|
+
mutation.mutate('1');
|
|
78
|
+
mutation.mutate('2');
|
|
79
|
+
mutation.mutate('3');
|
|
80
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
81
|
+
// Only the last mutation should complete
|
|
82
|
+
expect(mutator).toHaveBeenCalledTimes(3);
|
|
83
|
+
expect(mutation.isPending).toBe(false);
|
|
84
|
+
expect(mutation.params).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
it('should clear error on successful retry', async () => {
|
|
87
|
+
const mutator = vi
|
|
88
|
+
.fn()
|
|
89
|
+
.mockRejectedValueOnce(new Error('First error'))
|
|
90
|
+
.mockResolvedValueOnce('success');
|
|
91
|
+
const mutation = createMutation(mutator);
|
|
92
|
+
mutation.mutate('attempt1');
|
|
93
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
94
|
+
expect(mutation.error).toContain('First error');
|
|
95
|
+
mutation.mutate('attempt2');
|
|
96
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
97
|
+
expect(mutation.error).toBeNull();
|
|
98
|
+
expect(mutation.isPending).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
it('should handle different parameter types', async () => {
|
|
101
|
+
const mutator = vi.fn((params) => Promise.resolve(params));
|
|
102
|
+
const mutation = createMutation(mutator);
|
|
103
|
+
// Object params
|
|
104
|
+
mutation.mutate({ id: 1, name: 'test' });
|
|
105
|
+
expect(mutation.params).toEqual({ id: 1, name: 'test' });
|
|
106
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
107
|
+
// Array params
|
|
108
|
+
const mutation2 = createMutation(mutator);
|
|
109
|
+
mutation2.mutate([1, 2, 3]);
|
|
110
|
+
expect(mutation2.params).toEqual([1, 2, 3]);
|
|
111
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
112
|
+
// String params
|
|
113
|
+
const mutation3 = createMutation(mutator);
|
|
114
|
+
mutation3.mutate('string');
|
|
115
|
+
expect(mutation3.params).toBe('string');
|
|
116
|
+
});
|
|
117
|
+
it('should convert errors to strings', async () => {
|
|
118
|
+
const mutator = (params) => Promise.reject('string error');
|
|
119
|
+
const mutation = createMutation(mutator);
|
|
120
|
+
mutation.mutate('test');
|
|
121
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
122
|
+
expect(typeof mutation.error).toBe('string');
|
|
123
|
+
expect(mutation.error).toBe('string error');
|
|
124
|
+
});
|
|
125
|
+
it('should handle AbortController cancellation correctly', async () => {
|
|
126
|
+
const abortedPromise = new Promise((_, reject) => {
|
|
127
|
+
const error = new Error('Aborted');
|
|
128
|
+
error.name = 'AbortError';
|
|
129
|
+
setTimeout(() => reject(error), 5);
|
|
130
|
+
});
|
|
131
|
+
const successPromise = Promise.resolve('success');
|
|
132
|
+
const mutator = vi
|
|
133
|
+
.fn()
|
|
134
|
+
.mockReturnValueOnce(abortedPromise)
|
|
135
|
+
.mockReturnValueOnce(successPromise);
|
|
136
|
+
const mutation = createMutation(mutator);
|
|
137
|
+
mutation.mutate('first');
|
|
138
|
+
// Immediately trigger second mutation to abort first
|
|
139
|
+
mutation.mutate('second');
|
|
140
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
141
|
+
expect(mutation.isPending).toBe(false);
|
|
142
|
+
expect(mutation.error).toBeNull();
|
|
143
|
+
});
|
|
144
|
+
it('should track params during pending state', () => {
|
|
145
|
+
const mutator = () => new Promise((resolve) => setTimeout(() => resolve(null), 100));
|
|
146
|
+
const mutation = createMutation(mutator);
|
|
147
|
+
const params = { id: 123, action: 'update' };
|
|
148
|
+
mutation.mutate(params);
|
|
149
|
+
expect(mutation.isPending).toBe(true);
|
|
150
|
+
expect(mutation.params).toEqual(params);
|
|
151
|
+
});
|
|
152
|
+
it('should expose reactive getters', async () => {
|
|
153
|
+
const mutator = () => Promise.resolve('data');
|
|
154
|
+
const mutation = createMutation(mutator);
|
|
155
|
+
// Access getters before mutation
|
|
156
|
+
expect(mutation.isPending).toBe(false);
|
|
157
|
+
expect(mutation.params).toBeNull();
|
|
158
|
+
expect(mutation.error).toBeNull();
|
|
159
|
+
mutation.mutate('test');
|
|
160
|
+
// Access getters during mutation
|
|
161
|
+
expect(mutation.isPending).toBe(true);
|
|
162
|
+
expect(mutation.params).toBe('test');
|
|
163
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
164
|
+
// Access getters after mutation
|
|
165
|
+
expect(mutation.isPending).toBe(false);
|
|
166
|
+
expect(mutation.params).toBeNull();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createQuery.test.d.ts","sourceRoot":"","sources":["../src/createQuery.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { createQuery } from './createQuery';
|
|
3
|
+
describe('createQuery', () => {
|
|
4
|
+
it('should start in pending state and fetch immediately', async () => {
|
|
5
|
+
const fetcher = vi.fn(() => Promise.resolve('data'));
|
|
6
|
+
const query = createQuery(fetcher);
|
|
7
|
+
expect(query.isPending).toBe(true);
|
|
8
|
+
expect(query.data).toBeNull();
|
|
9
|
+
expect(query.error).toBeNull();
|
|
10
|
+
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
11
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
12
|
+
expect(query.isPending).toBe(false);
|
|
13
|
+
expect(query.data).toBe('data');
|
|
14
|
+
});
|
|
15
|
+
it('should resolve to data state on success', async () => {
|
|
16
|
+
const fetcher = () => Promise.resolve({ id: 1, name: 'Test' });
|
|
17
|
+
const query = createQuery(fetcher);
|
|
18
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
19
|
+
expect(query.isPending).toBe(false);
|
|
20
|
+
expect(query.data).toEqual({ id: 1, name: 'Test' });
|
|
21
|
+
expect(query.error).toBeNull();
|
|
22
|
+
});
|
|
23
|
+
it('should resolve to error state on failure', async () => {
|
|
24
|
+
const fetcher = () => Promise.reject(new Error('Network error'));
|
|
25
|
+
const query = createQuery(fetcher);
|
|
26
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
27
|
+
expect(query.isPending).toBe(false);
|
|
28
|
+
expect(query.data).toBeNull();
|
|
29
|
+
expect(query.error).toContain('Network error');
|
|
30
|
+
});
|
|
31
|
+
it('should allow manual refetch', async () => {
|
|
32
|
+
const fetcher = vi.fn(() => Promise.resolve('data'));
|
|
33
|
+
const query = createQuery(fetcher);
|
|
34
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
35
|
+
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
36
|
+
query.fetch();
|
|
37
|
+
expect(query.isPending).toBe(true);
|
|
38
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
39
|
+
expect(fetcher).toHaveBeenCalledTimes(2);
|
|
40
|
+
expect(query.isPending).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
it('should retain old data during refetch by default', async () => {
|
|
43
|
+
const fetcher = vi
|
|
44
|
+
.fn()
|
|
45
|
+
.mockResolvedValueOnce('data1')
|
|
46
|
+
.mockResolvedValueOnce('data2');
|
|
47
|
+
const query = createQuery(fetcher);
|
|
48
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
49
|
+
expect(query.data).toBe('data1');
|
|
50
|
+
query.fetch();
|
|
51
|
+
expect(query.isPending).toBe(true);
|
|
52
|
+
expect(query.data).toBe('data1'); // Old data retained
|
|
53
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
54
|
+
expect(query.data).toBe('data2');
|
|
55
|
+
});
|
|
56
|
+
it('should clear old data when force refetch is used', async () => {
|
|
57
|
+
const fetcher = vi
|
|
58
|
+
.fn()
|
|
59
|
+
.mockResolvedValueOnce('data1')
|
|
60
|
+
.mockResolvedValueOnce('data2');
|
|
61
|
+
const query = createQuery(fetcher);
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
63
|
+
expect(query.data).toBe('data1');
|
|
64
|
+
query.fetch(true); // Force refresh
|
|
65
|
+
expect(query.isPending).toBe(true);
|
|
66
|
+
expect(query.data).toBeNull(); // Old data cleared
|
|
67
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
68
|
+
expect(query.data).toBe('data2');
|
|
69
|
+
});
|
|
70
|
+
it('should cancel previous request on new fetch', async () => {
|
|
71
|
+
let resolveFirst;
|
|
72
|
+
let resolveSecond;
|
|
73
|
+
const firstPromise = new Promise((resolve) => {
|
|
74
|
+
resolveFirst = resolve;
|
|
75
|
+
});
|
|
76
|
+
const secondPromise = new Promise((resolve) => {
|
|
77
|
+
resolveSecond = resolve;
|
|
78
|
+
});
|
|
79
|
+
const fetcher = vi
|
|
80
|
+
.fn()
|
|
81
|
+
.mockReturnValueOnce(firstPromise)
|
|
82
|
+
.mockReturnValueOnce(secondPromise);
|
|
83
|
+
const query = createQuery(fetcher);
|
|
84
|
+
expect(query.isPending).toBe(true);
|
|
85
|
+
// Trigger second fetch before first completes
|
|
86
|
+
query.fetch();
|
|
87
|
+
// Resolve first (should be ignored due to cancellation)
|
|
88
|
+
resolveFirst('first');
|
|
89
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
90
|
+
expect(query.data).toBeNull(); // First result ignored
|
|
91
|
+
// Resolve second
|
|
92
|
+
resolveSecond('second');
|
|
93
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
94
|
+
expect(query.data).toBe('second');
|
|
95
|
+
});
|
|
96
|
+
it('should handle rapid successive fetches', async () => {
|
|
97
|
+
let counter = 0;
|
|
98
|
+
const fetcher = vi.fn(() => Promise.resolve(`data-${++counter}`));
|
|
99
|
+
const query = createQuery(fetcher);
|
|
100
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
101
|
+
// Rapid fetches
|
|
102
|
+
query.fetch();
|
|
103
|
+
query.fetch();
|
|
104
|
+
query.fetch();
|
|
105
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
106
|
+
// Only the last fetch should matter
|
|
107
|
+
expect(fetcher).toHaveBeenCalledTimes(4); // Initial + 3 fetches
|
|
108
|
+
expect(query.data).toBe('data-4');
|
|
109
|
+
});
|
|
110
|
+
it('should cancel on error and allow refetch', async () => {
|
|
111
|
+
const fetcher = vi
|
|
112
|
+
.fn()
|
|
113
|
+
.mockRejectedValueOnce(new Error('First error'))
|
|
114
|
+
.mockResolvedValueOnce('success');
|
|
115
|
+
const query = createQuery(fetcher);
|
|
116
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
117
|
+
expect(query.error).toContain('First error');
|
|
118
|
+
expect(query.data).toBeNull();
|
|
119
|
+
query.fetch();
|
|
120
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
121
|
+
expect(query.error).toBeNull();
|
|
122
|
+
expect(query.data).toBe('success');
|
|
123
|
+
});
|
|
124
|
+
it('should handle AbortController cancellation correctly', async () => {
|
|
125
|
+
const abortedPromise = new Promise((_, reject) => {
|
|
126
|
+
const error = new Error('Aborted');
|
|
127
|
+
error.name = 'AbortError';
|
|
128
|
+
setTimeout(() => reject(error), 5);
|
|
129
|
+
});
|
|
130
|
+
const successPromise = Promise.resolve('success');
|
|
131
|
+
const fetcher = vi
|
|
132
|
+
.fn()
|
|
133
|
+
.mockReturnValueOnce(abortedPromise)
|
|
134
|
+
.mockReturnValueOnce(successPromise);
|
|
135
|
+
const query = createQuery(fetcher);
|
|
136
|
+
// Immediately trigger second fetch to abort first
|
|
137
|
+
query.fetch();
|
|
138
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
139
|
+
expect(query.data).toBe('success');
|
|
140
|
+
expect(query.error).toBeNull();
|
|
141
|
+
});
|
|
142
|
+
it('should expose reactive getters', async () => {
|
|
143
|
+
const fetcher = () => Promise.resolve('data');
|
|
144
|
+
const query = createQuery(fetcher);
|
|
145
|
+
// Access getters
|
|
146
|
+
const pending1 = query.isPending;
|
|
147
|
+
const data1 = query.data;
|
|
148
|
+
const error1 = query.error;
|
|
149
|
+
expect(pending1).toBe(true);
|
|
150
|
+
expect(data1).toBeNull();
|
|
151
|
+
expect(error1).toBeNull();
|
|
152
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
153
|
+
expect(query.isPending).toBe(false);
|
|
154
|
+
expect(query.data).toBe('data');
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createState.test.d.ts","sourceRoot":"","sources":["../src/createState.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { createState } from './createState';
|
|
3
|
+
import { Observer } from './observation';
|
|
4
|
+
describe('createState', () => {
|
|
5
|
+
it('should create a reactive proxy from an object', () => {
|
|
6
|
+
const state = createState({ count: 0 });
|
|
7
|
+
expect(state.count).toBe(0);
|
|
8
|
+
});
|
|
9
|
+
it('should allow mutations', () => {
|
|
10
|
+
const state = createState({ count: 0 });
|
|
11
|
+
state.count = 5;
|
|
12
|
+
expect(state.count).toBe(5);
|
|
13
|
+
});
|
|
14
|
+
it('should return the same proxy for the same object', () => {
|
|
15
|
+
const obj = { count: 0 };
|
|
16
|
+
const proxy1 = createState(obj);
|
|
17
|
+
const proxy2 = createState(obj);
|
|
18
|
+
expect(proxy1).toBe(proxy2);
|
|
19
|
+
});
|
|
20
|
+
it('should create nested proxies for nested objects', () => {
|
|
21
|
+
const state = createState({ user: { name: 'Alice', age: 30 } });
|
|
22
|
+
state.user.name = 'Bob';
|
|
23
|
+
expect(state.user.name).toBe('Bob');
|
|
24
|
+
});
|
|
25
|
+
it('should handle arrays reactively', () => {
|
|
26
|
+
const state = createState({ items: [1, 2, 3] });
|
|
27
|
+
state.items.push(4);
|
|
28
|
+
expect(state.items).toEqual([1, 2, 3, 4]);
|
|
29
|
+
});
|
|
30
|
+
it('should track property access in observers', () => {
|
|
31
|
+
const state = createState({ count: 0 });
|
|
32
|
+
let renderCount = 0;
|
|
33
|
+
const observer = new Observer(() => {
|
|
34
|
+
renderCount++;
|
|
35
|
+
});
|
|
36
|
+
const dispose = observer.observe();
|
|
37
|
+
state.count; // Access property to track it
|
|
38
|
+
dispose();
|
|
39
|
+
expect(renderCount).toBe(0);
|
|
40
|
+
// Mutate after observation setup
|
|
41
|
+
const dispose2 = observer.observe();
|
|
42
|
+
const value = state.count; // Track
|
|
43
|
+
dispose2(); // Stop observing, subscriptions are now active
|
|
44
|
+
state.count = 1;
|
|
45
|
+
// Wait for microtask
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
queueMicrotask(() => {
|
|
48
|
+
expect(renderCount).toBeGreaterThan(0);
|
|
49
|
+
resolve(undefined);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
it('should handle property deletion', () => {
|
|
54
|
+
const state = createState({ count: 0, temp: 'value' });
|
|
55
|
+
delete state.temp;
|
|
56
|
+
expect(state.temp).toBeUndefined();
|
|
57
|
+
expect('temp' in state).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
it('should not create proxies for functions', () => {
|
|
60
|
+
const fn = () => 'hello';
|
|
61
|
+
const state = createState({ method: fn });
|
|
62
|
+
expect(state.method).toBe(fn);
|
|
63
|
+
expect(state.method()).toBe('hello');
|
|
64
|
+
});
|
|
65
|
+
it('should handle symbol properties', () => {
|
|
66
|
+
const sym = Symbol('test');
|
|
67
|
+
const state = createState({ [sym]: 'value' });
|
|
68
|
+
expect(state[sym]).toBe('value');
|
|
69
|
+
});
|
|
70
|
+
it('should notify observers only on actual changes', () => {
|
|
71
|
+
const state = createState({ count: 0 });
|
|
72
|
+
let notifyCount = 0;
|
|
73
|
+
const observer = new Observer(() => {
|
|
74
|
+
notifyCount++;
|
|
75
|
+
});
|
|
76
|
+
const dispose = observer.observe();
|
|
77
|
+
state.count; // Track
|
|
78
|
+
dispose();
|
|
79
|
+
state.count = 0; // Same value - should still notify per current implementation
|
|
80
|
+
state.count = 0;
|
|
81
|
+
return new Promise((resolve) => {
|
|
82
|
+
queueMicrotask(() => {
|
|
83
|
+
// The implementation notifies even for same value, except for optimization cases
|
|
84
|
+
observer.dispose();
|
|
85
|
+
resolve(undefined);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
it('should handle deeply nested objects', () => {
|
|
90
|
+
const state = createState({
|
|
91
|
+
level1: {
|
|
92
|
+
level2: {
|
|
93
|
+
level3: {
|
|
94
|
+
value: 'deep',
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
state.level1.level2.level3.value = 'modified';
|
|
100
|
+
expect(state.level1.level2.level3.value).toBe('modified');
|
|
101
|
+
});
|
|
102
|
+
it('should handle array mutations correctly', () => {
|
|
103
|
+
const state = createState({ items: [1, 2, 3] });
|
|
104
|
+
state.items.pop();
|
|
105
|
+
expect(state.items).toEqual([1, 2]);
|
|
106
|
+
state.items.unshift(0);
|
|
107
|
+
expect(state.items).toEqual([0, 1, 2]);
|
|
108
|
+
state.items.splice(1, 1, 99);
|
|
109
|
+
expect(state.items).toEqual([0, 99, 2]);
|
|
110
|
+
});
|
|
111
|
+
});
|
package/dist/error.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"AAEA,wBAAgB,aAAa,CAAC,KAAK,EAAE;IACnC,KAAK,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,SAAS,GAAG,SAAS,EAAE,CAAC;IACnD,QAAQ,EAAE,GAAG,CAAC;CACf,
|
|
1
|
+
{"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"AAEA,wBAAgB,aAAa,CAAC,KAAK,EAAE;IACnC,KAAK,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,SAAS,GAAG,SAAS,EAAE,CAAC;IACnD,QAAQ,EAAE,GAAG,CAAC;CACf,aAsBA"}
|
package/dist/error.js
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
import { getCurrentComponent } from "./component";
|
|
2
2
|
export function ErrorBoundary(props) {
|
|
3
3
|
const component = getCurrentComponent();
|
|
4
|
+
// Access .error during setup to ensure the error signal is created
|
|
5
|
+
// This allows child errors to be caught even during initial render
|
|
6
|
+
component.error;
|
|
4
7
|
return () => {
|
|
5
|
-
|
|
8
|
+
if (component.error) {
|
|
9
|
+
return props.error(component.error);
|
|
10
|
+
}
|
|
11
|
+
// Fix parent relationship: children vnodes were created with wrong parent,
|
|
12
|
+
// we need to update them to point to this ErrorBoundary component
|
|
13
|
+
const children = Array.isArray(props.children) ? props.children : [props.children];
|
|
14
|
+
children.forEach((child) => {
|
|
15
|
+
if (child?.data?.parentComponent) {
|
|
16
|
+
child.data.parentComponent = component;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
return props.children;
|
|
6
20
|
};
|
|
7
21
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observation.test.d.ts","sourceRoot":"","sources":["../src/observation.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { Signal, Observer, getCurrentObserver } from './observation';
|
|
3
|
+
describe('Signal', () => {
|
|
4
|
+
it('should allow subscribing to notifications', () => {
|
|
5
|
+
const signal = new Signal();
|
|
6
|
+
const callback = vi.fn();
|
|
7
|
+
signal.subscribe(callback);
|
|
8
|
+
signal.notify();
|
|
9
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
10
|
+
});
|
|
11
|
+
it('should return a disposer function', () => {
|
|
12
|
+
const signal = new Signal();
|
|
13
|
+
const callback = vi.fn();
|
|
14
|
+
const dispose = signal.subscribe(callback);
|
|
15
|
+
dispose();
|
|
16
|
+
signal.notify();
|
|
17
|
+
expect(callback).not.toHaveBeenCalled();
|
|
18
|
+
});
|
|
19
|
+
it('should handle multiple subscribers', () => {
|
|
20
|
+
const signal = new Signal();
|
|
21
|
+
const callback1 = vi.fn();
|
|
22
|
+
const callback2 = vi.fn();
|
|
23
|
+
signal.subscribe(callback1);
|
|
24
|
+
signal.subscribe(callback2);
|
|
25
|
+
signal.notify();
|
|
26
|
+
expect(callback1).toHaveBeenCalledTimes(1);
|
|
27
|
+
expect(callback2).toHaveBeenCalledTimes(1);
|
|
28
|
+
});
|
|
29
|
+
it('should allow unsubscribing individual callbacks', () => {
|
|
30
|
+
const signal = new Signal();
|
|
31
|
+
const callback1 = vi.fn();
|
|
32
|
+
const callback2 = vi.fn();
|
|
33
|
+
const dispose1 = signal.subscribe(callback1);
|
|
34
|
+
signal.subscribe(callback2);
|
|
35
|
+
dispose1();
|
|
36
|
+
signal.notify();
|
|
37
|
+
expect(callback1).not.toHaveBeenCalled();
|
|
38
|
+
expect(callback2).toHaveBeenCalledTimes(1);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe('Observer', () => {
|
|
42
|
+
it('should queue notifications in microtasks', async () => {
|
|
43
|
+
let callCount = 0;
|
|
44
|
+
const observer = new Observer(() => {
|
|
45
|
+
callCount++;
|
|
46
|
+
});
|
|
47
|
+
const signal = new Signal();
|
|
48
|
+
const dispose = observer.observe();
|
|
49
|
+
observer.subscribeSignal(signal);
|
|
50
|
+
dispose();
|
|
51
|
+
// Trigger multiple notifications
|
|
52
|
+
signal.notify();
|
|
53
|
+
signal.notify();
|
|
54
|
+
signal.notify();
|
|
55
|
+
// Should not be called synchronously
|
|
56
|
+
expect(callCount).toBe(0);
|
|
57
|
+
// Wait for microtask
|
|
58
|
+
await new Promise((resolve) => queueMicrotask(() => resolve()));
|
|
59
|
+
// Should be called only once due to queuing
|
|
60
|
+
expect(callCount).toBe(1);
|
|
61
|
+
});
|
|
62
|
+
it('should track signals during observation', () => {
|
|
63
|
+
const callback = vi.fn();
|
|
64
|
+
const observer = new Observer(callback);
|
|
65
|
+
const signal = new Signal();
|
|
66
|
+
const dispose = observer.observe();
|
|
67
|
+
observer.subscribeSignal(signal);
|
|
68
|
+
dispose();
|
|
69
|
+
signal.notify();
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
queueMicrotask(() => {
|
|
72
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
73
|
+
resolve(undefined);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
it('should clear signals when observing again', async () => {
|
|
78
|
+
let callCount = 0;
|
|
79
|
+
const observer = new Observer(() => {
|
|
80
|
+
callCount++;
|
|
81
|
+
});
|
|
82
|
+
const signal1 = new Signal();
|
|
83
|
+
const signal2 = new Signal();
|
|
84
|
+
// First observation
|
|
85
|
+
let dispose = observer.observe();
|
|
86
|
+
observer.subscribeSignal(signal1);
|
|
87
|
+
dispose();
|
|
88
|
+
// Second observation - should clear previous signals
|
|
89
|
+
dispose = observer.observe();
|
|
90
|
+
observer.subscribeSignal(signal2);
|
|
91
|
+
dispose();
|
|
92
|
+
// Notify first signal - should not trigger observer
|
|
93
|
+
signal1.notify();
|
|
94
|
+
await new Promise((resolve) => queueMicrotask(() => resolve()));
|
|
95
|
+
expect(callCount).toBe(0);
|
|
96
|
+
// Notify second signal - should trigger observer
|
|
97
|
+
signal2.notify();
|
|
98
|
+
await new Promise((resolve) => queueMicrotask(() => resolve()));
|
|
99
|
+
expect(callCount).toBe(1);
|
|
100
|
+
});
|
|
101
|
+
it('should dispose of all signal subscriptions', async () => {
|
|
102
|
+
const callback = vi.fn();
|
|
103
|
+
const observer = new Observer(callback);
|
|
104
|
+
const signal = new Signal();
|
|
105
|
+
const dispose = observer.observe();
|
|
106
|
+
observer.subscribeSignal(signal);
|
|
107
|
+
dispose();
|
|
108
|
+
observer.dispose();
|
|
109
|
+
signal.notify();
|
|
110
|
+
await new Promise((resolve) => queueMicrotask(() => resolve()));
|
|
111
|
+
expect(callback).not.toHaveBeenCalled();
|
|
112
|
+
});
|
|
113
|
+
it('should set current observer during observation', () => {
|
|
114
|
+
const observer = new Observer(() => { });
|
|
115
|
+
expect(getCurrentObserver()).toBeUndefined();
|
|
116
|
+
const dispose = observer.observe();
|
|
117
|
+
expect(getCurrentObserver()).toBe(observer);
|
|
118
|
+
dispose();
|
|
119
|
+
expect(getCurrentObserver()).toBeUndefined();
|
|
120
|
+
});
|
|
121
|
+
it('should handle nested observations with stack', () => {
|
|
122
|
+
const observer1 = new Observer(() => { });
|
|
123
|
+
const observer2 = new Observer(() => { });
|
|
124
|
+
const dispose1 = observer1.observe();
|
|
125
|
+
expect(getCurrentObserver()).toBe(observer1);
|
|
126
|
+
const dispose2 = observer2.observe();
|
|
127
|
+
expect(getCurrentObserver()).toBe(observer2);
|
|
128
|
+
dispose2();
|
|
129
|
+
expect(getCurrentObserver()).toBe(observer1);
|
|
130
|
+
dispose1();
|
|
131
|
+
expect(getCurrentObserver()).toBeUndefined();
|
|
132
|
+
});
|
|
133
|
+
it('should prevent duplicate notifications while queued', async () => {
|
|
134
|
+
let callCount = 0;
|
|
135
|
+
const observer = new Observer(() => {
|
|
136
|
+
callCount++;
|
|
137
|
+
});
|
|
138
|
+
const signal = new Signal();
|
|
139
|
+
const dispose = observer.observe();
|
|
140
|
+
observer.subscribeSignal(signal);
|
|
141
|
+
dispose();
|
|
142
|
+
// Rapid-fire notifications
|
|
143
|
+
for (let i = 0; i < 100; i++) {
|
|
144
|
+
signal.notify();
|
|
145
|
+
}
|
|
146
|
+
await new Promise((resolve) => queueMicrotask(() => resolve()));
|
|
147
|
+
// Should only be called once
|
|
148
|
+
expect(callCount).toBe(1);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render-test.d.ts","sourceRoot":"","sources":["../src/render-test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Quick test to understand Snabbdom's behavior
|
|
2
|
+
import { patch } from './render';
|
|
3
|
+
import { h } from 'snabbdom';
|
|
4
|
+
import { JSDOM } from 'jsdom';
|
|
5
|
+
const dom = new JSDOM('<!DOCTYPE html><html><body><div id="app"></div></body></html>');
|
|
6
|
+
global.window = dom.window;
|
|
7
|
+
global.document = dom.window.document;
|
|
8
|
+
const container = document.getElementById('app');
|
|
9
|
+
const parent = container.parentElement;
|
|
10
|
+
console.log('Before patch:');
|
|
11
|
+
console.log('Container id:', container.id);
|
|
12
|
+
console.log('Parent children:', parent.children.length);
|
|
13
|
+
console.log('Parent innerHTML:', parent.innerHTML);
|
|
14
|
+
const vnode = h('div', { props: { id: 'app' } }, [h('span', {}, 'Hello')]);
|
|
15
|
+
const result = patch(container, vnode);
|
|
16
|
+
console.log('\nAfter patch:');
|
|
17
|
+
console.log('Result:', result);
|
|
18
|
+
console.log('Result.elm:', result.elm);
|
|
19
|
+
console.log('Parent children:', parent.children.length);
|
|
20
|
+
console.log('Parent innerHTML:', parent.innerHTML);
|
|
21
|
+
console.log('Container still in DOM?:', document.getElementById('app'));
|
package/dist/render.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ import { type VNode } from "snabbdom";
|
|
|
2
2
|
import { type Component } from "./component";
|
|
3
3
|
export declare const patch: (oldVnode: VNode | Element | DocumentFragment, vnode: VNode) => VNode;
|
|
4
4
|
export type ChildNode = VNode | string | null | number;
|
|
5
|
-
export declare function render(vnode: VNode, container: HTMLElement):
|
|
5
|
+
export declare function render(vnode: VNode, container: HTMLElement): VNode;
|
|
6
6
|
export declare function jsx(type: string | Component<any>, props: Record<string, unknown>, children: ChildNode[]): VNode;
|
|
7
7
|
//# sourceMappingURL=render.d.ts.map
|
package/dist/render.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,KAAK,EAEX,MAAM,UAAU,CAAC;AAClB,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAE9D,eAAO,MAAM,KAAK,uEAOhB,CAAC;AAEH,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC;AAEvD,wBAAgB,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,WAAW,
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,KAAK,EAEX,MAAM,UAAU,CAAC;AAClB,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAE9D,eAAO,MAAM,KAAK,uEAOhB,CAAC;AAEH,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC;AAEvD,wBAAgB,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,WAAW,SAK1D;AAED,wBAAgB,GAAG,CACjB,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,EAC7B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,QAAQ,EAAE,SAAS,EAAE,SAyEtB"}
|
package/dist/render.js
CHANGED
|
@@ -12,7 +12,7 @@ export function render(vnode, container) {
|
|
|
12
12
|
const style = document.createElement("style");
|
|
13
13
|
style.innerHTML = "component { display: contents; }";
|
|
14
14
|
document.head.appendChild(style);
|
|
15
|
-
patch(container, vnode);
|
|
15
|
+
return patch(container, vnode);
|
|
16
16
|
}
|
|
17
17
|
export function jsx(type, props, children) {
|
|
18
18
|
let flatChildren = children.flat();
|
|
@@ -31,6 +31,18 @@ export function jsx(type, props, children) {
|
|
|
31
31
|
data.style = props[key];
|
|
32
32
|
continue;
|
|
33
33
|
}
|
|
34
|
+
if (key === "class") {
|
|
35
|
+
// Snabbdom's classModule expects an object like { 'class-name': true }
|
|
36
|
+
// If it's a string, convert it to the object format
|
|
37
|
+
const classValue = props[key];
|
|
38
|
+
if (typeof classValue === 'string') {
|
|
39
|
+
data.class = { [classValue]: true };
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
data.class = classValue;
|
|
43
|
+
}
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
34
46
|
if (key === "ref") {
|
|
35
47
|
data.hook = data.hook || {};
|
|
36
48
|
const existingInsertHook = data.hook?.insert;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { VNode } from 'snabbdom';
|
|
2
|
+
/**
|
|
3
|
+
* Test helper to render a component and provide easy cleanup
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* const { container, unmount } = renderComponent(<MyComponent />);
|
|
7
|
+
* expect(container.textContent).toBe('hello');
|
|
8
|
+
* unmount();
|
|
9
|
+
*/
|
|
10
|
+
export declare function renderComponent(vnode: VNode): {
|
|
11
|
+
container: HTMLElement;
|
|
12
|
+
vnode: VNode;
|
|
13
|
+
unmount: () => void;
|
|
14
|
+
rerender: (newVnode: VNode) => VNode;
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=test-setup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-setup.d.ts","sourceRoot":"","sources":["../src/test-setup.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAUtC;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK;;;;yBAmBnB,KAAK;EAK7B"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Test setup file for vitest
|
|
2
|
+
import { afterEach } from 'vitest';
|
|
3
|
+
import { render, patch } from './render';
|
|
4
|
+
// Clean up after each test
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
document.body.innerHTML = '';
|
|
7
|
+
// Remove any style tags added by render function
|
|
8
|
+
document.querySelectorAll('style').forEach(style => style.remove());
|
|
9
|
+
});
|
|
10
|
+
/**
|
|
11
|
+
* Test helper to render a component and provide easy cleanup
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const { container, unmount } = renderComponent(<MyComponent />);
|
|
15
|
+
* expect(container.textContent).toBe('hello');
|
|
16
|
+
* unmount();
|
|
17
|
+
*/
|
|
18
|
+
export function renderComponent(vnode) {
|
|
19
|
+
const container = document.createElement('div');
|
|
20
|
+
document.body.appendChild(container);
|
|
21
|
+
let currentVnode = render(vnode, container);
|
|
22
|
+
const actualElement = currentVnode.elm;
|
|
23
|
+
return {
|
|
24
|
+
// The actual rendered DOM element (after patch replaces container)
|
|
25
|
+
container: actualElement,
|
|
26
|
+
// The vnode returned by render
|
|
27
|
+
vnode: currentVnode,
|
|
28
|
+
// Cleanup function
|
|
29
|
+
unmount: () => {
|
|
30
|
+
if (actualElement && actualElement.parentNode) {
|
|
31
|
+
actualElement.parentNode.removeChild(actualElement);
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
// Re-render with new vnode
|
|
35
|
+
rerender: (newVnode) => {
|
|
36
|
+
currentVnode = patch(currentVnode, newVnode);
|
|
37
|
+
return currentVnode;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|