Experimental props. Cover photo by rawpixel.com on Pexels.
Original publication date: 2019-05-07.
Angular’s dependency injection mechanism lets us substitute specific scenarios. While this approach shines in automated testing, here we explore applying it to manual testing workflows.
In the earlier piece, “Testing and faking Angular dependencies”, we built a deprecation banner for Internet Explorer 11, complete with its test suites. But we have yet to run it in a real IE11 environment.
To keep things convenient, we’re building a browser faker component that stays active only in development, gated by a custom structural directive. As a bonus, we’ll implement text pipes so common string handling is available right in our templates.
Simulating a browser environment
Testing directly against your real browser targets—like IE11 here—is essential, but during development, having a quick way to emulate another environment without switching browsers is a real convenience.
// user-agent.token.ts
import { InjectionToken } from '@angular/core';
export const userAgentToken: InjectionToken<string> =
new InjectionToken('User agent string', {
factory: (): string => navigator.userAgent,
providedIn: 'root',
});
// is-internet-explorer-11.token.ts
import { inject, InjectionToken } from '@angular/core';
import { userAgentToken } from './user-agent.token';
export const isInternetExplorer11Token: InjectionToken<boolean> =
new InjectionToken('Internet Explorer 11 flag', {
factory: (): boolean =>
/Trident\/7\.0.+rv:11\.0/.test(inject(userAgentToken)),
providedIn: 'root',
});
<!-- internet-explorer-11-banner.component.html -->
<aside *ngIf="isBannerVisible">
Sorry, we will not continue to support Internet Explorer 11.<br />
Please upgrade to Microsoft Edge.<br />
<button (click)="onDismiss()">
Dismiss
</button>
</aside>
// internet-explorer-11-banner.component.ts
import { Component, Inject } from '@angular/core';
import { isInternetExplorer11Token } from './is-internet-explorer-11.token';
@Component({
selector: 'internet-explorer-11-banner',
templateUrl: './internet-explorer-11-banner.component.html',
})
export class InternetExplorer11BannerComponent {
private isDismissed = false;
get isBannerVisible() {
return this.isInternetExplorer11 && !this.isDismissed;
}
constructor(
@Inject(isInternetExplorer11Token) private isInternetExplorer11: boolean,
) {}
onDismiss() {
this.isDismissed = true;
}
}
A primitive value token is what the deprecation banner currently relies on.
As it stands, isInternetExplorer11Token is a hard-coded dependency inside the deprecation banner component. To swap it out on the fly, we’d need to place an ancestor component or directive that conditionally adds itself to the injector chain—but that’s not always feasible.
Swapping a dependency dynamically via a class-backed service
The factory for the user agent token runs a single time per module injector. When no ancestor component or directive supplies a replacement in its element injector, a different approach becomes necessary: instead of the token, we register a class-based service and make that the new dependency.
// internet-explorer-11-banner.component.ts
import { Component } from '@angular/core';
import { InternetExplorerService } from './internet-explorer.service';
@Component({
selector: 'internet-explorer-11-banner',
templateUrl: './internet-explorer-11-banner.component.html',
})
export class InternetExplorer11BannerComponent {
private isDismissed = false;
get isBannerVisible() {
return this.internetExplorer.isInternetExplorer11State && !this.isDismissed;
}
constructor(
private internetExplorer: InternetExplorerService,
) {}
onDismiss() {
this.isDismissed = true;
}
}
// internet-explorer-service.ts
import { Inject, Injectable } from '@angular/core';
import { userAgentToken } from './user-agent.token';
@Injectable({
providedIn: 'root',
})
export class InternetExplorerService {
get isInternetExplorer11State(): boolean {
return this.isInternetExplorer11(this.userAgent);
}
constructor(
@Inject(userAgentToken) private userAgent: string,
) {}
isInternetExplorer11(userAgent: string): boolean {
return /Trident\/7\.0.+rv:11\.0/.test(userAgent);
}
}
Isolating IE11 detection inside a dedicated service.
Our initial move is to shift the IE11 detection logic out of the dependency injection token and into the new InternetExplorerService class. Evaluating the token's value now routes through this service, which determines the result by inspecting the user agent.
The application itself remains functional at this stage. However, the existing test suite has been affected, prompting us to reorganize it so that it relies on the Internet Explorer service as well.
// internet-explorer-11-detection.spec.ts
import { TestBed } from '@angular/core/testing';
import { InternetExplorerService } from './internet-explorer.service';
import { FakeUserAgent } from './fake-user-agent';
describe('Internet Explorer 11 detection', () => {
function setup({ userAgent }: { userAgent: string }) {
const service: InternetExplorerService =
TestBed.get(InternetExplorerService);
return {
isInternetExplorer11: service.isInternetExplorer11(userAgent),
};
}
const nonInternetExplorerUserAgents: ReadonlyArray<string> =
Object.entries(FakeUserAgent)
.filter(([browser]) =>
!browser.toLowerCase().includes('internetexplorer'))
.map(([_browser, userAgent]) => userAgent);
it('accepts an Internet Explorer 11 user agent', () => {
const { isInternetExplorer11 } = setup({
userAgent: FakeUserAgent.InternetExplorer11,
});
expect(isInternetExplorer11).toBe(true);
});
it('rejects an Internet Explorer 10 user agent', () => {
const { isInternetExplorer11 } = setup({
userAgent: FakeUserAgent.InternetExplorer10,
});
expect(isInternetExplorer11).toBe(false);
});
it('rejects other user agents', () => {
nonInternetExplorerUserAgents.forEach(userAgent => {
const { isInternetExplorer11 } = setup({ userAgent });
expect(isInternetExplorer11).toBe(
false,
`Expected to reject user agent: "${userAgent}"`);
});
});
});
The Internet Explorer 11 detection test suite has been reorganized to depend on the Internet Explorer service.
Earlier, we noted that the user agent token would remain untouched—it won’t be swapped out via an element injector in the template. Our approach is to alter the state imperatively instead.
Observing a state through an observable
We’re dropping the user agent token from the Internet Explorer service. In its place, that service will rely on an observable sourced from a separate browser service.
// internet-explorer.service.ts
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { BrowserService } from './browser.service';
@Injectable({
providedIn: 'root',
})
export class InternetExplorerService {
isInternetExplorer11$: Observable<boolean> =
this.browser.userAgent$.pipe(
map(userAgent => this.isInternetExplorer11(userAgent)),
);
constructor(
private browser: BrowserService,
) {}
isInternetExplorer11(userAgent: string): boolean {
return /Trident\/7\.0.+rv:11\.0/.test(userAgent);
}
}
// browser.service.ts
import { Inject, Injectable, OnDestroy } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
import { FakeUserAgent } from './fake-user-agent';
import { userAgentToken } from './user-agent.token';
@Injectable({
providedIn: 'root',
})
export class BrowserService implements OnDestroy {
private userAgent = new BehaviorSubject(this.realUserAgent);
userAgent$ = this.userAgent.pipe(
distinctUntilChanged(),
);
constructor(
@Inject(userAgentToken) private realUserAgent: string,
) {}
ngOnDestroy() {
this.userAgent.complete();
}
fakeUserAgent(value: FakeUserAgent) {
this.userAgent.next(FakeUserAgent[value]);
}
stopFakingUserAgent() {
this.userAgent.next(this.realUserAgent);
}
}
A class-based service that keeps browser state observable.
Inside BrowserService, the current user agent is held in a BehaviorSubject<string>, and that subject is surfaced as the userAgent$ observable property. Any part of the app needing the user agent should rely on this observable.
When the service starts, the subject gets seeded with the actual user agent string obtained from the user agent token. That same value is kept around for later, because two separate commands permit altering the browser state.
Through the fakeUserAgent method, the user agent state can be switched to a fabricated string. In contrast, the stopFakingUserAgent method—available to any dependee—restores the state back to the genuine user agent string.
As a final polish, the behaviour subject is completed in case the service gets destroyed, preventing any leaks.
For the Internet Explorer service, a new observable property isInternetExplorer11$ has been introduced, which recomputes its value every time the browser service's user agent observable pushes a new value.
At this point, the deprecation banner component simply needs to subscribe to the observable Internet Explorer 11 detection property, replacing the old standard property that was taken away.
<!-- internet-explorer-11-banner.component.html -->
<aside *ngIf="isBannerVisible$ | async">
Sorry, we will not continue to support Internet Explorer 11.<br />
Please upgrade to Microsoft Edge.<br />
<button (click)="onDismiss()">
Dismiss
</button>
</aside>
// internet-explorer-11-banner.component.ts
import { Component } from '@angular/core';
import { BehaviorSubject, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { InternetExplorerService } from './internet-explorer.service';
@Component({
host: { style: 'display: block;' },
selector: 'internet-explorer-11-banner',
templateUrl: './internet-explorer-11-banner.component.html',
})
export class InternetExplorer11BannerComponent {
private isDismissed = new BehaviorSubject(false);
isBannerVisible$ = combineLatest(
this.internetExplorer.isInternetExplorer11$,
this.isDismissed,
).pipe(
map(([isInternetExplorer11, isDismissed]) =>
isInternetExplorer11 && !isDismissed),
);
constructor(
private internetExplorer: InternetExplorerService,
) {}
onDismiss(): void {
this.isDismissed.next(true);
}
}
The deprecation banner component relies on observable state.
Inside this component, the former Boolean isDismissed property is swapped out for a BehaviorSubject<boolean>, starting with a false value. This gives us the observable isBannerVisible$, which merges the state emitted by isDismissed with that from InternetExplorerService#isInternetExplorer11$. Although the component’s UI interactions stay the same, they are now handled within the observable chain.
Rather than setting a property directly, the onDismiss handler pushes true into the isDismissed behavior subject.
Nothing about the app’s runtime behavior has changed since the Internet Explorer and browser services were introduced. We have the commands for switching browser states, but what’s still missing is a way to fire them.
That’s where a browser faker component comes in—it lets us simulate a specific browser environment for the rest of the application.
<!-- browser-faker.component.html -->
<label>
Fake a browser
<select [formControl]="selectedBrowser">
<option value="">
My browser
</option>
<option *ngFor="let browser of browsers"
[value]="browser">
{{browser | replace:wordStartPattern:' $&' | trim}}
</option>
</select>
</label>
// browser-faker.component.ts
import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Observable, Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { BrowserService } from './browser.service';
import { FakeUserAgent } from './fake-user-agent';
@Component({
host: { style: 'display: block;' },
selector: 'browser-faker',
templateUrl: './browser-faker.component.html',
})
export class BrowserFakerComponent implements OnDestroy, OnInit {
private defaultOptionValue = '';
private destroy = new Subject<void>();
private fakeBrowserSelection$: Observable<FakeUserAgent>;
private realBrowserSelection$: Observable<void>;
browsers = Object.keys(FakeUserAgent);
selectedBrowser = new FormControl(this.defaultOptionValue);
wordStartPattern = /[A-Z]|\d+/g;
constructor(
private browser: BrowserService,
) {
this.realBrowserSelection$ = this.selectedBrowser.valueChanges.pipe(
filter(value => value === this.defaultOptionValue),
takeUntil(this.destroy),
);
this.fakeBrowserSelection$ = this.selectedBrowser.valueChanges.pipe(
filter(value => value !== this.defaultOptionValue),
takeUntil(this.destroy),
);
}
ngOnInit(): void {
this.bindEvents();
}
ngOnDestroy() {
this.unbindEvents();
}
private bindEvents(): void {
this.fakeBrowserSelection$.subscribe(userAgent =>
this.browser.fakeUserAgent(userAgent));
this.realBrowserSelection$.subscribe(() =>
this.browser.stopFakingUserAgent());
}
private unbindEvents(): void {
this.destroy.next();
this.destroy.complete();
}
}
The component that fakes a browser.
Through dependency injection, this component gets access to the browser service. A single form control inside it maps directly to a native <select> element. Picking a browser triggers user-agent faking via the service, whereas choosing the default placeholder clears that faked value.
For the app under test, I put together several text-related pipes intended for templates. These include the replace and trim pipes that the browser faker depends on.
With the faker in place, its usage should be restricted to development builds. To enforce that, we need a structural directive that only shows its content when the app runs in development mode.
// is-development-mode.token.ts
import { InjectionToken, isDevMode } from '@angular/core';
export const isDevelopmentModeToken: InjectionToken<boolean> =
new InjectionToken('Development mode flag', {
factory: (): boolean => isDevMode(),
providedIn: 'root',
});
// development-only.directive.ts
import {
Directive,
Inject,
OnDestroy,
OnInit,
TemplateRef,
ViewContainerRef,
} from '@angular/core';
import { isDevelopmentModeToken } from './is-development-mode.token';
@Directive({
exportAs: 'developmentOnly',
selector: '[developmentOnly]',
})
export class DevelopmentOnlyDirective implements OnDestroy, OnInit {
private get isEnabled(): boolean {
return this.isDevelopmentMode;
}
constructor(
private container: ViewContainerRef,
private template: TemplateRef<any>,
@Inject(isDevelopmentModeToken) private isDevelopmentMode: boolean,
) {}
ngOnInit(): void {
if (this.isEnabled) {
this.createAndAttachView();
}
}
ngOnDestroy(): void {
this.destroyView();
}
private createAndAttachView(): void {
this.container.createEmbeddedView(this.template);
}
private destroyView(): void {
this.container.clear();
}
}
// development-only.directive.spec.ts
import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DevelopmentOnlyDirective } from './development-only.directive';
import { isDevelopmentModeToken } from './is-development-mode.token';
@Component({
template: '<button *developmentOnly>God Mode</button>',
})
class TestComponent {}
describe(DevelopmentOnlyDirective.name, () => {
function setup({ isDevelopmentMode }: { isDevelopmentMode: boolean }) {
TestBed.configureTestingModule({
declarations: [
DevelopmentOnlyDirective,
TestComponent,
],
providers: [
{ provide: isDevelopmentModeToken, useValue: isDevelopmentMode },
],
});
const fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
const button = fixture.debugElement.query(By.css('button'));
return {
expectButtonToBeOmitted() {
expect(button).toBe(null);
},
expectButtonToBeRendered() {
expect(button.nativeElement).not.toBe(null);
expect(button.nativeElement.textContent).toContain('God Mode');
},
};
}
it('renders its element in development mode', () => {
const { expectButtonToBeRendered } = setup({ isDevelopmentMode: true });
expectButtonToBeRendered();
});
it('omits its element in production mode', () => {
const { expectButtonToBeOmitted } = setup({ isDevelopmentMode: false });
expectButtonToBeOmitted();
});
});
Structural directive intended solely for development.
When the app is in development mode—confirmed by its test suite—this structural directive renders whatever component or element it is placed on.
The remaining work is straightforward: attach the deprecation banner and the browser faker to our app.
<!-- app.component.html -->
<browser-faker *developmentOnly></browser-faker>
<internet-explorer-11-banner></internet-explorer-11-banner>
URL: <code><browser-url></browser-url></code>
// app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
})
export class AppComponent {}
A browser faker accompanies an Internet Explorer 11 deprecation notice within the Angular app.
Alongside that, you'll find a dedicated URL component in the application, which showcases how the Location API serves as an Angular dependency.


With a simulated browser environment in place, development and manual testing become easier. Still, the deprecation banner has to be verified in an actual Internet Explorer 11 browser. The Resources section covers how to do that.
Summary
We built a browser faker component that appears only in development mode, letting us mimic a user environment. The browser state lives in a class-based service that the application depends on; the faker uses that same service.
This browser faker illustrates a basic approach to dependency faking in Angular. Beyond that, we covered how to make Angular's dependency injection configurable at runtime.
Resources
The demo app that walks through faking dependencies in Angular resides in a StackBlitz project.
The accompanying test suite, which both tests and fakes Angular dependencies, is available in a separate StackBlitz project.
Microsoft's Modern.IE domain offers free resources for generating Internet Explorer browser snapshots and supplies free virtual machine images that run Internet Explorer on Windows 7 or 8.1.
Related articles
For methods on configuring and resolving dependencies in an Angular testing environment, see “Testing and faking Angular dependencies”.
In “Tree-shakable dependencies in Angular projects” you can find how to provide tree-shakable dependencies and handle complex Angular dependency injection setups. Our application takes that article as its base.
Reviewers
These contributors from the Angular community reviewed this article:
