29

I'm building some unit tests for a service in Angular2.

Within my Service I have the following code:

var hash: string; hash = this.window.location.hash;

However when I run a test which contains this code, it will fail.

It'd be great to utilise all the features of Window, but as I'm using PhantomJs, I don't think this is possible (I have also tried Chrome which yields the same results).

In AngularJs, I would have resorted to mocking $Window (or at least the properties in question), but as there is not a lot of documentation for Angular2 unit testing I'm not sure how to do this.

Can anyone help?

Rhys
  • 375
  • 1
  • 5
  • 10
  • It seems to be [quite straghtforward](http://stackoverflow.com/questions/34177221/angular2-how-to-inject-window-into-an-angular2-service). Probably an XY problem, because the router already [has the hash abstracted](https://github.com/angular/angular/blob/03627aa84d90f7f1d8d62f160997b783fdf9eaa4/modules/angular2/src/router/location/hash_location_strategy.ts#L64), the abstraction goes up to [DOM location](https://angular.io/docs/ts/latest/api/platform/browser/BrowserDomAdapter-class.html#!#getLocation). – Estus Flask Apr 12 '16 at 11:11

5 Answers5

35

In Angular 2 you can use the @Inject() function to inject the window object by naming it using a string token, like this

  constructor( @Inject('Window') private window: Window) { }

In the @NgModule you must then provide it using the same string:

@NgModule({
    declarations: [ ... ],
    imports: [ ... ],
    providers: [ { provide: 'Window', useValue: window } ],
})
export class AppModule {
}

Then you can also mock it using the token string

beforeEach(() => {
  let windowMock: Window = <any>{ };
  TestBed.configureTestingModule({
    providers: [
      ApiUriService,
      { provide: 'Window', useFactory: (() => { return windowMock; }) }
    ]
  });

This worked in Angular 2.1.1, the latest as of 2016-10-28.

Does not work with Angular 4.0.0 AOT. https://github.com/angular/angular/issues/15640

Marcel Tinner
  • 1,343
  • 11
  • 18
Klas Mellbourn
  • 42,571
  • 24
  • 140
  • 158
  • This works for testing but not in AoT compilation (compiles but with a warning and the app crashes in the browser) – Dunos Dec 29 '16 at 16:08
  • Tried this with an AoT build (along with typing `window` as `any` inside the constructor to work around a different bug), but my production build crashed when I tried to access a custom property on `window` that I'm setting in another file. I followed the solution here instead, and it worked: http://stackoverflow.com/a/37176929/1683187 – Timespace May 08 '17 at 16:48
  • This is the correct answer for changing the windowMock object dynamically in each test when needed – EugenSunic Aug 28 '19 at 05:11
  • it works for me: Angular 10 – Joand Apr 04 '22 at 13:34
9

As @estus mentioned in the comment, you'd be better getting the hash from the Router. But to answer your question directly, you need to inject window into the place you're using it, so that during testing you can mock it.

First, register window with the angular2 provider - probably somewhere global if you use this all over the place:

import { provide } from '@angular/core';
provide(Window, { useValue: window });

This tells angular when the dependency injection asks for the type Window, it should return the global window.

Now, in the place you're using it, you inject this into your class instead of using the global directly:

import { Component } from '@angular/core';

@Component({ ... })
export default class MyCoolComponent {
    constructor (
        window: Window
    ) {}

    public myCoolFunction () {
        let hash: string;
        hash = this.window.location.hash;
    }
}

Now you're ready to mock that value in your test.

import {
    beforeEach,
    beforeEachProviders,
    describe,
    expect,
    it,
    inject,
    injectAsync
} from 'angular2/testing';

let myMockWindow: Window;
beforeEachProviders(() => [
    //Probably mock your thing a bit better than this..
    myMockWindow = <any> { location: <any> { hash: 'WAOW-MOCK-HASH' }};
    provide(Window, {useValue: myMockWindow})
]);

it('should do the things', () => {
    let mockHash = myMockWindow.location.hash;
    //...
});
elwyn
  • 10,360
  • 11
  • 42
  • 52
  • 1
    You can also just inject `constructor(window:Window)` and provide it like `provide(Window, {useValue: window})` or `provide(Window, {useClass: MyWindowMock})`. No need to use a string key if there is a type available. – Günter Zöchbauer May 13 '16 at 04:33
  • 2
    [provide removed in RC6](https://github.com/angular/angular/blob/master/CHANGELOG.md#breaking-changes) from core, change it to `providers: [ {provide: Window, useValue: window}, ]` – Anand Rockzz May 07 '17 at 15:51
  • This is a lifesaver. Enabled me to mock the tests I needed without the page redirecting. Worked in Angular 11 – Cuga Mar 05 '21 at 17:20
  • TypeError: Cannot set properties of undefined (setting 'href') - when trying this.window.location.href = 'abc' – java-addict301 Jun 21 '22 at 00:46
5

After RC4 method provide() its depracated, so the way to handle this after RC4 is:

  let myMockWindow: Window;

  beforeEach(() => {
    myMockWindow = <any> { location: <any> {hash: 'WAOW-MOCK-HASH'}};
    addProviders([SomeService, {provide: Window, useValue: myMockWindow}]);
  });

It take me a while to figure it out, how it works.

ulou
  • 5,542
  • 5
  • 37
  • 47
2

Injection tokens with built-in factories seem like the way to go.

I'm using these for any browser global like window, document, localStorage, console, etc.

core/providers/window.provider.ts

import { InjectionToken } from '@angular/core';

export const WINDOW = new InjectionToken<Window>(
    'Window',
    {
        providedIn: 'root',
        factory(): Window {
            return window;
        }
    }
);

Injection:

constructor(@Inject(WINDOW) private window: Window)

Unit Tests:

const mockWindow = {
  setTimeout: jest.fn(),
  clearTImeout: jest.fn()
};

TestBed.configureTestingModule({
  providers: [
    {
      provide: WINDOW,
      useValue: mockWindow
    }
  ]
});
ArcadeRenegade
  • 804
  • 9
  • 14
  • This was a pretty conclusive answer for me. I put my injection token in to my app.module file and then just imported/injected it in to all components that needed to set the location href. – Ben Thomson Nov 25 '22 at 06:43
0

I really don't get why nobody provided the easiest solution which is the recommended way by the Angular-Team to test a service as you can see here. You even don't have to deal with the TestBed stuff at all in most of the cases.

Futhermore, you can use this approach for components and directives as well. In that case, you won't create a component-instance but a class-instance. This means, you don't have to deal with the child-components used within the components template as well.

Assuming you are able to inject Window into your constructor

constructor(@Inject(WINDOW_TOKEN) private _window: Window) {}

Just do the following in your .spec file:

describe('YourService', () => {
  let service: YourService;
  
  beforeEach(() => {
    service = new YourService(
      {
        location: {hash: 'YourHash'} as any,
        ...
      } as any,
      ...
    );
  });
}

I don't care about other properties, therefore I usually add a type cast to any. Feel free to include all other properties as well and type appropriately.

In case you need different values on the mocked properties you can simply spy on them and change the value using returnValue of jasmine:

const spy: any = spyOn((service as any)._window, 'location').and.returnValue({hash: 'AnotherHash'});

or

const spy: any = spyOn((service as any)._window.location, 'hash').and.returnValue('AnotherHash');
Ilker Cat
  • 1,862
  • 23
  • 17