59e995d0b5
Composables, tests, category barrels, and README for @robonen/vue.
215 lines
5.9 KiB
TypeScript
215 lines
5.9 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { ref } from 'vue';
|
|
import { useThrottleFn } from '.';
|
|
|
|
describe(useThrottleFn, () => {
|
|
beforeEach(() => vi.useFakeTimers());
|
|
afterEach(() => vi.useRealTimers());
|
|
|
|
it('invokes immediately on the leading edge', () => {
|
|
const fn = vi.fn();
|
|
const throttled = useThrottleFn(fn, 100);
|
|
|
|
throttled();
|
|
expect(fn).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('ignores calls within the window by default (no trailing)', () => {
|
|
const fn = vi.fn();
|
|
const throttled = useThrottleFn(fn, 100);
|
|
|
|
throttled();
|
|
throttled();
|
|
throttled();
|
|
vi.advanceTimersByTime(200);
|
|
|
|
expect(fn).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('invokes trailing when enabled', () => {
|
|
const fn = vi.fn();
|
|
const throttled = useThrottleFn(fn, 100, true);
|
|
|
|
throttled('a');
|
|
throttled('b');
|
|
expect(fn).toHaveBeenCalledOnce();
|
|
|
|
vi.advanceTimersByTime(100);
|
|
expect(fn).toHaveBeenCalledTimes(2);
|
|
expect(fn).toHaveBeenLastCalledWith('b');
|
|
});
|
|
|
|
it('resolves the promise with the function result', async () => {
|
|
const throttled = useThrottleFn((x: number) => x + 1, 100);
|
|
await expect(throttled(1)).resolves.toBe(2);
|
|
});
|
|
|
|
it('skips the leading edge when leading is false', () => {
|
|
const fn = vi.fn();
|
|
const throttled = useThrottleFn(fn, 100, true, false);
|
|
|
|
// first call only opens the window (no leading edge)
|
|
throttled('a');
|
|
expect(fn).not.toHaveBeenCalled();
|
|
|
|
// a call inside the same window schedules the trailing edge
|
|
throttled('b');
|
|
expect(fn).not.toHaveBeenCalled();
|
|
|
|
vi.advanceTimersByTime(100);
|
|
expect(fn).toHaveBeenCalledOnce();
|
|
expect(fn).toHaveBeenLastCalledWith('b');
|
|
});
|
|
|
|
describe('options object', () => {
|
|
it('accepts delay/trailing/leading via an options object', () => {
|
|
const fn = vi.fn();
|
|
const throttled = useThrottleFn(fn, { delay: 100, trailing: true });
|
|
|
|
throttled('a');
|
|
throttled('b');
|
|
expect(fn).toHaveBeenCalledOnce();
|
|
|
|
vi.advanceTimersByTime(100);
|
|
expect(fn).toHaveBeenCalledTimes(2);
|
|
expect(fn).toHaveBeenLastCalledWith('b');
|
|
});
|
|
|
|
it('defaults delay to 200 when omitted', () => {
|
|
const fn = vi.fn();
|
|
const throttled = useThrottleFn(fn, {});
|
|
|
|
throttled(); // leading edge
|
|
expect(fn).toHaveBeenCalledOnce();
|
|
|
|
vi.advanceTimersByTime(199);
|
|
throttled(); // still inside the 200ms window → dropped (trailing off)
|
|
expect(fn).toHaveBeenCalledOnce();
|
|
|
|
vi.advanceTimersByTime(1); // 200ms elapsed — window reached
|
|
throttled(); // new window opens on the leading edge
|
|
expect(fn).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|
|
|
|
describe('reactive delay', () => {
|
|
it('reads the current delay on each call', () => {
|
|
const fn = vi.fn();
|
|
const delay = ref(100);
|
|
const throttled = useThrottleFn(fn, delay);
|
|
|
|
throttled();
|
|
expect(fn).toHaveBeenCalledOnce();
|
|
|
|
vi.advanceTimersByTime(101);
|
|
throttled();
|
|
expect(fn).toHaveBeenCalledTimes(2);
|
|
|
|
delay.value = 1000;
|
|
vi.advanceTimersByTime(101);
|
|
throttled();
|
|
// window grew to 1000ms, so this call is throttled
|
|
expect(fn).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|
|
|
|
describe('zero delay', () => {
|
|
it('invokes synchronously on every call when delay <= 0', () => {
|
|
const fn = vi.fn();
|
|
const throttled = useThrottleFn(fn, 0);
|
|
|
|
throttled();
|
|
throttled();
|
|
throttled();
|
|
expect(fn).toHaveBeenCalledTimes(3);
|
|
});
|
|
});
|
|
|
|
describe('cancel', () => {
|
|
it('drops a pending trailing invocation', () => {
|
|
const fn = vi.fn();
|
|
const throttled = useThrottleFn(fn, 100, true);
|
|
|
|
throttled('a');
|
|
throttled('b');
|
|
expect(fn).toHaveBeenCalledOnce();
|
|
|
|
throttled.cancel();
|
|
vi.advanceTimersByTime(100);
|
|
expect(fn).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('resolves the pending promise with undefined by default', async () => {
|
|
const throttled = useThrottleFn((x: number) => x, 100, true);
|
|
|
|
throttled(1);
|
|
const pending = throttled(2);
|
|
throttled.cancel();
|
|
|
|
await expect(pending).resolves.toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('rejectOnCancel', () => {
|
|
it('rejects a superseded trailing promise', async () => {
|
|
const throttled = useThrottleFn((x: number) => x, 100, true, true, true);
|
|
|
|
throttled(1);
|
|
const superseded = throttled(2);
|
|
// next call within the window supersedes the previous trailing promise
|
|
const latest = throttled(3);
|
|
|
|
await expect(superseded).rejects.toThrow();
|
|
vi.advanceTimersByTime(100);
|
|
await expect(latest).resolves.toBe(3);
|
|
});
|
|
|
|
it('rejects on explicit cancel', async () => {
|
|
const throttled = useThrottleFn((x: number) => x, 100, true, true, true);
|
|
|
|
throttled(1);
|
|
const pending = throttled(2);
|
|
throttled.cancel();
|
|
|
|
await expect(pending).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('flush', () => {
|
|
it('invokes the pending trailing call immediately', async () => {
|
|
const fn = vi.fn((x: number) => x);
|
|
const throttled = useThrottleFn(fn, 100, true);
|
|
|
|
throttled(1);
|
|
const pending = throttled(2);
|
|
expect(fn).toHaveBeenCalledOnce();
|
|
|
|
throttled.flush();
|
|
expect(fn).toHaveBeenCalledTimes(2);
|
|
expect(fn).toHaveBeenLastCalledWith(2);
|
|
await expect(pending).resolves.toBe(2);
|
|
});
|
|
|
|
it('is a no-op when nothing is pending', () => {
|
|
const fn = vi.fn();
|
|
const throttled = useThrottleFn(fn, 100, true);
|
|
|
|
expect(() => throttled.flush()).not.toThrow();
|
|
expect(fn).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
it('preserves the calling context (this)', () => {
|
|
const obj = {
|
|
value: 42,
|
|
method: vi.fn(function (this: { value: number }) {
|
|
return this.value;
|
|
}),
|
|
};
|
|
const throttled = useThrottleFn(obj.method, 100);
|
|
|
|
throttled.call(obj);
|
|
expect(obj.method).toHaveReturnedWith(42);
|
|
});
|
|
});
|