技术问题:如何在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
Dateobject 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 callingtick(). You can do this withawait Promise.resolve()in async test functions. - Third-Party Libraries: Some libraries may bypass the global
setTimeout/DateAPIs. 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
相关产品推荐
相关产品推荐

