From d623123313c04056a2145cb2539aa17960b3c4b0 Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Sat, 20 Dec 2025 13:16:52 -0500 Subject: [PATCH] Fix restoration of mock getters When a mock was configured we captured its value and replaced it with the mocked value, then put the value back when the tests were complete. However by capturing and resetting the value, we lost metadata about the property such as its `writable` or `configurable` state. This would break certain types of objects if they were to be used by subsequent tests that had non-standard property definitions. --- test/MockUtils.ts | 75 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 58 insertions(+), 17 deletions(-) diff --git a/test/MockUtils.ts b/test/MockUtils.ts index 66c44d568..4b63a78df 100644 --- a/test/MockUtils.ts +++ b/test/MockUtils.ts @@ -242,15 +242,25 @@ export function mockGlobalObject>( property: K ): MockedObject { let realMock: MockedObject; + let originalDescriptor: PropertyDescriptor | undefined; const originalValue: T[K] = obj[property]; // Create the mock at setup setup(() => { + originalDescriptor = Object.getOwnPropertyDescriptor(obj, property); realMock = mockObject(obj[property]); - Object.defineProperty(obj, property, { value: realMock }); + Object.defineProperty(obj, property, { + value: realMock, + writable: true, + configurable: true, + }); }); - // Restore original value at teardown + // Restore original property descriptor at teardown teardown(() => { - Object.defineProperty(obj, property, { value: originalValue }); + if (originalDescriptor) { + Object.defineProperty(obj, property, originalDescriptor); + } else { + delete (obj as any)[property]; + } }); // Return the proxy to the real mock return new Proxy(originalValue, { @@ -301,32 +311,46 @@ function shallowClone(obj: T): T { */ export function mockGlobalModule(mod: T): MockedObject { let realMock: MockedObject; + const originalDescriptors = new Map(); const originalValue: T = shallowClone(mod); // Create the mock at setup setup(() => { realMock = mockObject(mod); for (const property of Object.getOwnPropertyNames(realMock)) { try { + const originalDescriptor = Object.getOwnPropertyDescriptor(mod, property); + if (originalDescriptor) { + originalDescriptors.set(property, originalDescriptor); + } Object.defineProperty(mod, property, { value: (realMock as any)[property], writable: true, + configurable: true, }); } catch { // Some properties of a module just can't be mocked and that's fine } } }); - // Restore original value at teardown + // Restore original property descriptors at teardown teardown(() => { for (const property of Object.getOwnPropertyNames(originalValue)) { try { - Object.defineProperty(mod, property, { - value: (originalValue as any)[property], - }); + const originalDescriptor = originalDescriptors.get(property); + if (originalDescriptor) { + Object.defineProperty(mod, property, originalDescriptor); + } else { + Object.defineProperty(mod, property, { + value: (originalValue as any)[property], + writable: true, + configurable: true, + }); + } } catch { // Some properties of a module just can't be mocked and that's fine } } + originalDescriptors.clear(); }); // Return the proxy to the real mock return new Proxy(originalValue, { @@ -374,15 +398,19 @@ export interface MockedValue { */ export function mockGlobalValue(obj: T, property: K): MockedValue { let setupComplete: boolean = false; - let originalValue: T[K]; - // Grab the original value during setup + let originalDescriptor: PropertyDescriptor | undefined; + // Grab the original property descriptor during setup setup(() => { - originalValue = obj[property]; + originalDescriptor = Object.getOwnPropertyDescriptor(obj, property); setupComplete = true; }); - // Restore the original value on teardown + // Restore the original property descriptor on teardown teardown(() => { - Object.defineProperty(obj, property, { value: originalValue }); + if (originalDescriptor) { + Object.defineProperty(obj, property, originalDescriptor); + } else { + delete (obj as any)[property]; + } setupComplete = false; }); // Return a ValueMock that allows for easy mocking of the value @@ -391,7 +419,11 @@ export function mockGlobalValue(obj: T, property: K): Mock if (!setupComplete) { throw new Error("Mocks cannot be accessed outside of test functions"); } - Object.defineProperty(obj, property, { value: value }); + Object.defineProperty(obj, property, { + value: value, + writable: true, + configurable: true, + }); }, }; } @@ -441,15 +473,24 @@ export function mockGlobalEvent>( property: K ): AsyncEventEmitter> { let eventEmitter: vscode.EventEmitter>; - const originalValue: T[K] = obj[property]; + let originalDescriptor: PropertyDescriptor | undefined; // Create the mock at setup setup(() => { + originalDescriptor = Object.getOwnPropertyDescriptor(obj, property); eventEmitter = new vscode.EventEmitter(); - Object.defineProperty(obj, property, { value: eventEmitter.event }); + Object.defineProperty(obj, property, { + value: eventEmitter.event, + writable: true, + configurable: true, + }); }); - // Restore original value at teardown + // Restore original property descriptor at teardown teardown(() => { - Object.defineProperty(obj, property, { value: originalValue }); + if (originalDescriptor) { + Object.defineProperty(obj, property, originalDescriptor); + } else { + delete (obj as any)[property]; + } }); // Return the proxy to the EventEmitter return new Proxy(new AsyncEventEmitter(), {