/** * @license * Copyright Google Inc. All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ /** An object with helpers for mocking/spying on an object's property. */ export interface IPropertySpyHelpers { /** * A `jasmine.Spy` for `get` operations on the property (i.e. reading the current property value). * (This is useful in case one needs to make assertions against property reads.) */ getSpy: jasmine.Spy; /** * A `jasmine.Spy` for `set` operations on the property (i.e. setting a new property value). * (This is useful in case one needs to make assertions against property writes.) */ setSpy: jasmine.Spy; /** Install the getter/setter spies for the property. */ installSpies(): void; /** * Uninstall the property spies and restore the original value (from before installing the * spies), including the property descriptor. */ uninstallSpies(): void; /** Update the current value of the mocked property. */ setMockValue(value: T[P]): void; } /** * Set up mocking an object's property (using spies) and return a function for updating the mocked * property's value during tests. * * This is, essentially, a wrapper around `spyProperty()` which additionally takes care of * installing the spies before each test (via `beforeEach()`) and uninstalling them after each test * (via `afterEach()`). * * Example usage: * * ```ts * describe('something', () => { * // Assuming `window.foo` is an object... * const mockWindowFooBar = mockProperty(window.foo, 'bar'); * * it('should do this', () => { * mockWindowFooBar('baz'); * expect(window.foo.bar).toBe('baz'); * * mockWindowFooBar('qux'); * expect(window.foo.bar).toBe('qux'); * }); * }); * ``` * * @param ctx The object whose property needs to be spied on. * @param prop The name of the property to spy on. * * @return A function for updating the current value of the mocked property. */ export const mockProperty = (ctx: T, prop: P): IPropertySpyHelpers['setMockValue'] => { const {setMockValue, installSpies, uninstallSpies} = spyProperty(ctx, prop); beforeEach(installSpies); afterEach(uninstallSpies); return setMockValue; }; /** * Return utility functions to help mock and spy on an object's property. * * It supports spying on properties that are either defined on the object instance itself or on its * prototype. It also supports spying on non-writable properties (as long as they are configurable). * * NOTE: Unlike `jasmine`'s spying utilities, spies are not automatically installed/uninstalled, so * the caller is responsible for manually taking care of that (by calling * `installSpies()`/`uninstallSpies()` as necessary). * * @param ctx The object whose property needs to be spied on. * @param prop The name of the property to spy on. * * @return An object with helpers for mocking/spying on an object's property. */ export const spyProperty = (ctx: T, prop: P): IPropertySpyHelpers => { const originalDescriptor = Object.getOwnPropertyDescriptor(ctx, prop); let value = ctx[prop]; const setMockValue = (mockValue: typeof value) => value = mockValue; const setSpy = jasmine.createSpy(`set ${prop}`).and.callFake(setMockValue); const getSpy = jasmine.createSpy(`get ${prop}`).and.callFake(() => value); const installSpies = () => { value = ctx[prop]; Object.defineProperty(ctx, prop, { configurable: true, enumerable: originalDescriptor ? originalDescriptor.enumerable : true, get: getSpy, set: setSpy, }); }; const uninstallSpies = () => originalDescriptor ? Object.defineProperty(ctx, prop, originalDescriptor) : delete ctx[prop]; return {installSpies, uninstallSpies, setMockValue, getSpy, setSpy}; };