| 
									
										
										
										
											2019-08-29 18:47:54 +03:00
										 |  |  | /** | 
					
						
							|  |  |  |  * @license | 
					
						
							| 
									
										
										
										
											2020-05-19 12:08:49 -07:00
										 |  |  |  * Copyright Google LLC All Rights Reserved. | 
					
						
							| 
									
										
										
										
											2019-08-29 18:47:54 +03:00
										 |  |  |  * | 
					
						
							|  |  |  |  * 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<T, P extends keyof T> { | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * 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 = | 
					
						
							|  |  |  |     <T, P extends keyof T>(ctx: T, prop: P): IPropertySpyHelpers<T, P>['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 = <T, P extends keyof T>(ctx: T, prop: P): IPropertySpyHelpers<T, P> => { | 
					
						
							|  |  |  |   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}; | 
					
						
							|  |  |  | }; |