mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-02 00:27:49 +01:00
Fix debounce with immediate firing twice on single call (#30082)
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
// From: https://davidwalsh.name/javascript-debounce-function
|
||||
|
||||
// Returns a function, that, as long as it continues to be invoked, will not
|
||||
// be triggered. The function will be called after it stops being called for
|
||||
// N milliseconds. If `immediate` is passed, trigger the function on the
|
||||
// leading edge and on the trailing.
|
||||
// leading edge. The trailing edge only fires if there were additional calls
|
||||
// during the wait period.
|
||||
|
||||
export const debounce = <T extends any[]>(
|
||||
func: (...args: T) => void,
|
||||
@@ -11,20 +10,35 @@ export const debounce = <T extends any[]>(
|
||||
immediate = false
|
||||
) => {
|
||||
let timeout: number | undefined;
|
||||
let trailingArgs: T | undefined;
|
||||
|
||||
const debouncedFunc = (...args: T): void => {
|
||||
const later = () => {
|
||||
timeout = undefined;
|
||||
func(...args);
|
||||
};
|
||||
const callNow = immediate && !timeout;
|
||||
const isLeading = immediate && !timeout;
|
||||
|
||||
if (timeout) {
|
||||
trailingArgs = args;
|
||||
}
|
||||
clearTimeout(timeout);
|
||||
timeout = window.setTimeout(later, wait);
|
||||
if (callNow) {
|
||||
|
||||
timeout = window.setTimeout(() => {
|
||||
timeout = undefined;
|
||||
if (trailingArgs) {
|
||||
func(...trailingArgs);
|
||||
trailingArgs = undefined;
|
||||
} else if (!immediate) {
|
||||
func(...args);
|
||||
}
|
||||
}, wait);
|
||||
|
||||
if (isLeading) {
|
||||
func(...args);
|
||||
}
|
||||
};
|
||||
|
||||
debouncedFunc.cancel = () => {
|
||||
clearTimeout(timeout);
|
||||
trailingArgs = undefined;
|
||||
};
|
||||
|
||||
return debouncedFunc;
|
||||
};
|
||||
|
||||
153
test/common/util/debounce.test.ts
Normal file
153
test/common/util/debounce.test.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { debounce } from "../../../src/common/util/debounce";
|
||||
|
||||
describe("debounce", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers({
|
||||
toFake: ["setTimeout", "clearTimeout"],
|
||||
shouldAdvanceTime: false,
|
||||
});
|
||||
vi.stubGlobal("window", {
|
||||
...window,
|
||||
setTimeout: globalThis.setTimeout,
|
||||
clearTimeout: globalThis.clearTimeout,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("without immediate", () => {
|
||||
it("calls function after wait period", () => {
|
||||
const fn = vi.fn();
|
||||
const debounced = debounce(fn, 500);
|
||||
|
||||
debounced();
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
|
||||
vi.advanceTimersByTime(500);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("resets timer on subsequent calls", () => {
|
||||
const fn = vi.fn();
|
||||
const debounced = debounce(fn, 500);
|
||||
|
||||
debounced();
|
||||
vi.advanceTimersByTime(300);
|
||||
debounced();
|
||||
vi.advanceTimersByTime(300);
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
|
||||
vi.advanceTimersByTime(200);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("uses the latest arguments", () => {
|
||||
const fn = vi.fn();
|
||||
const debounced = debounce(fn, 500);
|
||||
|
||||
debounced("a");
|
||||
debounced("b");
|
||||
debounced("c");
|
||||
|
||||
vi.advanceTimersByTime(500);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
expect(fn).toHaveBeenCalledWith("c");
|
||||
});
|
||||
});
|
||||
|
||||
describe("with immediate", () => {
|
||||
it("calls function immediately on first call", () => {
|
||||
const fn = vi.fn();
|
||||
const debounced = debounce(fn, 500, true);
|
||||
|
||||
debounced();
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not call again on trailing edge for a single call", () => {
|
||||
const fn = vi.fn();
|
||||
const debounced = debounce(fn, 500, true);
|
||||
|
||||
debounced();
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
|
||||
vi.advanceTimersByTime(500);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("calls trailing edge when there are additional calls during wait", () => {
|
||||
const fn = vi.fn();
|
||||
const debounced = debounce(fn, 500, true);
|
||||
|
||||
debounced("a");
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
expect(fn).toHaveBeenCalledWith("a");
|
||||
|
||||
debounced("b");
|
||||
debounced("c");
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
|
||||
vi.advanceTimersByTime(500);
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
expect(fn).toHaveBeenLastCalledWith("c");
|
||||
});
|
||||
|
||||
it("does not fire leading edge during cooldown", () => {
|
||||
const fn = vi.fn();
|
||||
const debounced = debounce(fn, 500, true);
|
||||
|
||||
debounced();
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
|
||||
vi.advanceTimersByTime(200);
|
||||
debounced();
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("allows a new leading call after cooldown expires", () => {
|
||||
const fn = vi.fn();
|
||||
const debounced = debounce(fn, 500, true);
|
||||
|
||||
debounced("first");
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
|
||||
vi.advanceTimersByTime(500);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
|
||||
debounced("second");
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
expect(fn).toHaveBeenLastCalledWith("second");
|
||||
});
|
||||
});
|
||||
|
||||
describe("cancel", () => {
|
||||
it("cancels pending trailing call", () => {
|
||||
const fn = vi.fn();
|
||||
const debounced = debounce(fn, 500);
|
||||
|
||||
debounced();
|
||||
debounced.cancel();
|
||||
|
||||
vi.advanceTimersByTime(500);
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("cancels pending trailing call with immediate", () => {
|
||||
const fn = vi.fn();
|
||||
const debounced = debounce(fn, 500, true);
|
||||
|
||||
debounced("a");
|
||||
debounced("b");
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
|
||||
debounced.cancel();
|
||||
|
||||
vi.advanceTimersByTime(500);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user