Skip to content

Commit

Permalink
fix: [#1377] Makes it possible to spy on Storage.prototype methods
Browse files Browse the repository at this point in the history
  • Loading branch information
capricorn86 committed Apr 6, 2024
1 parent 86c9166 commit debbd3a
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 46 deletions.
43 changes: 0 additions & 43 deletions packages/happy-dom/src/storage/Storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
25 changes: 23 additions & 2 deletions packages/happy-dom/src/storage/StorageFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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];
},
Expand Down
2 changes: 1 addition & 1 deletion packages/happy-dom/src/window/BrowserWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
18 changes: 18 additions & 0 deletions packages/happy-dom/test/storage/Storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
});
12 changes: 12 additions & 0 deletions packages/jest-environment/test/javascript/JavaScript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});

0 comments on commit debbd3a

Please sign in to comment.