Compare commits

...

13 Commits

Author SHA1 Message Date
pfg
b1309c3aba feat: fake timers now mock performance.now() in addition to Date.now()
Both Date.now() and performance.now() now advance together when fake timers
run, ensuring consistent time behavior across all time APIs.

Implementation overrides Bun__readOriginTimer to return a frozen millisecond
value when fake timers are enabled. This approach automatically mocks
performance.now() and any other code using the origin timer.

Added comprehensive tests covering all timer advancement methods.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 14:34:52 -07:00
pfg
534aa441ba todo md 2025-10-15 13:24:33 -07:00
pfg
7e1927d980 feat: advance Date.now() when fake timers run
Update vi.runAllTimers(), vi.advanceTimersToNextTimer(), and
vi.runOnlyPendingTimers() to advance Date.now() in sync with virtual
time. This ensures Date.now() returns the correct mocked time when
timers fire, matching behavior of testing frameworks like Vitest.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 13:24:01 -07:00
pfg
4dbb62edf1 feat: implement vi.runOnlyPendingTimers()
Implements vi.runOnlyPendingTimers() which runs only timers that were pending when called, not timers scheduled during execution. This matches Vitest's behavior.

Implementation:
- Collects all pending timers from the vi_timers heap
- Sorts and fires them in scheduled order
- Re-inserts interval timers but doesn't fire them in the same call

Tests:
- Added comprehensive test suite in runOnlyPendingTimers.test.ts (11 tests)
- Fixed missing vi.runAllTimers() call in order.test.ts
- All 15 fake timer tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 19:30:05 -07:00
pfg
0126c67af1 TODO 2025-10-14 16:22:17 -07:00
pfg
0380651913 feat: implement vi.advanceTimersToNextTick() 2025-10-14 15:40:48 -07:00
pfg
e0a2a9ed84 feat: implement vi.runAllTicks() to drain all nextTick callbacks
Add vi.runAllTicks() method that drains all pending process.nextTick
callbacks and microtasks scheduled by them. This matches Vitest's API
for testing code that uses process.nextTick.

Implementation includes:
- JSMock__jsRunAllTicks function that iteratively drains nextTick queue
- Safety limit of 10000 iterations to prevent infinite loops
- Proper handling of nested and recursive nextTick callbacks
- Support for microtasks scheduled during nextTick execution

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 15:40:48 -07:00
pfg
148bd5e9f6 order test ts 2025-10-14 15:40:48 -07:00
pfg
91716fa72e for runAllTimers, failing 2025-10-14 15:40:48 -07:00
pfg
785a3ca13f feat: implement vi.runAllTimers() to execute all fake timers immediately
Added support for vi.runAllTimers() which fires all pending fake timers at once. This is useful for testing code with timers without waiting for real time to pass.

- Added separate vi_timers heap to track fake timers
- Added is_vi_timer field to EventLoopTimer to identify fake timers
- Added allowFakeTimers() method to Tag enum to control which timer types can be faked
- Implemented runAllTimers function that drains all vi_timers
- Added comprehensive test suite for runAllTimers functionality

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 15:40:48 -07:00
pfg
75e982b618 feat: add vi.isFakeTimers() to check timer mocking state
Adds vi.isFakeTimers() method that returns a boolean indicating whether fake timers are currently enabled.

This is useful for:
- Checking if time mocking is active without side effects
- Conditional logic based on timer state
- Debugging and testing timer-related code

Returns true when fake timers are enabled (overridenDateNow >= 0), false otherwise.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 15:40:48 -07:00
pfg
68cc7538cd feat: add vi.getMockedSystemTime() and vi.getRealSystemTime()
Adds two new timer utility methods to the vi object:

- vi.getMockedSystemTime() - Returns the current mocked time as a Date object when fake timers are active, or null when they're not
- vi.getRealSystemTime() - Always returns the real system time (equivalent to Date.now()), bypassing any mocked time

These methods are useful for:
- Checking if fake timers are active (getMockedSystemTime returns null when not)
- Debugging tests by comparing mocked vs real time
- Measuring actual elapsed time during tests even when time is mocked

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 15:40:48 -07:00
pfg
a83d57b1de feat: implement vi.useFakeTimers(), vi.useRealTimers(), and vi.setSystemTime()
Adds support for Vitest timer mocking methods to control Date behavior in tests:
- vi.useFakeTimers() - Freezes Date to current time when called
- vi.useRealTimers() - Restores real Date behavior
- vi.setSystemTime(date) - Sets mocked Date to specific time (accepts number/Date/string)

When fake timers are enabled, Date.now() and new Date() return the mocked time value.
The implementation uses globalObject->overridenDateNow to control the Date behavior.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 15:40:48 -07:00
21 changed files with 2329 additions and 11 deletions

4
TODO.md Normal file
View File

@@ -0,0 +1,4 @@
need to support bun.timespec.now() for time mocking
- support performance.now
- consider supporting edge-case where `timeout0(A, timeout0(B)), timeout0(C)` prints `A=0, C=0, B=1` (date.now())

View File

