Skip to the code
cf.

Clear all timeouts

EasyTiming

Write createTimeouts(). It returns an object that makes timeouts and keeps track of them, so a widget or a test can cancel every timeout it started in one call, for example when it's removed from the page.

Its setTimeout(fn, delay, ...args) works like the global one: it calls fn(...args) once, delay ms later, and returns an ID.

Its clearTimeout(id) cancels the timeout with that ID, so fn never runs for it.

clearAll() cancels every timeout made through this object that hasn't run yet: their timers are cleared, so none is left waiting. Timeouts made elsewhere, with the global setTimeout or with another createTimeouts() object, keep running. The object still works afterwards: timeouts you make later run as usual.

pending() returns how many of its timeouts are still waiting: made, but not yet run or cancelled.

const timeouts = createTimeouts();
timeouts.setTimeout((name) => console.log("hi", name), 100, "Ada");
timeouts.setTimeout(() => console.log("saved"), 1000);
const id = timeouts.setTimeout(() => console.log("closed"), 2000);
timeouts.pending(); // 3
timeouts.clearTimeout(id);
timeouts.pending(); // 2
// at 100 ms: logs "hi Ada"
timeouts.pending(); // 1
timeouts.clearAll();
timeouts.pending(); // 0, and "saved" is never logged

What the tests check

  1. runs fn with its arguments after delay ms
    const clock = useFakeTimers();
    const timeouts = createTimeouts();
    const fn = spy();
    timeouts.setTimeout(fn, 100, "a", 1);
    clock.tick(99);
    expect(fn).toHaveBeenCalledTimes(0);
    clock.tick(1);
    expect(fn).toHaveBeenCalledTimes(1);
    expect(fn).toHaveBeenLastCalledWith("a", 1);
    
  2. clearTimeout(id) cancels that timeout only
    const clock = useFakeTimers();
    const timeouts = createTimeouts();
    const a = spy();
    const b = spy();
    const id = timeouts.setTimeout(a, 100);
    timeouts.setTimeout(b, 100);
    timeouts.clearTimeout(id);
    clock.tick(1000);
    expect(a).not.toHaveBeenCalled();
    expect(b).toHaveBeenCalledTimes(1);
    
  3. clearAll() cancels every timeout still waiting
    const clock = useFakeTimers();
    const timeouts = createTimeouts();
    const fns = [spy(), spy(), spy()];
    timeouts.setTimeout(fns[0], 0);
    timeouts.setTimeout(fns[1], 100);
    timeouts.setTimeout(fns[2], 200);
    timeouts.clearAll();
    expect(clock.pending()).toBe(0);
    clock.tick(1000);
    expect(fns.map((fn) => fn.callCount)).toEqual([0, 0, 0]);
    
  4. clearAll() leaves other timeouts running
    const clock = useFakeTimers();
    const mine = createTimeouts();
    const other = createTimeouts();
    const global = spy();
    const theirs = spy();
    const ours = spy();
    setTimeout(global, 100);
    other.setTimeout(theirs, 100);
    mine.setTimeout(ours, 100);
    mine.clearAll();
    clock.tick(100);
    expect(ours).not.toHaveBeenCalled();
    expect(theirs).toHaveBeenCalledTimes(1);
    expect(global).toHaveBeenCalledTimes(1);
    
  5. pending() counts the timeouts still waiting
    const clock = useFakeTimers();
    const timeouts = createTimeouts();
    expect(timeouts.pending()).toBe(0);
    const id = timeouts.setTimeout(() => {}, 100);
    timeouts.setTimeout(() => {}, 200);
    timeouts.setTimeout(() => {}, 300);
    expect(timeouts.pending()).toBe(3);
    timeouts.clearTimeout(id);
    expect(timeouts.pending()).toBe(2);
    clock.tick(200);
    expect(timeouts.pending()).toBe(1);
    timeouts.clearAll();
    expect(timeouts.pending()).toBe(0);
    
  6. keeps working after clearAll()
    const clock = useFakeTimers();
    const timeouts = createTimeouts();
    timeouts.setTimeout(() => {}, 100);
    timeouts.clearAll();
    const fn = spy();
    timeouts.setTimeout(fn, 100);
    expect(timeouts.pending()).toBe(1);
    clock.tick(100);
    expect(fn).toHaveBeenCalledTimes(1);
    expect(timeouts.pending()).toBe(0);
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).