In Angular project (Angular CLI: 9.1.4) I have a component with has function using window.location.href = EXTERNAL_URL
a.component.ts
import { Component, OnInit } from '@angular/core';
import { Service } from 'services/service';
@Component({
selector: 'a',
templateUrl: './a.component.html',
styleUrls: ['./a.component.scss']
})
export class AComponent implements OnInit {
constructor(private service: Service) {}
ngOnInit(): void {}
redirect() {
window.location.href = 'https://sample.com'
}
}
For unit test, I have a.component.spec.ts
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AComponent } from './A.component';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
describe('#AComponent', () => {
let component: AComponent;
let fixture: ComponentFixture<AComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [HttpClientModule],
declarations: [AComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
describe('test', () => {
it('test redirect function', () => {
component.redirect();
expect(window.location.href).toEqual('https://sample.com');
});
});
});
it must check the URL 'window.location.href' is same as I expected.
But when running test, showed error which says
Expected 'http://localhost:0987/context.html' to equal 'https://sample.com'
Why window.location.href param is like this?
Also, Karma (v5.0.4) showed test result on browser normally, but on this test, browser page is suddenly changed.

It happens because 'window.location.href' had run on test browser( it tried to open https://sample.com). But because of this redirection, I cannot see Karma result.
So, my question is
P.S. I had thought that using window.location.href was not good, but I couldn't find any ways to redirect external URL.
Here the biggest problem is in the window object itself. But we can redefine it for the test environment. My solution:
import { Component } from '@angular/core';
@Component({
selector: 'a',
templateUrl: './a.component.html',
styleUrls: ['./a.component.scss']
})
export class AComponent {
private window = window;
constructor() {}
redirect() {
this.window.location.href = 'https://sample.com'
}
}
spec:
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AComponent } from './A.component';
import { By } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
describe('#AComponent', () => {
let component: AComponent;
let fixture: ComponentFixture<AComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [HttpClientModule],
declarations: [AComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
describe('test', () => {
it('test redirect function', () => {
let currentLocation;
component['window'] = {
location: {
set href(value) { currentLocation = value;}
}
} as Window & typeof globalThis;
component.redirect();
expect(currentLocation).toBe('https://sample.com');
});
});
});
The cleanest solution is to use an InjectionToken to provide the window-object.
See this blog post for more details on this approach for the window object.
Roughly it can achieved as:
// variable in component class
private window: (Window & typeof globalThis) | null
// component constructor
constructor(
@Inject(DOCUMENT) private document: Document,
) {
this.window = this.document.defaultView
}
// redirection in component function
this.window?.location.assign('https://sample.com')
// in unit test
it('test redirect function', () => {
let currentLocation;
component['window'] = {
location: {
set href(value) { currentLocation = value;}
}
} as Window & typeof globalThis;
component.redirect();
expect(currentLocation).toBe('https://sample.com');
}
Note that the test is quite similar to the other answer. Though by using an InjectionToken it has the major benefit that it does not depend on the browser being the running environment.
You could also directly use this.document.defaultView instead of assigning this.window though. It depends on your taste.
It's better to define external navigation as a service and provide the window as a token for window because this allows the most efficient stubbing.
// The navigation service
export class ExternalNavigationService {
private API_HOST = 'example.com'
private DASHBOARD_URL = `${this.API_HOST}/dashboard`
constructor(
@Inject(WINDOW) public window: Window & typeof globalThis,
) { }
toDashboard(){
this.navigate(this.DASHBOARD_URL)
}
private navigate(url: string | URL){
this.window.location.assign(url)
}
}
import { InjectionToken } from '@angular/core';
export const WINDOW = new InjectionToken<Window>('Global window object', {
factory: () => window
});
// Unit tests for the navigation sevice
describe('ExternalNavigationService', () => {
let externalNavigationService: ExternalNavigationService
let windowStub: Window & typeof globalThis
beforeEach(() => {
windowStub = {
location: { assign(value: string) { return value }},
} as unknown as Window & typeof globalThis
TestBed.configureTestingModule({
providers: [ { provide: WINDOW, useValue: windowStub } ]
})
externalNavigationService = TestBed.inject(ExternalNavigationService)
})
describe('#navigate', () => {
it('redirects to the given url', () => {
spyOn(windowStub.location, 'assign')
externalNavigationService['navigate']('/test')
expect(windowStub.location.assign).toHaveBeenCalledWith('/test')
})
})
describe('#toDashboard', () => {
it('navigates to the dashboard', () => {
spyOn(externalNavigationService, <never>'navigate')
externalNavigationService.toDashboard()
expect(externalNavigationService['navigate']).toHaveBeenCalledWith('example.com/dashboard')
})
})
})
Now you can mock the ExternalNavigationService anywhere and just test that itself calls the required navigation method:
expect(mockedExternalNavigationService.toDashboard).toHaveBeenCalled()
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With