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:
2026-06-07 16:29:39 +07:00
parent e6919de29e
commit c7644ade69
203 changed files with 23016 additions and 141 deletions
@@ -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 100250 (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;
}