mirror of
https://github.com/oven-sh/bun
synced 2026-02-12 03:48:56 +00:00
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:
1
packages/bun-types/test.d.ts
vendored
1
packages/bun-types/test.d.ts
vendored
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
151
test/js/bun/test/vi-timer.test.ts
Normal file
151
test/js/bun/test/vi-timer.test.ts
Normal 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();
|
||||
});
|
||||
68
test/regression/issue/vi-timer-edge-cases.test.ts
Normal file
68
test/regression/issue/vi-timer-edge-cases.test.ts
Normal 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();
|
||||
});
|
||||
Reference in New Issue
Block a user