From debbd3a54e7896e22c03324dc9ce748b2f9c6e58 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Sat, 6 Apr 2024 14:19:30 +0200 Subject: [PATCH 1/2] fix: [#1377] Makes it possible to spy on Storage.prototype methods --- packages/happy-dom/src/storage/Storage.ts | 43 ------------------- .../happy-dom/src/storage/StorageFactory.ts | 25 ++++++++++- .../happy-dom/src/window/BrowserWindow.ts | 2 +- .../happy-dom/test/storage/Storage.test.ts | 18 ++++++++ .../test/javascript/JavaScript.test.ts | 12 ++++++ 5 files changed, 54 insertions(+), 46 deletions(-) diff --git a/packages/happy-dom/src/storage/Storage.ts b/packages/happy-dom/src/storage/Storage.ts index e4d35aaac..e77bca8a9 100644 --- a/packages/happy-dom/src/storage/Storage.ts +++ b/packages/happy-dom/src/storage/Storage.ts @@ -8,49 +8,6 @@ import * as PropertySymbol from '../PropertySymbol.js'; export default class Storage { public [PropertySymbol.data]: { [key: string]: string } = {}; - /** - * - */ - constructor() { - const descriptors = Object.getOwnPropertyDescriptors(Storage.prototype); - - Object.defineProperty(this, 'length', { - enumerable: false, - configurable: true, - get: descriptors['length'].get.bind(this) - }); - - Object.defineProperty(this, 'key', { - enumerable: false, - configurable: true, - value: descriptors['key'].value.bind(this) - }); - - Object.defineProperty(this, 'setItem', { - enumerable: false, - configurable: true, - value: descriptors['setItem'].value.bind(this) - }); - - Object.defineProperty(this, 'getItem', { - enumerable: false, - configurable: true, - value: descriptors['getItem'].value.bind(this) - }); - - Object.defineProperty(this, 'removeItem', { - enumerable: false, - configurable: true, - value: descriptors['removeItem'].value.bind(this) - }); - - Object.defineProperty(this, 'clear', { - enumerable: false, - configurable: true, - value: descriptors['clear'].value.bind(this) - }); - } - /** * Returns length. * diff --git a/packages/happy-dom/src/storage/StorageFactory.ts b/packages/happy-dom/src/storage/StorageFactory.ts index b7bb4504a..7b94c7121 100644 --- a/packages/happy-dom/src/storage/StorageFactory.ts +++ b/packages/happy-dom/src/storage/StorageFactory.ts @@ -12,11 +12,32 @@ export default class StorageFactory { * Creates a new storage. */ public static createStorage(): Storage { + const boundMethods: { [k: string]: (...arg) => any } = {}; + const boundGetters: { [k: string]: () => any } = {}; + // Documentation for Proxy: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy return new Proxy(new Storage(), { - get(storage: Storage, key: string): string { + get(storage: Storage, key: string): string | number | boolean | Function { if (Storage.prototype.hasOwnProperty(key)) { + if (boundMethods[key] !== undefined) { + return boundMethods[key]; + } + if (boundGetters[key] !== undefined) { + return boundGetters[key](); + } + const descriptor = Object.getOwnPropertyDescriptor(Storage.prototype, key); + if (descriptor.value !== undefined) { + if (typeof descriptor.value === 'function') { + boundMethods[key] = storage[key].bind(storage); + return boundMethods[key]; + } + return descriptor.value; + } + if (descriptor.get) { + boundGetters[key] = descriptor.get.bind(storage); + return boundGetters[key](); + } return storage[key]; } return storage[PropertySymbol.data][key]; @@ -30,7 +51,7 @@ export default class StorageFactory { }, deleteProperty(storage: Storage, key: string): boolean { if (Storage.prototype.hasOwnProperty(key)) { - return false; + return true; } return delete storage[PropertySymbol.data][key]; }, diff --git a/packages/happy-dom/src/window/BrowserWindow.ts b/packages/happy-dom/src/window/BrowserWindow.ts index 14f2e0053..13a0e991e 100644 --- a/packages/happy-dom/src/window/BrowserWindow.ts +++ b/packages/happy-dom/src/window/BrowserWindow.ts @@ -1327,7 +1327,7 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal /** * Binds methods, getters and setters to a scope. * - * Getters and setters need to be bound to show up in Object.getOwnPropertyNames(), which is something Vitest relies on. + * Getters and setters need to be bound to show up in Object.getOwnPropertyNames(), which is something Vitest and GlobalRegistrator relies on. * * @see https://github.com/capricorn86/happy-dom/issues/1339 */ diff --git a/packages/happy-dom/test/storage/Storage.test.ts b/packages/happy-dom/test/storage/Storage.test.ts index cbc5f390a..e4718ce50 100644 --- a/packages/happy-dom/test/storage/Storage.test.ts +++ b/packages/happy-dom/test/storage/Storage.test.ts @@ -147,5 +147,23 @@ describe('Storage', () => { storage.getItem('key1'); expect(spy).toHaveBeenCalled(); }); + + it('Should be able to mock implementation once.', () => { + vi.spyOn(storage, 'getItem').mockImplementationOnce(() => 'mocked'); + expect(storage.getItem('key1')).toBe('mocked'); + expect(storage.getItem('key1')).toBe(null); + + vi.spyOn(storage, 'setItem').mockImplementationOnce(() => { + throw new Error('error'); + }); + + expect(() => storage.setItem('key1', 'value1')).toThrow('error'); + }); + + it('Should be able to spy on prototype methods.', () => { + Storage.prototype.getItem = vi.fn(() => 'mocked'); + + expect(storage.getItem('key1')).toBe('mocked'); + }); }); }); diff --git a/packages/jest-environment/test/javascript/JavaScript.test.ts b/packages/jest-environment/test/javascript/JavaScript.test.ts index 311b56ecf..845fc9a04 100644 --- a/packages/jest-environment/test/javascript/JavaScript.test.ts +++ b/packages/jest-environment/test/javascript/JavaScript.test.ts @@ -146,4 +146,16 @@ describe('JavaScript', () => { removeEventListener('click', eventListener); clearTimeout(setTimeout(eventListener)); }); + + it('Should be able to spy on Window.localStorage methods.', () => { + jest.spyOn(Storage.prototype, 'getItem').mockImplementationOnce(() => 'mocked'); + expect(localStorage.getItem('key1')).toBe('mocked'); + expect(localStorage.getItem('key1')).toBe(null); + + jest.spyOn(Storage.prototype, 'setItem').mockImplementationOnce(() => { + throw new Error('error'); + }); + + expect(() => Storage.prototype.setItem('key1', 'value1')).toThrow('error'); + }); }); From 64e4a37c79696dd3973ddcd68d3eeb13f2e14421 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Sat, 6 Apr 2024 15:04:28 +0200 Subject: [PATCH 2/2] fix: [#1377] Makes it possible to spy on Storage.prototype methods --- packages/happy-dom/src/storage/StorageFactory.ts | 15 ++------------- packages/happy-dom/test/storage/Storage.test.ts | 2 +- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/packages/happy-dom/src/storage/StorageFactory.ts b/packages/happy-dom/src/storage/StorageFactory.ts index 7b94c7121..499977746 100644 --- a/packages/happy-dom/src/storage/StorageFactory.ts +++ b/packages/happy-dom/src/storage/StorageFactory.ts @@ -12,31 +12,20 @@ export default class StorageFactory { * Creates a new storage. */ public static createStorage(): Storage { - const boundMethods: { [k: string]: (...arg) => any } = {}; - const boundGetters: { [k: string]: () => any } = {}; - // Documentation for Proxy: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy return new Proxy(new Storage(), { get(storage: Storage, key: string): string | number | boolean | Function { if (Storage.prototype.hasOwnProperty(key)) { - if (boundMethods[key] !== undefined) { - return boundMethods[key]; - } - if (boundGetters[key] !== undefined) { - return boundGetters[key](); - } const descriptor = Object.getOwnPropertyDescriptor(Storage.prototype, key); if (descriptor.value !== undefined) { if (typeof descriptor.value === 'function') { - boundMethods[key] = storage[key].bind(storage); - return boundMethods[key]; + return storage[key].bind(storage); } return descriptor.value; } if (descriptor.get) { - boundGetters[key] = descriptor.get.bind(storage); - return boundGetters[key](); + return descriptor.get.call(storage); } return storage[key]; } diff --git a/packages/happy-dom/test/storage/Storage.test.ts b/packages/happy-dom/test/storage/Storage.test.ts index e4718ce50..cffa1a6da 100644 --- a/packages/happy-dom/test/storage/Storage.test.ts +++ b/packages/happy-dom/test/storage/Storage.test.ts @@ -161,7 +161,7 @@ describe('Storage', () => { }); it('Should be able to spy on prototype methods.', () => { - Storage.prototype.getItem = vi.fn(() => 'mocked'); + vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => 'mocked'); expect(storage.getItem('key1')).toBe('mocked'); });