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>
This commit is contained in:
pfg
2025-10-03 17:04:55 -07:00
parent dc36d5601c
commit a83d57b1de
5 changed files with 268 additions and 4 deletions

View File

@@ -97,6 +97,7 @@ declare module "bun:test" {
function setTimeout(milliseconds: number): void;
function useFakeTimers(): void;
function useRealTimers(): void;
function setSystemTime(now: string | number | Date): void;
function spyOn<T extends object, K extends keyof T>(
obj: T,
methodOrPropertyValue: K,

View File

@@ -1418,9 +1418,16 @@ 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
// 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();
}
return JSValue::encode(callframe->thisValue());
}
@@ -1438,14 +1445,50 @@ 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());
}

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, 9);
vi.put(globalObject, ZigString.static("fn"), mockFn);
vi.put(globalObject, ZigString.static("mock"), mockModuleFn);
vi.put(globalObject, ZigString.static("spyOn"), spyOn);
@@ -254,6 +254,7 @@ 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);
module.put(globalObject, ZigString.static("vi"), vi);
}

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();
});