fix(vue): eslint/tsconfig migration + resolve type errors
@robonen/vue (toolkit): migrate to eslint flat config + composite tsconfig; fix composable + test type errors (writable computed returns, null guards, overload-compatible signatures, typed test helpers) — all type-level.
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { effectScope, nextTick, reactive, ref } from 'vue';
|
||||
import { debouncedWatch, watchDebounced } from '.';
|
||||
|
||||
describe(watchDebounced, () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('does not fire before the source changes', () => {
|
||||
const count = ref(0);
|
||||
const cb = vi.fn();
|
||||
|
||||
watchDebounced(count, cb, { debounce: 100, flush: 'sync' });
|
||||
|
||||
vi.advanceTimersByTime(200);
|
||||
expect(cb).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('defers the callback by the debounce delay', () => {
|
||||
const count = ref(0);
|
||||
const cb = vi.fn();
|
||||
|
||||
watchDebounced(count, cb, { debounce: 100, flush: 'sync' });
|
||||
|
||||
count.value = 1;
|
||||
expect(cb).not.toHaveBeenCalled();
|
||||
|
||||
vi.advanceTimersByTime(50);
|
||||
expect(cb).not.toHaveBeenCalled();
|
||||
|
||||
vi.advanceTimersByTime(50);
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
expect(cb).toHaveBeenLastCalledWith(1, 0, expect.any(Function));
|
||||
});
|
||||
|
||||
it('coalesces rapid changes into a single trailing call', () => {
|
||||
const count = ref(0);
|
||||
const cb = vi.fn();
|
||||
|
||||
watchDebounced(count, cb, { debounce: 100, flush: 'sync' });
|
||||
|
||||
count.value = 1;
|
||||
vi.advanceTimersByTime(80);
|
||||
count.value = 2;
|
||||
vi.advanceTimersByTime(80);
|
||||
count.value = 3;
|
||||
|
||||
expect(cb).not.toHaveBeenCalled();
|
||||
|
||||
vi.advanceTimersByTime(100);
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
// The filtered callback receives the args of the latest watch trigger:
|
||||
// new value 3, and old value 2 (the source value just before the last change).
|
||||
expect(cb).toHaveBeenLastCalledWith(3, 2, expect.any(Function));
|
||||
});
|
||||
|
||||
it('fires synchronously with no debounce (debounce = 0)', () => {
|
||||
const count = ref(0);
|
||||
const cb = vi.fn();
|
||||
|
||||
watchDebounced(count, cb, { flush: 'sync' });
|
||||
|
||||
count.value = 1;
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
expect(cb).toHaveBeenLastCalledWith(1, 0, expect.any(Function));
|
||||
});
|
||||
|
||||
it('enforces maxWait under sustained changes', () => {
|
||||
const count = ref(0);
|
||||
const cb = vi.fn();
|
||||
|
||||
watchDebounced(count, cb, { debounce: 100, maxWait: 250, flush: 'sync' });
|
||||
|
||||
// Keep changing before the debounce timer can ever settle.
|
||||
count.value = 1;
|
||||
vi.advanceTimersByTime(80);
|
||||
count.value = 2;
|
||||
vi.advanceTimersByTime(80);
|
||||
count.value = 3;
|
||||
vi.advanceTimersByTime(80);
|
||||
// 240ms elapsed, debounce has reset each time, but maxWait is 250ms.
|
||||
expect(cb).not.toHaveBeenCalled();
|
||||
|
||||
count.value = 4;
|
||||
vi.advanceTimersByTime(20);
|
||||
// maxWait (250ms) elapsed -> forced invocation with the latest trigger args.
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
expect(cb).toHaveBeenLastCalledWith(4, 3, expect.any(Function));
|
||||
});
|
||||
|
||||
it('does not double-fire when maxWait and debounce settle together', () => {
|
||||
const count = ref(0);
|
||||
const cb = vi.fn();
|
||||
|
||||
watchDebounced(count, cb, { debounce: 100, maxWait: 200, flush: 'sync' });
|
||||
|
||||
count.value = 1;
|
||||
// debounce settles at 100ms; maxWait would be at 200ms but is cleared.
|
||||
vi.advanceTimersByTime(100);
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
|
||||
vi.advanceTimersByTime(200);
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('supports a reactive debounce delay', () => {
|
||||
const count = ref(0);
|
||||
const delay = ref(100);
|
||||
const cb = vi.fn();
|
||||
|
||||
watchDebounced(count, cb, { debounce: delay, flush: 'sync' });
|
||||
|
||||
count.value = 1;
|
||||
vi.advanceTimersByTime(100);
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
|
||||
delay.value = 300;
|
||||
count.value = 2;
|
||||
vi.advanceTimersByTime(100);
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
|
||||
vi.advanceTimersByTime(200);
|
||||
expect(cb).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('works with a getter source', () => {
|
||||
const count = ref(0);
|
||||
const cb = vi.fn();
|
||||
|
||||
watchDebounced(() => count.value * 2, cb, { debounce: 100, flush: 'sync' });
|
||||
|
||||
count.value = 5;
|
||||
vi.advanceTimersByTime(100);
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
expect(cb).toHaveBeenLastCalledWith(10, 0, expect.any(Function));
|
||||
});
|
||||
|
||||
it('works with an array of sources', () => {
|
||||
const a = ref(0);
|
||||
const b = ref('x');
|
||||
const cb = vi.fn();
|
||||
|
||||
watchDebounced([a, b], cb, { debounce: 100, flush: 'sync' });
|
||||
|
||||
a.value = 1;
|
||||
vi.advanceTimersByTime(100);
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
expect(cb).toHaveBeenLastCalledWith([1, 'x'], [0, 'x'], expect.any(Function));
|
||||
});
|
||||
|
||||
it('works with a reactive object source and deep option', () => {
|
||||
const state = reactive({ nested: { value: 0 } });
|
||||
const cb = vi.fn();
|
||||
|
||||
watchDebounced(state, cb, { debounce: 100, deep: true, flush: 'sync' });
|
||||
|
||||
state.nested.value = 1;
|
||||
vi.advanceTimersByTime(100);
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('fires immediately with the immediate option', () => {
|
||||
const count = ref(0);
|
||||
const cb = vi.fn();
|
||||
|
||||
watchDebounced(count, cb, { debounce: 100, immediate: true, flush: 'sync' });
|
||||
|
||||
// immediate runs through the filter synchronously only when debounce=0;
|
||||
// with a positive debounce the immediate run is also debounced.
|
||||
vi.advanceTimersByTime(100);
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
expect(cb).toHaveBeenLastCalledWith(0, undefined, expect.any(Function));
|
||||
});
|
||||
|
||||
it('respects a custom flush timing', async () => {
|
||||
const count = ref(0);
|
||||
const cb = vi.fn();
|
||||
|
||||
watchDebounced(count, cb, { debounce: 100, flush: 'post' });
|
||||
|
||||
count.value = 1;
|
||||
await nextTick();
|
||||
expect(cb).not.toHaveBeenCalled();
|
||||
|
||||
vi.advanceTimersByTime(100);
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('returns a handle that stops watching', () => {
|
||||
const count = ref(0);
|
||||
const cb = vi.fn();
|
||||
|
||||
const stop = watchDebounced(count, cb, { debounce: 100, flush: 'sync' });
|
||||
|
||||
stop();
|
||||
|
||||
count.value = 1;
|
||||
vi.advanceTimersByTime(200);
|
||||
expect(cb).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('stops watching when the owning scope is disposed', () => {
|
||||
const count = ref(0);
|
||||
const cb = vi.fn();
|
||||
const scope = effectScope();
|
||||
|
||||
scope.run(() => watchDebounced(count, cb, { debounce: 100, flush: 'sync' }));
|
||||
|
||||
scope.stop();
|
||||
|
||||
count.value = 1;
|
||||
vi.advanceTimersByTime(200);
|
||||
expect(cb).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('honours a caller-supplied eventFilter over debounce/maxWait', () => {
|
||||
const count = ref(0);
|
||||
const cb = vi.fn();
|
||||
const passthrough = vi.fn((invoke: () => void) => invoke());
|
||||
|
||||
watchDebounced(count, cb, {
|
||||
debounce: 1000,
|
||||
eventFilter: passthrough,
|
||||
flush: 'sync',
|
||||
});
|
||||
|
||||
count.value = 1;
|
||||
// The custom filter invokes immediately, bypassing the debounce timer.
|
||||
expect(passthrough).toHaveBeenCalledTimes(1);
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
expect(cb).toHaveBeenLastCalledWith(1, 0, expect.any(Function));
|
||||
});
|
||||
|
||||
it('passes onCleanup to the callback', () => {
|
||||
const count = ref(0);
|
||||
const cleanup = vi.fn();
|
||||
|
||||
watchDebounced(count, (_value, _old, onCleanup) => {
|
||||
onCleanup(cleanup);
|
||||
}, { debounce: 100, flush: 'sync' });
|
||||
|
||||
count.value = 1;
|
||||
vi.advanceTimersByTime(100);
|
||||
count.value = 2;
|
||||
vi.advanceTimersByTime(100);
|
||||
|
||||
// cleanup registered on the first settled run fires before the second.
|
||||
expect(cleanup).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('exposes debouncedWatch as an alias', () => {
|
||||
expect(debouncedWatch).toBe(watchDebounced);
|
||||
});
|
||||
|
||||
it('runs in a non-DOM scope without touching globals (SSR-safe)', () => {
|
||||
const count = ref(0);
|
||||
const cb = vi.fn();
|
||||
const scope = effectScope();
|
||||
|
||||
expect(() => {
|
||||
scope.run(() => watchDebounced(count, cb, { debounce: 100, flush: 'sync' }));
|
||||
}).not.toThrow();
|
||||
|
||||
scope.stop();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user