Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: [#1377] Makes it possible to spy on Storage.prototype methods #1385

Merged
merged 2 commits into from
Apr 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
14 changes: 12 additions & 2 deletions packages/happy-dom/src/storage/StorageFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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];
},
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.', () => {
vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => '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');
});
});
Loading