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..499977746 100644 --- a/packages/happy-dom/src/storage/StorageFactory.ts +++ b/packages/happy-dom/src/storage/StorageFactory.ts @@ -15,8 +15,18 @@ export default class StorageFactory { // 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)) { + const descriptor = Object.getOwnPropertyDescriptor(Storage.prototype, key); + if (descriptor.value !== undefined) { + if (typeof descriptor.value === 'function') { + return storage[key].bind(storage); + } + return descriptor.value; + } + if (descriptor.get) { + return descriptor.get.call(storage); + } return storage[key]; } return storage[PropertySymbol.data][key]; @@ -30,7 +40,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..cffa1a6da 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.', () => { + vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => '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'); + }); });