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,168 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { effectScope, nextTick, ref } from 'vue';
|
||||
import { refThrottled } from '.';
|
||||
|
||||
describe(refThrottled, () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('initializes with the source value', () => {
|
||||
const scope = effectScope();
|
||||
scope.run(() => {
|
||||
const source = ref('init');
|
||||
const throttled = refThrottled(source, 100);
|
||||
expect(throttled.value).toBe('init');
|
||||
});
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('updates immediately on the leading edge', async () => {
|
||||
const scope = effectScope();
|
||||
await scope.run(async () => {
|
||||
const source = ref(0);
|
||||
const throttled = refThrottled(source, 100);
|
||||
|
||||
source.value = 1;
|
||||
await nextTick();
|
||||
expect(throttled.value).toBe(1);
|
||||
});
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('throttles intermediate updates within the window', async () => {
|
||||
const scope = effectScope();
|
||||
await scope.run(async () => {
|
||||
const source = ref(0);
|
||||
const throttled = refThrottled(source, 100);
|
||||
|
||||
source.value = 1;
|
||||
await nextTick();
|
||||
expect(throttled.value).toBe(1);
|
||||
|
||||
source.value = 2;
|
||||
await nextTick();
|
||||
// Still within the window: not yet propagated as a fresh leading update.
|
||||
expect(throttled.value).toBe(1);
|
||||
|
||||
source.value = 3;
|
||||
await nextTick();
|
||||
expect(throttled.value).toBe(1);
|
||||
|
||||
// Trailing edge fires with the most recent value.
|
||||
vi.advanceTimersByTime(100);
|
||||
await nextTick();
|
||||
expect(throttled.value).toBe(3);
|
||||
});
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('does not emit a trailing update when trailing is false', async () => {
|
||||
const scope = effectScope();
|
||||
await scope.run(async () => {
|
||||
const source = ref(0);
|
||||
const throttled = refThrottled(source, 100, false);
|
||||
|
||||
source.value = 1;
|
||||
await nextTick();
|
||||
expect(throttled.value).toBe(1);
|
||||
|
||||
source.value = 2;
|
||||
await nextTick();
|
||||
expect(throttled.value).toBe(1);
|
||||
|
||||
vi.advanceTimersByTime(200);
|
||||
await nextTick();
|
||||
// No trailing edge: value stays at the leading update.
|
||||
expect(throttled.value).toBe(1);
|
||||
});
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('skips the leading update when leading is false', async () => {
|
||||
const scope = effectScope();
|
||||
await scope.run(async () => {
|
||||
const source = ref(0);
|
||||
const throttled = refThrottled(source, 100, true, false);
|
||||
|
||||
source.value = 1;
|
||||
await nextTick();
|
||||
// Leading suppressed: initial value retained until the trailing edge.
|
||||
expect(throttled.value).toBe(0);
|
||||
|
||||
vi.advanceTimersByTime(100);
|
||||
await nextTick();
|
||||
expect(throttled.value).toBe(1);
|
||||
});
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('mirrors the source synchronously when delay <= 0', async () => {
|
||||
const scope = effectScope();
|
||||
await scope.run(async () => {
|
||||
const source = ref(0);
|
||||
const throttled = refThrottled(source, 0);
|
||||
|
||||
source.value = 1;
|
||||
await nextTick();
|
||||
expect(throttled.value).toBe(1);
|
||||
|
||||
source.value = 2;
|
||||
await nextTick();
|
||||
expect(throttled.value).toBe(2);
|
||||
});
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('mirrors the source synchronously when delay is negative', async () => {
|
||||
const scope = effectScope();
|
||||
await scope.run(async () => {
|
||||
const source = ref('a');
|
||||
const throttled = refThrottled(source, -50);
|
||||
|
||||
source.value = 'b';
|
||||
await nextTick();
|
||||
expect(throttled.value).toBe('b');
|
||||
});
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('accepts a getter as the source', async () => {
|
||||
const scope = effectScope();
|
||||
await scope.run(async () => {
|
||||
const obj = ref({ n: 1 });
|
||||
const throttled = refThrottled(() => obj.value.n, 100);
|
||||
expect(throttled.value).toBe(1);
|
||||
|
||||
obj.value = { n: 2 };
|
||||
await nextTick();
|
||||
expect(throttled.value).toBe(2);
|
||||
});
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('reopens the leading edge after the window elapses', async () => {
|
||||
const scope = effectScope();
|
||||
await scope.run(async () => {
|
||||
const source = ref(0);
|
||||
const throttled = refThrottled(source, 100);
|
||||
|
||||
source.value = 1;
|
||||
await nextTick();
|
||||
expect(throttled.value).toBe(1);
|
||||
|
||||
// Advance past the window so the next change is a fresh leading update.
|
||||
vi.advanceTimersByTime(100);
|
||||
await nextTick();
|
||||
|
||||
source.value = 2;
|
||||
await nextTick();
|
||||
expect(throttled.value).toBe(2);
|
||||
});
|
||||
scope.stop();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { ref, toValue, watch } from 'vue';
|
||||
import type { MaybeRefOrGetter, Ref } from 'vue';
|
||||
import { createFilterWrapper, throttleFilter } from '@/utils/filters';
|
||||
|
||||
export type RefThrottledReturn<T = any> = Ref<T>;
|
||||
|
||||
/**
|
||||
* @name refThrottled
|
||||
* @category Reactivity
|
||||
* @description A ref whose value updates are throttled. The returned ref mirrors
|
||||
* the source but propagates changes at most once per `delay` window, making it
|
||||
* useful for rate-limiting reactive updates driven by high-frequency events such
|
||||
* as `scroll` or `resize`.
|
||||
*
|
||||
* @param {MaybeRefOrGetter<T>} source The ref, getter, or value to watch and throttle
|
||||
* @param {number} [delay=200] A zero-or-greater delay in milliseconds; values around 100–250 (or higher) are most useful
|
||||
* @param {boolean} [trailing=true] Update the value again on the trailing edge after the window elapses
|
||||
* @param {boolean} [leading=true] Update the value on the leading edge of the window
|
||||
* @returns {RefThrottledReturn<T>} A ref reflecting the throttled source value
|
||||
*
|
||||
* @example
|
||||
* const input = ref('');
|
||||
* const throttled = refThrottled(input, 1000);
|
||||
*
|
||||
* @example
|
||||
* const scrollY = ref(0);
|
||||
* const throttledY = refThrottled(scrollY, 100, true, false);
|
||||
*
|
||||
* @since 0.0.15
|
||||
*/
|
||||
export function refThrottled<T = any>(
|
||||
source: MaybeRefOrGetter<T>,
|
||||
delay = 200,
|
||||
trailing = true,
|
||||
leading = true,
|
||||
): RefThrottledReturn<T> {
|
||||
const throttled = ref(toValue(source)) as Ref<T>;
|
||||
|
||||
// A non-positive delay disables throttling: mirror the source synchronously.
|
||||
if (delay <= 0) {
|
||||
watch(() => toValue(source), (value) => {
|
||||
throttled.value = value;
|
||||
});
|
||||
|
||||
return throttled;
|
||||
}
|
||||
|
||||
const update = createFilterWrapper(
|
||||
throttleFilter(delay, trailing, leading),
|
||||
() => {
|
||||
throttled.value = toValue(source);
|
||||
},
|
||||
);
|
||||
|
||||
watch(() => toValue(source), () => {
|
||||
update();
|
||||
});
|
||||
|
||||
return throttled;
|
||||
}
|
||||
Reference in New Issue
Block a user