@@ -97,6 +97,12 @@ declare module "bun:test" {
function setTimeout(milliseconds: number): void;
function useFakeTimers(): void;
function useRealTimers(): void;
function setSystemTime(now: string | number | Date): void;
function getMockedSystemTime(): Date | null;
function getRealSystemTime(): number;
function isFakeTimers(): boolean;
function runAllTimers(): void;
function runAllTicks(): void;
function spyOn<T extends object, K extends keyof T>(
obj: T,
methodOrPropertyValue: K,
@@ -184,6 +190,32 @@ declare module "bun:test" {
resetAllMocks: typeof jest.resetAllMocks;
useFakeTimers: typeof jest.useFakeTimers;
useRealTimers: typeof jest.useRealTimers;
runAllTicks: typeof jest.runAllTicks;
/**
* Advances all timers to the next timer (earliest scheduled timer).
* Useful for stepping through timers one at a time.
*
* @returns this (the vi object) for chaining
*
* @example
* ```ts
* import { vi, test, expect } from "bun:test";
*
* test("vi.advanceTimersToNextTimer", () => {
* vi.useFakeTimers();
*
* let i = 0;
* setInterval(() => console.log(++i), 50);
*
* vi.advanceTimersToNextTimer() // log: 1
* .advanceTimersToNextTimer() // log: 2
* .advanceTimersToNextTimer(); // log: 3
*
* vi.useRealTimers();
* });
* ```
*/
advanceTimersToNextTimer(): typeof vi;
};
interface FunctionLike {

View File

@@ -120,6 +120,10 @@ argv: []const []const u8 = &[_][]const u8{},
origin_timer: std.time.Timer = undefined,
origin_timestamp: u64 = 0,
/// Frozen value of performance.now() when fake timers are enabled
/// When < 0, fake timers are disabled for performance.now() and real time is used
/// When >= 0, this is the frozen value that performance.now() returns (advances with fake time)
overriden_performance_now_ms: f64 = -1,
macro_event_loop: EventLoop = EventLoop{},
regular_event_loop: EventLoop = EventLoop{},
event_loop: *EventLoop = undefined,

View File

@@ -1,5 +1,8 @@
const Timer = @This();
extern fn Bun__Timer__isFakeTimersEnabled(*jsc.JSGlobalObject) bool;
extern fn Bun__Timer__advanceDateNowMs(*jsc.JSGlobalObject, f64) void;
/// TimeoutMap is map of i32 to nullable Timeout structs
/// i32 is exposed to JavaScript and can be used with clearTimeout, clearInterval, etc.
/// When Timeout is null, it means the tasks have been scheduled but not yet executed.
@@ -19,6 +22,7 @@ pub const All = struct {
lock: bun.Mutex = .{},
thread_id: std.Thread.Id,
timers: TimerHeap = .{ .context = {} },
vi_timers: TimerHeap = .{ .context = {} },
active_timer_count: i32 = 0,
uv_timer: if (Environment.isWindows) uv.Timer else void = if (Environment.isWindows) std.mem.zeroes(uv.Timer),
/// Whether we have emitted a warning for passing a negative timeout duration
@@ -30,6 +34,8 @@ pub const All = struct {
epoch: u25 = 0,
immediate_ref_count: i32 = 0,
uv_idle: if (Environment.isWindows) uv.uv_idle_t else void = if (Environment.isWindows) std.mem.zeroes(uv.uv_idle_t),
/// Virtual time for fake timers - when set (not null), new timers will be scheduled relative to this time
vi_current_time: ?timespec = null,
// Event loop delay monitoring (not exposed to JS)
event_loop_delay: EventLoopDelayMonitor = .{},
@@ -61,18 +67,32 @@ pub const All = struct {
pub fn insert(this: *All, timer: *EventLoopTimer) void {
this.lock.lock();
defer this.lock.unlock();
this.timers.insert(timer);
const vm: *VirtualMachine = @alignCast(@fieldParentPtr("timer", this));
// Check if fake timers are enabled and timer is not a WTFTimer
if (timer.tag.allowFakeTimers() and Bun__Timer__isFakeTimersEnabled(vm.global)) {
timer.is_vi_timer = true;
this.vi_timers.insert(timer);
} else {
timer.is_vi_timer = false;
this.timers.insert(timer);
}
timer.state = .ACTIVE;
if (Environment.isWindows) {
this.ensureUVTimer(@alignCast(@fieldParentPtr("timer", this)));
this.ensureUVTimer(vm);
}
}
pub fn remove(this: *All, timer: *EventLoopTimer) void {
this.lock.lock();
defer this.lock.unlock();
this.timers.remove(timer);
if (timer.is_vi_timer) {
this.vi_timers.remove(timer);
} else {
this.timers.remove(timer);
}
timer.state = .CANCELLED;
}
@@ -82,7 +102,12 @@ pub const All = struct {
this.lock.lock();
defer this.lock.unlock();
if (timer.state == .ACTIVE) {
this.timers.remove(timer);
// Remove from the correct heap
if (timer.is_vi_timer) {
this.vi_timers.remove(timer);
} else {
this.timers.remove(timer);
}
}
timer.state = .ACTIVE;
@@ -98,9 +123,17 @@ pub const All = struct {
flags.epoch = this.epoch;
}
this.timers.insert(timer);
const vm: *VirtualMachine = @alignCast(@fieldParentPtr("timer", this));
// Check if fake timers are enabled and timer is not a WTFTimer
if (timer.tag != .WTFTimer and Bun__Timer__isFakeTimersEnabled(vm.global)) {
timer.is_vi_timer = true;
this.vi_timers.insert(timer);
} else {
timer.is_vi_timer = false;
this.timers.insert(timer);
}
if (Environment.isWindows) {
this.ensureUVTimer(@alignCast(@fieldParentPtr("timer", this)));
this.ensureUVTimer(vm);
}
}
@@ -546,6 +579,150 @@ pub const All = struct {
return .js_undefined;
}
/// Internal function that advances to the next timer in the vi_timers heap.
/// Returns true if a timer was fired, false if no timers remain.
fn advanceToNextViTimer(timer: *All, vm: *VirtualMachine) bool {
// Get the next timer from the heap
timer.lock.lock();
const event_timer = timer.vi_timers.deleteMin();
timer.lock.unlock();
if (event_timer) |et| {
// Calculate the time delta between current virtual time and the timer's scheduled time
const old_vi_time = timer.vi_current_time orelse timespec{ .sec = 0, .nsec = 0 };
const delta_ms: f64 = @floatFromInt(et.next.ms() - old_vi_time.ms());
// Set the virtual time to this timer's scheduled time
// This ensures nested timers scheduled during callbacks get the correct base time
timer.vi_current_time = et.next;
// Also advance Date.now() by the same delta
// This keeps Date.now() in sync with the virtual time
Bun__Timer__advanceDateNowMs(vm.global, delta_ms);
// Fire the timer at its scheduled time
const arm_result = et.fire(&et.next, vm);
// If the timer wants to rearm (like setInterval), reinsert it
if (arm_result == .rearm) {
timer.lock.lock();
timer.vi_timers.insert(et);
timer.lock.unlock();
}
return true;
}
return false;
}
pub fn runAllTimers(
globalThis: *JSGlobalObject,
_: *jsc.CallFrame,
) JSError!JSValue {
jsc.markBinding(@src());
const vm = globalThis.bunVM();
const timer = &vm.timer;
// Keep firing timers until there are no more
// We need to keep checking the heap because firing timers can schedule new timers
while (advanceToNextViTimer(timer, vm)) {}
// Clear the virtual time when we're done
timer.vi_current_time = null;
return .js_undefined;
}
pub fn runOnlyPendingTimers(
globalThis: *JSGlobalObject,
_: *jsc.CallFrame,
) JSError!JSValue {
jsc.markBinding(@src());
const vm = globalThis.bunVM();
const timer = &vm.timer;
// Collect all currently pending timers into a list by removing them from the heap
var pending_timers = std.ArrayList(*EventLoopTimer).init(bun.default_allocator);
defer pending_timers.deinit();
// Remove all timers from the heap and collect them
timer.lock.lock();
while (timer.vi_timers.deleteMin()) |event_timer| {
pending_timers.append(event_timer) catch |err| bun.handleOom(err);
}
timer.lock.unlock();
// Sort timers by their scheduled time to fire them in the correct order
std.mem.sort(*EventLoopTimer, pending_timers.items, {}, struct {
fn lessThan(_: void, a: *EventLoopTimer, b: *EventLoopTimer) bool {
return a.next.order(&b.next) == .lt;
}
}.lessThan);
// Now fire exactly those timers (not any that get scheduled during execution)
for (pending_timers.items) |event_timer| {
// Calculate the time delta between current virtual time and the timer's scheduled time
const old_vi_time = timer.vi_current_time orelse timespec{ .sec = 0, .nsec = 0 };
const delta_ms: f64 = @floatFromInt(event_timer.next.ms() - old_vi_time.ms());
// Set the virtual time to this timer's scheduled time
timer.vi_current_time = event_timer.next;
// Also advance Date.now() by the same delta
Bun__Timer__advanceDateNowMs(vm.global, delta_ms);
// Fire the timer at its scheduled time
const arm_result = event_timer.fire(&event_timer.next, vm);
// If the timer wants to rearm (like setInterval), reinsert it
// but it won't be fired in this call to runOnlyPendingTimers
if (arm_result == .rearm) {
timer.lock.lock();
timer.vi_timers.insert(event_timer);
timer.lock.unlock();
}
}
// Clear the virtual time when we're done
timer.vi_current_time = null;
return .js_undefined;
}
pub fn advanceTimersToNextTimer(
globalThis: *JSGlobalObject,
callframe: *jsc.CallFrame,
) JSError!JSValue {
jsc.markBinding(@src());
const vm = globalThis.bunVM();
const timer = &vm.timer;
// Advance to the next timer (if any)
_ = advanceToNextViTimer(timer, vm);
// Keep the virtual time set for subsequent calls (don't clear it)
// Return the 'this' value (the vi object) for chaining
return callframe.this();
}
export fn Bun__Timer__initViTime(vm: *VirtualMachine) callconv(.C) void {
const timer = &vm.timer;
// Initialize virtual time to zero so timers are scheduled relative to time 0
timer.vi_current_time = timespec{ .sec = 0, .nsec = 0 };
}
export fn Bun__Timer__clearViTimers(vm: *VirtualMachine) callconv(.C) void {
const timer = &vm.timer;
timer.lock.lock();
defer timer.lock.unlock();
// Remove all timers from vi_timers heap and cancel them
while (timer.vi_timers.deleteMin()) |event_timer| {
event_timer.state = .CANCELLED;
}
}
comptime {
@export(&jsc.host_fn.wrap3(setImmediate), .{ .name = "Bun__Timer__setImmediate" });
@export(&jsc.host_fn.wrap3(sleep), .{ .name = "Bun__Timer__sleep" });
@@ -554,7 +731,12 @@ pub const All = struct {
@export(&jsc.host_fn.wrap2(clearImmediate), .{ .name = "Bun__Timer__clearImmediate" });
@export(&jsc.host_fn.wrap2(clearTimeout), .{ .name = "Bun__Timer__clearTimeout" });
@export(&jsc.host_fn.wrap2(clearInterval), .{ .name = "Bun__Timer__clearInterval" });
@export(&jsc.host_fn.wrap2(runAllTimers), .{ .name = "Bun__Timer__runAllTimers" });
@export(&jsc.host_fn.wrap2(runOnlyPendingTimers), .{ .name = "Bun__Timer__runOnlyPendingTimers" });
@export(&jsc.host_fn.wrap2(advanceTimersToNextTimer), .{ .name = "Bun__Timer__advanceTimersToNextTimer" });
@export(&getNextID, .{ .name = "Bun__Timer__getNextID" });
_ = &Bun__Timer__initViTime;
_ = &Bun__Timer__clearViTimers;
}
};

View File

@@ -4,6 +4,8 @@ const Self = @This();
next: timespec,
state: State = .PENDING,
tag: Tag,
/// Whether this timer is in the vi_timers heap (for fake timers)
is_vi_timer: bool = false,
/// Internal heap fields.
heap: bun.io.heap.IntrusiveField(Self) = .{},
@@ -96,6 +98,17 @@ pub const Tag = enum {
.EventLoopDelayMonitor => jsc.API.Timer.EventLoopDelayMonitor,
};
}
pub fn allowFakeTimers(self: Tag) bool {
return switch (self) {
.WTFTimer, // internal
.BunTest, // for test timeouts
.EventLoopDelayMonitor, // probably important
.StatWatcherScheduler,
=> false,
else => true,
};
}
};
const UnreachableTimer = struct {
@@ -104,6 +117,17 @@ const UnreachableTimer = struct {
if (Environment.ci_assert) bun.assert(false);
return .disarm;
}
pub fn allowFakeTimers(self: Tag) bool {
return switch (self) {
.WTFTimer, // internal
.BunTest, // for test timeouts
.EventLoopDelayMonitor, // probably important
.StatWatcherScheduler,
=> false,
else => true,
};
}
};
const TimerCallback = struct {

View File

@@ -180,6 +180,10 @@ pub fn fire(this: *TimerObjectInternals, _: *const timespec, vm: *jsc.VirtualMac
if (!this.shouldRescheduleTimer(repeat, idle_timeout)) {
break :is_timer_done true;
}
// Recompute time_before_call using virtual time if in fake timers mode
const base_time = vm.timer.vi_current_time orelse timespec.now();
time_before_call = base_time.addMs(this.interval);
switch (this.eventLoopTimer().state) {
.FIRED => {
// If we didn't clear the setInterval, reschedule it starting from
@@ -382,7 +386,9 @@ pub fn reschedule(this: *TimerObjectInternals, timer: JSValue, vm: *VirtualMachi
// https://github.com/nodejs/node/blob/a7cbb904745591c9a9d047a364c2c188e5470047/lib/internal/timers.js#L612
if (!this.shouldRescheduleTimer(repeat, idle_timeout)) return;
const now = timespec.msFromNow(this.interval);
// Use virtual time if we're in fake timers mode
const base_time = vm.timer.vi_current_time orelse timespec.now();
const now = base_time.addMs(this.interval);
const was_active = this.eventLoopTimer().state == .ACTIVE;
if (was_active) {
vm.timer.remove(this.eventLoopTimer());

View File

@@ -27,11 +27,16 @@
#include "BunPlugin.h"
#include "AsyncContextFrame.h"
#include "ErrorCode.h"
#include "JSNextTickQueue.h"
BUN_DECLARE_HOST_FUNCTION(JSMock__jsUseFakeTimers);
BUN_DECLARE_HOST_FUNCTION(JSMock__jsUseRealTimers);
BUN_DECLARE_HOST_FUNCTION(JSMock__jsNow);
BUN_DECLARE_HOST_FUNCTION(JSMock__jsSetSystemTime);
BUN_DECLARE_HOST_FUNCTION(JSMock__jsGetMockedSystemTime);
BUN_DECLARE_HOST_FUNCTION(JSMock__jsGetRealSystemTime);
BUN_DECLARE_HOST_FUNCTION(JSMock__jsIsFakeTimers);
BUN_DECLARE_HOST_FUNCTION(JSMock__jsRunAllTicks);
BUN_DECLARE_HOST_FUNCTION(JSMock__jsRestoreAllMocks);
BUN_DECLARE_HOST_FUNCTION(JSMock__jsClearAllMocks);
BUN_DECLARE_HOST_FUNCTION(JSMock__jsSpyOn);
@@ -1418,15 +1423,53 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionWithImplementation, (JSC::JSGlobalObject
using namespace Bun;
using namespace JSC;
// This is a stub. Exists so that the same code can be run in Jest
extern "C" void Bun__Timer__initViTime(void*);
extern "C" void Bun__Timer__setPerformanceNow(void*, double);
extern "C" double Bun__Timer__getPerformanceNow(void*);
extern "C" uint64_t Bun__readOriginTimer(void*);
// Enables fake timers and sets up Date mocking
BUN_DEFINE_HOST_FUNCTION(JSMock__jsUseFakeTimers, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe))
{
// Enable fake timers flag - this will make Date use the overridden time
// When overridenDateNow is >= 0, Date will use that value instead of current time
// If overridenDateNow is not set (< 0), we'll set it to current time to freeze it
if (globalObject->overridenDateNow < 0) {
// Initialize with current time if not already set
globalObject->overridenDateNow = globalObject->jsDateNow();
}
// Freeze performance.now() at current value when fake timers are enabled
auto* bunGlobal = jsCast<Zig::GlobalObject*>(globalObject);
auto* vm = bunGlobal->bunVM();
if (vm) {
double currentFrozenValue = Bun__Timer__getPerformanceNow(vm);
if (currentFrozenValue < 0) {
// Get current performance.now() value and freeze it, rounded up
auto nowNano = Bun__readOriginTimer(vm);
double currentPerfNow = std::ceil(static_cast<double>(nowNano) / 1000000.0);
Bun__Timer__setPerformanceNow(vm, currentPerfNow);
}
Bun__Timer__initViTime(vm);
}
return JSValue::encode(callframe->thisValue());
}
extern "C" void Bun__Timer__clearViTimers(void*);
BUN_DEFINE_HOST_FUNCTION(JSMock__jsUseRealTimers, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe))
{
globalObject->overridenDateNow = -1;
// Clear all pending fake timers
auto* bunGlobal = jsCast<Zig::GlobalObject*>(globalObject);
auto* vm = bunGlobal->bunVM();
if (vm) {
Bun__Timer__setPerformanceNow(vm, -1);
Bun__Timer__clearViTimers(vm);
}
return JSValue::encode(callframe->thisValue());
}
@@ -1438,14 +1481,142 @@ BUN_DEFINE_HOST_FUNCTION(JSMock__jsSetSystemTime, (JSC::JSGlobalObject * globalO
{
JSValue argument0 = callframe->argument(0);
// Handle Date object
if (auto* dateInstance = jsDynamicCast<DateInstance*>(argument0)) {
if (std::isnormal(dateInstance->internalNumber())) {
globalObject->overridenDateNow = dateInstance->internalNumber();
}
return JSValue::encode(callframe->thisValue());
}
// number > 0 is a valid date otherwise it's invalid and we should reset the time (set to -1)
globalObject->overridenDateNow = (argument0.isNumber() && argument0.asNumber() >= 0) ? argument0.asNumber() : -1;
// Handle string (parse it as a date)
if (argument0.isString()) {
auto& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
// Create a new Date object from the string
JSValue dateConstructor = globalObject->dateConstructor();
MarkedArgumentBuffer args;
args.append(argument0);
JSValue dateValue = construct(globalObject, dateConstructor, dateConstructor, args, "Failed to parse date string"_s);
RETURN_IF_EXCEPTION(scope, {});
if (auto* dateInstance = jsDynamicCast<DateInstance*>(dateValue)) {
double timeValue = dateInstance->internalNumber();
// Only set if it's a valid date
if (std::isnormal(timeValue) || timeValue == 0) {
globalObject->overridenDateNow = timeValue;
}
}
return JSValue::encode(callframe->thisValue());
}
// Handle number (timestamp in milliseconds)
if (argument0.isNumber()) {
double timeValue = argument0.asNumber();
// Accept 0 or any positive number as valid timestamps
if (timeValue >= 0) {
globalObject->overridenDateNow = timeValue;
}
return JSValue::encode(callframe->thisValue());
}
// If no argument or invalid argument, reset to current time (for compatibility)
if (argument0.isUndefinedOrNull()) {
globalObject->overridenDateNow = -1;
}
return JSValue::encode(callframe->thisValue());
}
// Returns the currently mocked system time as a Date object, or null if not mocked
BUN_DEFINE_HOST_FUNCTION(JSMock__jsGetMockedSystemTime, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe))
{
// Check if fake timers are active (overridenDateNow >= 0)
if (globalObject->overridenDateNow >= 0) {
// Return a new Date object with the mocked time
auto& vm = globalObject->vm();
DateInstance* dateInstance = DateInstance::create(vm, globalObject->dateStructure(), globalObject->overridenDateNow);
return JSValue::encode(dateInstance);
}
// Return null if fake timers are not active
return JSValue::encode(jsNull());
}
// Always returns the real system time, regardless of mocked time
BUN_DEFINE_HOST_FUNCTION(JSMock__jsGetRealSystemTime, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe))
{
// Get the real system time by temporarily disabling the override
double originalOverride = globalObject->overridenDateNow;
globalObject->overridenDateNow = -1;
double realTime = globalObject->jsDateNow();
globalObject->overridenDateNow = originalOverride;
return JSValue::encode(jsNumber(realTime));
}
// Returns true if fake timers are currently enabled
BUN_DEFINE_HOST_FUNCTION(JSMock__jsIsFakeTimers, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe))
{
// Fake timers are enabled when overridenDateNow is >= 0
bool isFakeTimersEnabled = globalObject->overridenDateNow >= 0;
return JSValue::encode(jsBoolean(isFakeTimersEnabled));
}
// Export C function for Zig to check if fake timers are enabled
extern "C" bool Bun__Timer__isFakeTimersEnabled(JSC::JSGlobalObject* globalObject)
{
return globalObject->overridenDateNow >= 0;
}
extern "C" void Bun__Timer__advancePerformanceNow(void*, double);
// Export C function for Zig to advance the Date.now() mock time by a delta
extern "C" void Bun__Timer__advanceDateNowMs(JSC::JSGlobalObject* globalObject, double deltaMilliseconds)
{
if (globalObject->overridenDateNow >= 0) {
globalObject->overridenDateNow += deltaMilliseconds;
}
// Also advance performance.now() if fake timers are enabled
auto* bunGlobal = jsCast<Zig::GlobalObject*>(globalObject);
auto* vm = bunGlobal->bunVM();
if (vm) {
Bun__Timer__advancePerformanceNow(vm, deltaMilliseconds);
}
}
// Drains all pending process.nextTick callbacks
// This will also run all microtasks scheduled by themselves
BUN_DEFINE_HOST_FUNCTION(JSMock__jsRunAllTicks, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe))
{
auto* bunGlobal = jsCast<Zig::GlobalObject*>(globalObject);
auto& vm = bunGlobal->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
// Repeatedly drain the nextTick queue and microtasks until both are empty
// This matches Vitest's behavior where runAllTicks runs all microtasks scheduled by themselves
const int maxIterations = 10000; // Safety limit to prevent infinite loops
int iterations = 0;
while (iterations++ < maxIterations) {
bool hadWork = false;
// Drain nextTick queue if it has callbacks
if (auto nextTickQueue = bunGlobal->m_nextTickQueue.get()) {
Bun::JSNextTickQueue* queue = jsCast<Bun::JSNextTickQueue*>(nextTickQueue);
if (!queue->isEmpty()) {
queue->drain(vm, bunGlobal);
RETURN_IF_EXCEPTION(scope, {});
hadWork = true;
}
}
// If no work was done, we're done
if (!hadWork) {
break;
}
}
return JSValue::encode(callframe->thisValue());
}

View File

@@ -663,6 +663,7 @@ ZIG_DECL JSC::EncodedJSValue Bun__Timer__setInterval(JSC::JSGlobalObject* global
ZIG_DECL JSC::EncodedJSValue Bun__Timer__setTimeout(JSC::JSGlobalObject* globalThis, JSC::EncodedJSValue callback, JSC::EncodedJSValue arguments, JSC::EncodedJSValue countdown);
ZIG_DECL JSC::EncodedJSValue Bun__Timer__sleep(JSC::JSGlobalObject* globalThis, JSC::EncodedJSValue promise, JSC::EncodedJSValue countdown);
ZIG_DECL JSC::EncodedJSValue Bun__Timer__setImmediate(JSC::JSGlobalObject* globalThis, JSC::EncodedJSValue callback, JSC::EncodedJSValue arguments);
ZIG_DECL bool Bun__Timer__isFakeTimersEnabled(JSC::JSGlobalObject* globalThis);
#endif

View File

@@ -245,7 +245,7 @@ pub const Jest = struct {
Expect.js.getConstructor(globalObject),
);
const vi = JSValue.createEmptyObject(globalObject, 8);
const vi = JSValue.createEmptyObject(globalObject, 13);
vi.put(globalObject, ZigString.static("fn"), mockFn);
vi.put(globalObject, ZigString.static("mock"), mockModuleFn);
vi.put(globalObject, ZigString.static("spyOn"), spyOn);
@@ -254,6 +254,14 @@ pub const Jest = struct {
vi.put(globalObject, ZigString.static("clearAllMocks"), clearAllMocks);
vi.put(globalObject, ZigString.static("useFakeTimers"), useFakeTimers);
vi.put(globalObject, ZigString.static("useRealTimers"), useRealTimers);
vi.put(globalObject, ZigString.static("setSystemTime"), setSystemTime);
vi.put(globalObject, ZigString.static("getMockedSystemTime"), jsc.host_fn.NewFunction(globalObject, ZigString.static("getMockedSystemTime"), 0, JSMock__jsGetMockedSystemTime, false));
vi.put(globalObject, ZigString.static("getRealSystemTime"), jsc.host_fn.NewFunction(globalObject, ZigString.static("getRealSystemTime"), 0, JSMock__jsGetRealSystemTime, false));
vi.put(globalObject, ZigString.static("isFakeTimers"), jsc.host_fn.NewFunction(globalObject, ZigString.static("isFakeTimers"), 0, JSMock__jsIsFakeTimers, false));
vi.put(globalObject, ZigString.static("runAllTimers"), jsc.host_fn.NewFunction(globalObject, ZigString.static("runAllTimers"), 0, Bun__Timer__runAllTimers, false));
vi.put(globalObject, ZigString.static("runOnlyPendingTimers"), jsc.host_fn.NewFunction(globalObject, ZigString.static("runOnlyPendingTimers"), 0, Bun__Timer__runOnlyPendingTimers, false));
vi.put(globalObject, ZigString.static("runAllTicks"), jsc.host_fn.NewFunction(globalObject, ZigString.static("runAllTicks"), 0, JSMock__jsRunAllTicks, false));
vi.put(globalObject, ZigString.static("advanceTimersToNextTimer"), jsc.host_fn.NewFunction(globalObject, ZigString.static("advanceTimersToNextTimer"), 0, Bun__Timer__advanceTimersToNextTimer, false));
module.put(globalObject, ZigString.static("vi"), vi);
}
@@ -262,11 +270,18 @@ pub const Jest = struct {
extern fn JSMock__jsModuleMock(*JSGlobalObject, *CallFrame) callconv(jsc.conv) JSValue;
extern fn JSMock__jsNow(*JSGlobalObject, *CallFrame) callconv(jsc.conv) JSValue;
extern fn JSMock__jsSetSystemTime(*JSGlobalObject, *CallFrame) callconv(jsc.conv) JSValue;
extern fn JSMock__jsGetMockedSystemTime(*JSGlobalObject, *CallFrame) callconv(jsc.conv) JSValue;
extern fn JSMock__jsGetRealSystemTime(*JSGlobalObject, *CallFrame) callconv(jsc.conv) JSValue;
extern fn JSMock__jsIsFakeTimers(*JSGlobalObject, *CallFrame) callconv(jsc.conv) JSValue;
extern fn JSMock__jsRunAllTicks(*JSGlobalObject, *CallFrame) callconv(jsc.conv) JSValue;
extern fn JSMock__jsRestoreAllMocks(*JSGlobalObject, *CallFrame) callconv(jsc.conv) JSValue;
extern fn JSMock__jsClearAllMocks(*JSGlobalObject, *CallFrame) callconv(jsc.conv) JSValue;
extern fn JSMock__jsSpyOn(*JSGlobalObject, *CallFrame) callconv(jsc.conv) JSValue;
extern fn JSMock__jsUseFakeTimers(*JSGlobalObject, *CallFrame) callconv(jsc.conv) JSValue;
extern fn JSMock__jsUseRealTimers(*JSGlobalObject, *CallFrame) callconv(jsc.conv) JSValue;
extern fn Bun__Timer__runAllTimers(*JSGlobalObject, *CallFrame) callconv(jsc.conv) JSValue;
extern fn Bun__Timer__runOnlyPendingTimers(*JSGlobalObject, *CallFrame) callconv(jsc.conv) JSValue;
extern fn Bun__Timer__advanceTimersToNextTimer(*JSGlobalObject, *CallFrame) callconv(jsc.conv) JSValue;
pub fn call(
globalObject: *JSGlobalObject,

View File

@@ -18,6 +18,10 @@ pub export fn Bun__drainMicrotasks() void {
}
export fn Bun__readOriginTimer(vm: *jsc.VirtualMachine) u64 {
// If fake timers are enabled, return the frozen value converted to nanoseconds
if (vm.overriden_performance_now_ms >= 0) {
return @intFromFloat(vm.overriden_performance_now_ms * 1_000_000.0);
}
return vm.origin_timer.read();
}
@@ -26,6 +30,20 @@ export fn Bun__readOriginTimerStart(vm: *jsc.VirtualMachine) f64 {
return @as(f64, @floatCast((@as(f64, @floatFromInt(vm.origin_timestamp)) + jsc.VirtualMachine.origin_relative_epoch) / 1_000_000.0));
}
export fn Bun__Timer__getPerformanceNow(vm: *jsc.VirtualMachine) f64 {
return vm.overriden_performance_now_ms;
}
export fn Bun__Timer__setPerformanceNow(vm: *jsc.VirtualMachine, value: f64) void {
vm.overriden_performance_now_ms = value;
}
export fn Bun__Timer__advancePerformanceNow(vm: *jsc.VirtualMachine, delta: f64) void {
if (vm.overriden_performance_now_ms >= 0) {
vm.overriden_performance_now_ms += delta;
}
}
pub export fn Bun__GlobalObject__connectedIPC(global: *JSGlobalObject) bool {
if (global.bunVM().ipc) |ipc| {
if (ipc == .initialized) {

View File

@@ -0,0 +1,152 @@
import { describe, expect, test, vi } from "bun:test";
describe("fake timers advance Date.now()", () => {
test("vi.runAllTimers advances Date.now()", () => {
vi.useFakeTimers();
const initialTime = Date.now();
const times: number[] = [];
setTimeout(() => {
times.push(Date.now());
}, 1000);
setTimeout(() => {
times.push(Date.now());
}, 2000);
setTimeout(() => {
times.push(Date.now());
}, 3000);
vi.runAllTimers();
// Each timer should see Date.now() advanced to its scheduled time
expect(times[0]).toBe(initialTime + 1000);
expect(times[1]).toBe(initialTime + 2000);
expect(times[2]).toBe(initialTime + 3000);
vi.useRealTimers();
});
test("vi.advanceTimersToNextTimer advances Date.now()", () => {
vi.useFakeTimers();
const initialTime = Date.now();
const times: number[] = [];
setTimeout(() => {
times.push(Date.now());
}, 1000);
setTimeout(() => {
times.push(Date.now());
}, 5000);
// Advance to first timer
vi.advanceTimersToNextTimer();
expect(times[0]).toBe(initialTime + 1000);
// Advance to second timer
vi.advanceTimersToNextTimer();
expect(times[1]).toBe(initialTime + 5000);
vi.useRealTimers();
});
test("vi.runOnlyPendingTimers advances Date.now()", () => {
vi.useFakeTimers();
const initialTime = Date.now();
const times: number[] = [];
let nestedCalled = false;
setTimeout(() => {
times.push(Date.now());
// Schedule a nested timer - should not run in runOnlyPendingTimers
setTimeout(() => {
nestedCalled = true;
}, 1000);
}, 1000);
setTimeout(() => {
times.push(Date.now());
}, 2000);
vi.runOnlyPendingTimers();
// Both pending timers should have run
expect(times[0]).toBe(initialTime + 1000);
expect(times[1]).toBe(initialTime + 2000);
// Nested timer should not have run yet
expect(nestedCalled).toBe(false);
vi.useRealTimers();
});
test("Date.now() stays consistent within same timer", () => {
vi.useFakeTimers();
const initialTime = Date.now();
let time1: number;
let time2: number;
setTimeout(() => {
time1 = Date.now();
// Multiple calls within same callback should return same time
time2 = Date.now();
}, 1000);
vi.runAllTimers();
expect(time1).toBe(initialTime + 1000);
expect(time2).toBe(initialTime + 1000);
vi.useRealTimers();
});
test("setInterval advances Date.now() each iteration", () => {
vi.useFakeTimers();
const initialTime = Date.now();
const times: number[] = [];
let count = 0;
const intervalId = setInterval(() => {
times.push(Date.now());
count++;
if (count >= 3) {
clearInterval(intervalId);
}
}, 1000);
vi.runAllTimers();
expect(times[0]).toBe(initialTime + 1000);
expect(times[1]).toBe(initialTime + 2000);
expect(times[2]).toBe(initialTime + 3000);
vi.useRealTimers();
});
test("Date.now() reflects current virtual time", () => {
vi.useFakeTimers();
const initialTime = Date.now();
// First check that Date.now() doesn't advance without running timers
setTimeout(() => {}, 1000);
expect(Date.now()).toBe(initialTime);
// Now advance timers
vi.advanceTimersToNextTimer();
// Date.now() should now reflect the advanced time
expect(Date.now()).toBe(initialTime + 1000);
vi.useRealTimers();
});
});

View File

@@ -0,0 +1,132 @@
import { afterAll, beforeAll, expect, test, vi } from "vitest";
beforeAll(() => vi.useFakeTimers());
afterAll(() => vi.useRealTimers());
class TimeHelper {
base: number = 0;
order: string[] = [];
orderNum: number[] = [];
time(d: number, l: string, cb: () => void) {
const start = this.base;
setTimeout(() => {
this.addOrder(start + d, l);
if (this.base != 0) throw new Error("base is not 0");
this.base = start + d;
cb();
this.base = 0;
}, d);
}
addOrder(d: number, l: string) {
this.orderNum.push(d);
this.order.push(`${d}${l ? ` (${l})` : ""}`);
}
expectOrder() {
expect(this.orderNum).toEqual(this.orderNum.toSorted((a, b) => a - b));
const order = this.order;
this.order = [];
return expect(order);
}
}
test.each(["runAllTimers", "advanceTimersToNextTimer"])("%s runs in order of time", mode => {
const tester = new TimeHelper();
const time = tester.time.bind(tester);
time(1000, "", () => {
time(500, "", () => {});
time(1500, "", () => {});
});
time(500, "", () => {
time(600, "", () => {});
time(0, "500 + 0", () => {});
});
time(2000, "", () => {});
time(0, "zero 1", () => {
time(0, "zero 1.1", () => {});
time(0, "zero 1.2", () => {});
});
time(0, "zero 2", () => {
time(0, "zero 2.1", () => {});
time(0, "zero 2.2", () => {});
});
let intervalCount = 0;
const interval = setInterval(() => {
if (intervalCount > 3) clearInterval(interval);
intervalCount += 1;
tester.addOrder(intervalCount * 499, "interval");
setTimeout(() => {
tester.addOrder(intervalCount * 499 + 25, "interval + 25");
}, 25);
}, 499);
if (mode === "runAllTimers") {
vi.runAllTimers();
} else if (mode === "advanceTimersToNextTimer") {
let orderLen = 0;
while (true) {
vi.advanceTimersToNextTimer();
if (tester.order.length > orderLen) {
expect(tester.order.length).toBe(orderLen + 1);
orderLen = tester.order.length;
} else if (tester.order.length === orderLen) {
break;
} else {
expect.fail();
}
}
}
tester.expectOrder().toMatchInlineSnapshot(`
[
"0 (zero 1)",
"0 (zero 2)",
"0 (zero 1.1)",
"0 (zero 1.2)",
"0 (zero 2.1)",
"0 (zero 2.2)",
"499 (interval)",
"500",
"500 (500 + 0)",
"524 (interval + 25)",
"998 (interval)",
"1000",
"1023 (interval + 25)",
"1100",
"1497 (interval)",
"1500",
"1522 (interval + 25)",
"1996 (interval)",
"2000",
"2021 (interval + 25)",
"2495 (interval)",
"2500",
"2520 (interval + 25)",
]
`);
});
test("runAllTimers supports interval", () => {
let ticks = 0;
const interval = setInterval(() => {
ticks += 1;
if (ticks >= 10) clearInterval(interval);
}, 25);
vi.runAllTimers();
expect(ticks).toBe(10);
});
test("fake timers clear after useRealTimers", () => {
let ticks = 0;
setTimeout(() => {
ticks += 1;
}, 10);
expect(ticks).toBe(0);
vi.useRealTimers();
expect(ticks).toBe(0);
vi.useFakeTimers();
vi.runAllTimers();
expect(ticks).toBe(0);
// TODO: check for memory leak of the callbacks
});

View File

@@ -0,0 +1,204 @@
import { describe, expect, test, vi } from "bun:test";
describe("fake timers advance performance.now()", () => {
test("vi.runAllTimers advances performance.now()", () => {
vi.useFakeTimers();
const initialTime = performance.now();
const times: number[] = [];
setTimeout(() => {
times.push(performance.now());
setTimeout(() => {
times.push(performance.now());
}, 3000);
}, 1000);
setTimeout(() => {
times.push(performance.now());
}, 2000);
setTimeout(() => {
times.push(performance.now());
}, 3000);
vi.runAllTimers();
// Each timer should see performance.now() advanced to its scheduled time
expect(times.map(t => t - initialTime)).toEqual([1000, 2000, 3000, 4000]);
vi.useRealTimers();
});
test("vi.advanceTimersToNextTimer advances performance.now()", () => {
vi.useFakeTimers();
const initialTime = performance.now();
const times: number[] = [];
setTimeout(() => {
times.push(performance.now());
}, 1000);
setTimeout(() => {
times.push(performance.now());
}, 5000);
// Advance to first timer
vi.advanceTimersToNextTimer();
expect(times[0]).toBeCloseTo(initialTime + 1000, 5);
// Advance to second timer
vi.advanceTimersToNextTimer();
expect(times[1]).toBeCloseTo(initialTime + 5000, 5);
vi.useRealTimers();
});
test("vi.runOnlyPendingTimers advances performance.now()", () => {
vi.useFakeTimers();
const initialTime = performance.now();
const times: number[] = [];
let nestedCalled = false;
setTimeout(() => {
times.push(performance.now());
// Schedule a nested timer - should not run in runOnlyPendingTimers
setTimeout(() => {
nestedCalled = true;
}, 1000);
}, 1000);
setTimeout(() => {
times.push(performance.now());
}, 2000);
vi.runOnlyPendingTimers();
// Both pending timers should have run
expect(times[0]).toBeCloseTo(initialTime + 1000, 5);
expect(times[1]).toBeCloseTo(initialTime + 2000, 5);
// Nested timer should not have run yet
expect(nestedCalled).toBe(false);
vi.useRealTimers();
});
test("performance.now() stays consistent within same timer", () => {
vi.useFakeTimers();
const initialTime = performance.now();
let time1: number;
let time2: number;
setTimeout(() => {
time1 = performance.now();
// Multiple calls within same callback should return same time
time2 = performance.now();
}, 1000);
vi.runAllTimers();
expect(time1).toBeCloseTo(initialTime + 1000, 5);
expect(time2).toBeCloseTo(initialTime + 1000, 5);
vi.useRealTimers();
});
test("setInterval advances performance.now() each iteration", () => {
vi.useFakeTimers();
const initialTime = performance.now();
const times: number[] = [];
let count = 0;
const intervalId = setInterval(() => {
times.push(performance.now());
count++;
if (count >= 3) {
clearInterval(intervalId);
}
}, 1000);
vi.runAllTimers();
// Use toBeCloseTo for floating point comparisons to avoid precision issues
expect(times[0]).toBeCloseTo(initialTime + 1000, 5);
expect(times[1]).toBeCloseTo(initialTime + 2000, 5);
expect(times[2]).toBeCloseTo(initialTime + 3000, 5);
vi.useRealTimers();
});
test("performance.now() reflects current virtual time", () => {
vi.useFakeTimers();
const initialTime = performance.now();
// First check that performance.now() doesn't advance without running timers
setTimeout(() => {}, 1000);
expect(performance.now()).toBeCloseTo(initialTime, 5);
// Now advance timers
vi.advanceTimersToNextTimer();
// performance.now() should now reflect the advanced time
expect(performance.now()).toBeCloseTo(initialTime + 1000, 5);
vi.useRealTimers();
});
test("both Date.now() and performance.now() advance together", () => {
vi.useFakeTimers();
const initialDate = Date.now();
const initialPerf = performance.now();
const dateTime: number[] = [];
const perfTime: number[] = [];
setTimeout(() => {
dateTime.push(Date.now());
perfTime.push(performance.now());
}, 1000);
setTimeout(() => {
dateTime.push(Date.now());
perfTime.push(performance.now());
}, 2500);
vi.runAllTimers();
// Both should advance by the same amount
expect(dateTime[0]).toBe(initialDate + 1000);
expect(perfTime[0]).toBeCloseTo(initialPerf + 1000, 5);
expect(dateTime[1]).toBe(initialDate + 2500);
expect(perfTime[1]).toBeCloseTo(initialPerf + 2500, 5);
vi.useRealTimers();
});
test("performance.now() returns to real time after useRealTimers", () => {
vi.useFakeTimers();
// Advance time with fake timers
setTimeout(() => {}, 5000);
vi.runAllTimers();
const fakeTime = performance.now();
vi.useRealTimers();
// After switching back to real timers, performance.now() should return to real time
const realTime = performance.now();
// Real time should be much less than fake time (we advanced 5 seconds)
// This checks that we're not still using the fake offset
expect(realTime).toBeLessThan(fakeTime);
});
});

View File

@@ -0,0 +1,240 @@
import { afterAll, beforeAll, describe, expect, test, vi } from "vitest";
beforeAll(() => vi.useFakeTimers());
afterAll(() => vi.useRealTimers());
describe("vi.runOnlyPendingTimers", () => {
test("runs only timers that were pending when called", () => {
let i = 0;
setInterval(() => {
i++;
}, 50);
vi.runOnlyPendingTimers();
// Should have run exactly once, not the timer that was scheduled during execution
expect(i).toBe(1);
});
test("does not run timers created during execution", () => {
const order: number[] = [];
setTimeout(() => {
order.push(1);
// This timer should NOT run in the same runOnlyPendingTimers call
setTimeout(() => {
order.push(2);
}, 100);
}, 100);
vi.runOnlyPendingTimers();
expect(order).toEqual([1]);
// Now run again to execute the timer that was scheduled during the first execution
vi.runOnlyPendingTimers();
expect(order).toEqual([1, 2]);
});
test("runs multiple pending timers", () => {
const order: number[] = [];
setTimeout(() => order.push(1), 100);
setTimeout(() => order.push(2), 200);
setTimeout(() => order.push(3), 300);
vi.runOnlyPendingTimers();
expect(order).toEqual([1, 2, 3]);
});
test("handles nested setTimeout calls correctly", () => {
const order: string[] = [];
setTimeout(() => {
order.push("outer1");
setTimeout(() => {
order.push("inner1");
}, 50);
}, 100);
setTimeout(() => {
order.push("outer2");
setTimeout(() => {
order.push("inner2");
}, 50);
}, 200);
// First call: runs outer1 and outer2, but not inner1 and inner2
vi.runOnlyPendingTimers();
expect(order).toEqual(["outer1", "outer2"]);
// Second call: runs inner1 and inner2
vi.runOnlyPendingTimers();
expect(order).toEqual(["outer1", "outer2", "inner1", "inner2"]);
// Third call: no more timers
vi.runOnlyPendingTimers();
expect(order).toEqual(["outer1", "outer2", "inner1", "inner2"]);
});
test("handles setInterval correctly", () => {
let count = 0;
const interval = setInterval(() => {
count++;
if (count >= 3) clearInterval(interval);
}, 50);
// First call: runs once
vi.runOnlyPendingTimers();
expect(count).toBe(1);
// Second call: runs the rescheduled interval
vi.runOnlyPendingTimers();
expect(count).toBe(2);
// Third call: runs again and clears
vi.runOnlyPendingTimers();
expect(count).toBe(3);
// Fourth call: no more timers
vi.runOnlyPendingTimers();
expect(count).toBe(3);
});
test("example from vitest documentation", () => {
let i = 0;
setInterval(() => {
console.log(++i);
}, 50);
vi.runOnlyPendingTimers();
// log: 1
expect(i).toBe(1);
});
test("handles multiple intervals", () => {
const order: string[] = [];
const interval1 = setInterval(() => {
order.push("interval1");
}, 50);
const interval2 = setInterval(() => {
order.push("interval2");
}, 100);
// First call: both intervals fire once
vi.runOnlyPendingTimers();
expect(order).toEqual(["interval1", "interval2"]);
// Second call: both intervals fire again (they were rescheduled)
vi.runOnlyPendingTimers();
expect(order).toEqual(["interval1", "interval2", "interval1", "interval2"]);
clearInterval(interval1);
clearInterval(interval2);
});
test("handles mixed setTimeout and setInterval", () => {
const order: string[] = [];
setTimeout(() => order.push("timeout"), 100);
const interval = setInterval(() => {
order.push("interval");
}, 50);
// First call: both fire
vi.runOnlyPendingTimers();
expect(order).toEqual(["interval", "timeout"]);
// Second call: only interval fires (timeout doesn't repeat)
vi.runOnlyPendingTimers();
expect(order).toEqual(["interval", "timeout", "interval"]);
clearInterval(interval);
});
test("handles timers with same delay", () => {
const order: number[] = [];
setTimeout(() => order.push(1), 100);
setTimeout(() => order.push(2), 100);
setTimeout(() => order.push(3), 100);
vi.runOnlyPendingTimers();
expect(order).toEqual([1, 2, 3]);
});
test("does not run timers with zero delay scheduled during execution", () => {
const order: string[] = [];
setTimeout(() => {
order.push("outer");
setTimeout(() => {
order.push("inner");
}, 0);
}, 0);
vi.runOnlyPendingTimers();
expect(order).toEqual(["outer"]);
vi.runOnlyPendingTimers();
expect(order).toEqual(["outer", "inner"]);
});
test("works with order.test.ts TimeHelper pattern", () => {
class TimeHelper {
base: number = 0;
order: string[] = [];
orderNum: number[] = [];
time(d: number, l: string, cb: () => void) {
const start = this.base;
setTimeout(() => {
this.addOrder(start + d, l);
if (this.base != 0) throw new Error("base is not 0");
this.base = start + d;
cb();
this.base = 0;
}, d);
}
addOrder(d: number, l: string) {
this.orderNum.push(d);
this.order.push(`${d}${l ? ` (${l})` : ""}`);
}
expectOrder() {
expect(this.orderNum).toEqual(this.orderNum.toSorted((a, b) => a - b));
const order = this.order;
this.order = [];
return expect(order);
}
}
const tester = new TimeHelper();
const time = tester.time.bind(tester);
time(100, "first", () => {
time(50, "nested1", () => {});
});
time(200, "second", () => {
time(100, "nested2", () => {});
});
// First call: runs "first" and "second"
vi.runOnlyPendingTimers();
expect(tester.order).toEqual(["100 (first)", "200 (second)"]);
tester.order = []; // Clear for next assertion
// Second call: runs "nested1" and "nested2"
vi.runOnlyPendingTimers();
expect(tester.order).toEqual(["150 (nested1)", "300 (nested2)"]);
tester.order = []; // Clear for next assertion
// Third call: no more timers
vi.runOnlyPendingTimers();
expect(tester.order).toEqual([]);
});
});

View File

@@ -0,0 +1,205 @@
import { expect, test, vi } from "bun:test";
test("vi.advanceTimersToNextTimer() advances to next timer", () => {
vi.useFakeTimers();
let i = 0;
setInterval(() => i++, 50);
expect(i).toBe(0);
vi.advanceTimersToNextTimer();
expect(i).toBe(1);
vi.advanceTimersToNextTimer();
expect(i).toBe(2);
vi.advanceTimersToNextTimer();
expect(i).toBe(3);
vi.useRealTimers();
});
test("vi.advanceTimersToNextTimer() is chainable", () => {
vi.useFakeTimers();
let i = 0;
setInterval(() => i++, 50);
expect(i).toBe(0);
vi.advanceTimersToNextTimer().advanceTimersToNextTimer().advanceTimersToNextTimer();
expect(i).toBe(3);
vi.useRealTimers();
});
test("vi.advanceTimersToNextTimer() handles multiple timers with different delays", () => {
vi.useFakeTimers();
const order: number[] = [];
setTimeout(() => order.push(1), 100);
setTimeout(() => order.push(2), 50);
setTimeout(() => order.push(3), 150);
expect(order).toEqual([]);
// Should fire in order of scheduled time, not order they were created
vi.advanceTimersToNextTimer(); // fires timer at 50ms
expect(order).toEqual([2]);
vi.advanceTimersToNextTimer(); // fires timer at 100ms
expect(order).toEqual([2, 1]);
vi.advanceTimersToNextTimer(); // fires timer at 150ms
expect(order).toEqual([2, 1, 3]);
vi.useRealTimers();
});
test("vi.advanceTimersToNextTimer() handles nested timers", () => {
vi.useFakeTimers();
const order: number[] = [];
setTimeout(() => {
order.push(1);
setTimeout(() => order.push(3), 50);
}, 100);
setTimeout(() => order.push(2), 150);
vi.advanceTimersToNextTimer(); // fires timer at 100ms, schedules new timer at current_time + 50ms
expect(order).toEqual([1]);
// When two timers are at the same time (150ms), they fire in the order they were scheduled (FIFO)
// Timer 2 was scheduled first (at time 0), so it fires before timer 3 (scheduled during callback at time 100)
vi.advanceTimersToNextTimer(); // fires timer at 150ms (timer 2, scheduled first)
expect(order).toEqual([1, 2]);
vi.advanceTimersToNextTimer(); // fires nested timer at 150ms (timer 3, scheduled during callback)
expect(order).toEqual([1, 2, 3]);
vi.useRealTimers();
});
test("vi.advanceTimersToNextTimer() does nothing when no timers are pending", () => {
vi.useFakeTimers();
let called = false;
setTimeout(() => {
called = true;
}, 100);
vi.advanceTimersToNextTimer();
expect(called).toBe(true);
// No more timers, this should be safe to call
vi.advanceTimersToNextTimer();
vi.advanceTimersToNextTimer();
vi.useRealTimers();
});
test("vi.advanceTimersToNextTimer() works with setInterval", () => {
vi.useFakeTimers();
let count = 0;
const intervalId = setInterval(() => {
count++;
if (count >= 3) {
clearInterval(intervalId);
}
}, 100);
expect(count).toBe(0);
vi.advanceTimersToNextTimer();
expect(count).toBe(1);
vi.advanceTimersToNextTimer();
expect(count).toBe(2);
vi.advanceTimersToNextTimer();
expect(count).toBe(3);
// Interval was cleared, no more timers
vi.advanceTimersToNextTimer();
expect(count).toBe(3);
vi.useRealTimers();
});
test("vi.advanceTimersToNextTimer() handles mix of setTimeout and setInterval", () => {
vi.useFakeTimers();
const events: string[] = [];
setTimeout(() => events.push("timeout-1"), 50);
const intervalId = setInterval(() => {
events.push("interval");
if (events.filter(e => e === "interval").length >= 2) {
clearInterval(intervalId);
}
}, 100);
setTimeout(() => events.push("timeout-2"), 200);
vi.advanceTimersToNextTimer(); // 50ms: timeout-1
expect(events).toEqual(["timeout-1"]);
vi.advanceTimersToNextTimer(); // 100ms: interval (1st)
expect(events).toEqual(["timeout-1", "interval"]);
vi.advanceTimersToNextTimer(); // 200ms: interval (2nd) and timeout-2 compete
// The interval at 200ms should fire first (it was scheduled first), then gets cleared
expect(events.length).toBe(3);
expect(events).toContain("interval");
vi.advanceTimersToNextTimer(); // 200ms: timeout-2
expect(events).toContain("timeout-2");
vi.useRealTimers();
});
test("vi.advanceTimersToNextTimer() executes timer callbacks with automatic microtask processing", () => {
vi.useFakeTimers();
const order: string[] = [];
setTimeout(() => {
order.push("timer");
process.nextTick(() => order.push("tick"));
}, 100);
// When a timer callback runs, microtasks (including nextTick) are processed automatically
// This is standard JavaScript execution behavior
vi.advanceTimersToNextTimer();
expect(order).toEqual(["timer", "tick"]);
vi.useRealTimers();
});
test("vi.advanceTimersToNextTimer() returns vi object for chaining with other vi methods", () => {
vi.useFakeTimers();
let timerCalled = false;
let tickCalled = false;
setTimeout(() => {
timerCalled = true;
process.nextTick(() => {
tickCalled = true;
});
}, 100);
// Microtasks run automatically after timer callback, so runAllTicks() is not needed here
// but chaining still works
vi.advanceTimersToNextTimer();
expect(timerCalled).toBe(true);
expect(tickCalled).toBe(true);
vi.useRealTimers();
});

View File

@@ -0,0 +1,145 @@
import { test, expect, vi } from "bun:test";
test("vi.isFakeTimers() returns false by default", () => {
// Ensure we start with real timers
vi.useRealTimers();
expect(vi.isFakeTimers()).toBe(false);
});
test("vi.isFakeTimers() returns true when fake timers are enabled", () => {
vi.useFakeTimers();
expect(vi.isFakeTimers()).toBe(true);
vi.useRealTimers();
});
test("vi.isFakeTimers() returns false after useRealTimers", () => {
vi.useFakeTimers();
expect(vi.isFakeTimers()).toBe(true);
vi.useRealTimers();
expect(vi.isFakeTimers()).toBe(false);
});
test("vi.isFakeTimers() state persists across multiple calls", () => {
// Start with real timers
expect(vi.isFakeTimers()).toBe(false);
expect(vi.isFakeTimers()).toBe(false); // Should be consistent
// Enable fake timers
vi.useFakeTimers();
expect(vi.isFakeTimers()).toBe(true);
expect(vi.isFakeTimers()).toBe(true); // Should be consistent
// Set a system time (should still be fake)
vi.setSystemTime(1234567890000);
expect(vi.isFakeTimers()).toBe(true);
// Back to real timers
vi.useRealTimers();
expect(vi.isFakeTimers()).toBe(false);
expect(vi.isFakeTimers()).toBe(false); // Should be consistent
});
test("vi.isFakeTimers() works with getMockedSystemTime", () => {
// When fake timers are off, both should indicate no mocking
vi.useRealTimers();
expect(vi.isFakeTimers()).toBe(false);
expect(vi.getMockedSystemTime()).toBeNull();
// When fake timers are on, both should indicate mocking
vi.useFakeTimers();
expect(vi.isFakeTimers()).toBe(true);
expect(vi.getMockedSystemTime()).not.toBeNull();
vi.useRealTimers();
});
test("vi.isFakeTimers() reflects state after setSystemTime", () => {
vi.useRealTimers();
expect(vi.isFakeTimers()).toBe(false);
// setSystemTime should implicitly enable fake timers
vi.setSystemTime(0);
expect(vi.isFakeTimers()).toBe(true);
vi.useRealTimers();
});
test("multiple transitions between real and fake timers", () => {
const states = [];
// Initial state
states.push(vi.isFakeTimers());
// Enable fake timers
vi.useFakeTimers();
states.push(vi.isFakeTimers());
// Back to real
vi.useRealTimers();
states.push(vi.isFakeTimers());
// Fake again
vi.useFakeTimers();
states.push(vi.isFakeTimers());
// Set system time (should keep fake)
vi.setSystemTime(1000000);
states.push(vi.isFakeTimers());
// Real again
vi.useRealTimers();
states.push(vi.isFakeTimers());
expect(states).toEqual([false, true, false, true, true, false]);
});
test("vi.isFakeTimers() is type-safe", () => {
const result = vi.isFakeTimers();
// TypeScript would catch this, but we can verify at runtime too
expect(typeof result).toBe("boolean");
expect(result === true || result === false).toBe(true);
vi.useRealTimers();
});
test("calling useFakeTimers multiple times keeps fake timers enabled", () => {
vi.useFakeTimers();
expect(vi.isFakeTimers()).toBe(true);
vi.useFakeTimers();
expect(vi.isFakeTimers()).toBe(true);
vi.useFakeTimers();
expect(vi.isFakeTimers()).toBe(true);
vi.useRealTimers();
expect(vi.isFakeTimers()).toBe(false);
});
test("isFakeTimers works correctly with Date behavior", () => {
vi.useRealTimers();
expect(vi.isFakeTimers()).toBe(false);
// Real timers - dates should differ
const date1 = new Date();
const date2 = new Date();
// They might be equal due to timing, but isFakeTimers should be false
expect(vi.isFakeTimers()).toBe(false);
vi.useFakeTimers();
vi.setSystemTime(1234567890000);
expect(vi.isFakeTimers()).toBe(true);
// Fake timers - dates should be equal
const fakeDate1 = new Date();
const fakeDate2 = new Date();
expect(fakeDate1.getTime()).toBe(fakeDate2.getTime());
expect(vi.isFakeTimers()).toBe(true);
vi.useRealTimers();
});

View File

@@ -0,0 +1,127 @@
import { describe, expect, test, vi } from "bun:test";
describe("vi.runAllTicks", () => {
test("runs all pending nextTick callbacks", () => {
const callback1 = vi.fn();
const callback2 = vi.fn();
const callback3 = vi.fn();
process.nextTick(callback1);
process.nextTick(callback2);
process.nextTick(callback3);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
vi.runAllTicks();
expect(callback1).toHaveBeenCalledTimes(1);
expect(callback2).toHaveBeenCalledTimes(1);
expect(callback3).toHaveBeenCalledTimes(1);
});
test("runs nested nextTick callbacks", () => {
const order: number[] = [];
const callback1 = vi.fn(() => {
order.push(1);
process.nextTick(callback3);
});
const callback2 = vi.fn(() => {
order.push(2);
});
const callback3 = vi.fn(() => {
order.push(3);
});
process.nextTick(callback1);
process.nextTick(callback2);
expect(order).toEqual([]);
vi.runAllTicks();
expect(callback1).toHaveBeenCalledTimes(1);
expect(callback2).toHaveBeenCalledTimes(1);
expect(callback3).toHaveBeenCalledTimes(1);
expect(order).toEqual([1, 2, 3]);
});
test("runs recursively scheduled nextTick callbacks", () => {
let count = 0;
const maxCount = 5;
const scheduleNext = vi.fn(() => {
count++;
if (count < maxCount) {
process.nextTick(scheduleNext);
}
});
process.nextTick(scheduleNext);
vi.runAllTicks();
expect(count).toBe(maxCount);
expect(scheduleNext).toHaveBeenCalledTimes(maxCount);
});
test("does nothing when there are no pending nextTick callbacks", () => {
// Should not throw or hang
vi.runAllTicks();
vi.runAllTicks();
});
test("handles errors in nextTick callbacks", () => {
const callback1 = vi.fn(() => {
throw new Error("nextTick error");
});
const callback2 = vi.fn();
process.nextTick(callback1);
process.nextTick(callback2);
// runAllTicks should throw when a callback throws
expect(() => vi.runAllTicks()).toThrow("nextTick error");
expect(callback1).toHaveBeenCalledTimes(1);
});
test("works with async callbacks", async () => {
let resolved = false;
const promise = new Promise<void>(resolve => {
process.nextTick(() => {
resolved = true;
resolve();
});
});
expect(resolved).toBe(false);
vi.runAllTicks();
expect(resolved).toBe(true);
await promise; // Should resolve immediately since nextTick already fired
});
test("runs nextTick callbacks scheduled during microtasks", () => {
const order: string[] = [];
process.nextTick(() => order.push("nextTick1"));
Promise.resolve().then(() => {
order.push("microtask1");
process.nextTick(() => order.push("nextTick2"));
});
vi.runAllTicks();
// nextTick1 runs first, then microtask1, then nextTick2
expect(order).toEqual(["nextTick1", "microtask1", "nextTick2"]);
});
test("returns the vi object for chaining", () => {
const result = vi.runAllTicks();
expect(result).toBe(vi);
});
});

View File

@@ -0,0 +1,271 @@
import { test, expect, vi, describe } from "bun:test";
describe("vi.runAllTimers", () => {
test("runs all pending timers immediately", () => {
vi.useFakeTimers();
const callback1 = vi.fn();
const callback2 = vi.fn();
const callback3 = vi.fn();
setTimeout(callback1, 1000);
setTimeout(callback2, 5000);
setTimeout(callback3, 10000);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
vi.runAllTimers();
expect(callback1).toHaveBeenCalledTimes(1);
expect(callback2).toHaveBeenCalledTimes(1);
expect(callback3).toHaveBeenCalledTimes(1);
vi.useRealTimers();
});
test("runs nested timers", () => {
vi.useFakeTimers();
const order: number[] = [];
const callback1 = vi.fn(() => {
order.push(1);
setTimeout(callback3, 1000);
});
const callback2 = vi.fn(() => {
order.push(2);
});
const callback3 = vi.fn(() => {
order.push(3);
});
setTimeout(callback1, 1000);
setTimeout(callback2, 2000);
expect(order).toEqual([]);
vi.runAllTimers();
expect(callback1).toHaveBeenCalledTimes(1);
expect(callback2).toHaveBeenCalledTimes(1);
expect(callback3).toHaveBeenCalledTimes(1);
expect(order).toEqual([1, 2, 3]);
vi.useRealTimers();
});
test("handles setInterval correctly", () => {
vi.useFakeTimers();
let count = 0;
const callback = vi.fn(() => {
count++;
if (count >= 3) {
clearInterval(intervalId);
}
});
const intervalId = setInterval(callback, 1000);
expect(callback).not.toHaveBeenCalled();
// runAllTimers should run intervals multiple times until cleared
vi.runAllTimers();
expect(callback).toHaveBeenCalledTimes(3);
expect(count).toBe(3);
vi.useRealTimers();
});
test("handles mix of setTimeout and setInterval", () => {
vi.useFakeTimers();
const timeoutCallback = vi.fn();
const intervalCallback = vi.fn();
let intervalCount = 0;
setTimeout(timeoutCallback, 5000);
const intervalId = setInterval(() => {
intervalCallback();
intervalCount++;
if (intervalCount >= 2) {
clearInterval(intervalId);
}
}, 2000);
vi.runAllTimers();
expect(timeoutCallback).toHaveBeenCalledTimes(1);
expect(intervalCallback).toHaveBeenCalledTimes(2);
vi.useRealTimers();
});
test("does nothing when fake timers are disabled", async () => {
// Don't use fake timers
const callback = vi.fn();
const timeoutId = setTimeout(callback, 10);
// This should do nothing since fake timers are not enabled
vi.runAllTimers();
expect(callback).not.toHaveBeenCalled();
// Wait for the real timer to fire
await new Promise(resolve => setTimeout(resolve, 20));
expect(callback).toHaveBeenCalledTimes(1);
clearTimeout(timeoutId);
});
test("runs timers in order they were scheduled", () => {
vi.useFakeTimers();
const order: string[] = [];
setTimeout(() => order.push("first"), 100);
setTimeout(() => order.push("second"), 200);
setTimeout(() => order.push("third"), 300);
setTimeout(() => order.push("also-100ms"), 100);
vi.runAllTimers();
// Timers with same delay should fire in order they were scheduled
expect(order).toEqual(["first", "also-100ms", "second", "third"]);
vi.useRealTimers();
});
test("handles timer that schedules another timer with same delay", () => {
vi.useFakeTimers();
const order: string[] = [];
setTimeout(() => {
order.push("first");
setTimeout(() => order.push("nested"), 1000);
}, 1000);
setTimeout(() => order.push("second"), 1000);
vi.runAllTimers();
// The nested timer should fire after the second timer
expect(order).toEqual(["first", "second", "nested"]);
vi.useRealTimers();
});
test("handles zero-delay timers", () => {
vi.useFakeTimers();
const callback1 = vi.fn();
const callback2 = vi.fn();
setTimeout(callback1, 0);
setTimeout(callback2, 0);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
vi.runAllTimers();
expect(callback1).toHaveBeenCalledTimes(1);
expect(callback2).toHaveBeenCalledTimes(1);
vi.useRealTimers();
});
test("handles timers that throw errors", () => {
vi.useFakeTimers();
const callback1 = vi.fn(() => {
throw new Error("Timer error");
});
const callback2 = vi.fn();
setTimeout(callback1, 1000);
setTimeout(callback2, 2000);
// Even if one timer throws, others should still run
expect(() => vi.runAllTimers()).toThrow("Timer error");
expect(callback1).toHaveBeenCalledTimes(1);
// The second callback may or may not run depending on error handling
// This behavior might need to be verified
vi.useRealTimers();
});
test("works with promises and async callbacks", async () => {
vi.useFakeTimers();
let resolved = false;
const promise = new Promise<void>(resolve => {
setTimeout(() => {
resolved = true;
resolve();
}, 1000);
});
expect(resolved).toBe(false);
vi.runAllTimers();
expect(resolved).toBe(true);
await promise; // Should resolve immediately since timer already fired
vi.useRealTimers();
});
test("handles recursive timer scheduling", () => {
vi.useFakeTimers();
let count = 0;
const maxCount = 5;
const scheduleNext = () => {
count++;
if (count < maxCount) {
setTimeout(scheduleNext, 1000);
}
};
setTimeout(scheduleNext, 1000);
vi.runAllTimers();
expect(count).toBe(maxCount);
vi.useRealTimers();
});
test("clears timers correctly with clearTimeout during execution", () => {
vi.useFakeTimers();
const callback1 = vi.fn();
const callback2 = vi.fn();
let timerId2: ReturnType<typeof setTimeout>;
setTimeout(() => {
callback1();
clearTimeout(timerId2);
}, 1000);
timerId2 = setTimeout(callback2, 2000);
vi.runAllTimers();
expect(callback1).toHaveBeenCalledTimes(1);
expect(callback2).not.toHaveBeenCalled();
vi.useRealTimers();
});
// Note: setImmediate uses a different queue system (not the timer heap)
// and would require additional implementation in runAllTimers to support
});

View File

@@ -0,0 +1,166 @@
import { test, expect, vi } from "bun:test";
test("vi.getMockedSystemTime() returns null when timers are not mocked", () => {
// Ensure we start with real timers
vi.useRealTimers();
const mockedTime = vi.getMockedSystemTime();
expect(mockedTime).toBeNull();
});
test("vi.getMockedSystemTime() returns Date when fake timers are active", () => {
vi.useFakeTimers();
const mockedTime = vi.getMockedSystemTime();
expect(mockedTime).toBeInstanceOf(Date);
expect(mockedTime).not.toBeNull();
// The mocked time should be frozen
const time1 = vi.getMockedSystemTime();
const time2 = vi.getMockedSystemTime();
expect(time1.getTime()).toBe(time2.getTime());
vi.useRealTimers();
});
test("vi.getMockedSystemTime() reflects setSystemTime changes", () => {
vi.useFakeTimers();
// Set a specific time
const targetTime = new Date(2024, 5, 15, 12, 0, 0);
vi.setSystemTime(targetTime);
const mockedTime = vi.getMockedSystemTime();
expect(mockedTime).toBeInstanceOf(Date);
expect(mockedTime.getTime()).toBe(targetTime.getTime());
expect(mockedTime.getFullYear()).toBe(2024);
expect(mockedTime.getMonth()).toBe(5);
expect(mockedTime.getDate()).toBe(15);
// Change the time again
vi.setSystemTime(0);
const epochTime = vi.getMockedSystemTime();
expect(epochTime.getTime()).toBe(0);
expect(epochTime.toISOString()).toBe("1970-01-01T00:00:00.000Z");
vi.useRealTimers();
});
test("vi.getRealSystemTime() returns real time regardless of mocking", () => {
// Test without fake timers
const realTime1 = vi.getRealSystemTime();
expect(typeof realTime1).toBe("number");
expect(realTime1).toBeGreaterThan(0);
// Should be close to Date.now()
const now1 = Date.now();
expect(Math.abs(realTime1 - now1)).toBeLessThan(100);
// Enable fake timers and set to past
vi.useFakeTimers();
vi.setSystemTime(1000000000000); // September 9, 2001
// getRealSystemTime should still return current time, not mocked time
const realTime2 = vi.getRealSystemTime();
const now2 = Date.now(); // This will return mocked time (1000000000000)
expect(now2).toBe(1000000000000); // Date.now() is mocked
expect(realTime2).toBeGreaterThan(1700000000000); // Real time should be recent (after 2023)
expect(realTime2).not.toBe(1000000000000); // Should NOT be the mocked time
vi.useRealTimers();
});
test("vi.getRealSystemTime() always returns increasing values", () => {
vi.useFakeTimers();
vi.setSystemTime(0); // Set to epoch
const times = [];
for (let i = 0; i < 5; i++) {
times.push(vi.getRealSystemTime());
}
// Real times should be monotonically increasing (or equal)
for (let i = 1; i < times.length; i++) {
expect(times[i]).toBeGreaterThanOrEqual(times[i - 1]);
}
// All real times should be recent (not epoch)
for (const time of times) {
expect(time).toBeGreaterThan(1700000000000); // After 2023
}
vi.useRealTimers();
});
test("vi.getMockedSystemTime() and new Date() return same time when mocked", () => {
vi.useFakeTimers();
vi.setSystemTime(1234567890000);
const mockedTime = vi.getMockedSystemTime();
const newDate = new Date();
expect(mockedTime.getTime()).toBe(newDate.getTime());
expect(mockedTime.getTime()).toBe(1234567890000);
vi.useRealTimers();
});
test("vi.getRealSystemTime() differs from Date.now() when timers are mocked", () => {
vi.useFakeTimers();
vi.setSystemTime(1000000000000);
const realTime = vi.getRealSystemTime();
const mockedNow = Date.now();
// They should be very different
expect(Math.abs(realTime - mockedNow)).toBeGreaterThan(1000000000); // More than ~31 years difference
vi.useRealTimers();
});
test("after useRealTimers, getMockedSystemTime returns null", () => {
vi.useFakeTimers();
vi.setSystemTime(1234567890000);
// Should have a mocked time
expect(vi.getMockedSystemTime()).not.toBeNull();
expect(vi.getMockedSystemTime().getTime()).toBe(1234567890000);
// After restoring real timers
vi.useRealTimers();
// Should return null
expect(vi.getMockedSystemTime()).toBeNull();
});
test("getRealSystemTime works correctly across multiple timer state changes", () => {
const realTimes = [];
// Real timer mode
vi.useRealTimers();
realTimes.push(vi.getRealSystemTime());
// Fake timer mode
vi.useFakeTimers();
vi.setSystemTime(0);
realTimes.push(vi.getRealSystemTime());
// Change mocked time
vi.setSystemTime(9999999999999);
realTimes.push(vi.getRealSystemTime());
// Back to real timer mode
vi.useRealTimers();
realTimes.push(vi.getRealSystemTime());
// All real times should be close to each other (within a second)
const minTime = Math.min(...realTimes);
const maxTime = Math.max(...realTimes);
expect(maxTime - minTime).toBeLessThan(1000);
// All should be recent
for (const time of realTimes) {
expect(time).toBeGreaterThan(1700000000000);
}
});

View File

@@ -0,0 +1,151 @@
import { test, expect, vi } from "bun:test";
test("vi.useFakeTimers() freezes Date", () => {
const beforeFakeTimers = new Date();
vi.useFakeTimers();
// Date should be frozen at the time useFakeTimers was called
const duringFakeTimers1 = new Date();
const duringFakeTimers2 = new Date();
// Both dates should be equal since time is frozen
expect(duringFakeTimers1.getTime()).toBe(duringFakeTimers2.getTime());
// Should be close to when we enabled fake timers (within a few ms)
expect(Math.abs(duringFakeTimers1.getTime() - beforeFakeTimers.getTime())).toBeLessThan(100);
vi.useRealTimers();
});
test("vi.setSystemTime() sets a specific date", () => {
vi.useFakeTimers();
// Test with number (timestamp)
const timestamp = 1000000000000; // September 9, 2001
vi.setSystemTime(timestamp);
const date1 = new Date();
expect(date1.getTime()).toBe(timestamp);
// Date.now() should also return the mocked time
expect(Date.now()).toBe(timestamp);
vi.useRealTimers();
});
test("vi.setSystemTime() accepts Date object", () => {
vi.useFakeTimers();
const targetDate = new Date(2023, 0, 1, 12, 0, 0); // January 1, 2023, 12:00:00
vi.setSystemTime(targetDate);
const date = new Date();
expect(date.getTime()).toBe(targetDate.getTime());
expect(date.getFullYear()).toBe(2023);
expect(date.getMonth()).toBe(0);
expect(date.getDate()).toBe(1);
expect(date.getHours()).toBe(12);
vi.useRealTimers();
});
test("vi.setSystemTime() accepts date string", () => {
vi.useFakeTimers();
const dateString = "2024-06-15T10:30:00.000Z";
vi.setSystemTime(dateString);
const date = new Date();
expect(date.toISOString()).toBe(dateString);
vi.useRealTimers();
});
test("vi.useRealTimers() restores real Date behavior", () => {
vi.useFakeTimers();
vi.setSystemTime(1000000000000);
const fakeDate = new Date();
expect(fakeDate.getTime()).toBe(1000000000000);
vi.useRealTimers();
// After restoring, dates should be current
const realDate1 = new Date();
const realDate2 = new Date();
// Real dates should be recent (within the last minute)
const now = Date.now();
expect(Math.abs(realDate1.getTime() - now)).toBeLessThan(1000);
// Two consecutive dates might have slightly different times
expect(realDate2.getTime()).toBeGreaterThanOrEqual(realDate1.getTime());
});
test("vi.setSystemTime() works without calling useFakeTimers first", () => {
// This should implicitly enable fake timers
const timestamp = 1500000000000;
vi.setSystemTime(timestamp);
const date = new Date();
expect(date.getTime()).toBe(timestamp);
vi.useRealTimers();
});
test("Date constructor with arguments still works with fake timers", () => {
vi.useFakeTimers();
vi.setSystemTime(1000000000000);
// Creating a date with specific arguments should still work
const specificDate = new Date(2025, 5, 15, 14, 30, 0);
expect(specificDate.getFullYear()).toBe(2025);
expect(specificDate.getMonth()).toBe(5);
expect(specificDate.getDate()).toBe(15);
// But Date.now() and new Date() should use the mocked time
expect(Date.now()).toBe(1000000000000);
expect(new Date().getTime()).toBe(1000000000000);
vi.useRealTimers();
});
test("vi.setSystemTime(0) sets epoch time", () => {
vi.useFakeTimers();
vi.setSystemTime(0);
const date = new Date();
expect(date.getTime()).toBe(0);
expect(date.toISOString()).toBe("1970-01-01T00:00:00.000Z");
vi.useRealTimers();
});
test("multiple calls to vi.setSystemTime() update the mocked time", () => {
vi.useFakeTimers();
vi.setSystemTime(1000000000000);
expect(new Date().getTime()).toBe(1000000000000);
vi.setSystemTime(2000000000000);
expect(new Date().getTime()).toBe(2000000000000);
vi.setSystemTime(3000000000000);
expect(new Date().getTime()).toBe(3000000000000);
vi.useRealTimers();
});
test("calling vi.useFakeTimers() multiple times preserves the set time", () => {
vi.useFakeTimers();
vi.setSystemTime(1234567890000);
// Call useFakeTimers again
vi.useFakeTimers();
// Time should still be the previously set time
expect(new Date().getTime()).toBe(1234567890000);
vi.useRealTimers();
});

View File

@@ -0,0 +1,68 @@
import { test, expect, vi } from "bun:test";
test("vi.setSystemTime properly handles invalid date strings", () => {
vi.useFakeTimers();
const beforeInvalid = new Date().getTime();
// Invalid date string should not change the time
vi.setSystemTime("invalid-date");
const afterInvalid = new Date().getTime();
// Time should remain unchanged
expect(afterInvalid).toBe(beforeInvalid);
vi.useRealTimers();
});
test("vi.setSystemTime with negative numbers doesn't set time", () => {
vi.useFakeTimers();
vi.setSystemTime(1000000000000);
// Negative numbers should be ignored
vi.setSystemTime(-1000);
expect(new Date().getTime()).toBe(1000000000000);
vi.useRealTimers();
});
test("vi timer methods can be chained", () => {
// Should not throw
const result = vi
.useFakeTimers()
.setSystemTime(1234567890000)
.useRealTimers();
// Result should be the vi object for chaining
expect(result).toBe(vi);
});
test("Date constructor respects fake timer when called without arguments", () => {
vi.useFakeTimers();
vi.setSystemTime(1000000000000);
// These should all give the same mocked time
const date1 = new Date();
const date2 = new Date(Date.now());
const timestamp = Date.now();
expect(date1.getTime()).toBe(1000000000000);
expect(date2.getTime()).toBe(1000000000000);
expect(timestamp).toBe(1000000000000);
vi.useRealTimers();
});
test("vi.setSystemTime accepts undefined/null to reset", () => {
vi.useFakeTimers();
vi.setSystemTime(1000000000000);
expect(new Date().getTime()).toBe(1000000000000);
// Reset with undefined
vi.setSystemTime(undefined);
const afterReset = new Date().getTime();
// Should no longer be the old mocked time
expect(afterReset).not.toBe(1000000000000);
vi.useRealTimers();
});