From a83d57b1de3d419e3c193786d138ac76e92f9922 Mon Sep 17 00:00:00 2001 From: pfg Date: Fri, 3 Oct 2025 17:04:55 -0700 Subject: [PATCH] feat: implement vi.useFakeTimers(), vi.useRealTimers(), and vi.setSystemTime() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- packages/bun-types/test.d.ts | 1 + src/bun.js/bindings/JSMockFunction.cpp | 49 +++++- src/bun.js/test/jest.zig | 3 +- test/js/bun/test/vi-timer.test.ts | 151 ++++++++++++++++++ .../issue/vi-timer-edge-cases.test.ts | 68 ++++++++ 5 files changed, 268 insertions(+), 4 deletions(-) create mode 100644 test/js/bun/test/vi-timer.test.ts create mode 100644 test/regression/issue/vi-timer-edge-cases.test.ts diff --git a/packages/bun-types/test.d.ts b/packages/bun-types/test.d.ts index ca5aa18aea..55cefc6cbc 100644 --- a/packages/bun-types/test.d.ts +++ b/packages/bun-types/test.d.ts @@ -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( obj: T, methodOrPropertyValue: K, diff --git a/src/bun.js/bindings/JSMockFunction.cpp b/src/bun.js/bindings/JSMockFunction.cpp index c705e01bf1..f27cd9bc46 100644 --- a/src/bun.js/bindings/JSMockFunction.cpp +++ b/src/bun.js/bindings/JSMockFunction.cpp @@ -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(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(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()); } diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index 350b75a069..8f034bb372 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -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); } diff --git a/test/js/bun/test/vi-timer.test.ts b/test/js/bun/test/vi-timer.test.ts new file mode 100644 index 0000000000..dc46671a67 --- /dev/null +++ b/test/js/bun/test/vi-timer.test.ts @@ -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(); +}); \ No newline at end of file diff --git a/test/regression/issue/vi-timer-edge-cases.test.ts b/test/regression/issue/vi-timer-edge-cases.test.ts new file mode 100644 index 0000000000..8b69e9eec3 --- /dev/null +++ b/test/regression/issue/vi-timer-edge-cases.test.ts @@ -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(); +}); \ No newline at end of file