Skip to content

Commit 3839627

Browse files
committed
feat: Add more composables
1 parent a64456d commit 3839627

File tree

11 files changed

+860
-47
lines changed

11 files changed

+860
-47
lines changed

packages/vue/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ const fixtures = [
272272

273273
For the small price of 9kb gziped.    [🏁Get started now](https://dataclient.io/docs/getting-started/installation)
274274

275-
## Planned Features
275+
## Features
276276

277277
- [x] ![TS](./typescript.svg?sanitize=true) Strong [Typescript](https://www.typescriptlang.org/) inference
278278
- [x] 🔄 Vue 3 [Composition API](https://vuejs.org/guide/extras/composition-api-faq.html) composables
@@ -293,7 +293,7 @@ For the small price of 9kb gziped.    [🏁Get started now](https://da
293293
- [x] 🏇 Automatic race condition elimination
294294
- [x] 👯 Global referential equality guarantees
295295

296-
## Planned API
296+
## API
297297

298298
- Rendering: `useSuspense()`, `useLive()`, `useCache()`, `useDLE()`, `useQuery()`, `useLoading()`, `useDebounce()`, `useCancelling()`
299299
- Event handling: `useController()` returns [Controller](https://dataclient.io/docs/api/Controller)

packages/vue/node.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './dist/index.js';

packages/vue/package.json

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@data-client/vue",
3-
"version": "0.0.2",
3+
"version": "0.0.4",
44
"description": "Async State Management without the Management. REST, GraphQL, SSE, Websockets, Fetch",
55
"homepage": "https://dataclient.io",
66
"repository": {
@@ -49,24 +49,6 @@
4949
"module": "lib/index.js",
5050
"unpkg": "dist/index.umd.min.js",
5151
"types": "lib/index.d.ts",
52-
"typesVersions": {
53-
">=4.0": {
54-
"": [
55-
"lib/index.d.ts"
56-
],
57-
"*": [
58-
"lib/index.d.ts"
59-
]
60-
},
61-
">=3.4": {
62-
"": [
63-
"ts3.4/index.d.ts"
64-
],
65-
"*": [
66-
"ts3.4/index.d.ts"
67-
]
68-
}
69-
},
7052
"exports": {
7153
".": {
7254
"module": "./lib/index.js",
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
import { mount } from '@vue/test-utils';
2+
import { defineComponent, h, nextTick, ref } from 'vue';
3+
4+
import useDebounce from '../consumers/useDebounce';
5+
6+
describe('vue useDebounce()', () => {
7+
beforeEach(() => {
8+
jest.useFakeTimers();
9+
});
10+
11+
afterEach(() => {
12+
jest.runOnlyPendingTimers();
13+
jest.useRealTimers();
14+
});
15+
16+
it('debounces value updates with correct delay', async () => {
17+
const TestComponent = defineComponent({
18+
name: 'TestComponent',
19+
setup() {
20+
const query = ref('initial');
21+
const [debouncedQuery, isPending] = useDebounce(query, 200);
22+
23+
const updateQuery = (newValue: string) => {
24+
query.value = newValue;
25+
};
26+
27+
return () =>
28+
h('div', [
29+
h('input', {
30+
value: query.value,
31+
onInput: (e: any) => updateQuery(e.target.value),
32+
'data-testid': 'input',
33+
}),
34+
h('div', { 'data-testid': 'original' }, query.value),
35+
h('div', { 'data-testid': 'debounced' }, debouncedQuery.value),
36+
h('div', { 'data-testid': 'pending' }, isPending.value.toString()),
37+
]);
38+
},
39+
});
40+
41+
const wrapper = mount(TestComponent);
42+
43+
// Initially, debounced value should match original
44+
expect(wrapper.find('[data-testid="original"]').text()).toBe('initial');
45+
expect(wrapper.find('[data-testid="debounced"]').text()).toBe('initial');
46+
expect(wrapper.find('[data-testid="pending"]').text()).toBe('false');
47+
48+
// Update the input
49+
const input = wrapper.find('[data-testid="input"]');
50+
await input.setValue('updated');
51+
await nextTick();
52+
53+
// Original should update immediately, debounced should not
54+
expect(wrapper.find('[data-testid="original"]').text()).toBe('updated');
55+
expect(wrapper.find('[data-testid="debounced"]').text()).toBe('initial');
56+
expect(wrapper.find('[data-testid="pending"]').text()).toBe('true');
57+
58+
// Fast-forward time by less than delay
59+
jest.advanceTimersByTime(100);
60+
await nextTick();
61+
62+
// Debounced should still be old value
63+
expect(wrapper.find('[data-testid="debounced"]').text()).toBe('initial');
64+
expect(wrapper.find('[data-testid="pending"]').text()).toBe('true');
65+
66+
// Fast-forward time by remaining delay
67+
jest.advanceTimersByTime(100);
68+
await nextTick();
69+
70+
// Now debounced should update
71+
expect(wrapper.find('[data-testid="debounced"]').text()).toBe('updated');
72+
expect(wrapper.find('[data-testid="pending"]').text()).toBe('false');
73+
});
74+
75+
it('cancels previous timeout when value changes rapidly', async () => {
76+
const TestComponent = defineComponent({
77+
name: 'TestComponent',
78+
setup() {
79+
const query = ref('initial');
80+
const [debouncedQuery, isPending] = useDebounce(query, 200);
81+
82+
const updateQuery = (newValue: string) => {
83+
query.value = newValue;
84+
};
85+
86+
return () =>
87+
h('div', [
88+
h('input', {
89+
value: query.value,
90+
onInput: (e: any) => updateQuery(e.target.value),
91+
'data-testid': 'input',
92+
}),
93+
h('div', { 'data-testid': 'debounced' }, debouncedQuery.value),
94+
h('div', { 'data-testid': 'pending' }, isPending.value.toString()),
95+
]);
96+
},
97+
});
98+
99+
const wrapper = mount(TestComponent);
100+
101+
// Rapid updates
102+
const input = wrapper.find('[data-testid="input"]');
103+
await input.setValue('first');
104+
await nextTick();
105+
106+
expect(wrapper.find('[data-testid="pending"]').text()).toBe('true');
107+
108+
// Fast-forward partway
109+
jest.advanceTimersByTime(100);
110+
111+
// Another update before timeout
112+
await input.setValue('second');
113+
await nextTick();
114+
115+
// Fast-forward full delay from second update
116+
jest.advanceTimersByTime(200);
117+
await nextTick();
118+
119+
// Should show the second value, not first
120+
expect(wrapper.find('[data-testid="debounced"]').text()).toBe('second');
121+
expect(wrapper.find('[data-testid="pending"]').text()).toBe('false');
122+
});
123+
124+
it('respects updatable parameter', async () => {
125+
const TestComponent = defineComponent({
126+
name: 'TestComponent',
127+
setup() {
128+
const query = ref('initial');
129+
const updatable = ref(true);
130+
const [debouncedQuery, isPending] = useDebounce(query, 200, updatable);
131+
132+
const updateQuery = (newValue: string) => {
133+
query.value = newValue;
134+
};
135+
136+
const toggleUpdatable = () => {
137+
updatable.value = !updatable.value;
138+
};
139+
140+
return () =>
141+
h('div', [
142+
h('input', {
143+
value: query.value,
144+
onInput: (e: any) => updateQuery(e.target.value),
145+
'data-testid': 'input',
146+
}),
147+
h(
148+
'button',
149+
{
150+
onClick: toggleUpdatable,
151+
'data-testid': 'toggle',
152+
},
153+
`Updatable: ${updatable.value}`,
154+
),
155+
h('div', { 'data-testid': 'debounced' }, debouncedQuery.value),
156+
h('div', { 'data-testid': 'pending' }, isPending.value.toString()),
157+
]);
158+
},
159+
});
160+
161+
const wrapper = mount(TestComponent);
162+
163+
// Disable updates
164+
await wrapper.find('[data-testid="toggle"]').trigger('click');
165+
await nextTick();
166+
167+
// Update input
168+
await wrapper.find('[data-testid="input"]').setValue('disabled');
169+
await nextTick();
170+
171+
// Should not be pending since updates are disabled
172+
expect(wrapper.find('[data-testid="pending"]').text()).toBe('false');
173+
174+
// Fast-forward time
175+
jest.advanceTimersByTime(300);
176+
await nextTick();
177+
178+
// Debounced value should not have updated
179+
expect(wrapper.find('[data-testid="debounced"]').text()).toBe('initial');
180+
});
181+
182+
it('cleans up timeout on unmount', async () => {
183+
const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout');
184+
185+
const TestComponent = defineComponent({
186+
name: 'TestComponent',
187+
setup() {
188+
const query = ref('initial');
189+
const [debouncedQuery] = useDebounce(query, 200);
190+
191+
const updateQuery = (newValue: string) => {
192+
query.value = newValue;
193+
};
194+
195+
return () =>
196+
h('div', [
197+
h('input', {
198+
value: query.value,
199+
onInput: (e: any) => updateQuery(e.target.value),
200+
'data-testid': 'input',
201+
}),
202+
h('div', { 'data-testid': 'debounced' }, debouncedQuery.value),
203+
]);
204+
},
205+
});
206+
207+
const wrapper = mount(TestComponent);
208+
209+
// Update input to trigger timeout
210+
await wrapper.find('[data-testid="input"]').setValue('will unmount');
211+
await nextTick();
212+
213+
// Unmount component
214+
wrapper.unmount();
215+
216+
// Should have called clearTimeout
217+
expect(clearTimeoutSpy).toHaveBeenCalled();
218+
219+
clearTimeoutSpy.mockRestore();
220+
});
221+
222+
it('works with reactive refs as input', async () => {
223+
const TestComponent = defineComponent({
224+
name: 'TestComponent',
225+
setup() {
226+
const query = ref('initial');
227+
const [debouncedQuery, isPending] = useDebounce(query, 200);
228+
229+
const updateQuery = (newValue: string) => {
230+
query.value = newValue;
231+
};
232+
233+
return () =>
234+
h('div', [
235+
h(
236+
'button',
237+
{
238+
onClick: () => updateQuery('reactive'),
239+
'data-testid': 'button',
240+
},
241+
'Update',
242+
),
243+
h('div', { 'data-testid': 'debounced' }, debouncedQuery.value),
244+
h('div', { 'data-testid': 'pending' }, isPending.value.toString()),
245+
]);
246+
},
247+
});
248+
249+
const wrapper = mount(TestComponent);
250+
251+
// Click to update
252+
await wrapper.find('[data-testid="button"]').trigger('click');
253+
await nextTick();
254+
255+
expect(wrapper.find('[data-testid="pending"]').text()).toBe('true');
256+
257+
// Fast-forward time
258+
jest.advanceTimersByTime(200);
259+
await nextTick();
260+
261+
expect(wrapper.find('[data-testid="debounced"]').text()).toBe('reactive');
262+
expect(wrapper.find('[data-testid="pending"]').text()).toBe('false');
263+
});
264+
});

0 commit comments

Comments
 (0)