You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

技术问题:如何在Puppeteer中实现类似Jasmine Fake Timers的时间管理?

Implementing Jasmine-Style Fake Timers in the Browser

Great question! Fake timers eliminate the need to wait for real time to pass in tests by hijacking the browser's native time-related APIs and letting you manually control the flow of time. Let's break down how to build this functionality from scratch.

Core Concept

The idea is straightforward:

  • Replace native timers (setTimeout, setInterval, clearTimeout, clearInterval) with custom implementations that store tasks in a queue instead of scheduling them with the browser.
  • Override the Date object to return a virtual time that we control, instead of the system's real time.
  • Add a method to "fast-forward" this virtual time, executing any tasks that would have been triggered in that window.

Full Implementation Code

Here's a robust FakeTimers class that replicates Jasmine's core functionality:

class FakeTimers {
  constructor() {
    // Save references to native APIs so we can restore them later
    this.native = {
      setTimeout: window.setTimeout,
      setInterval: window.setInterval,
      clearTimeout: window.clearTimeout,
      clearInterval: window.clearInterval,
      Date: window.Date
    };

    this.virtualTime = Date.now(); // Initialize with real time to start
    this.taskQueue = [];
    this.nextTaskId = 0;
  }

  // Activate fake timers by replacing native APIs
  install() {
    const self = this;

    // Override setTimeout
    window.setTimeout = function(callback, delay = 0) {
      const taskId = self.nextTaskId++;
      self.taskQueue.push({
        id: taskId,
        callback,
        delay,
        isInterval: false,
        remainingDelay: delay
      });
      return taskId;
    };

    // Override setInterval
    window.setInterval = function(callback, delay = 0) {
      const taskId = self.nextTaskId++;
      self.taskQueue.push({
        id: taskId,
        callback,
        delay,
        isInterval: true,
        remainingDelay: delay
      });
      return taskId;
    };

    // Override clearTimeout/clearInterval
    window.clearTimeout = window.clearInterval = function(taskId) {
      self.taskQueue = self.taskQueue.filter(task => task.id !== taskId);
    };

    // Override Date constructor and static methods
    window.Date = function(...args) {
      if (new.target) {
        // Handle `new Date()` with no args (use virtual time)
        if (args.length === 0) return new self.native.Date(self.virtualTime);
        // Handle other Date constructor calls (pass through args)
        return new self.native.Date(...args);
      }
      // Handle `Date()` call (returns string)
      return self.native.Date(self.virtualTime).toString();
    };

    window.Date.now = () => self.virtualTime;
  }

  // Fast-forward virtual time by a given number of milliseconds
  tick(milliseconds = 0) {
    this.virtualTime += milliseconds;

    // Update remaining delay for all tasks and collect those ready to run
    const readyTasks = [];
    this.taskQueue.forEach(task => {
      task.remainingDelay -= milliseconds;
      if (task.remainingDelay <= 0) readyTasks.push(task);
    });

    // Execute ready tasks and handle intervals
    readyTasks.forEach(task => {
      try {
        task.callback();
      } catch (err) {
        console.error('Error running fake timer task:', err);
      }

      if (task.isInterval) {
        // Reset remaining delay for next interval cycle
        task.remainingDelay = task.delay;
      } else {
        // Remove one-time timeout tasks from the queue
        this.taskQueue = this.taskQueue.filter(t => t.id !== task.id);
      }
    });
  }

  // Restore native APIs and clean up
  uninstall() {
    Object.assign(window, this.native);
    this.taskQueue = [];
    this.virtualTime = Date.now();
  }
}

How to Use in Tests

Integrate this into your test suite (works with Jest, Mocha, or any browser-based test runner):

// Example test for a countdown component
describe('Countdown Component', () => {
  let fakeTimers;

  // Set up fake timers before each test
  beforeEach(() => {
    fakeTimers = new FakeTimers();
    fakeTimers.install();
  });

  // Clean up after each test to avoid side effects
  afterEach(() => {
    fakeTimers.uninstall();
  });

  it('triggers completion callback after 5 seconds', () => {
    const onComplete = jest.fn(); // Use a mock function to track calls
    const countdown = new Countdown(5000, onComplete);
    countdown.start();

    // Fast-forward 5 seconds of virtual time
    fakeTimers.tick(5000);

    expect(onComplete).toHaveBeenCalledExactlyOnce();
  });

  it('updates progress every second', () => {
    const onProgress = jest.fn();
    const countdown = new Countdown(3000, null, onProgress);
    countdown.start();

    fakeTimers.tick(1000);
    expect(onProgress).toHaveBeenCalledWith(33); // 33% progress

    fakeTimers.tick(1000);
    expect(onProgress).toHaveBeenCalledWith(66); // 66% progress

    fakeTimers.tick(1000);
    expect(onProgress).toHaveBeenCalledWith(100); // 100% progress
  });
});

Key Considerations

  • Microtasks vs. Macrotasks: This implementation handles macrotasks (setTimeout/setInterval), but if your code uses Promises or other microtasks, you'll need to flush the microtask queue after calling tick(). You can do this with await Promise.resolve() in async test functions.
  • Third-Party Libraries: Some libraries may bypass the global setTimeout/Date APIs. For these cases, you may need to mock the library's time-related methods directly.
  • Test Isolation: Always call uninstall() after each test to restore native APIs—failure to do so will break other tests or even the test runner itself.

内容的提问来源于stack exchange,提问作者Vadim

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 08:49:21