angular-cn/aio/content/examples/testing/specs.stackblitz.no-link.html

5890 lines
190 KiB
HTML

<html lang="en"><head></head><body>
<form id="mainForm" method="post" action="https://run.stackblitz.com/api/angular/v1?file=src/app/app.component.html" target="_self"><input type="hidden" name="files[src/expected.ts]" value="/* Ignore. Satisfies static analysis of router config in app.component.router.spec.ts */
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/index.html]" value="<!--
Intentionally empty placeholder for Stackblitz.
Do not need index.html in zip-download either as you should run tests with `npm test`
-->
<!--
Copyright Google LLC. 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
-->"><input type="hidden" name="files[src/main.ts]" value="import './testing/global-jasmine';
import 'jasmine-core/lib/jasmine-core/jasmine-html.js';
import 'jasmine-core/lib/jasmine-core/boot.js';
declare var jasmine;
import './polyfills';
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
// Spec files to include in the Stackblitz tests
import './tests.sb.ts';
//
bootstrap();
//
function bootstrap() {
if ((window as any).jasmineRef) {
location.reload();
return;
} else {
window.onload(undefined);
(window as any).jasmineRef = jasmine.getEnv();
}
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/styles.css]" value="/* Global Styles */
* {
font-family: Arial, Helvetica, sans-serif;
}
h1 {
color: #264D73;
font-size: 2.5rem;
}
h2, h3 {
color: #444;
font-weight: lighter;
}
h3 {
font-size: 1.3rem;
}
body {
padding: .5rem;
max-width: 1000px;
margin: auto;
}
@media (min-width: 600px) {
body {
padding: 2rem;
}
}
body, input[text] {
color: #333;
font-family: Cambria, Georgia, serif;
}
a {
cursor: pointer;
}
button {
background-color: #eee;
border: none;
border-radius: 4px;
cursor: pointer;
color: black;
font-size: 1.2rem;
padding: 1rem;
margin-right: 1rem;
margin-bottom: 1rem;
}
button:hover {
background-color: black;
color: white;
}
button:disabled {
background-color: #eee;
color: #aaa;
cursor: auto;
}
/* Navigation link styles */
nav a {
padding: 5px 10px;
text-decoration: none;
margin-right: 10px;
margin-top: 10px;
display: inline-block;
background-color: #e8e8e8;
color: #3d3d3d;
border-radius: 4px;
}
nav a:hover {
color: white;
background-color: #42545C;
}
nav a.active {
background-color: black;
color: white;
}
hr {
margin: 1.5rem 0;
}
input[type=&quot;text&quot;] {
box-sizing: border-box;
width: 100%;
padding: .5rem;
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/test.css]" value="@import &quot;~jasmine-core/lib/jasmine-core/jasmine.css&quot;
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/tests.sb.ts]" value="// Import spec files individually for Stackblitz
import './app/about/about.component.spec.ts';
import './app/app-initial.component.spec.ts';
import './app/app.component.router.spec.ts';
import './app/app.component.spec.ts';
import './app/banner/banner-initial.component.spec.ts';
import './app/banner/banner.component.spec.ts';
import './app/banner/banner.component.detect-changes.spec.ts';
import './app/banner/banner-external.component.spec.ts';
import './app/dashboard/dashboard-hero.component.spec.ts';
import './app/dashboard/dashboard.component.no-testbed.spec.ts';
import './app/dashboard/dashboard.component.spec.ts';
import './app/demo/async-helper.spec.ts';
import './app/demo/demo.spec.ts';
import './app/demo/demo.testbed.spec.ts';
import './app/hero/hero-detail.component.no-testbed.spec.ts';
import './app/hero/hero-detail.component.spec.ts';
import './app/hero/hero-list.component.spec.ts';
import './app/model/hero.service.spec.ts';
import './app/model/testing/http-client.spec.ts';
import './app/shared/canvas.component.spec.ts';
import './app/shared/highlight.directive.spec.ts';
import './app/shared/title-case.pipe.spec.ts';
import './app/twain/twain.component.spec.ts';
import './app/twain/twain.component.marbles.spec.ts';
import './app/welcome/welcome.component.spec.ts';
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/banner/banner-external.component.css]" value="h1 { color: green; font-size: 350%}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/dashboard/dashboard-hero.component.css]" value=".hero {
padding: 20px;
position: relative;
text-align: center;
color: #eee;
max-height: 120px;
min-width: 120px;
background-color: #607D8B;
border-radius: 2px;
}
.hero:hover {
background-color: #EEE;
cursor: pointer;
color: #607d8b;
}
@media (max-width: 600px) {
.hero {
font-size: 10px;
max-height: 75px; }
}
@media (max-width: 1024px) {
.hero {
min-width: 60px;
}
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/dashboard/dashboard.component.css]" value="[class*='col-'] {
float: left;
}
*, *::after, *::before {
box-sizing: border-box;
}
h3 {
text-align: center;
margin-bottom: 0;
}
[class*='col-'] {
padding-right: 20px;
padding-bottom: 20px;
}
[class*='col-']:last-of-type {
padding-right: 0;
}
.grid {
margin: 0;
}
.col-1-4 {
width: 25%;
}
.grid-pad {
padding: 10px 0;
}
.grid-pad > [class*='col-']:last-of-type {
padding-right: 20px;
}
@media (max-width: 1024px) {
.grid {
margin: 0;
}
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/hero/hero-detail.component.css]" value="label {
display: inline-block;
width: 3em;
margin: .5em 0;
color: #607D8B;
font-weight: bold;
}
input {
height: 2em;
font-size: 1em;
padding-left: .4em;
}
button {
margin-top: 20px;
font-family: Arial, sans-serif;
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #cfd8dc;
}
button:disabled {
background-color: #eee;
color: #ccc;
cursor: auto;
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/hero/hero-list.component.css]" value=".selected {
background-color: #CFD8DC !important;
color: white;
}
.heroes {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 15em;
}
.heroes li {
cursor: pointer;
position: relative;
left: 0;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
}
.heroes li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.heroes li.selected:hover {
background-color: #BBD8DC !important;
color: white;
}
.heroes .text {
position: relative;
top: -3px;
}
.heroes .badge {
display: inline-block;
font-size: small;
color: white;
padding: 0.8em 0.7em 0 0.7em;
background-color: #607D8B;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}
button {
font-family: Arial, sans-serif;
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #cfd8dc;
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/app.component.html]" value="<app-banner></app-banner>
<app-welcome></app-welcome>
<nav>
<a routerLink=&quot;/dashboard&quot;>Dashboard</a>
<a routerLink=&quot;/heroes&quot;>Heroes</a>
<a routerLink=&quot;/about&quot;>About</a>
</nav>
<router-outlet></router-outlet>
<!--
Copyright Google LLC. 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
-->"><input type="hidden" name="files[src/app/banner/banner-external.component.html]" value="<h1>{{title}}</h1>
<!--
Copyright Google LLC. 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
-->"><input type="hidden" name="files[src/app/dashboard/dashboard.component.html]" value="<h2 highlight>{{title}}</h2>
<div class=&quot;grid grid-pad&quot;>
<dashboard-hero *ngFor=&quot;let hero of heroes&quot; class=&quot;col-1-4&quot;
[hero]=hero (selected)=&quot;gotoDetail($event)&quot; >
</dashboard-hero>
</div>
<!--
Copyright Google LLC. 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
-->"><input type="hidden" name="files[src/app/demo/demo-external-template.html]" value="<span>from external template</span>
<!--
Copyright Google LLC. 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
-->"><input type="hidden" name="files[src/app/hero/hero-detail.component.html]" value="<div *ngIf=&quot;hero&quot;>
<h2><span>{{hero.name | titlecase}}</span> Details</h2>
<div>
<label>id: </label>{{hero.id}}</div>
<div>
<label for=&quot;name&quot;>name: </label>
<input id=&quot;name&quot; [(ngModel)]=&quot;hero.name&quot; placeholder=&quot;name&quot; />
</div>
<button (click)=&quot;save()&quot;>Save</button>
<button (click)=&quot;cancel()&quot;>Cancel</button>
</div>
<!--
Copyright Google LLC. 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
-->"><input type="hidden" name="files[src/app/hero/hero-list.component.html]" value="<h2 highlight=&quot;gold&quot;>My Heroes</h2>
<ul class=&quot;heroes&quot;>
<li *ngFor=&quot;let hero of heroes | async &quot;
[class.selected]=&quot;hero === selectedHero&quot;
(click)=&quot;onSelect(hero)&quot;>
<span class=&quot;badge&quot;>{{hero.id}}</span> {{hero.name}}
</li>
</ul>
<!--
Copyright Google LLC. 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
-->"><input type="hidden" name="files[src/app/about/about.component.spec.ts]" value="import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AboutComponent } from './about.component';
import { HighlightDirective } from '../shared/highlight.directive';
let fixture: ComponentFixture<AboutComponent>;
describe('AboutComponent (highlightDirective)', () => {
beforeEach(() => {
fixture = TestBed.configureTestingModule({
declarations: [ AboutComponent, HighlightDirective ],
schemas: [ NO_ERRORS_SCHEMA ]
})
.createComponent(AboutComponent);
fixture.detectChanges(); // initial binding
});
it('should have skyblue <h2>', () => {
const h2: HTMLElement = fixture.nativeElement.querySelector('h2');
const bgColor = h2.style.backgroundColor;
expect(bgColor).toBe('skyblue');
});
});
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/about/about.component.ts]" value="import { Component } from '@angular/core';
@Component({
template: `
<h2 highlight=&quot;skyblue&quot;>About</h2>
<h3>Quote of the day:</h3>
<twain-quote></twain-quote>
`
})
export class AboutComponent { }
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/app-initial.component.spec.ts]" value="import { TestBed, waitForAsync } from '@angular/core/testing';
import { AppComponent } from './app-initial.component';
/*
import { AppComponent } from './app.component';
describe('AppComponent', () => {
*/
describe('AppComponent (initial CLI version)', () => {
beforeEach(waitForAsync(() => {
TestBed
.configureTestingModule({
declarations: [AppComponent],
})
.compileComponents();
}));
it('should create the app', waitForAsync(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
}));
it(`should have as title 'app'`, waitForAsync(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('app');
}));
it('should render title', waitForAsync(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!');
}));
});
/// As it should be
import { DebugElement } from '@angular/core';
import { ComponentFixture } from '@angular/core/testing';
describe('AppComponent (initial CLI version - as it should be)', () => {
let app: AppComponent;
let de: DebugElement;
let fixture: ComponentFixture<AppComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [AppComponent],
});
fixture = TestBed.createComponent(AppComponent);
app = fixture.componentInstance;
de = fixture.debugElement;
});
it('should create the app', () => {
expect(app).toBeDefined();
});
it(`should have as title 'app'`, () => {
expect(app.title).toEqual('app');
});
it('should render title in an h1 tag', () => {
fixture.detectChanges();
expect(de.nativeElement.querySelector('h1').textContent).toContain('Welcome to app!');
});
});
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/app-initial.component.ts]" value="// Reduced version of the initial AppComponent generated by CLI
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: '<h1>Welcome to {{title}}!</h1>'
})
export class AppComponent {
title = 'app';
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/app-routing.module.ts]" value="import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AboutComponent } from './about/about.component';
export const routes: Routes = [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full'},
{ path: 'about', component: AboutComponent },
{ path: 'heroes', loadChildren: () => import('./hero/hero.module').then(m => m.HeroModule)},
];
@NgModule({
imports: [
RouterModule.forRoot(routes),
],
exports: [ RouterModule ] // re-export the module declarations
})
export class AppRoutingModule { }
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/app.component.router.spec.ts]" value="// For more examples:
// https://github.com/angular/angular/blob/master/modules/@angular/router/test/integration.spec.ts
import { waitForAsync, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { asyncData } from '../testing';
import { RouterTestingModule } from '@angular/router/testing';
import { SpyLocation } from '@angular/common/testing';
import { Router, RouterLinkWithHref } from '@angular/router';
import { By } from '@angular/platform-browser';
import { DebugElement, Type } from '@angular/core';
import { Location } from '@angular/common';
import { click } from '../testing';
import { routes } from './app-routing.module';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
import { AboutComponent } from './about/about.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { HeroService, TestHeroService } from './model/testing/test-hero.service';
import { TwainService } from './twain/twain.service';
let comp: AppComponent;
let fixture: ComponentFixture<AppComponent>;
let page: Page;
let router: Router;
let location: SpyLocation;
describe('AppComponent &amp; RouterTestingModule', () => {
beforeEach(waitForAsync(() => {
TestBed
.configureTestingModule({
imports: [
AppModule,
RouterTestingModule.withRoutes(routes),
],
providers: [{provide: HeroService, useClass: TestHeroService}]
})
.compileComponents();
}));
it('should navigate to &quot;Dashboard&quot; immediately', fakeAsync(() => {
createComponent();
tick(); // wait for async data to arrive
expectPathToBe('/dashboard', 'after initialNavigation()');
expectElementOf(DashboardComponent);
}));
it('should navigate to &quot;About&quot; on click', fakeAsync(() => {
createComponent();
click(page.aboutLinkDe);
// page.aboutLinkDe.nativeElement.click(); // ok but fails in phantom
advance();
expectPathToBe('/about');
expectElementOf(AboutComponent);
}));
it('should navigate to &quot;About&quot; w/ browser location URL change', fakeAsync(() => {
createComponent();
location.simulateHashChange('/about');
// location.go('/about'); // also works ... except, perhaps, in Stackblitz
advance();
expectPathToBe('/about');
expectElementOf(AboutComponent);
}));
// Can't navigate to lazy loaded modules with this technique
xit('should navigate to &quot;Heroes&quot; on click (not working yet)', fakeAsync(() => {
createComponent();
page.heroesLinkDe.nativeElement.click();
advance();
expectPathToBe('/heroes');
}));
});
///////////////
import { NgModuleFactoryLoader } from '@angular/core';
import { SpyNgModuleFactoryLoader } from '@angular/router/testing';
import { HeroModule } from './hero/hero.module'; // should be lazy loaded
import { HeroListComponent } from './hero/hero-list.component';
let loader: SpyNgModuleFactoryLoader;
///////// Can't get lazy loaded Heroes to work yet
xdescribe('AppComponent &amp; Lazy Loading (not working yet)', () => {
beforeEach(waitForAsync(() => {
TestBed
.configureTestingModule({
imports: [
AppModule,
RouterTestingModule.withRoutes(routes),
],
})
.compileComponents();
}));
beforeEach(fakeAsync(() => {
createComponent();
loader = TestBed.inject(NgModuleFactoryLoader) as SpyNgModuleFactoryLoader;
loader.stubbedModules = {expected: HeroModule};
router.resetConfig([{path: 'heroes', loadChildren: 'expected'}]);
}));
it('should navigate to &quot;Heroes&quot; on click', waitForAsync(() => {
page.heroesLinkDe.nativeElement.click();
advance();
expectPathToBe('/heroes');
expectElementOf(HeroListComponent);
}));
it('can navigate to &quot;Heroes&quot; w/ browser location URL change', fakeAsync(() => {
location.go('/heroes');
advance();
expectPathToBe('/heroes');
expectElementOf(HeroListComponent);
}));
});
////// Helpers /////////
/**
* Advance to the routed page
* Wait a tick, then detect changes, and tick again
*/
function advance(): void {
tick(); // wait while navigating
fixture.detectChanges(); // update view
tick(); // wait for async data to arrive
}
function createComponent() {
fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance;
const injector = fixture.debugElement.injector;
location = injector.get(Location) as SpyLocation;
router = injector.get(Router);
router.initialNavigation();
spyOn(injector.get(TwainService), 'getQuote')
// fake fast async observable
.and.returnValue(asyncData('Test Quote'));
advance();
page = new Page();
}
class Page {
aboutLinkDe: DebugElement;
dashboardLinkDe: DebugElement;
heroesLinkDe: DebugElement;
// for debugging
comp: AppComponent;
location: SpyLocation;
router: Router;
fixture: ComponentFixture<AppComponent>;
constructor() {
const links = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref));
this.aboutLinkDe = links[2];
this.dashboardLinkDe = links[0];
this.heroesLinkDe = links[1];
// for debugging
this.comp = comp;
this.fixture = fixture;
this.router = router;
}
}
function expectPathToBe(path: string, expectationFailOutput?: any) {
expect(location.path()).toEqual(path, expectationFailOutput || 'location.path()');
}
function expectElementOf(type: Type<any>): any {
const el = fixture.debugElement.query(By.directive(type));
expect(el).toBeTruthy('expected an element for ' + type.name);
return el;
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/app.component.spec.ts]" value="import { Component, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { RouterLinkDirectiveStub } from '../testing';
import { AppComponent } from './app.component';
@Component({selector: 'app-banner', template: ''})
class BannerStubComponent {
}
@Component({selector: 'router-outlet', template: ''})
class RouterOutletStubComponent {
}
@Component({selector: 'app-welcome', template: ''})
class WelcomeStubComponent {
}
let comp: AppComponent;
let fixture: ComponentFixture<AppComponent>;
describe('AppComponent &amp; TestModule', () => {
beforeEach(waitForAsync(() => {
TestBed
.configureTestingModule({
declarations: [
AppComponent, RouterLinkDirectiveStub, BannerStubComponent, RouterOutletStubComponent,
WelcomeStubComponent
]
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance;
});
}));
tests();
});
//////// Testing w/ NO_ERRORS_SCHEMA //////
describe('AppComponent &amp; NO_ERRORS_SCHEMA', () => {
beforeEach(waitForAsync(() => {
TestBed
.configureTestingModule({
declarations: [
AppComponent,
BannerStubComponent,
RouterLinkDirectiveStub
],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance;
});
}));
tests();
});
//////// Testing w/ real root module //////
// Tricky because we are disabling the router and its configuration
// Better to use RouterTestingModule
import { AppModule } from './app.module';
import { AppRoutingModule } from './app-routing.module';
describe('AppComponent &amp; AppModule', () => {
beforeEach(waitForAsync(() => {
TestBed
.configureTestingModule({imports: [AppModule]})
// Get rid of app's Router configuration otherwise many failures.
// Doing so removes Router declarations; add the Router stubs
.overrideModule(AppModule, {
remove: {imports: [AppRoutingModule]},
add: {declarations: [RouterLinkDirectiveStub, RouterOutletStubComponent]}
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance;
});
}));
tests();
});
function tests() {
let routerLinks: RouterLinkDirectiveStub[];
let linkDes: DebugElement[];
beforeEach(() => {
fixture.detectChanges(); // trigger initial data binding
// find DebugElements with an attached RouterLinkStubDirective
linkDes = fixture.debugElement.queryAll(By.directive(RouterLinkDirectiveStub));
// get attached link directive instances
// using each DebugElement's injector
routerLinks = linkDes.map(de => de.injector.get(RouterLinkDirectiveStub));
});
it('can instantiate the component', () => {
expect(comp).not.toBeNull();
});
it('can get RouterLinks from template', () => {
expect(routerLinks.length).toBe(3, 'should have 3 routerLinks');
expect(routerLinks[0].linkParams).toBe('/dashboard');
expect(routerLinks[1].linkParams).toBe('/heroes');
expect(routerLinks[2].linkParams).toBe('/about');
});
it('can click Heroes link in template', () => {
const heroesLinkDe = linkDes[1]; // heroes link DebugElement
const heroesLink = routerLinks[1]; // heroes link directive
expect(heroesLink.navigatedTo).toBeNull('should not have navigated yet');
heroesLinkDe.triggerEventHandler('click', null);
fixture.detectChanges();
expect(heroesLink.navigatedTo).toBe('/heroes');
});
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/app.component.ts]" value="import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent { }
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/app.module.ts]" value="import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { AboutComponent } from './about/about.component';
import { BannerComponent } from './banner/banner.component';
import { HeroService } from './model/hero.service';
import { UserService } from './model/user.service';
import { TwainComponent } from './twain/twain.component';
import { TwainService } from './twain/twain.service';
import { WelcomeComponent } from './welcome/welcome.component';
import { DashboardModule } from './dashboard/dashboard.module';
import { SharedModule } from './shared/shared.module';
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';
@NgModule({
imports: [
BrowserModule,
DashboardModule,
AppRoutingModule,
SharedModule,
HttpClientModule,
// The HttpClientInMemoryWebApiModule module intercepts HTTP requests
// and returns simulated server responses.
// Remove it when a real server is ready to receive requests.
HttpClientInMemoryWebApiModule.forRoot(
InMemoryDataService, { dataEncapsulation: false }
)
],
providers: [
HeroService,
TwainService,
UserService
],
declarations: [
AppComponent,
AboutComponent,
BannerComponent,
TwainComponent,
WelcomeComponent,
],
bootstrap: [ AppComponent ]
})
export class AppModule { }
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/banner/banner-external.component.spec.ts]" value="import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { BannerComponent } from './banner-external.component';
describe('BannerComponent (external files)', () => {
let component: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
let h1: HTMLElement;
describe('Two beforeEach', () => {
beforeEach(waitForAsync(() => {
TestBed
.configureTestingModule({
declarations: [BannerComponent],
})
.compileComponents(); // compile template and css
}));
// synchronous beforeEach
beforeEach(() => {
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance; // BannerComponent test instance
h1 = fixture.nativeElement.querySelector('h1');
});
tests();
});
describe('One beforeEach', () => {
beforeEach(waitForAsync(() => {
TestBed
.configureTestingModule({
declarations: [BannerComponent],
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance;
h1 = fixture.nativeElement.querySelector('h1');
});
}));
tests();
});
function tests() {
it('no title in the DOM until manually call `detectChanges`', () => {
expect(h1.textContent).toEqual('');
});
it('should display original title', () => {
fixture.detectChanges();
expect(h1.textContent).toContain(component.title);
});
it('should display a different test title', () => {
component.title = 'Test Title';
fixture.detectChanges();
expect(h1.textContent).toContain('Test Title');
});
}
});
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/banner/banner-external.component.ts]" value="import { Component } from '@angular/core';
@Component({
selector: 'app-banner',
templateUrl: './banner-external.component.html',
styleUrls: ['./banner-external.component.css']
})
export class BannerComponent {
title = 'Test Tour of Heroes';
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/banner/banner-initial.component.spec.ts]" value="import { DebugElement } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { BannerComponent } from './banner-initial.component';
/*
import { BannerComponent } from './banner.component';
describe('BannerComponent', () => {
*/
describe('BannerComponent (initial CLI generated)', () => {
let component: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({declarations: [BannerComponent]}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeDefined();
});
});
describe('BannerComponent (minimal)', () => {
it('should create', () => {
TestBed.configureTestingModule({declarations: [BannerComponent]});
const fixture = TestBed.createComponent(BannerComponent);
const component = fixture.componentInstance;
expect(component).toBeDefined();
});
});
describe('BannerComponent (with beforeEach)', () => {
let component: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
beforeEach(() => {
TestBed.configureTestingModule({declarations: [BannerComponent]});
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance;
});
it('should create', () => {
expect(component).toBeDefined();
});
it('should contain &quot;banner works!&quot;', () => {
const bannerElement: HTMLElement = fixture.nativeElement;
expect(bannerElement.textContent).toContain('banner works!');
});
it('should have <p> with &quot;banner works!&quot;', () => {
const bannerElement: HTMLElement = fixture.nativeElement;
const p = bannerElement.querySelector('p');
expect(p.textContent).toEqual('banner works!');
});
it('should find the <p> with fixture.debugElement.nativeElement)', () => {
const bannerDe: DebugElement = fixture.debugElement;
const bannerEl: HTMLElement = bannerDe.nativeElement;
const p = bannerEl.querySelector('p');
expect(p.textContent).toEqual('banner works!');
});
it('should find the <p> with fixture.debugElement.query(By.css)', () => {
const bannerDe: DebugElement = fixture.debugElement;
const paragraphDe = bannerDe.query(By.css('p'));
const p: HTMLElement = paragraphDe.nativeElement;
expect(p.textContent).toEqual('banner works!');
});
});
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/banner/banner-initial.component.ts]" value="// BannerComponent as initially generated by the CLI
import { Component } from '@angular/core';
@Component({
selector: 'app-banner',
template: `<p>banner works!</p>`,
styles: []
})
export class BannerComponent { }
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/banner/banner.component.detect-changes.spec.ts]" value="import { async } from '@angular/core/testing';
import { ComponentFixtureAutoDetect } from '@angular/core/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BannerComponent } from './banner.component';
describe('BannerComponent (AutoChangeDetect)', () => {
let comp: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
let h1: HTMLElement;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ],
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true }
]
});
fixture = TestBed.createComponent(BannerComponent);
comp = fixture.componentInstance;
h1 = fixture.nativeElement.querySelector('h1');
});
it('should display original title', () => {
// Hooray! No `fixture.detectChanges()` needed
expect(h1.textContent).toContain(comp.title);
});
it('should still see original title after comp.title change', () => {
const oldTitle = comp.title;
comp.title = 'Test Title';
// Displayed title is old because Angular didn't hear the change :(
expect(h1.textContent).toContain(oldTitle);
});
it('should display updated title after detectChanges', () => {
comp.title = 'Test Title';
fixture.detectChanges(); // detect changes explicitly
expect(h1.textContent).toContain(comp.title);
});
});
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/banner/banner.component.spec.ts]" value="import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DebugElement } from '@angular/core';
import { BannerComponent } from './banner.component';
describe('BannerComponent (inline template)', () => {
let component: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
let h1: HTMLElement;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ],
});
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance; // BannerComponent test instance
h1 = fixture.nativeElement.querySelector('h1');
});
it('no title in the DOM after createComponent()', () => {
expect(h1.textContent).toEqual('');
});
it('should display original title', () => {
fixture.detectChanges();
expect(h1.textContent).toContain(component.title);
});
it('should display original title after detectChanges()', () => {
fixture.detectChanges();
expect(h1.textContent).toContain(component.title);
});
it('should display a different test title', () => {
component.title = 'Test Title';
fixture.detectChanges();
expect(h1.textContent).toContain('Test Title');
});
});
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/banner/banner.component.ts]" value="import { Component } from '@angular/core';
@Component({
selector: 'app-banner',
template: '<h1>{{title}}</h1>',
styles: ['h1 { color: green; font-size: 350%}']
})
export class BannerComponent {
title = 'Test Tour of Heroes';
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/dashboard/dashboard-hero.component.spec.ts]" value="
import { DebugElement } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { addMatchers, click } from '../../testing';
import { Hero } from '../model/hero';
import { DashboardHeroComponent } from './dashboard-hero.component';
beforeEach(addMatchers);
describe('DashboardHeroComponent class only', () => {
it('raises the selected event when clicked', () => {
const comp = new DashboardHeroComponent();
const hero: Hero = {id: 42, name: 'Test'};
comp.hero = hero;
comp.selected.subscribe((selectedHero: Hero) => expect(selectedHero).toBe(hero));
comp.click();
});
});
describe('DashboardHeroComponent when tested directly', () => {
let comp: DashboardHeroComponent;
let expectedHero: Hero;
let fixture: ComponentFixture<DashboardHeroComponent>;
let heroDe: DebugElement;
let heroEl: HTMLElement;
beforeEach(waitForAsync(() => {
TestBed
.configureTestingModule({declarations: [DashboardHeroComponent]})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DashboardHeroComponent);
comp = fixture.componentInstance;
// find the hero's DebugElement and element
heroDe = fixture.debugElement.query(By.css('.hero'));
heroEl = heroDe.nativeElement;
// mock the hero supplied by the parent component
expectedHero = {id: 42, name: 'Test Name'};
// simulate the parent setting the input property with that hero
comp.hero = expectedHero;
// trigger initial data binding
fixture.detectChanges();
});
it('should display hero name in uppercase', () => {
const expectedPipedName = expectedHero.name.toUpperCase();
expect(heroEl.textContent).toContain(expectedPipedName);
});
it('should raise selected event when clicked (triggerEventHandler)', () => {
let selectedHero: Hero;
comp.selected.subscribe((hero: Hero) => selectedHero = hero);
heroDe.triggerEventHandler('click', null);
expect(selectedHero).toBe(expectedHero);
});
it('should raise selected event when clicked (element.click)', () => {
let selectedHero: Hero;
comp.selected.subscribe((hero: Hero) => selectedHero = hero);
heroEl.click();
expect(selectedHero).toBe(expectedHero);
});
it('should raise selected event when clicked (click helper)', () => {
let selectedHero: Hero;
comp.selected.subscribe((hero: Hero) => selectedHero = hero);
click(heroDe); // click helper with DebugElement
click(heroEl); // click helper with native element
expect(selectedHero).toBe(expectedHero);
});
});
//////////////////
describe('DashboardHeroComponent when inside a test host', () => {
let testHost: TestHostComponent;
let fixture: ComponentFixture<TestHostComponent>;
let heroEl: HTMLElement;
beforeEach(waitForAsync(() => {
TestBed
.configureTestingModule({declarations: [DashboardHeroComponent, TestHostComponent]})
.compileComponents();
}));
beforeEach(() => {
// create TestHostComponent instead of DashboardHeroComponent
fixture = TestBed.createComponent(TestHostComponent);
testHost = fixture.componentInstance;
heroEl = fixture.nativeElement.querySelector('.hero');
fixture.detectChanges(); // trigger initial data binding
});
it('should display hero name', () => {
const expectedPipedName = testHost.hero.name.toUpperCase();
expect(heroEl.textContent).toContain(expectedPipedName);
});
it('should raise selected event when clicked', () => {
click(heroEl);
// selected hero should be the same data bound hero
expect(testHost.selectedHero).toBe(testHost.hero);
});
});
////// Test Host Component //////
import { Component } from '@angular/core';
@Component({
template: `
<dashboard-hero
[hero]=&quot;hero&quot; (selected)=&quot;onSelected($event)&quot;>
</dashboard-hero>`
})
class TestHostComponent {
hero: Hero = {id: 42, name: 'Test Name'};
selectedHero: Hero;
onSelected(hero: Hero) {
this.selectedHero = hero;
}
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/dashboard/dashboard-hero.component.ts]" value="import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Hero } from '../model/hero';
@Component({
selector: 'dashboard-hero',
template: `
<div (click)=&quot;click()&quot; class=&quot;hero&quot;>
{{hero.name | uppercase}}
</div>`,
styleUrls: [ './dashboard-hero.component.css' ]
})
export class DashboardHeroComponent {
@Input() hero: Hero;
@Output() selected = new EventEmitter<Hero>();
click() { this.selected.emit(this.hero); }
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/dashboard/dashboard.component.no-testbed.spec.ts]" value="import { Router } from '@angular/router';
import { DashboardComponent } from './dashboard.component';
import { Hero } from '../model/hero';
import { addMatchers } from '../../testing';
import { TestHeroService, HeroService } from '../model/testing/test-hero.service';
class FakeRouter {
navigateByUrl(url: string) { return url; }
}
describe('DashboardComponent class only', () => {
let comp: DashboardComponent;
let heroService: TestHeroService;
let router: Router;
beforeEach(() => {
addMatchers();
router = new FakeRouter() as any as Router;
heroService = new TestHeroService();
comp = new DashboardComponent(router, heroService);
});
it('should NOT have heroes before calling OnInit', () => {
expect(comp.heroes.length).toBe(0,
'should not have heroes before OnInit');
});
it('should NOT have heroes immediately after OnInit', () => {
comp.ngOnInit(); // ngOnInit -> getHeroes
expect(comp.heroes.length).toBe(0,
'should not have heroes until service promise resolves');
});
it('should HAVE heroes after HeroService gets them', (done: DoneFn) => {
comp.ngOnInit(); // ngOnInit -> getHeroes
heroService.lastResult // the one from getHeroes
.subscribe(
() => {
// throw new Error('deliberate error'); // see it fail gracefully
expect(comp.heroes.length).toBeGreaterThan(0,
'should have heroes after service promise resolves');
done();
},
done.fail);
});
it('should tell ROUTER to navigate by hero id', () => {
const hero: Hero = {id: 42, name: 'Abbracadabra' };
const spy = spyOn(router, 'navigateByUrl');
comp.gotoDetail(hero);
const navArgs = spy.calls.mostRecent().args[0];
expect(navArgs).toBe('/heroes/42', 'should nav to HeroDetail for Hero 42');
});
});
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/dashboard/dashboard.component.spec.ts]" value="import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing';
import { addMatchers, asyncData, click } from '../../testing';
import { HeroService } from '../model/hero.service';
import { getTestHeroes } from '../model/testing/test-heroes';
import { By } from '@angular/platform-browser';
import { Router } from '@angular/router';
import { DashboardComponent } from './dashboard.component';
import { DashboardModule } from './dashboard.module';
beforeEach(addMatchers);
let comp: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
//////// Deep ////////////////
describe('DashboardComponent (deep)', () => {
beforeEach(() => {
TestBed.configureTestingModule({imports: [DashboardModule]});
});
compileAndCreate();
tests(clickForDeep);
function clickForDeep() {
// get first <div class=&quot;hero&quot;>
const heroEl: HTMLElement = fixture.nativeElement.querySelector('.hero');
click(heroEl);
}
});
//////// Shallow ////////////////
import { NO_ERRORS_SCHEMA } from '@angular/core';
describe('DashboardComponent (shallow)', () => {
beforeEach(() => {
TestBed.configureTestingModule(
{declarations: [DashboardComponent], schemas: [NO_ERRORS_SCHEMA]});
});
compileAndCreate();
tests(clickForShallow);
function clickForShallow() {
// get first <dashboard-hero> DebugElement
const heroDe = fixture.debugElement.query(By.css('dashboard-hero'));
heroDe.triggerEventHandler('selected', comp.heroes[0]);
}
});
/** Add TestBed providers, compile, and create DashboardComponent */
function compileAndCreate() {
beforeEach(waitForAsync(() => {
const routerSpy = jasmine.createSpyObj('Router', ['navigateByUrl']);
const heroServiceSpy = jasmine.createSpyObj('HeroService', ['getHeroes']);
TestBed
.configureTestingModule({
providers: [
{provide: HeroService, useValue: heroServiceSpy}, {provide: Router, useValue: routerSpy}
]
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(DashboardComponent);
comp = fixture.componentInstance;
// getHeroes spy returns observable of test heroes
heroServiceSpy.getHeroes.and.returnValue(asyncData(getTestHeroes()));
});
}));
}
/**
* The (almost) same tests for both.
* Only change: the way that the first hero is clicked
*/
function tests(heroClick: () => void) {
it('should NOT have heroes before ngOnInit', () => {
expect(comp.heroes.length).toBe(0, 'should not have heroes before ngOnInit');
});
it('should NOT have heroes immediately after ngOnInit', () => {
fixture.detectChanges(); // runs initial lifecycle hooks
expect(comp.heroes.length).toBe(0, 'should not have heroes until service promise resolves');
});
describe('after get dashboard heroes', () => {
let router: Router;
// Trigger component so it gets heroes and binds to them
beforeEach(waitForAsync(() => {
router = fixture.debugElement.injector.get(Router);
fixture.detectChanges(); // runs ngOnInit -> getHeroes
fixture.whenStable() // No need for the `lastPromise` hack!
.then(() => fixture.detectChanges()); // bind to heroes
}));
it('should HAVE heroes', () => {
expect(comp.heroes.length)
.toBeGreaterThan(0, 'should have heroes after service promise resolves');
});
it('should DISPLAY heroes', () => {
// Find and examine the displayed heroes
// Look for them in the DOM by css class
const heroes = fixture.nativeElement.querySelectorAll('dashboard-hero');
expect(heroes.length).toBe(4, 'should display 4 heroes');
});
it('should tell ROUTER to navigate when hero clicked', () => {
heroClick(); // trigger click on first inner <div class=&quot;hero&quot;>
// args passed to router.navigateByUrl() spy
const spy = router.navigateByUrl as jasmine.Spy;
const navArgs = spy.calls.first().args[0];
// expecting to navigate to id of the component's first hero
const id = comp.heroes[0].id;
expect(navArgs).toBe('/heroes/' + id, 'should nav to HeroDetail for first hero');
});
});
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/dashboard/dashboard.component.ts]" value="import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Hero } from '../model/hero';
import { HeroService } from '../model/hero.service';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: [ './dashboard.component.css' ]
})
export class DashboardComponent implements OnInit {
heroes: Hero[] = [];
constructor(
private router: Router,
private heroService: HeroService) {
}
ngOnInit() {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes.slice(1, 5));
}
gotoDetail(hero: Hero) {
const url = `/heroes/${hero.id}`;
this.router.navigateByUrl(url);
}
get title() {
const cnt = this.heroes.length;
return cnt === 0 ? 'No Heroes' :
cnt === 1 ? 'Top Hero' : `Top ${cnt} Heroes`;
}
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/dashboard/dashboard.module.ts]" value="import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SharedModule } from '../shared/shared.module';
import { DashboardComponent } from './dashboard.component';
import { DashboardHeroComponent } from './dashboard-hero.component';
const routes: Routes = [
{ path: 'dashboard', component: DashboardComponent },
];
@NgModule({
imports: [
SharedModule,
RouterModule.forChild(routes)
],
declarations: [ DashboardComponent, DashboardHeroComponent ]
})
export class DashboardModule { }
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/demo/async-helper.spec.ts]" value="// tslint:disable-next-line:no-unused-variable
import {fakeAsync, tick, waitForAsync} from '@angular/core/testing';
import {interval, of} from 'rxjs';
import {delay, take} from 'rxjs/operators';
describe('Angular async helper', () => {
describe('async', () => {
let actuallyDone = false;
beforeEach(() => {
actuallyDone = false;
});
afterEach(() => {
expect(actuallyDone).toBe(true, 'actuallyDone should be true');
});
it('should run normal test', () => {
actuallyDone = true;
});
it('should run normal async test', (done: DoneFn) => {
setTimeout(() => {
actuallyDone = true;
done();
}, 0);
});
it('should run async test with task', waitForAsync(() => {
setTimeout(() => {
actuallyDone = true;
}, 0);
}));
it('should run async test with task', waitForAsync(() => {
const id = setInterval(() => {
actuallyDone = true;
clearInterval(id);
}, 100);
}));
it('should run async test with successful promise', waitForAsync(() => {
const p = new Promise(resolve => {
setTimeout(resolve, 10);
});
p.then(() => {
actuallyDone = true;
});
}));
it('should run async test with failed promise', waitForAsync(() => {
const p = new Promise((resolve, reject) => {
setTimeout(reject, 10);
});
p.catch(() => {
actuallyDone = true;
});
}));
// Use done. Can also use async or fakeAsync.
it('should run async test with successful delayed Observable', (done: DoneFn) => {
const source = of(true).pipe(delay(10));
source.subscribe(val => actuallyDone = true, err => fail(err), done);
});
it('should run async test with successful delayed Observable', waitForAsync(() => {
const source = of(true).pipe(delay(10));
source.subscribe(val => actuallyDone = true, err => fail(err));
}));
it('should run async test with successful delayed Observable', fakeAsync(() => {
const source = of(true).pipe(delay(10));
source.subscribe(val => actuallyDone = true, err => fail(err));
tick(10);
}));
});
describe('fakeAsync', () => {
it('should run timeout callback with delay after call tick with millis', fakeAsync(() => {
let called = false;
setTimeout(() => {
called = true;
}, 100);
tick(100);
expect(called).toBe(true);
}));
it('should run new macro task callback with delay after call tick with millis',
fakeAsync(() => {
function nestedTimer(cb: () => any): void {
setTimeout(() => setTimeout(() => cb()));
}
const callback = jasmine.createSpy('callback');
nestedTimer(callback);
expect(callback).not.toHaveBeenCalled();
tick(0);
// the nested timeout will also be triggered
expect(callback).toHaveBeenCalled();
}));
it('should not run new macro task callback with delay after call tick with millis',
fakeAsync(() => {
function nestedTimer(cb: () => any): void {
setTimeout(() => setTimeout(() => cb()));
}
const callback = jasmine.createSpy('callback');
nestedTimer(callback);
expect(callback).not.toHaveBeenCalled();
tick(0, {processNewMacroTasksSynchronously: false});
// the nested timeout will not be triggered
expect(callback).not.toHaveBeenCalled();
tick(0);
expect(callback).toHaveBeenCalled();
}));
it('should get Date diff correctly in fakeAsync', fakeAsync(() => {
const start = Date.now();
tick(100);
const end = Date.now();
expect(end - start).toBe(100);
}));
it('should get Date diff correctly in fakeAsync with rxjs scheduler', fakeAsync(() => {
// need to add `import 'zone.js/plugins/zone-patch-rxjs-fake-async'
// to patch rxjs scheduler
let result = null;
of('hello').pipe(delay(1000)).subscribe(v => {
result = v;
});
expect(result).toBeNull();
tick(1000);
expect(result).toBe('hello');
const start = new Date().getTime();
let dateDiff = 0;
interval(1000).pipe(take(2)).subscribe(() => dateDiff = (new Date().getTime() - start));
tick(1000);
expect(dateDiff).toBe(1000);
tick(1000);
expect(dateDiff).toBe(2000);
}));
});
describe('use jasmine.clock()', () => {
// need to config __zone_symbol__fakeAsyncPatchLock flag
// before loading zone.js/testing
beforeEach(() => {
jasmine.clock().install();
});
afterEach(() => {
jasmine.clock().uninstall();
});
it('should auto enter fakeAsync', () => {
// is in fakeAsync now, don't need to call fakeAsync(testFn)
let called = false;
setTimeout(() => {
called = true;
}, 100);
jasmine.clock().tick(100);
expect(called).toBe(true);
});
});
describe('test jsonp', () => {
function jsonp(url: string, callback: () => void) {
// do a jsonp call which is not zone aware
}
// need to config __zone_symbol__supportWaitUnResolvedChainedPromise flag
// before loading zone.js/testing
it('should wait until promise.then is called', waitForAsync(() => {
let finished = false;
new Promise<void>(res => {
jsonp('localhost:8080/jsonp', () => {
// success callback and resolve the promise
finished = true;
res();
});
}).then(() => {
// async will wait until promise.then is called
// if __zone_symbol__supportWaitUnResolvedChainedPromise is set
expect(finished).toBe(true);
});
}));
});
});
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/demo/demo-main.ts]" value="// main app entry point
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { DemoModule } from './demo';
platformBrowserDynamic().bootstrapModule(DemoModule);
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/demo/demo.spec.ts]" value="import {
LightswitchComponent,
MasterService,
ValueService,
ReversePipe
} from './demo';
///////// Fakes /////////
export class FakeValueService extends ValueService {
value = 'faked service value';
}
////////////////////////
describe('demo (no TestBed):', () => {
// Straight Jasmine testing without Angular's testing support
describe('ValueService', () => {
let service: ValueService;
beforeEach(() => { service = new ValueService(); });
it('#getValue should return real value', () => {
expect(service.getValue()).toBe('real value');
});
it('#getObservableValue should return value from observable',
(done: DoneFn) => {
service.getObservableValue().subscribe(value => {
expect(value).toBe('observable value');
done();
});
});
it('#getPromiseValue should return value from a promise',
(done: DoneFn) => {
service.getPromiseValue().then(value => {
expect(value).toBe('promise value');
done();
});
});
});
// MasterService requires injection of a ValueService
describe('MasterService without Angular testing support', () => {
let masterService: MasterService;
it('#getValue should return real value from the real service', () => {
masterService = new MasterService(new ValueService());
expect(masterService.getValue()).toBe('real value');
});
it('#getValue should return faked value from a fakeService', () => {
masterService = new MasterService(new FakeValueService());
expect(masterService.getValue()).toBe('faked service value');
});
it('#getValue should return faked value from a fake object', () => {
const fake = { getValue: () => 'fake value' };
masterService = new MasterService(fake as ValueService);
expect(masterService.getValue()).toBe('fake value');
});
it('#getValue should return stubbed value from a spy', () => {
// create `getValue` spy on an object representing the ValueService
const valueServiceSpy =
jasmine.createSpyObj('ValueService', ['getValue']);
// set the value to return when the `getValue` spy is called.
const stubValue = 'stub value';
valueServiceSpy.getValue.and.returnValue(stubValue);
masterService = new MasterService(valueServiceSpy);
expect(masterService.getValue())
.toBe(stubValue, 'service returned stub value');
expect(valueServiceSpy.getValue.calls.count())
.toBe(1, 'spy method was called once');
expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
.toBe(stubValue);
});
});
describe('MasterService (no beforeEach)', () => {
it('#getValue should return stubbed value from a spy', () => {
const { masterService, stubValue, valueServiceSpy } = setup();
expect(masterService.getValue())
.toBe(stubValue, 'service returned stub value');
expect(valueServiceSpy.getValue.calls.count())
.toBe(1, 'spy method was called once');
expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
.toBe(stubValue);
});
function setup() {
const valueServiceSpy =
jasmine.createSpyObj('ValueService', ['getValue']);
const stubValue = 'stub value';
const masterService = new MasterService(valueServiceSpy);
valueServiceSpy.getValue.and.returnValue(stubValue);
return { masterService, stubValue, valueServiceSpy };
}
});
describe('ReversePipe', () => {
let pipe: ReversePipe;
beforeEach(() => { pipe = new ReversePipe(); });
it('transforms &quot;abc&quot; to &quot;cba&quot;', () => {
expect(pipe.transform('abc')).toBe('cba');
});
it('no change to palindrome: &quot;able was I ere I saw elba&quot;', () => {
const palindrome = 'able was I ere I saw elba';
expect(pipe.transform(palindrome)).toBe(palindrome);
});
});
describe('LightswitchComp', () => {
it('#clicked() should toggle #isOn', () => {
const comp = new LightswitchComponent();
expect(comp.isOn).toBe(false, 'off at first');
comp.clicked();
expect(comp.isOn).toBe(true, 'on after click');
comp.clicked();
expect(comp.isOn).toBe(false, 'off after second click');
});
it('#clicked() should set #message to &quot;is on&quot;', () => {
const comp = new LightswitchComponent();
expect(comp.message).toMatch(/is off/i, 'off at first');
comp.clicked();
expect(comp.message).toMatch(/is on/i, 'on after clicked');
});
});
});
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/demo/demo.testbed.spec.ts]" value="import {
DemoModule,
BankAccountComponent, BankAccountParentComponent,
LightswitchComponent,
Child1Component, Child2Component, Child3Component,
MasterService,
ValueService,
ExternalTemplateComponent,
InputComponent,
IoComponent, IoParentComponent,
MyIfComponent, MyIfChildComponent, MyIfParentComponent,
NeedsContentComponent, ParentComponent,
TestProvidersComponent, TestViewProvidersComponent,
ReversePipeComponent, ShellComponent
} from './demo';
import { By } from '@angular/platform-browser';
import { Component,
DebugElement,
Injectable } from '@angular/core';
import { FormsModule } from '@angular/forms';
// Forms symbols imported only for a specific test below
import { NgModel, NgControl } from '@angular/forms';
import {
ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync
} from '@angular/core/testing';
import { addMatchers, click } from '../../testing';
export class NotProvided extends ValueService { /* example below */ }
beforeEach(addMatchers);
describe('demo (with TestBed):', () => {
//////// Service Tests /////////////
describe('ValueService', () => {
let service: ValueService;
beforeEach(() => {
TestBed.configureTestingModule({ providers: [ValueService] });
service = TestBed.inject(ValueService);
});
it('should use ValueService', () => {
service = TestBed.inject(ValueService);
expect(service.getValue()).toBe('real value');
});
it('can inject a default value when service is not provided', () => {
service = TestBed.inject(NotProvided, null); // service is null
});
it('test should wait for ValueService.getPromiseValue', waitForAsync(() => {
service.getPromiseValue().then(
value => expect(value).toBe('promise value')
);
}));
it('test should wait for ValueService.getObservableValue', waitForAsync(() => {
service.getObservableValue().subscribe(
value => expect(value).toBe('observable value')
);
}));
// Must use done. See https://github.com/angular/angular/issues/10127
it('test should wait for ValueService.getObservableDelayValue', (done: DoneFn) => {
service.getObservableDelayValue().subscribe(value => {
expect(value).toBe('observable delay value');
done();
});
});
it('should allow the use of fakeAsync', fakeAsync(() => {
let value: any;
service.getPromiseValue().then((val: any) => value = val);
tick(); // Trigger JS engine cycle until all promises resolve.
expect(value).toBe('promise value');
}));
});
describe('MasterService', () => {
let masterService: MasterService;
let valueServiceSpy: jasmine.SpyObj<ValueService>;
beforeEach(() => {
const spy = jasmine.createSpyObj('ValueService', ['getValue']);
TestBed.configureTestingModule({
// Provide both the service-to-test and its (spy) dependency
providers: [
MasterService,
{ provide: ValueService, useValue: spy }
]
});
// Inject both the service-to-test and its (spy) dependency
masterService = TestBed.inject(MasterService);
valueServiceSpy = TestBed.inject(ValueService) as jasmine.SpyObj<ValueService>;
});
it('#getValue should return stubbed value from a spy', () => {
const stubValue = 'stub value';
valueServiceSpy.getValue.and.returnValue(stubValue);
expect(masterService.getValue())
.toBe(stubValue, 'service returned stub value');
expect(valueServiceSpy.getValue.calls.count())
.toBe(1, 'spy method was called once');
expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
.toBe(stubValue);
});
});
describe('use inject within `it`', () => {
beforeEach(() => {
TestBed.configureTestingModule({ providers: [ValueService] });
});
it('should use modified providers',
inject([ValueService], (service: ValueService) => {
service.setValue('value modified in beforeEach');
expect(service.getValue())
.toBe('value modified in beforeEach');
})
);
});
describe('using waitForAsync(inject) within beforeEach', () => {
let serviceValue: string;
beforeEach(() => {
TestBed.configureTestingModule({ providers: [ValueService] });
});
beforeEach(waitForAsync(inject([ValueService], (service: ValueService) => {
service.getPromiseValue().then(value => serviceValue = value);
})));
it('should use asynchronously modified value ... in synchronous test', () => {
expect(serviceValue).toBe('promise value');
});
});
/////////// Component Tests //////////////////
describe('TestBed component tests', () => {
beforeEach(waitForAsync(() => {
TestBed
.configureTestingModule({
imports: [DemoModule],
})
// Compile everything in DemoModule
.compileComponents();
}));
it('should create a component with inline template', () => {
const fixture = TestBed.createComponent(Child1Component);
fixture.detectChanges();
expect(fixture).toHaveText('Child');
});
it('should create a component with external template', () => {
const fixture = TestBed.createComponent(ExternalTemplateComponent);
fixture.detectChanges();
expect(fixture).toHaveText('from external template');
});
it('should allow changing members of the component', () => {
const fixture = TestBed.createComponent(MyIfComponent);
fixture.detectChanges();
expect(fixture).toHaveText('MyIf()');
fixture.componentInstance.showMore = true;
fixture.detectChanges();
expect(fixture).toHaveText('MyIf(More)');
});
it('should create a nested component bound to inputs/outputs', () => {
const fixture = TestBed.createComponent(IoParentComponent);
fixture.detectChanges();
const heroes = fixture.debugElement.queryAll(By.css('.hero'));
expect(heroes.length).toBeGreaterThan(0, 'has heroes');
const comp = fixture.componentInstance;
const hero = comp.heroes[0];
click(heroes[0]);
fixture.detectChanges();
const selected = fixture.debugElement.query(By.css('p'));
expect(selected).toHaveText(hero.name);
});
it('can access the instance variable of an `*ngFor` row component', () => {
const fixture = TestBed.createComponent(IoParentComponent);
const comp = fixture.componentInstance;
const heroName = comp.heroes[0].name; // first hero's name
fixture.detectChanges();
const ngForRow = fixture.debugElement.query(By.directive(IoComponent)); // first hero ngForRow
const hero = ngForRow.context.hero; // the hero object passed into the row
expect(hero.name).toBe(heroName, 'ngRow.context.hero');
const rowComp = ngForRow.componentInstance;
// jasmine.any is an &quot;instance-of-type&quot; test.
expect(rowComp).toEqual(jasmine.any(IoComponent), 'component is IoComp');
expect(rowComp.hero.name).toBe(heroName, 'component.hero');
});
it('should support clicking a button', () => {
const fixture = TestBed.createComponent(LightswitchComponent);
const btn = fixture.debugElement.query(By.css('button'));
const span = fixture.debugElement.query(By.css('span')).nativeElement;
fixture.detectChanges();
expect(span.textContent).toMatch(/is off/i, 'before click');
click(btn);
fixture.detectChanges();
expect(span.textContent).toMatch(/is on/i, 'after click');
});
// ngModel is async so we must wait for it with promise-based `whenStable`
it('should support entering text in input box (ngModel)', waitForAsync(() => {
const expectedOrigName = 'John';
const expectedNewName = 'Sally';
const fixture = TestBed.createComponent(InputComponent);
fixture.detectChanges();
const comp = fixture.componentInstance;
const input = fixture.debugElement.query(By.css('input')).nativeElement as HTMLInputElement;
expect(comp.name).toBe(expectedOrigName,
`At start name should be ${expectedOrigName} `);
// wait until ngModel binds comp.name to input box
fixture.whenStable().then(() => {
expect(input.value).toBe(expectedOrigName,
`After ngModel updates input box, input.value should be ${expectedOrigName} `);
// simulate user entering new name in input
input.value = expectedNewName;
// that change doesn't flow to the component immediately
expect(comp.name).toBe(expectedOrigName,
`comp.name should still be ${expectedOrigName} after value change, before binding happens`);
// Dispatch a DOM event so that Angular learns of input value change.
// then wait while ngModel pushes input.box value to comp.name
// In older browsers, such as IE, you might need a CustomEvent instead. See
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
input.dispatchEvent(new Event('input'));
return fixture.whenStable();
})
.then(() => {
expect(comp.name).toBe(expectedNewName,
`After ngModel updates the model, comp.name should be ${expectedNewName} `);
});
}));
// fakeAsync version of ngModel input test enables sync test style
// synchronous `tick` replaces asynchronous promise-base `whenStable`
it('should support entering text in input box (ngModel) - fakeAsync', fakeAsync(() => {
const expectedOrigName = 'John';
const expectedNewName = 'Sally';
const fixture = TestBed.createComponent(InputComponent);
fixture.detectChanges();
const comp = fixture.componentInstance;
const input = fixture.debugElement.query(By.css('input')).nativeElement as HTMLInputElement;
expect(comp.name).toBe(expectedOrigName,
`At start name should be ${expectedOrigName} `);
// wait until ngModel binds comp.name to input box
tick();
expect(input.value).toBe(expectedOrigName,
`After ngModel updates input box, input.value should be ${expectedOrigName} `);
// simulate user entering new name in input
input.value = expectedNewName;
// that change doesn't flow to the component immediately
expect(comp.name).toBe(expectedOrigName,
`comp.name should still be ${expectedOrigName} after value change, before binding happens`);
// Dispatch a DOM event so that Angular learns of input value change.
// then wait a tick while ngModel pushes input.box value to comp.name
// In older browsers, such as IE, you might need a CustomEvent instead. See
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
input.dispatchEvent(new Event('input'));
tick();
expect(comp.name).toBe(expectedNewName,
`After ngModel updates the model, comp.name should be ${expectedNewName} `);
}));
it('ReversePipeComp should reverse the input text', fakeAsync(() => {
const inputText = 'the quick brown fox.';
const expectedText = '.xof nworb kciuq eht';
const fixture = TestBed.createComponent(ReversePipeComponent);
fixture.detectChanges();
const comp = fixture.componentInstance;
const input = fixture.debugElement.query(By.css('input')).nativeElement as HTMLInputElement;
const span = fixture.debugElement.query(By.css('span')).nativeElement as HTMLElement;
// simulate user entering new name in input
input.value = inputText;
// Dispatch a DOM event so that Angular learns of input value change.
// then wait a tick while ngModel pushes input.box value to comp.text
// and Angular updates the output span
// In older browsers, such as IE, you might need a CustomEvent instead. See
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
input.dispatchEvent(new Event('input'));
tick();
fixture.detectChanges();
expect(span.textContent).toBe(expectedText, 'output span');
expect(comp.text).toBe(inputText, 'component.text');
}));
// Use this technique to find attached directives of any kind
it('can examine attached directives and listeners', () => {
const fixture = TestBed.createComponent(InputComponent);
fixture.detectChanges();
const inputEl = fixture.debugElement.query(By.css('input'));
expect(inputEl.providerTokens).toContain(NgModel, 'NgModel directive');
const ngControl = inputEl.injector.get(NgControl);
expect(ngControl).toEqual(jasmine.any(NgControl), 'NgControl directive');
expect(inputEl.listeners.length).toBeGreaterThan(2, 'several listeners attached');
});
it('BankAccountComponent should set attributes, styles, classes, and properties', () => {
const fixture = TestBed.createComponent(BankAccountParentComponent);
fixture.detectChanges();
const comp = fixture.componentInstance;
// the only child is debugElement of the BankAccount component
const el = fixture.debugElement.children[0];
const childComp = el.componentInstance as BankAccountComponent;
expect(childComp).toEqual(jasmine.any(BankAccountComponent));
expect(el.context).toBe(childComp, 'context is the child component');
expect(el.attributes.account).toBe(childComp.id, 'account attribute');
expect(el.attributes.bank).toBe(childComp.bank, 'bank attribute');
expect(el.classes.closed).toBe(true, 'closed class');
expect(el.classes.open).toBeFalsy('open class');
expect(el.styles.color).toBe(comp.color, 'color style');
expect(el.styles.width).toBe(comp.width + 'px', 'width style');
// Removed on 12/02/2016 when ceased public discussion of the `Renderer`. Revive in future?
// expect(el.properties['customProperty']).toBe(true, 'customProperty');
});
});
describe('TestBed component overrides:', () => {
it('should override ChildComp\'s template', () => {
const fixture = TestBed.configureTestingModule({
declarations: [Child1Component],
})
.overrideComponent(Child1Component, {
set: { template: '<span>Fake</span>' }
})
.createComponent(Child1Component);
fixture.detectChanges();
expect(fixture).toHaveText('Fake');
});
it('should override TestProvidersComp\'s ValueService provider', () => {
const fixture = TestBed.configureTestingModule({
declarations: [TestProvidersComponent],
})
.overrideComponent(TestProvidersComponent, {
remove: { providers: [ValueService] },
add: { providers: [{ provide: ValueService, useClass: FakeValueService }] },
// Or replace them all (this component has only one provider)
// set: { providers: [{ provide: ValueService, useClass: FakeValueService }] },
})
.createComponent(TestProvidersComponent);
fixture.detectChanges();
expect(fixture).toHaveText('injected value: faked value', 'text');
// Explore the providerTokens
const tokens = fixture.debugElement.providerTokens;
expect(tokens).toContain(fixture.componentInstance.constructor, 'component ctor');
expect(tokens).toContain(TestProvidersComponent, 'TestProvidersComp');
expect(tokens).toContain(ValueService, 'ValueService');
});
it('should override TestViewProvidersComp\'s ValueService viewProvider', () => {
const fixture = TestBed.configureTestingModule({
declarations: [TestViewProvidersComponent],
})
.overrideComponent(TestViewProvidersComponent, {
// remove: { viewProviders: [ValueService]},
// add: { viewProviders: [{ provide: ValueService, useClass: FakeValueService }] },
// Or replace them all (this component has only one viewProvider)
set: { viewProviders: [{ provide: ValueService, useClass: FakeValueService }] },
})
.createComponent(TestViewProvidersComponent);
fixture.detectChanges();
expect(fixture).toHaveText('injected value: faked value');
});
it('injected provider should not be same as component\'s provider', () => {
// TestComponent is parent of TestProvidersComponent
@Component({ template: '<my-service-comp></my-service-comp>' })
class TestComponent { }
// 3 levels of ValueService provider: module, TestCompomponent, TestProvidersComponent
const fixture = TestBed.configureTestingModule({
declarations: [TestComponent, TestProvidersComponent],
providers: [ValueService]
})
.overrideComponent(TestComponent, {
set: { providers: [{ provide: ValueService, useValue: {} }] }
})
.overrideComponent(TestProvidersComponent, {
set: { providers: [{ provide: ValueService, useClass: FakeValueService }] }
})
.createComponent(TestComponent);
let testBedProvider: ValueService;
let tcProvider: ValueService;
let tpcProvider: FakeValueService;
// `inject` uses TestBed's injector
inject([ValueService], (s: ValueService) => testBedProvider = s)();
tcProvider = fixture.debugElement.injector.get(ValueService) as ValueService;
tpcProvider = fixture.debugElement.children[0].injector.get(ValueService) as FakeValueService;
expect(testBedProvider).not.toBe(tcProvider, 'testBed/tc not same providers');
expect(testBedProvider).not.toBe(tpcProvider, 'testBed/tpc not same providers');
expect(testBedProvider instanceof ValueService).toBe(true, 'testBedProvider is ValueService');
expect(tcProvider).toEqual({} as ValueService, 'tcProvider is {}');
expect(tpcProvider instanceof FakeValueService).toBe(true, 'tpcProvider is FakeValueService');
});
it('can access template local variables as references', () => {
const fixture = TestBed.configureTestingModule({
declarations: [ShellComponent, NeedsContentComponent, Child1Component, Child2Component, Child3Component],
})
.overrideComponent(ShellComponent, {
set: {
selector: 'test-shell',
template: `
<needs-content #nc>
<child-1 #content text=&quot;My&quot;></child-1>
<child-2 #content text=&quot;dog&quot;></child-2>
<child-2 text=&quot;has&quot;></child-2>
<child-3 #content text=&quot;fleas&quot;></child-3>
<div #content>!</div>
</needs-content>
`
}
})
.createComponent(ShellComponent);
fixture.detectChanges();
// NeedsContentComp is the child of ShellComp
const el = fixture.debugElement.children[0];
const comp = el.componentInstance;
expect(comp.children.toArray().length).toBe(4,
'three different child components and an ElementRef with #content');
expect(el.references.nc).toBe(comp, '#nc reference to component');
// Filter for DebugElements with a #content reference
const contentRefs = el.queryAll( de => de.references.content);
expect(contentRefs.length).toBe(4, 'elements w/ a #content reference');
});
});
describe('nested (one-deep) component override', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ParentComponent, FakeChildComponent]
});
});
it('ParentComp should use Fake Child component', () => {
const fixture = TestBed.createComponent(ParentComponent);
fixture.detectChanges();
expect(fixture).toHaveText('Parent(Fake Child)');
});
});
describe('nested (two-deep) component override', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ParentComponent, FakeChildWithGrandchildComponent, FakeGrandchildComponent]
});
});
it('should use Fake Grandchild component', () => {
const fixture = TestBed.createComponent(ParentComponent);
fixture.detectChanges();
expect(fixture).toHaveText('Parent(Fake Child(Fake Grandchild))');
});
});
describe('lifecycle hooks w/ MyIfParentComp', () => {
let fixture: ComponentFixture<MyIfParentComponent>;
let parent: MyIfParentComponent;
let child: MyIfChildComponent;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [FormsModule],
declarations: [MyIfChildComponent, MyIfParentComponent]
});
fixture = TestBed.createComponent(MyIfParentComponent);
parent = fixture.componentInstance;
});
it('should instantiate parent component', () => {
expect(parent).not.toBeNull('parent component should exist');
});
it('parent component OnInit should NOT be called before first detectChanges()', () => {
expect(parent.ngOnInitCalled).toBe(false);
});
it('parent component OnInit should be called after first detectChanges()', () => {
fixture.detectChanges();
expect(parent.ngOnInitCalled).toBe(true);
});
it('child component should exist after OnInit', () => {
fixture.detectChanges();
getChild();
expect(child instanceof MyIfChildComponent).toBe(true, 'should create child');
});
it('should have called child component\'s OnInit ', () => {
fixture.detectChanges();
getChild();
expect(child.ngOnInitCalled).toBe(true);
});
it('child component called OnChanges once', () => {
fixture.detectChanges();
getChild();
expect(child.ngOnChangesCounter).toBe(1);
});
it('changed parent value flows to child', () => {
fixture.detectChanges();
getChild();
parent.parentValue = 'foo';
fixture.detectChanges();
expect(child.ngOnChangesCounter).toBe(2,
'expected 2 changes: initial value and changed value');
expect(child.childValue).toBe('foo',
'childValue should eq changed parent value');
});
// must be async test to see child flow to parent
it('changed child value flows to parent', waitForAsync(() => {
fixture.detectChanges();
getChild();
child.childValue = 'bar';
return new Promise<void>(resolve => {
// Wait one JS engine turn!
setTimeout(() => resolve(), 0);
})
.then(() => {
fixture.detectChanges();
expect(child.ngOnChangesCounter).toBe(2,
'expected 2 changes: initial value and changed value');
expect(parent.parentValue).toBe('bar',
'parentValue should eq changed parent value');
});
}));
it('clicking &quot;Close Child&quot; triggers child OnDestroy', () => {
fixture.detectChanges();
getChild();
const btn = fixture.debugElement.query(By.css('button'));
click(btn);
fixture.detectChanges();
expect(child.ngOnDestroyCalled).toBe(true);
});
////// helpers ///
/**
* Get the MyIfChildComp from parent; fail w/ good message if cannot.
*/
function getChild() {
let childDe: DebugElement; // DebugElement that should hold the MyIfChildComp
// The Hard Way: requires detailed knowledge of the parent template
try {
childDe = fixture.debugElement.children[4].children[0];
} catch (err) { /* we'll report the error */ }
// DebugElement.queryAll: if we wanted all of many instances:
childDe = fixture.debugElement
.queryAll(de => de.componentInstance instanceof MyIfChildComponent)[0];
// WE'LL USE THIS APPROACH !
// DebugElement.query: find first instance (if any)
childDe = fixture.debugElement
.query(de => de.componentInstance instanceof MyIfChildComponent);
if (childDe &amp;&amp; childDe.componentInstance) {
child = childDe.componentInstance;
} else {
fail('Unable to find MyIfChildComp within MyIfParentComp');
}
return child;
}
});
});
////////// Fakes ///////////
@Component({
selector: 'child-1',
template: `Fake Child`
})
class FakeChildComponent { }
@Component({
selector: 'child-1',
template: `Fake Child(<grandchild-1></grandchild-1>)`
})
class FakeChildWithGrandchildComponent { }
@Component({
selector: 'grandchild-1',
template: `Fake Grandchild`
})
class FakeGrandchildComponent { }
@Injectable()
class FakeValueService extends ValueService {
value = 'faked value';
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/demo/demo.ts]" value="// tslint:disable: directive-selector forin no-input-rename
import { Component, ContentChildren, Directive, EventEmitter,
Injectable, Input, Output, Optional,
HostBinding, HostListener,
OnInit, OnChanges, OnDestroy,
Pipe, PipeTransform,
SimpleChanges } from '@angular/core';
import { of } from 'rxjs';
import { delay } from 'rxjs/operators';
////////// The App: Services and Components for the tests. //////////////
export interface Hero {
name: string;
}
////////// Services ///////////////
@Injectable()
export class ValueService {
value = 'real value';
getValue() { return this.value; }
setValue(value: string) { this.value = value; }
getObservableValue() { return of('observable value'); }
getPromiseValue() { return Promise.resolve('promise value'); }
getObservableDelayValue() {
return of('observable delay value').pipe(delay(10));
}
}
@Injectable()
export class MasterService {
constructor(private valueService: ValueService) { }
getValue() { return this.valueService.getValue(); }
}
/////////// Pipe ////////////////
/*
* Reverse the input string.
*/
@Pipe({ name: 'reverse' })
export class ReversePipe implements PipeTransform {
transform(s: string) {
let r = '';
for (let i = s.length; i; ) { r += s[--i]; }
return r;
}
}
//////////// Components /////////////
@Component({
selector: 'bank-account',
template: `
Bank Name: {{bank}}
Account Id: {{id}}
`
})
export class BankAccountComponent {
@Input() bank: string;
@Input('account') id: string;
// Removed on 12/02/2016 when ceased public discussion of the `Renderer`. Revive in future?
// constructor(private renderer: Renderer, private el: ElementRef ) {
// renderer.setElementProperty(el.nativeElement, 'customProperty', true);
// }
}
/** A component with attributes, styles, classes, and property setting */
@Component({
selector: 'bank-account-parent',
template: `
<bank-account
bank=&quot;RBC&quot;
account=&quot;4747&quot;
[style.width.px]=&quot;width&quot;
[style.color]=&quot;color&quot;
[class.closed]=&quot;isClosed&quot;
[class.open]=&quot;!isClosed&quot;>
</bank-account>
`
})
export class BankAccountParentComponent {
width = 200;
color = 'red';
isClosed = true;
}
@Component({
selector: 'lightswitch-comp',
template: `
<button (click)=&quot;clicked()&quot;>Click me!</button>
<span>{{message}}</span>`
})
export class LightswitchComponent {
isOn = false;
clicked() { this.isOn = !this.isOn; }
get message() { return `The light is ${this.isOn ? 'On' : 'Off'}`; }
}
@Component({
selector: 'child-1',
template: `<span>Child-1({{text}})</span>`
})
export class Child1Component {
@Input() text = 'Original';
}
@Component({
selector: 'child-2',
template: '<div>Child-2({{text}})</div>'
})
export class Child2Component {
@Input() text: string;
}
@Component({
selector: 'child-3',
template: '<div>Child-3({{text}})</div>'
})
export class Child3Component {
@Input() text: string;
}
@Component({
selector: 'input-comp',
template: `<input [(ngModel)]=&quot;name&quot;>`
})
export class InputComponent {
name = 'John';
}
/* Prefer this metadata syntax */
// @Directive({
// selector: 'input[value]',
// host: {
// '[value]': 'value',
// '(input)': 'valueChange.emit($event.target.value)'
// },
// inputs: ['value'],
// outputs: ['valueChange']
// })
// export class InputValueBinderDirective {
// value: any;
// valueChange: EventEmitter<any> = new EventEmitter();
// }
// As the styleguide recommends
@Directive({ selector: 'input[value]' })
export class InputValueBinderDirective {
@HostBinding()
@Input()
value: any;
@Output()
valueChange: EventEmitter<any> = new EventEmitter();
@HostListener('input', ['$event.target.value'])
onInput(value: any) { this.valueChange.emit(value); }
}
@Component({
selector: 'input-value-comp',
template: `
Name: <input [(value)]=&quot;name&quot;> {{name}}
`
})
export class InputValueBinderComponent {
name = 'Sally'; // initial value
}
@Component({
selector: 'parent-comp',
template: `Parent(<child-1></child-1>)`
})
export class ParentComponent { }
@Component({
selector: 'io-comp',
template: `<div class=&quot;hero&quot; (click)=&quot;click()&quot;>Original {{hero.name}}</div>`
})
export class IoComponent {
@Input() hero: Hero;
@Output() selected = new EventEmitter<Hero>();
click() { this.selected.emit(this.hero); }
}
@Component({
selector: 'io-parent-comp',
template: `
<p *ngIf=&quot;!selectedHero&quot;><i>Click to select a hero</i></p>
<p *ngIf=&quot;selectedHero&quot;>The selected hero is {{selectedHero.name}}</p>
<io-comp
*ngFor=&quot;let hero of heroes&quot;
[hero]=hero
(selected)=&quot;onSelect($event)&quot;>
</io-comp>
`
})
export class IoParentComponent {
heroes: Hero[] = [ {name: 'Bob'}, {name: 'Carol'}, {name: 'Ted'}, {name: 'Alice'} ];
selectedHero: Hero;
onSelect(hero: Hero) { this.selectedHero = hero; }
}
@Component({
selector: 'my-if-comp',
template: `MyIf(<span *ngIf=&quot;showMore&quot;>More</span>)`
})
export class MyIfComponent {
showMore = false;
}
@Component({
selector: 'my-service-comp',
template: `injected value: {{valueService.value}}`,
providers: [ValueService]
})
export class TestProvidersComponent {
constructor(public valueService: ValueService) {}
}
@Component({
selector: 'my-service-comp',
template: `injected value: {{valueService.value}}`,
viewProviders: [ValueService]
})
export class TestViewProvidersComponent {
constructor(public valueService: ValueService) {}
}
@Component({
selector: 'external-template-comp',
templateUrl: './demo-external-template.html'
})
export class ExternalTemplateComponent implements OnInit {
serviceValue: string;
constructor(@Optional() private service?: ValueService) { }
ngOnInit() {
if (this.service) { this.serviceValue = this.service.getValue(); }
}
}
@Component({
selector: 'comp-w-ext-comp',
template: `
<h3>comp-w-ext-comp</h3>
<external-template-comp></external-template-comp>
`
})
export class InnerCompWithExternalTemplateComponent { }
@Component({selector: 'needs-content', template: '<ng-content></ng-content>'})
export class NeedsContentComponent {
// children with #content local variable
@ContentChildren('content') children: any;
}
///////// MyIfChildComp ////////
@Component({
selector: 'my-if-child-1',
template: `
<h4>MyIfChildComp</h4>
<div>
<label>Child value: <input [(ngModel)]=&quot;childValue&quot;> </label>
</div>
<p><i>Change log:</i></p>
<div *ngFor=&quot;let log of changeLog; let i=index&quot;>{{i + 1}} - {{log}}</div>`
})
export class MyIfChildComponent implements OnInit, OnChanges, OnDestroy {
@Input() value = '';
@Output() valueChange = new EventEmitter<string>();
get childValue() { return this.value; }
set childValue(v: string) {
if (this.value === v) { return; }
this.value = v;
this.valueChange.emit(v);
}
changeLog: string[] = [];
ngOnInitCalled = false;
ngOnChangesCounter = 0;
ngOnDestroyCalled = false;
ngOnInit() {
this.ngOnInitCalled = true;
this.changeLog.push('ngOnInit called');
}
ngOnDestroy() {
this.ngOnDestroyCalled = true;
this.changeLog.push('ngOnDestroy called');
}
ngOnChanges(changes: SimpleChanges) {
for (const propName in changes) {
this.ngOnChangesCounter += 1;
const prop = changes[propName];
const cur = JSON.stringify(prop.currentValue);
const prev = JSON.stringify(prop.previousValue);
this.changeLog.push(`${propName}: currentValue = ${cur}, previousValue = ${prev}`);
}
}
}
///////// MyIfParentComp ////////
@Component({
selector: 'my-if-parent-comp',
template: `
<h3>MyIfParentComp</h3>
<label>Parent value:
<input [(ngModel)]=&quot;parentValue&quot;>
</label>
<button (click)=&quot;clicked()&quot;>{{toggleLabel}} Child</button><br>
<div *ngIf=&quot;showChild&quot;
style=&quot;margin: 4px; padding: 4px; background-color: aliceblue;&quot;>
<my-if-child-1 [(value)]=&quot;parentValue&quot;></my-if-child-1>
</div>
`
})
export class MyIfParentComponent implements OnInit {
ngOnInitCalled = false;
parentValue = 'Hello, World';
showChild = false;
toggleLabel = 'Unknown';
ngOnInit() {
this.ngOnInitCalled = true;
this.clicked();
}
clicked() {
this.showChild = !this.showChild;
this.toggleLabel = this.showChild ? 'Close' : 'Show';
}
}
@Component({
selector: 'reverse-pipe-comp',
template: `
<input [(ngModel)]=&quot;text&quot;>
<span>{{text | reverse}}</span>
`
})
export class ReversePipeComponent {
text = 'my dog has fleas.';
}
@Component({template: '<div>Replace Me</div>'})
export class ShellComponent { }
@Component({
selector: 'demo-comp',
template: `
<h1>Specs Demo</h1>
<my-if-parent-comp></my-if-parent-comp>
<hr>
<h3>Input/Output Component</h3>
<io-parent-comp></io-parent-comp>
<hr>
<h3>External Template Component</h3>
<external-template-comp></external-template-comp>
<hr>
<h3>Component With External Template Component</h3>
<comp-w-ext-comp></comp-w-ext-comp>
<hr>
<h3>Reverse Pipe</h3>
<reverse-pipe-comp></reverse-pipe-comp>
<hr>
<h3>InputValueBinder Directive</h3>
<input-value-comp></input-value-comp>
<hr>
<h3>Button Component</h3>
<lightswitch-comp></lightswitch-comp>
<hr>
<h3>Needs Content</h3>
<needs-content #nc>
<child-1 #content text=&quot;My&quot;></child-1>
<child-2 #content text=&quot;dog&quot;></child-2>
<child-2 text=&quot;has&quot;></child-2>
<child-3 #content text=&quot;fleas&quot;></child-3>
<div #content>!</div>
</needs-content>
`
})
export class DemoComponent { }
//////// Aggregations ////////////
export const demoDeclarations = [
DemoComponent,
BankAccountComponent, BankAccountParentComponent,
LightswitchComponent,
Child1Component, Child2Component, Child3Component,
ExternalTemplateComponent, InnerCompWithExternalTemplateComponent,
InputComponent,
InputValueBinderDirective, InputValueBinderComponent,
IoComponent, IoParentComponent,
MyIfComponent, MyIfChildComponent, MyIfParentComponent,
NeedsContentComponent, ParentComponent,
TestProvidersComponent, TestViewProvidersComponent,
ReversePipe, ReversePipeComponent, ShellComponent
];
export const demoProviders = [MasterService, ValueService];
////////////////////
////////////
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
@NgModule({
imports: [BrowserModule, FormsModule],
declarations: demoDeclarations,
providers: demoProviders,
entryComponents: [DemoComponent],
bootstrap: [DemoComponent]
})
export class DemoModule { }
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/dummy.module.ts]" value="// These unused NgModules keep the Angular Language Service happy.
// The AppModule registers the final versions of these components
import { NgModule } from '@angular/core';
import { AppComponent as app_initial } from './app-initial.component';
@NgModule({ declarations: [ app_initial ] })
export class AppModuleInitial {}
import { BannerComponent as bc_initial } from './banner/banner-initial.component';
@NgModule({ declarations: [ bc_initial ] })
export class BannerModuleInitial {}
import { BannerComponent as bc_external } from './banner/banner-external.component';
@NgModule({ declarations: [ bc_external ] })
export class BannerModuleExternal {}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/hero/hero-detail.component.no-testbed.spec.ts]" value="import { asyncData, ActivatedRouteStub } from '../../testing';
import { HeroDetailComponent } from './hero-detail.component';
import { Hero } from '../model/hero';
////////// Tests ////////////////////
describe('HeroDetailComponent - no TestBed', () => {
let comp: HeroDetailComponent;
let expectedHero: Hero;
let hds: any;
let router: any;
beforeEach((done: DoneFn) => {
expectedHero = {id: 42, name: 'Bubba' };
const activatedRoute = new ActivatedRouteStub({ id: expectedHero.id });
router = jasmine.createSpyObj('router', ['navigate']);
hds = jasmine.createSpyObj('HeroDetailService', ['getHero', 'saveHero']);
hds.getHero.and.returnValue(asyncData(expectedHero));
hds.saveHero.and.returnValue(asyncData(expectedHero));
comp = new HeroDetailComponent(hds, activatedRoute as any, router);
comp.ngOnInit();
// OnInit calls HDS.getHero; wait for it to get the fake hero
hds.getHero.calls.first().returnValue.subscribe(done);
});
it('should expose the hero retrieved from the service', () => {
expect(comp.hero).toBe(expectedHero);
});
it('should navigate when click cancel', () => {
comp.cancel();
expect(router.navigate.calls.any()).toBe(true, 'router.navigate called');
});
it('should save when click save', () => {
comp.save();
expect(hds.saveHero.calls.any()).toBe(true, 'HeroDetailService.save called');
expect(router.navigate.calls.any()).toBe(false, 'router.navigate not called yet');
});
it('should navigate when click save resolves', (done: DoneFn) => {
comp.save();
// waits for async save to complete before navigating
hds.saveHero.calls.first().returnValue
.subscribe(() => {
expect(router.navigate.calls.any()).toBe(true, 'router.navigate called');
done();
});
});
});
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/hero/hero-detail.component.spec.ts]" value="import { ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { Router } from '@angular/router';
import {
ActivatedRoute, ActivatedRouteStub, asyncData, click
} from '../../testing';
import { Hero } from '../model/hero';
import { HeroDetailComponent } from './hero-detail.component';
import { HeroDetailService } from './hero-detail.service';
import { HeroModule } from './hero.module';
////// Testing Vars //////
let activatedRoute: ActivatedRouteStub;
let component: HeroDetailComponent;
let fixture: ComponentFixture<HeroDetailComponent>;
let page: Page;
////// Tests //////
describe('HeroDetailComponent', () => {
beforeEach(() => {
activatedRoute = new ActivatedRouteStub();
});
describe('with HeroModule setup', heroModuleSetup);
describe('when override its provided HeroDetailService', overrideSetup);
describe('with FormsModule setup', formsModuleSetup);
describe('with SharedModule setup', sharedModuleSetup);
});
///////////////////
function overrideSetup() {
class HeroDetailServiceSpy {
testHero: Hero = {id: 42, name: 'Test Hero'};
/* emit cloned test hero */
getHero = jasmine.createSpy('getHero').and.callFake(
() => asyncData(Object.assign({}, this.testHero)));
/* emit clone of test hero, with changes merged in */
saveHero = jasmine.createSpy('saveHero')
.and.callFake((hero: Hero) => asyncData(Object.assign(this.testHero, hero)));
}
// the `id` value is irrelevant because ignored by service stub
beforeEach(() => activatedRoute.setParamMap({id: 99999}));
beforeEach(waitForAsync(() => {
const routerSpy = createRouterSpy();
TestBed
.configureTestingModule({
imports: [HeroModule],
providers: [
{provide: ActivatedRoute, useValue: activatedRoute},
{provide: Router, useValue: routerSpy},
// HeroDetailService at this level is IRRELEVANT!
{provide: HeroDetailService, useValue: {}}
]
})
// Override component's own provider
.overrideComponent(
HeroDetailComponent,
{set: {providers: [{provide: HeroDetailService, useClass: HeroDetailServiceSpy}]}})
.compileComponents();
}));
let hdsSpy: HeroDetailServiceSpy;
beforeEach(waitForAsync(() => {
createComponent();
// get the component's injected HeroDetailServiceSpy
hdsSpy = fixture.debugElement.injector.get(HeroDetailService) as any;
}));
it('should have called `getHero`', () => {
expect(hdsSpy.getHero.calls.count()).toBe(1, 'getHero called once');
});
it('should display stub hero\'s name', () => {
expect(page.nameDisplay.textContent).toBe(hdsSpy.testHero.name);
});
it('should save stub hero change', fakeAsync(() => {
const origName = hdsSpy.testHero.name;
const newName = 'New Name';
page.nameInput.value = newName;
// In older browsers, such as IE, you might need a CustomEvent instead. See
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
page.nameInput.dispatchEvent(new Event('input')); // tell Angular
expect(component.hero.name).toBe(newName, 'component hero has new name');
expect(hdsSpy.testHero.name).toBe(origName, 'service hero unchanged before save');
click(page.saveBtn);
expect(hdsSpy.saveHero.calls.count()).toBe(1, 'saveHero called once');
tick(); // wait for async save to complete
expect(hdsSpy.testHero.name).toBe(newName, 'service hero has new name after save');
expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called');
}));
it('fixture injected service is not the component injected service',
// inject gets the service from the fixture
inject([HeroDetailService], (fixtureService: HeroDetailService) => {
// use `fixture.debugElement.injector` to get service from component
const componentService = fixture.debugElement.injector.get(HeroDetailService);
expect(fixtureService).not.toBe(componentService, 'service injected from fixture');
}));
}
////////////////////
import { getTestHeroes, TestHeroService, HeroService } from '../model/testing/test-hero.service';
const firstHero = getTestHeroes()[0];
function heroModuleSetup() {
beforeEach(waitForAsync(() => {
const routerSpy = createRouterSpy();
TestBed
.configureTestingModule({
imports: [HeroModule],
// declarations: [ HeroDetailComponent ], // NO! DOUBLE DECLARATION
providers: [
{provide: ActivatedRoute, useValue: activatedRoute},
{provide: HeroService, useClass: TestHeroService},
{provide: Router, useValue: routerSpy},
]
})
.compileComponents();
}));
describe('when navigate to existing hero', () => {
let expectedHero: Hero;
beforeEach(waitForAsync(() => {
expectedHero = firstHero;
activatedRoute.setParamMap({id: expectedHero.id});
createComponent();
}));
it('should display that hero\'s name', () => {
expect(page.nameDisplay.textContent).toBe(expectedHero.name);
});
it('should navigate when click cancel', () => {
click(page.cancelBtn);
expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called');
});
it('should save when click save but not navigate immediately', () => {
// Get service injected into component and spy on its`saveHero` method.
// It delegates to fake `HeroService.updateHero` which delivers a safe test result.
const hds = fixture.debugElement.injector.get(HeroDetailService);
const saveSpy = spyOn(hds, 'saveHero').and.callThrough();
click(page.saveBtn);
expect(saveSpy.calls.any()).toBe(true, 'HeroDetailService.save called');
expect(page.navigateSpy.calls.any()).toBe(false, 'router.navigate not called');
});
it('should navigate when click save and save resolves', fakeAsync(() => {
click(page.saveBtn);
tick(); // wait for async save to complete
expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called');
}));
it('should convert hero name to Title Case', () => {
// get the name's input and display elements from the DOM
const hostElement = fixture.nativeElement;
const nameInput: HTMLInputElement = hostElement.querySelector('input');
const nameDisplay: HTMLElement = hostElement.querySelector('span');
// simulate user entering a new name into the input box
nameInput.value = 'quick BROWN fOx';
// Dispatch a DOM event so that Angular learns of input value change.
// In older browsers, such as IE, you might need a CustomEvent instead. See
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
nameInput.dispatchEvent(new Event('input'));
// Tell Angular to update the display binding through the title pipe
fixture.detectChanges();
expect(nameDisplay.textContent).toBe('Quick Brown Fox');
});
});
describe('when navigate with no hero id', () => {
beforeEach(waitForAsync(createComponent));
it('should have hero.id === 0', () => {
expect(component.hero.id).toBe(0);
});
it('should display empty hero name', () => {
expect(page.nameDisplay.textContent).toBe('');
});
});
describe('when navigate to non-existent hero id', () => {
beforeEach(waitForAsync(() => {
activatedRoute.setParamMap({id: 99999});
createComponent();
}));
it('should try to navigate back to hero list', () => {
expect(page.gotoListSpy.calls.any()).toBe(true, 'comp.gotoList called');
expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called');
});
});
// Why we must use `fixture.debugElement.injector` in `Page()`
it('cannot use `inject` to get component\'s provided HeroDetailService', () => {
let service: HeroDetailService;
fixture = TestBed.createComponent(HeroDetailComponent);
expect(
// Throws because `inject` only has access to TestBed's injector
// which is an ancestor of the component's injector
inject([HeroDetailService], (hds: HeroDetailService) => service = hds))
.toThrowError(/No provider for HeroDetailService/);
// get `HeroDetailService` with component's own injector
service = fixture.debugElement.injector.get(HeroDetailService);
expect(service).toBeDefined('debugElement.injector');
});
}
/////////////////////
import { FormsModule } from '@angular/forms';
import { TitleCasePipe } from '../shared/title-case.pipe';
function formsModuleSetup() {
beforeEach(waitForAsync(() => {
const routerSpy = createRouterSpy();
TestBed
.configureTestingModule({
imports: [FormsModule],
declarations: [HeroDetailComponent, TitleCasePipe],
providers: [
{provide: ActivatedRoute, useValue: activatedRoute},
{provide: HeroService, useClass: TestHeroService},
{provide: Router, useValue: routerSpy},
]
})
.compileComponents();
}));
it('should display 1st hero\'s name', waitForAsync(() => {
const expectedHero = firstHero;
activatedRoute.setParamMap({id: expectedHero.id});
createComponent().then(() => {
expect(page.nameDisplay.textContent).toBe(expectedHero.name);
});
}));
}
///////////////////////
import { SharedModule } from '../shared/shared.module';
function sharedModuleSetup() {
beforeEach(waitForAsync(() => {
const routerSpy = createRouterSpy();
TestBed
.configureTestingModule({
imports: [SharedModule],
declarations: [HeroDetailComponent],
providers: [
{provide: ActivatedRoute, useValue: activatedRoute},
{provide: HeroService, useClass: TestHeroService},
{provide: Router, useValue: routerSpy},
]
})
.compileComponents();
}));
it('should display 1st hero\'s name', waitForAsync(() => {
const expectedHero = firstHero;
activatedRoute.setParamMap({id: expectedHero.id});
createComponent().then(() => {
expect(page.nameDisplay.textContent).toBe(expectedHero.name);
});
}));
}
/////////// Helpers /////
/** Create the HeroDetailComponent, initialize it, set test variables */
function createComponent() {
fixture = TestBed.createComponent(HeroDetailComponent);
component = fixture.componentInstance;
page = new Page(fixture);
// 1st change detection triggers ngOnInit which gets a hero
fixture.detectChanges();
return fixture.whenStable().then(() => {
// 2nd change detection displays the async-fetched hero
fixture.detectChanges();
});
}
class Page {
// getter properties wait to query the DOM until called.
get buttons() {
return this.queryAll<HTMLButtonElement>('button');
}
get saveBtn() {
return this.buttons[0];
}
get cancelBtn() {
return this.buttons[1];
}
get nameDisplay() {
return this.query<HTMLElement>('span');
}
get nameInput() {
return this.query<HTMLInputElement>('input');
}
gotoListSpy: jasmine.Spy;
navigateSpy: jasmine.Spy;
constructor(someFixture: ComponentFixture<HeroDetailComponent>) {
// get the navigate spy from the injected router spy object
const routerSpy = someFixture.debugElement.injector.get(Router) as any;
this.navigateSpy = routerSpy.navigate;
// spy on component's `gotoList()` method
const someComponent = someFixture.componentInstance;
this.gotoListSpy = spyOn(someComponent, 'gotoList').and.callThrough();
}
//// query helpers ////
private query<T>(selector: string): T {
return fixture.nativeElement.querySelector(selector);
}
private queryAll<T>(selector: string): T[] {
return fixture.nativeElement.querySelectorAll(selector);
}
}
function createRouterSpy() {
return jasmine.createSpyObj('Router', ['navigate']);
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/hero/hero-detail.component.ts]" value="/* tslint:disable:member-ordering */
import { Component, Input, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Hero } from '../model/hero';
import { HeroDetailService } from './hero-detail.service';
@Component({
selector: 'app-hero-detail',
templateUrl: './hero-detail.component.html',
styleUrls: ['./hero-detail.component.css' ],
providers: [ HeroDetailService ]
})
export class HeroDetailComponent implements OnInit {
constructor(
private heroDetailService: HeroDetailService,
private route: ActivatedRoute,
private router: Router) {
}
@Input() hero: Hero;
ngOnInit(): void {
// get hero when `id` param changes
this.route.paramMap.subscribe(pmap => this.getHero(pmap.get('id')));
}
private getHero(id: string): void {
// when no id or id===0, create new blank hero
if (!id) {
this.hero = { id: 0, name: '' } as Hero;
return;
}
this.heroDetailService.getHero(id).subscribe(hero => {
if (hero) {
this.hero = hero;
} else {
this.gotoList(); // id not found; navigate to list
}
});
}
save(): void {
this.heroDetailService.saveHero(this.hero).subscribe(() => this.gotoList());
}
cancel() { this.gotoList(); }
gotoList() {
this.router.navigate(['../'], {relativeTo: this.route});
}
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/hero/hero-detail.service.ts]" value="import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Hero } from '../model/hero';
import { HeroService } from '../model/hero.service';
@Injectable()
export class HeroDetailService {
constructor(private heroService: HeroService) { }
// Returns a clone which caller may modify safely
getHero(id: number | string): Observable<Hero> {
if (typeof id === 'string') {
id = parseInt(id, 10);
}
return this.heroService.getHero(id).pipe(
map(hero => {
return hero ? Object.assign({}, hero) : null; // clone or null
})
);
}
saveHero(hero: Hero) {
return this.heroService.updateHero(hero);
}
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/hero/hero-list.component.spec.ts]" value="import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { Router } from '@angular/router';
import { addMatchers } from '../../testing';
import { HeroService } from '../model/hero.service';
import { getTestHeroes, TestHeroService } from '../model/testing/test-hero.service';
import { HeroModule } from './hero.module';
import { HeroListComponent } from './hero-list.component';
import { HighlightDirective } from '../shared/highlight.directive';
const HEROES = getTestHeroes();
let comp: HeroListComponent;
let fixture: ComponentFixture<HeroListComponent>;
let page: Page;
/////// Tests //////
describe('HeroListComponent', () => {
beforeEach(waitForAsync(() => {
addMatchers();
const routerSpy = jasmine.createSpyObj('Router', ['navigate']);
TestBed
.configureTestingModule({
imports: [HeroModule],
providers: [
{provide: HeroService, useClass: TestHeroService},
{provide: Router, useValue: routerSpy}
]
})
.compileComponents()
.then(createComponent);
}));
it('should display heroes', () => {
expect(page.heroRows.length).toBeGreaterThan(0);
});
it('1st hero should match 1st test hero', () => {
const expectedHero = HEROES[0];
const actualHero = page.heroRows[0].textContent;
expect(actualHero).toContain(expectedHero.id.toString(), 'hero.id');
expect(actualHero).toContain(expectedHero.name, 'hero.name');
});
it('should select hero on click', fakeAsync(() => {
const expectedHero = HEROES[1];
const li = page.heroRows[1];
// In older browsers, such as IE, you might need a CustomEvent instead. See
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
li.dispatchEvent(new Event('click'));
tick();
// `.toEqual` because selectedHero is clone of expectedHero; see FakeHeroService
expect(comp.selectedHero).toEqual(expectedHero);
}));
it('should navigate to selected hero detail on click', fakeAsync(() => {
const expectedHero = HEROES[1];
const li = page.heroRows[1];
// In older browsers, such as IE, you might need a CustomEvent instead. See
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
li.dispatchEvent(new Event('click'));
tick();
// should have navigated
expect(page.navSpy.calls.any()).toBe(true, 'navigate called');
// composed hero detail will be URL like 'heroes/42'
// expect link array with the route path and hero id
// first argument to router.navigate is link array
const navArgs = page.navSpy.calls.first().args[0];
expect(navArgs[0]).toContain('heroes', 'nav to heroes detail URL');
expect(navArgs[1]).toBe(expectedHero.id, 'expected hero.id');
}));
it('should find `HighlightDirective` with `By.directive', () => {
// Can find DebugElement either by css selector or by directive
const h2 = fixture.debugElement.query(By.css('h2'));
const directive = fixture.debugElement.query(By.directive(HighlightDirective));
expect(h2).toBe(directive);
});
it('should color header with `HighlightDirective`', () => {
const h2 = page.highlightDe.nativeElement as HTMLElement;
const bgColor = h2.style.backgroundColor;
// different browsers report color values differently
const isExpectedColor = bgColor === 'gold' || bgColor === 'rgb(255, 215, 0)';
expect(isExpectedColor).toBe(true, 'backgroundColor');
});
it('the `HighlightDirective` is among the element\'s providers', () => {
expect(page.highlightDe.providerTokens).toContain(HighlightDirective, 'HighlightDirective');
});
});
/////////// Helpers /////
/** Create the component and set the `page` test variables */
function createComponent() {
fixture = TestBed.createComponent(HeroListComponent);
comp = fixture.componentInstance;
// change detection triggers ngOnInit which gets a hero
fixture.detectChanges();
return fixture.whenStable().then(() => {
// got the heroes and updated component
// change detection updates the view
fixture.detectChanges();
page = new Page();
});
}
class Page {
/** Hero line elements */
heroRows: HTMLLIElement[];
/** Highlighted DebugElement */
highlightDe: DebugElement;
/** Spy on router navigate method */
navSpy: jasmine.Spy;
constructor() {
const heroRowNodes = fixture.nativeElement.querySelectorAll('li');
this.heroRows = Array.from(heroRowNodes);
// Find the first element with an attached HighlightDirective
this.highlightDe = fixture.debugElement.query(By.directive(HighlightDirective));
// Get the component's injected router navigation spy
const routerSpy = fixture.debugElement.injector.get(Router);
this.navSpy = routerSpy.navigate as jasmine.Spy;
}
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/hero/hero-list.component.ts]" value="import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs';
import { Hero } from '../model/hero';
import { HeroService } from '../model/hero.service';
@Component({
selector: 'app-heroes',
templateUrl: './hero-list.component.html',
styleUrls: [ './hero-list.component.css' ]
})
export class HeroListComponent implements OnInit {
heroes: Observable<Hero[]>;
selectedHero: Hero;
constructor(
private router: Router,
private heroService: HeroService) { }
ngOnInit() {
this.heroes = this.heroService.getHeroes();
}
onSelect(hero: Hero) {
this.selectedHero = hero;
this.router.navigate(['../heroes', this.selectedHero.id ]);
}
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/hero/hero-routing.module.ts]" value="import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HeroListComponent } from './hero-list.component';
import { HeroDetailComponent } from './hero-detail.component';
const routes: Routes = [
{ path: '', component: HeroListComponent },
{ path: ':id', component: HeroDetailComponent }
];
export const routedComponents = [HeroDetailComponent, HeroListComponent];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class HeroRoutingModule {}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/hero/hero.module.ts]" value="import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { routedComponents, HeroRoutingModule } from './hero-routing.module';
@NgModule({
imports: [ SharedModule, HeroRoutingModule ],
declarations: [ routedComponents ]
})
export class HeroModule { }
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/in-memory-data.service.ts]" value="import { InMemoryDbService } from 'angular-in-memory-web-api';
import { QUOTES } from './twain/twain.data';
// Adjust to reduce number of quotes
const maxQuotes = Infinity; // 0;
/** Create in-memory database of heroes and quotes */
export class InMemoryDataService implements InMemoryDbService {
createDb() {
const heroes = [
{ id: 11, name: 'Dr Nice' },
{ id: 12, name: 'Narco' },
{ id: 13, name: 'Bombasto' },
{ id: 14, name: 'Celeritas' },
{ id: 15, name: 'Magneta' },
{ id: 16, name: 'RubberMan' },
{ id: 17, name: 'Dynama' },
{ id: 18, name: 'Dr IQ' },
{ id: 19, name: 'Magma' },
{ id: 20, name: 'Tornado' }
];
return { heroes, quotes: QUOTES.slice(0, maxQuotes) };
}
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/model/hero.service.spec.ts]" value="import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
// Other imports
import { TestBed } from '@angular/core/testing';
import { HttpClient, HttpResponse, HttpErrorResponse } from '@angular/common/http';
import { asyncData, asyncError } from '../../testing/async-observable-helpers';
import { Hero } from './hero';
import { HeroService } from './hero.service';
describe ('HeroesService (with spies)', () => {
let httpClientSpy: { get: jasmine.Spy };
let heroService: HeroService;
beforeEach(() => {
// TODO: spy on other methods too
httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']);
heroService = new HeroService(httpClientSpy as any);
});
it('should return expected heroes (HttpClient called once)', () => {
const expectedHeroes: Hero[] =
[{ id: 1, name: 'A' }, { id: 2, name: 'B' }];
httpClientSpy.get.and.returnValue(asyncData(expectedHeroes));
heroService.getHeroes().subscribe(
heroes => expect(heroes).toEqual(expectedHeroes, 'expected heroes'),
fail
);
expect(httpClientSpy.get.calls.count()).toBe(1, 'one call');
});
it('should return an error when the server returns a 404', () => {
const errorResponse = new HttpErrorResponse({
error: 'test 404 error',
status: 404, statusText: 'Not Found'
});
httpClientSpy.get.and.returnValue(asyncError(errorResponse));
heroService.getHeroes().subscribe(
heroes => fail('expected an error, not heroes'),
error => expect(error.message).toContain('test 404 error')
);
});
});
describe('HeroesService (with mocks)', () => {
let httpClient: HttpClient;
let httpTestingController: HttpTestingController;
let heroService: HeroService;
beforeEach(() => {
TestBed.configureTestingModule({
// Import the HttpClient mocking services
imports: [ HttpClientTestingModule ],
// Provide the service-under-test
providers: [ HeroService ]
});
// Inject the http, test controller, and service-under-test
// as they will be referenced by each test.
httpClient = TestBed.inject(HttpClient);
httpTestingController = TestBed.inject(HttpTestingController);
heroService = TestBed.inject(HeroService);
});
afterEach(() => {
// After every test, assert that there are no more pending requests.
httpTestingController.verify();
});
/// HeroService method tests begin ///
describe('#getHeroes', () => {
let expectedHeroes: Hero[];
beforeEach(() => {
heroService = TestBed.inject(HeroService);
expectedHeroes = [
{ id: 1, name: 'A' },
{ id: 2, name: 'B' },
] as Hero[];
});
it('should return expected heroes (called once)', () => {
heroService.getHeroes().subscribe(
heroes => expect(heroes).toEqual(expectedHeroes, 'should return expected heroes'),
fail
);
// HeroService should have made one request to GET heroes from expected URL
const req = httpTestingController.expectOne(heroService.heroesUrl);
expect(req.request.method).toEqual('GET');
// Respond with the mock heroes
req.flush(expectedHeroes);
});
it('should be OK returning no heroes', () => {
heroService.getHeroes().subscribe(
heroes => expect(heroes.length).toEqual(0, 'should have empty heroes array'),
fail
);
const req = httpTestingController.expectOne(heroService.heroesUrl);
req.flush([]); // Respond with no heroes
});
it('should turn 404 into a user-friendly error', () => {
const msg = 'Deliberate 404';
heroService.getHeroes().subscribe(
heroes => fail('expected to fail'),
error => expect(error.message).toContain(msg)
);
const req = httpTestingController.expectOne(heroService.heroesUrl);
// respond with a 404 and the error message in the body
req.flush(msg, {status: 404, statusText: 'Not Found'});
});
it('should return expected heroes (called multiple times)', () => {
heroService.getHeroes().subscribe();
heroService.getHeroes().subscribe();
heroService.getHeroes().subscribe(
heroes => expect(heroes).toEqual(expectedHeroes, 'should return expected heroes'),
fail
);
const requests = httpTestingController.match(heroService.heroesUrl);
expect(requests.length).toEqual(3, 'calls to getHeroes()');
// Respond to each request with different mock hero results
requests[0].flush([]);
requests[1].flush([{id: 1, name: 'bob'}]);
requests[2].flush(expectedHeroes);
});
});
describe('#updateHero', () => {
// Expecting the query form of URL so should not 404 when id not found
const makeUrl = (id: number) => `${heroService.heroesUrl}/?id=${id}`;
it('should update a hero and return it', () => {
const updateHero: Hero = { id: 1, name: 'A' };
heroService.updateHero(updateHero).subscribe(
data => expect(data).toEqual(updateHero, 'should return the hero'),
fail
);
// HeroService should have made one request to PUT hero
const req = httpTestingController.expectOne(heroService.heroesUrl);
expect(req.request.method).toEqual('PUT');
expect(req.request.body).toEqual(updateHero);
// Expect server to return the hero after PUT
const expectedResponse = new HttpResponse(
{ status: 200, statusText: 'OK', body: updateHero });
req.event(expectedResponse);
});
it('should turn 404 error into user-facing error', () => {
const msg = 'Deliberate 404';
const updateHero: Hero = { id: 1, name: 'A' };
heroService.updateHero(updateHero).subscribe(
heroes => fail('expected to fail'),
error => expect(error.message).toContain(msg)
);
const req = httpTestingController.expectOne(heroService.heroesUrl);
// respond with a 404 and the error message in the body
req.flush(msg, {status: 404, statusText: 'Not Found'});
});
it('should turn network error into user-facing error', () => {
const emsg = 'simulated network error';
const updateHero: Hero = { id: 1, name: 'A' };
heroService.updateHero(updateHero).subscribe(
heroes => fail('expected to fail'),
error => expect(error.message).toContain(emsg)
);
const req = httpTestingController.expectOne(heroService.heroesUrl);
// Create mock ErrorEvent, raised when something goes wrong at the network level.
// Connection timeout, DNS error, offline, etc
const errorEvent = new ErrorEvent('so sad', {
message: emsg,
// The rest of this is optional and not used.
// Just showing that you could provide this too.
filename: 'HeroService.ts',
lineno: 42,
colno: 21
});
// Respond with mock error
req.error(errorEvent);
});
});
// TODO: test other HeroService methods
});
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/model/hero.service.ts]" value="import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { Observable } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
import { Hero } from './hero';
const httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
@Injectable()
export class HeroService {
readonly heroesUrl = 'api/heroes'; // URL to web api
constructor(private http: HttpClient) { }
/** GET heroes from the server */
getHeroes(): Observable<Hero[]> {
return this.http.get<Hero[]>(this.heroesUrl)
.pipe(
tap(heroes => this.log(`fetched heroes`)),
catchError(this.handleError('getHeroes'))
) as Observable<Hero[]>;
}
/** GET hero by id. Return `undefined` when id not found */
getHero<Data>(id: number | string): Observable<Hero> {
if (typeof id === 'string') {
id = parseInt(id, 10);
}
const url = `${this.heroesUrl}/?id=${id}`;
return this.http.get<Hero[]>(url)
.pipe(
map(heroes => heroes[0]), // returns a {0|1} element array
tap(h => {
const outcome = h ? `fetched` : `did not find`;
this.log(`${outcome} hero id=${id}`);
}),
catchError(this.handleError<Hero>(`getHero id=${id}`))
);
}
//////// Save methods //////////
/** POST: add a new hero to the server */
addHero(hero: Hero): Observable<Hero> {
return this.http.post<Hero>(this.heroesUrl, hero, httpOptions).pipe(
tap((addedHero) => this.log(`added hero w/ id=${addedHero.id}`)),
catchError(this.handleError<Hero>('addHero'))
);
}
/** DELETE: delete the hero from the server */
deleteHero(hero: Hero | number): Observable<Hero> {
const id = typeof hero === 'number' ? hero : hero.id;
const url = `${this.heroesUrl}/${id}`;
return this.http.delete<Hero>(url, httpOptions).pipe(
tap(_ => this.log(`deleted hero id=${id}`)),
catchError(this.handleError<Hero>('deleteHero'))
);
}
/** PUT: update the hero on the server */
updateHero(hero: Hero): Observable<any> {
return this.http.put(this.heroesUrl, hero, httpOptions).pipe(
tap(_ => this.log(`updated hero id=${hero.id}`)),
catchError(this.handleError<any>('updateHero'))
);
}
/**
* Returns a function that handles Http operation failures.
* This error handler lets the app continue to run as if no error occurred.
* @param operation - name of the operation that failed
*/
private handleError<T>(operation = 'operation') {
return (error: HttpErrorResponse): Observable<T> => {
// TODO: send the error to remote logging infrastructure
console.error(error); // log to console instead
const message = (error.error instanceof ErrorEvent) ?
error.error.message :
`server returned code ${error.status} with body &quot;${error.error}&quot;`;
// TODO: better job of transforming error for user consumption
throw new Error(`${operation} failed: ${message}`);
};
}
private log(message: string) {
console.log('HeroService: ' + message);
}
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/model/hero.ts]" value="export interface Hero {
id: number;
name: string;
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/model/index.ts]" value="export * from './hero';
export * from './hero.service';
export * from './user.service';
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/model/testing/http-client.spec.ts]" value="// Http testing module and mocking controller
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
// Other imports
import { TestBed } from '@angular/core/testing';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { HttpHeaders } from '@angular/common/http';
interface Data {
name: string;
}
const testUrl = '/data';
describe('HttpClient testing', () => {
let httpClient: HttpClient;
let httpTestingController: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ HttpClientTestingModule ]
});
// Inject the http service and test controller for each test
httpClient = TestBed.inject(HttpClient);
httpTestingController = TestBed.inject(HttpTestingController);
});
afterEach(() => {
// After every test, assert that there are no more pending requests.
httpTestingController.verify();
});
/// Tests begin ///
it('can test HttpClient.get', () => {
const testData: Data = {name: 'Test Data'};
// Make an HTTP GET request
httpClient.get<Data>(testUrl)
.subscribe(data =>
// When observable resolves, result should match test data
expect(data).toEqual(testData)
);
// The following `expectOne()` will match the request's URL.
// If no requests or multiple requests matched that URL
// `expectOne()` would throw.
const req = httpTestingController.expectOne('/data');
// Assert that the request is a GET.
expect(req.request.method).toEqual('GET');
// Respond with mock data, causing Observable to resolve.
// Subscribe callback asserts that correct data was returned.
req.flush(testData);
// Finally, assert that there are no outstanding requests.
httpTestingController.verify();
});
it('can test HttpClient.get with matching header', () => {
const testData: Data = {name: 'Test Data'};
// Make an HTTP GET request with specific header
httpClient.get<Data>(testUrl, {
headers: new HttpHeaders({Authorization: 'my-auth-token'})
})
.subscribe(data =>
expect(data).toEqual(testData)
);
// Find request with a predicate function.
// Expect one request with an authorization header
const req = httpTestingController.expectOne(
request => request.headers.has('Authorization')
);
req.flush(testData);
});
it('can test multiple requests', () => {
const testData: Data[] = [
{ name: 'bob' }, { name: 'carol' },
{ name: 'ted' }, { name: 'alice' }
];
// Make three requests in a row
httpClient.get<Data[]>(testUrl)
.subscribe(d => expect(d.length).toEqual(0, 'should have no data'));
httpClient.get<Data[]>(testUrl)
.subscribe(d => expect(d).toEqual([testData[0]], 'should be one element array'));
httpClient.get<Data[]>(testUrl)
.subscribe(d => expect(d).toEqual(testData, 'should be expected data'));
// get all pending requests that match the given URL
const requests = httpTestingController.match(testUrl);
expect(requests.length).toEqual(3);
// Respond to each request with different results
requests[0].flush([]);
requests[1].flush([testData[0]]);
requests[2].flush(testData);
});
it('can test for 404 error', () => {
const emsg = 'deliberate 404 error';
httpClient.get<Data[]>(testUrl).subscribe(
data => fail('should have failed with the 404 error'),
(error: HttpErrorResponse) => {
expect(error.status).toEqual(404, 'status');
expect(error.error).toEqual(emsg, 'message');
}
);
const req = httpTestingController.expectOne(testUrl);
// Respond with mock error
req.flush(emsg, { status: 404, statusText: 'Not Found' });
});
it('can test for network error', () => {
const emsg = 'simulated network error';
httpClient.get<Data[]>(testUrl).subscribe(
data => fail('should have failed with the network error'),
(error: HttpErrorResponse) => {
expect(error.error.message).toEqual(emsg, 'message');
}
);
const req = httpTestingController.expectOne(testUrl);
// Create mock ErrorEvent, raised when something goes wrong at the network level.
// Connection timeout, DNS error, offline, etc
const errorEvent = new ErrorEvent('so sad', {
message: emsg,
// The rest of this is optional and not used.
// Just showing that you could provide this too.
filename: 'HeroService.ts',
lineno: 42,
colno: 21
});
// Respond with mock error
req.error(errorEvent);
});
it('httpTestingController.verify should fail if HTTP response not simulated', () => {
// Sends request
httpClient.get('some/api').subscribe();
// verify() should fail because haven't handled the pending request.
expect(() => httpTestingController.verify()).toThrow();
// Now get and flush the request so that afterEach() doesn't fail
const req = httpTestingController.expectOne('some/api');
req.flush(null);
});
// Proves that verify in afterEach() really would catch error
// if test doesn't simulate the HTTP response.
//
// Must disable this test because can't catch an error in an afterEach().
// Uncomment if you want to confirm that afterEach() does the job.
// it('afterEach() should fail when HTTP response not simulated',() => {
// // Sends request which is never handled by this test
// httpClient.get('some/api').subscribe();
// });
});
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/model/testing/index.ts]" value="export * from './test-hero.service';
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/model/testing/test-hero.service.ts]" value="import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { asyncData } from '../../../testing';
import { map } from 'rxjs/operators';
// re-export for tester convenience
export { Hero } from '../hero';
export { HeroService } from '../hero.service';
export { getTestHeroes } from './test-heroes';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
import { getTestHeroes } from './test-heroes';
@Injectable()
/**
* FakeHeroService pretends to make real http requests.
* implements only as much of HeroService as is actually consumed by the app
*/
export class TestHeroService extends HeroService {
constructor() {
super(null);
}
heroes = getTestHeroes();
lastResult: Observable<any>; // result from last method call
addHero(hero: Hero): Observable<Hero> {
throw new Error('Method not implemented.');
}
deleteHero(hero: number | Hero): Observable<Hero> {
throw new Error('Method not implemented.');
}
getHeroes(): Observable<Hero[]> {
return this.lastResult = asyncData(this.heroes);
}
getHero(id: number | string): Observable<Hero> {
if (typeof id === 'string') {
id = parseInt(id, 10);
}
const hero = this.heroes.find(h => h.id === id);
return this.lastResult = asyncData(hero);
}
updateHero(hero: Hero): Observable<Hero> {
return this.lastResult = this.getHero(hero.id).pipe(
map(h => {
if (h) {
return Object.assign(h, hero);
}
throw new Error(`Hero ${hero.id} not found`);
})
);
}
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/model/testing/test-heroes.ts]" value="import { Hero } from '../hero';
/** return fresh array of test heroes */
export function getTestHeroes(): Hero[] {
return [
{id: 41, name: 'Bob' },
{id: 42, name: 'Carol' },
{id: 43, name: 'Ted' },
{id: 44, name: 'Alice' },
{id: 45, name: 'Speedy' },
{id: 46, name: 'Stealthy' }
];
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/model/user.service.ts]" value="import { Injectable } from '@angular/core';
@Injectable()
export class UserService {
isLoggedIn = true;
user = {name: 'Sam Spade'};
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/shared/canvas.component.spec.ts]" value="import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { CanvasComponent } from './canvas.component';
describe('CanvasComponent', () => {
beforeEach(() => {
(window as any).__zone_symbol__FakeAsyncTestMacroTask = [
{
source: 'HTMLCanvasElement.toBlob',
callbackArgs: [{size: 200}],
},
];
});
beforeEach(waitForAsync(() => {
TestBed
.configureTestingModule({
declarations: [CanvasComponent],
})
.compileComponents();
}));
it('should be able to generate blob data from canvas', fakeAsync(() => {
const fixture = TestBed.createComponent(CanvasComponent);
const canvasComp = fixture.componentInstance;
fixture.detectChanges();
expect(canvasComp.blobSize).toBe(0);
tick();
expect(canvasComp.blobSize).toBeGreaterThan(0);
}));
});
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/shared/canvas.component.ts]" value="// Import patch to make async `HTMLCanvasElement` methods (such as `.toBlob()`) Zone.js-aware.
// Either import in `polyfills.ts` (if used in more than one places in the app) or in the component
// file using `HTMLCanvasElement` (if it is only used in a single file).
import 'zone.js/plugins/zone-patch-canvas';
import { Component, AfterViewInit, ViewChild, ElementRef } from '@angular/core';
@Component({
selector: 'sample-canvas',
template: '<canvas #sampleCanvas width=&quot;200&quot; height=&quot;200&quot;></canvas>',
})
export class CanvasComponent implements AfterViewInit {
blobSize = 0;
@ViewChild('sampleCanvas') sampleCanvas: ElementRef;
ngAfterViewInit() {
const canvas: HTMLCanvasElement = this.sampleCanvas.nativeElement;
const context = canvas.getContext('2d');
context.clearRect(0, 0, 200, 200);
context.fillStyle = '#FF1122';
context.fillRect(0, 0, 200, 200);
canvas.toBlob(blob => {
this.blobSize = blob.size;
});
}
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/shared/highlight.directive.spec.ts]" value="import { Component, DebugElement } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { HighlightDirective } from './highlight.directive';
@Component({
template: `
<h2 highlight=&quot;yellow&quot;>Something Yellow</h2>
<h2 highlight>The Default (Gray)</h2>
<h2>No Highlight</h2>
<input #box [highlight]=&quot;box.value&quot; value=&quot;cyan&quot;/>`
})
class TestComponent { }
describe('HighlightDirective', () => {
let fixture: ComponentFixture<TestComponent>;
let des: DebugElement[]; // the three elements w/ the directive
let bareH2: DebugElement; // the <h2> w/o the directive
beforeEach(() => {
fixture = TestBed.configureTestingModule({
declarations: [ HighlightDirective, TestComponent ]
})
.createComponent(TestComponent);
fixture.detectChanges(); // initial binding
// all elements with an attached HighlightDirective
des = fixture.debugElement.queryAll(By.directive(HighlightDirective));
// the h2 without the HighlightDirective
bareH2 = fixture.debugElement.query(By.css('h2:not([highlight])'));
});
// color tests
it('should have three highlighted elements', () => {
expect(des.length).toBe(3);
});
it('should color 1st <h2> background &quot;yellow&quot;', () => {
const bgColor = des[0].nativeElement.style.backgroundColor;
expect(bgColor).toBe('yellow');
});
it('should color 2nd <h2> background w/ default color', () => {
const dir = des[1].injector.get(HighlightDirective) as HighlightDirective;
const bgColor = des[1].nativeElement.style.backgroundColor;
expect(bgColor).toBe(dir.defaultColor);
});
it('should bind <input> background to value color', () => {
// easier to work with nativeElement
const input = des[2].nativeElement as HTMLInputElement;
expect(input.style.backgroundColor).toBe('cyan', 'initial backgroundColor');
input.value = 'green';
// Dispatch a DOM event so that Angular responds to the input value change.
// In older browsers, such as IE, you might need a CustomEvent instead. See
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
expect(input.style.backgroundColor).toBe('green', 'changed backgroundColor');
});
it('bare <h2> should not have a customProperty', () => {
expect(bareH2.properties.customProperty).toBeUndefined();
});
// Removed on 12/02/2016 when ceased public discussion of the `Renderer`. Revive in future?
// // customProperty tests
// it('all highlighted elements should have a true customProperty', () => {
// const allTrue = des.map(de => !!de.properties['customProperty']).every(v => v === true);
// expect(allTrue).toBe(true);
// });
// injected directive
// attached HighlightDirective can be injected
it('can inject `HighlightDirective` in 1st <h2>', () => {
const dir = des[0].injector.get(HighlightDirective);
expect(dir).toBeTruthy();
});
it('cannot inject `HighlightDirective` in 3rd <h2>', () => {
const dir = bareH2.injector.get(HighlightDirective, null);
expect(dir).toBe(null);
});
// DebugElement.providerTokens
// attached HighlightDirective should be listed in the providerTokens
it('should have `HighlightDirective` in 1st <h2> providerTokens', () => {
expect(des[0].providerTokens).toContain(HighlightDirective);
});
it('should not have `HighlightDirective` in 3rd <h2> providerTokens', () => {
expect(bareH2.providerTokens).not.toContain(HighlightDirective);
});
});
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/shared/highlight.directive.ts]" value="// tslint:disable: directive-selector
import { Directive, ElementRef, Input, OnChanges } from '@angular/core';
@Directive({ selector: '[highlight]' })
/**
* Set backgroundColor for the attached element to highlight color
* and set the element's customProperty to true
*/
export class HighlightDirective implements OnChanges {
defaultColor = 'rgb(211, 211, 211)'; // lightgray
@Input('highlight') bgColor: string;
constructor(private el: ElementRef) {
el.nativeElement.style.customProperty = true;
}
ngOnChanges() {
this.el.nativeElement.style.backgroundColor = this.bgColor || this.defaultColor;
}
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/shared/shared.module.ts]" value="import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { HighlightDirective } from './highlight.directive';
import { TitleCasePipe } from './title-case.pipe';
import { CanvasComponent } from './canvas.component';
@NgModule({
imports: [ CommonModule ],
exports: [
CommonModule,
// SharedModule importers won't have to import FormsModule too
FormsModule,
HighlightDirective,
TitleCasePipe,
CanvasComponent
],
declarations: [ HighlightDirective, TitleCasePipe, CanvasComponent ]
})
export class SharedModule { }
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/shared/title-case.pipe.spec.ts]" value="import { TitleCasePipe } from './title-case.pipe';
describe('TitleCasePipe', () => {
// This pipe is a pure, stateless function so no need for BeforeEach
const pipe = new TitleCasePipe();
it('transforms &quot;abc&quot; to &quot;Abc&quot;', () => {
expect(pipe.transform('abc')).toBe('Abc');
});
it('transforms &quot;abc def&quot; to &quot;Abc Def&quot;', () => {
expect(pipe.transform('abc def')).toBe('Abc Def');
});
// ... more tests ...
it('leaves &quot;Abc Def&quot; unchanged', () => {
expect(pipe.transform('Abc Def')).toBe('Abc Def');
});
it('transforms &quot;abc-def&quot; to &quot;Abc-def&quot;', () => {
expect(pipe.transform('abc-def')).toBe('Abc-def');
});
it('transforms &quot; abc def&quot; to &quot; Abc Def&quot; (preserves spaces) ', () => {
expect(pipe.transform(' abc def')).toBe(' Abc Def');
});
});
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/shared/title-case.pipe.ts]" value="import { Pipe, PipeTransform } from '@angular/core';
@Pipe({name: 'titlecase', pure: true})
/** Transform to Title Case: uppercase the first letter of the words in a string. */
export class TitleCasePipe implements PipeTransform {
transform(input: string): string {
return input.length === 0 ? '' :
input.replace(/\w\S*/g, (txt => txt[0].toUpperCase() + txt.substr(1).toLowerCase() ));
}
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/twain/quote.ts]" value="export class Quote {
id: number;
quote: string;
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/twain/twain.component.marbles.spec.ts]" value="import { async, fakeAsync, ComponentFixture, TestBed, tick } from '@angular/core/testing';
import { cold, getTestScheduler } from 'jasmine-marbles';
import { TwainService } from './twain.service';
import { TwainComponent } from './twain.component';
describe('TwainComponent (marbles)', () => {
let component: TwainComponent;
let fixture: ComponentFixture<TwainComponent>;
let getQuoteSpy: jasmine.Spy;
let quoteEl: HTMLElement;
let testQuote: string;
// Helper function to get the error message element value
// An *ngIf keeps it out of the DOM until there is an error
const errorMessage = () => {
const el = fixture.nativeElement.querySelector('.error');
return el ? el.textContent : null;
};
beforeEach(() => {
// Create a fake TwainService object with a `getQuote()` spy
const twainService = jasmine.createSpyObj('TwainService', ['getQuote']);
getQuoteSpy = twainService.getQuote;
TestBed.configureTestingModule({
declarations: [ TwainComponent ],
providers: [
{ provide: TwainService, useValue: twainService }
]
});
fixture = TestBed.createComponent(TwainComponent);
component = fixture.componentInstance;
quoteEl = fixture.nativeElement.querySelector('.twain');
testQuote = 'Test Quote';
});
// A synchronous test that simulates async behavior
it('should show quote after getQuote (marbles)', () => {
// observable test quote value and complete(), after delay
const q$ = cold('---x|', { x: testQuote });
getQuoteSpy.and.returnValue( q$ );
fixture.detectChanges(); // ngOnInit()
expect(quoteEl.textContent).toBe('...', 'should show placeholder');
getTestScheduler().flush(); // flush the observables
fixture.detectChanges(); // update view
expect(quoteEl.textContent).toBe(testQuote, 'should show quote');
expect(errorMessage()).toBeNull('should not show error');
});
// Still need fakeAsync() because of component's setTimeout()
it('should display error when TwainService fails', fakeAsync(() => {
// observable error after delay
const q$ = cold('---#|', null, new Error('TwainService test failure'));
getQuoteSpy.and.returnValue( q$ );
fixture.detectChanges(); // ngOnInit()
expect(quoteEl.textContent).toBe('...', 'should show placeholder');
getTestScheduler().flush(); // flush the observables
tick(); // component shows error after a setTimeout()
fixture.detectChanges(); // update error message
expect(errorMessage()).toMatch(/test failure/, 'should display error');
expect(quoteEl.textContent).toBe('...', 'should show placeholder');
}));
});
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/twain/twain.component.spec.ts]" value="import { fakeAsync, ComponentFixture, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { asyncData, asyncError } from '../../testing';
import { of, throwError } from 'rxjs';
import { last } from 'rxjs/operators';
import { TwainComponent } from './twain.component';
import { TwainService } from './twain.service';
describe('TwainComponent', () => {
let component: TwainComponent;
let fixture: ComponentFixture<TwainComponent>;
let getQuoteSpy: jasmine.Spy;
let quoteEl: HTMLElement;
let testQuote: string;
// Helper function to get the error message element value
// An *ngIf keeps it out of the DOM until there is an error
const errorMessage = () => {
const el = fixture.nativeElement.querySelector('.error');
return el ? el.textContent : null;
};
beforeEach(() => {
testQuote = 'Test Quote';
// Create a fake TwainService object with a `getQuote()` spy
const twainService = jasmine.createSpyObj('TwainService', ['getQuote']);
// Make the spy return a synchronous Observable with the test data
getQuoteSpy = twainService.getQuote.and.returnValue(of(testQuote));
TestBed.configureTestingModule({
declarations: [TwainComponent],
providers: [{provide: TwainService, useValue: twainService}]
});
fixture = TestBed.createComponent(TwainComponent);
component = fixture.componentInstance;
quoteEl = fixture.nativeElement.querySelector('.twain');
});
describe('when test with synchronous observable', () => {
it('should not show quote before OnInit', () => {
expect(quoteEl.textContent).toBe('', 'nothing displayed');
expect(errorMessage()).toBeNull('should not show error element');
expect(getQuoteSpy.calls.any()).toBe(false, 'getQuote not yet called');
});
// The quote would not be immediately available if the service were truly async.
it('should show quote after component initialized', () => {
fixture.detectChanges(); // onInit()
// sync spy result shows testQuote immediately after init
expect(quoteEl.textContent).toBe(testQuote);
expect(getQuoteSpy.calls.any()).toBe(true, 'getQuote called');
});
// The error would not be immediately available if the service were truly async.
// Use `fakeAsync` because the component error calls `setTimeout`
it('should display error when TwainService fails', fakeAsync(() => {
// tell spy to return an error observable
getQuoteSpy.and.returnValue(throwError('TwainService test failure'));
fixture.detectChanges(); // onInit()
// sync spy errors immediately after init
tick(); // flush the component's setTimeout()
fixture.detectChanges(); // update errorMessage within setTimeout()
expect(errorMessage()).toMatch(/test failure/, 'should display error');
expect(quoteEl.textContent).toBe('...', 'should show placeholder');
}));
});
describe('when test with asynchronous observable', () => {
beforeEach(() => {
// Simulate delayed observable values with the `asyncData()` helper
getQuoteSpy.and.returnValue(asyncData(testQuote));
});
it('should not show quote before OnInit', () => {
expect(quoteEl.textContent).toBe('', 'nothing displayed');
expect(errorMessage()).toBeNull('should not show error element');
expect(getQuoteSpy.calls.any()).toBe(false, 'getQuote not yet called');
});
it('should still not show quote after component initialized', () => {
fixture.detectChanges();
// getQuote service is async => still has not returned with quote
// so should show the start value, '...'
expect(quoteEl.textContent).toBe('...', 'should show placeholder');
expect(errorMessage()).toBeNull('should not show error');
expect(getQuoteSpy.calls.any()).toBe(true, 'getQuote called');
});
it('should show quote after getQuote (fakeAsync)', fakeAsync(() => {
fixture.detectChanges(); // ngOnInit()
expect(quoteEl.textContent).toBe('...', 'should show placeholder');
tick(); // flush the observable to get the quote
fixture.detectChanges(); // update view
expect(quoteEl.textContent).toBe(testQuote, 'should show quote');
expect(errorMessage()).toBeNull('should not show error');
}));
it('should show quote after getQuote (async)', waitForAsync(() => {
fixture.detectChanges(); // ngOnInit()
expect(quoteEl.textContent).toBe('...', 'should show placeholder');
fixture.whenStable().then(() => { // wait for async getQuote
fixture.detectChanges(); // update view with quote
expect(quoteEl.textContent).toBe(testQuote);
expect(errorMessage()).toBeNull('should not show error');
});
}));
it('should show last quote (quote done)', (done: DoneFn) => {
fixture.detectChanges();
component.quote.pipe(last()).subscribe(() => {
fixture.detectChanges(); // update view with quote
expect(quoteEl.textContent).toBe(testQuote);
expect(errorMessage()).toBeNull('should not show error');
done();
});
});
it('should show quote after getQuote (spy done)', (done: DoneFn) => {
fixture.detectChanges();
// the spy's most recent call returns the observable with the test quote
getQuoteSpy.calls.mostRecent().returnValue.subscribe(() => {
fixture.detectChanges(); // update view with quote
expect(quoteEl.textContent).toBe(testQuote);
expect(errorMessage()).toBeNull('should not show error');
done();
});
});
it('should display error when TwainService fails', fakeAsync(() => {
// tell spy to return an async error observable
getQuoteSpy.and.returnValue(asyncError<string>('TwainService test failure'));
fixture.detectChanges();
tick(); // component shows error after a setTimeout()
fixture.detectChanges(); // update error message
expect(errorMessage()).toMatch(/test failure/, 'should display error');
expect(quoteEl.textContent).toBe('...', 'should show placeholder');
}));
});
});
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/twain/twain.component.ts]" value="import { Component, OnInit } from '@angular/core';
import { Observable, of } from 'rxjs';
import { catchError, startWith } from 'rxjs/operators';
import { TwainService } from './twain.service';
@Component({
selector: 'twain-quote',
template: `
<p class=&quot;twain&quot;><i>{{quote | async}}</i></p>
<button (click)=&quot;getQuote()&quot;>Next quote</button>
<p class=&quot;error&quot; *ngIf=&quot;errorMessage&quot;>{{ errorMessage }}</p>`,
styles: [
`.twain { font-style: italic; } .error { color: red; }`
]
})
export class TwainComponent implements OnInit {
errorMessage: string;
quote: Observable<string>;
constructor(private twainService: TwainService) {}
ngOnInit(): void {
this.getQuote();
}
getQuote() {
this.errorMessage = '';
this.quote = this.twainService.getQuote().pipe(
startWith('...'),
catchError( (err: any) => {
// Wait a turn because errorMessage already set once this turn
setTimeout(() => this.errorMessage = err.message || err.toString());
return of('...'); // reset message to placeholder
})
);
}
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/twain/twain.data.ts]" value="import { Quote } from './quote';
export const QUOTES: Quote[] = [
'Always do right. This will gratify some people and astonish the rest.',
'I have never let my schooling interfere with my education.',
'Don\'t go around saying the world owes you a living. The world owes you nothing. It was here first.',
'Whenever you find yourself on the side of the majority, it is time to pause and reflect.',
'If you tell the truth, you don\'t have to remember anything.',
'Clothes make the man. Naked people have little or no influence on society.',
'It\'s not the size of the dog in the fight, it\'s the size of the fight in the dog.',
'Truth is stranger than fiction, but it is because Fiction is obliged to stick to possibilities; Truth isn\'t.',
'The man who does not read good books has no advantage over the man who cannot read them.',
'Get your facts first, and then you can distort them as much as you please.',
]
.map((q, i) => ({ id: i + 1, quote: q }));
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/twain/twain.service.ts]" value="// Mark Twain Quote service gets quotes from server
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, of, throwError, Observer } from 'rxjs';
import { concat, map, retryWhen, switchMap, take, tap } from 'rxjs/operators';
import { Quote } from './quote';
@Injectable()
export class TwainService {
constructor(private http: HttpClient) { }
private nextId = 1;
getQuote(): Observable<string> {
return Observable.create((observer: Observer<number>) => observer.next(this.nextId++)).pipe(
// tap((id: number) => console.log(id)),
// tap((id: number) => { throw new Error('Simulated server error'); }),
switchMap((id: number) => this.http.get<Quote>(`api/quotes/${id}`)),
// tap((q : Quote) => console.log(q)),
map((q: Quote) => q.quote),
// `errors` is observable of http.get errors
retryWhen(errors => errors.pipe(
switchMap((error: HttpErrorResponse) => {
if (error.status === 404) {
// Queried for quote that doesn't exist.
this.nextId = 1; // retry with quote id:1
return of(null); // signal OK to retry
}
// Some other HTTP error.
console.error(error);
return throwError('Cannot get Twain quotes from the server');
}),
take(2),
// If a second retry value, then didn't find id:1 and triggers the following error
concat(throwError('There are no Twain quotes')) // didn't find id:1
))
);
}
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/welcome/welcome.component.spec.ts]" value="import { ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { UserService } from '../model/user.service';
import { WelcomeComponent } from './welcome.component';
class MockUserService {
isLoggedIn = true;
user = { name: 'Test User'};
}
describe('WelcomeComponent (class only)', () => {
let comp: WelcomeComponent;
let userService: UserService;
beforeEach(() => {
TestBed.configureTestingModule({
// provide the component-under-test and dependent service
providers: [
WelcomeComponent,
{ provide: UserService, useClass: MockUserService }
]
});
// inject both the component and the dependent service.
comp = TestBed.inject(WelcomeComponent);
userService = TestBed.inject(UserService);
});
it('should not have welcome message after construction', () => {
expect(comp.welcome).toBeUndefined();
});
it('should welcome logged in user after Angular calls ngOnInit', () => {
comp.ngOnInit();
expect(comp.welcome).toContain(userService.user.name);
});
it('should ask user to log in if not logged in after ngOnInit', () => {
userService.isLoggedIn = false;
comp.ngOnInit();
expect(comp.welcome).not.toContain(userService.user.name);
expect(comp.welcome).toContain('log in');
});
});
describe('WelcomeComponent', () => {
let comp: WelcomeComponent;
let fixture: ComponentFixture<WelcomeComponent>;
let componentUserService: UserService; // the actually injected service
let userService: UserService; // the TestBed injected service
let el: HTMLElement; // the DOM element with the welcome message
let userServiceStub: Partial<UserService>;
beforeEach(() => {
// stub UserService for test purposes
userServiceStub = {
isLoggedIn: true,
user: { name: 'Test User' },
};
TestBed.configureTestingModule({
declarations: [ WelcomeComponent ],
// providers: [ UserService ], // NO! Don't provide the real service!
// Provide a test-double instead
providers: [ { provide: UserService, useValue: userServiceStub } ],
});
fixture = TestBed.createComponent(WelcomeComponent);
comp = fixture.componentInstance;
// UserService actually injected into the component
userService = fixture.debugElement.injector.get(UserService);
componentUserService = userService;
// UserService from the root injector
userService = TestBed.inject(UserService);
// get the &quot;welcome&quot; element by CSS selector (e.g., by class name)
el = fixture.nativeElement.querySelector('.welcome');
});
it('should welcome the user', () => {
fixture.detectChanges();
const content = el.textContent;
expect(content).toContain('Welcome', '&quot;Welcome ...&quot;');
expect(content).toContain('Test User', 'expected name');
});
it('should welcome &quot;Bubba&quot;', () => {
userService.user.name = 'Bubba'; // welcome message hasn't been shown yet
fixture.detectChanges();
expect(el.textContent).toContain('Bubba');
});
it('should request login if not logged in', () => {
userService.isLoggedIn = false; // welcome message hasn't been shown yet
fixture.detectChanges();
const content = el.textContent;
expect(content).not.toContain('Welcome', 'not welcomed');
expect(content).toMatch(/log in/i, '&quot;log in&quot;');
});
it('should inject the component\'s UserService instance',
inject([UserService], (service: UserService) => {
expect(service).toBe(componentUserService);
}));
it('TestBed and Component UserService should be the same', () => {
expect(userService === componentUserService).toBe(true);
});
});
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/app/welcome/welcome.component.ts]" value="import { Component, OnInit } from '@angular/core';
import { UserService } from '../model/user.service';
@Component({
selector: 'app-welcome',
template: '<h3 class=&quot;welcome&quot;><i>{{welcome}}</i></h3>'
})
export class WelcomeComponent implements OnInit {
welcome: string;
constructor(private userService: UserService) { }
ngOnInit(): void {
this.welcome = this.userService.isLoggedIn ?
'Welcome, ' + this.userService.user.name : 'Please log in.';
}
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/testing/activated-route-stub.ts]" value="// export for convenience.
export { ActivatedRoute } from '@angular/router';
import { convertToParamMap, ParamMap, Params } from '@angular/router';
import { ReplaySubject } from 'rxjs';
/**
* An ActivateRoute test double with a `paramMap` observable.
* Use the `setParamMap()` method to add the next `paramMap` value.
*/
export class ActivatedRouteStub {
// Use a ReplaySubject to share previous values with subscribers
// and pump new values into the `paramMap` observable
private subject = new ReplaySubject<ParamMap>();
constructor(initialParams?: Params) {
this.setParamMap(initialParams);
}
/** The mock paramMap observable */
readonly paramMap = this.subject.asObservable();
/** Set the paramMap observables's next value */
setParamMap(params?: Params) {
this.subject.next(convertToParamMap(params));
}
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/testing/async-observable-helpers.ts]" value="/*
* Mock async observables that return asynchronously.
* The observable either emits once and completes or errors.
*
* Must call `tick()` when test with `fakeAsync()`.
*
* THE FOLLOWING DON'T WORK
* Using `of().delay()` triggers TestBed errors;
* see https://github.com/angular/angular/issues/10127 .
*
* Using `asap` scheduler - as in `of(value, asap)` - doesn't work either.
*/
import { defer } from 'rxjs';
/**
* Create async observable that emits-once and completes
* after a JS engine turn
*/
export function asyncData<T>(data: T) {
return defer(() => Promise.resolve(data));
}
/**
* Create async observable error that errors
* after a JS engine turn
*/
export function asyncError<T>(errorObject: any) {
return defer(() => Promise.reject(errorObject));
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/testing/global-jasmine.ts]" value="import jasmineRequire from 'jasmine-core/lib/jasmine-core/jasmine.js';
(window as any).jasmineRequire = jasmineRequire;
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/testing/index.ts]" value="import { DebugElement } from '@angular/core';
import { tick, ComponentFixture } from '@angular/core/testing';
export * from './async-observable-helpers';
export * from './activated-route-stub';
export * from './jasmine-matchers';
export * from './router-link-directive-stub';
///// Short utilities /////
/** Wait a tick, then detect changes */
export function advance(f: ComponentFixture<any>): void {
tick();
f.detectChanges();
}
// See https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
/** Button events to pass to `DebugElement.triggerEventHandler` for RouterLink event handler */
export const ButtonClickEvents = {
left: { button: 0 },
right: { button: 2 }
};
/** Simulate element click. Defaults to mouse left-button click event. */
export function click(el: DebugElement | HTMLElement, eventObj: any = ButtonClickEvents.left): void {
if (el instanceof HTMLElement) {
el.click();
} else {
el.triggerEventHandler('click', eventObj);
}
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/testing/jasmine-matchers.d.ts]" value="// tslint:disable-next-line: no-namespace
declare namespace jasmine {
interface Matchers<T> {
toHaveText(actual: any, expectationFailOutput?: any): jasmine.CustomMatcher;
}
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/testing/jasmine-matchers.ts]" value="// tslint:disable-next-line: no-reference
/// <reference path=&quot;./jasmine-matchers.d.ts&quot; />
//// Jasmine Custom Matchers ////
// Be sure to extend jasmine-matchers.d.ts when adding matchers
export function addMatchers(): void {
jasmine.addMatchers({
toHaveText
});
}
function toHaveText(): jasmine.CustomMatcher {
return {
compare: (actual: any, expectedText: string, expectationFailOutput?: any): jasmine.CustomMatcherResult => {
const actualText = elementText(actual);
const pass = actualText.indexOf(expectedText) > -1;
const message = pass ? '' : composeMessage();
return { pass, message };
function composeMessage() {
const a = (actualText.length < 100 ? actualText : actualText.substr(0, 100) + '...');
const efo = expectationFailOutput ? ` '${expectationFailOutput}'` : '';
return `Expected element to have text content '${expectedText}' instead of '${a}'${efo}`;
}
}
};
}
function elementText(n: any): string {
if (n instanceof Array) {
return n.map(elementText).join('');
}
if (n.nodeType === Node.COMMENT_NODE) {
return '';
}
if (n.nodeType === Node.ELEMENT_NODE &amp;&amp; n.hasChildNodes()) {
return elementText(Array.prototype.slice.call(n.childNodes));
}
if (n.nativeElement) { n = n.nativeElement; }
return n.textContent;
}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/testing/router-link-directive-stub.ts]" value="import { Directive, Input, HostListener } from '@angular/core';
// export for convenience.
export { RouterLink} from '@angular/router';
// tslint:disable: directive-class-suffix directive-selector
@Directive({
selector: '[routerLink]'
})
export class RouterLinkDirectiveStub {
@Input('routerLink') linkParams: any;
navigatedTo: any = null;
@HostListener('click')
onClick() {
this.navigatedTo = this.linkParams;
}
}
/// Dummy module to satisfy Angular Language service. Never used.
import { NgModule } from '@angular/core';
@NgModule({
declarations: [
RouterLinkDirectiveStub
]
})
export class RouterStubsModule {}
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/environments/environment.prod.ts]" value="export const environment = {
production: true
};
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[src/environments/environment.ts]" value="// This file can be replaced during build by using the `fileReplacements` array.
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[angular.json]" value="{
&quot;$schema&quot;: &quot;./node_modules/@angular/cli/lib/config/schema.json&quot;,
&quot;version&quot;: 1,
&quot;newProjectRoot&quot;: &quot;projects&quot;,
&quot;projects&quot;: {
&quot;angular.io-example&quot;: {
&quot;projectType&quot;: &quot;application&quot;,
&quot;schematics&quot;: {
&quot;@schematics/angular:application&quot;: {
&quot;strict&quot;: true
}
},
&quot;root&quot;: &quot;&quot;,
&quot;sourceRoot&quot;: &quot;src&quot;,
&quot;prefix&quot;: &quot;app&quot;,
&quot;architect&quot;: {
&quot;build&quot;: {
&quot;builder&quot;: &quot;@angular-devkit/build-angular:browser&quot;,
&quot;options&quot;: {
&quot;outputPath&quot;: &quot;dist&quot;,
&quot;index&quot;: &quot;src/index.html&quot;,
&quot;main&quot;: &quot;src/main.ts&quot;,
&quot;polyfills&quot;: &quot;src/polyfills.ts&quot;,
&quot;tsConfig&quot;: &quot;tsconfig.app.json&quot;,
&quot;aot&quot;: true,
&quot;assets&quot;: [
&quot;src/favicon.ico&quot;,
&quot;src/assets&quot;
],
&quot;styles&quot;: [
&quot;src/styles.css&quot;,
&quot;src/test.css&quot;
],
&quot;scripts&quot;: []
},
&quot;configurations&quot;: {
&quot;production&quot;: {
&quot;fileReplacements&quot;: [
{
&quot;replace&quot;: &quot;src/environments/environment.ts&quot;,
&quot;with&quot;: &quot;src/environments/environment.prod.ts&quot;
}
],
&quot;optimization&quot;: true,
&quot;outputHashing&quot;: &quot;all&quot;,
&quot;sourceMap&quot;: false,
&quot;namedChunks&quot;: false,
&quot;extractLicenses&quot;: true,
&quot;vendorChunk&quot;: false,
&quot;buildOptimizer&quot;: true,
&quot;budgets&quot;: [
{
&quot;type&quot;: &quot;initial&quot;,
&quot;maximumWarning&quot;: &quot;500kb&quot;,
&quot;maximumError&quot;: &quot;1mb&quot;
},
{
&quot;type&quot;: &quot;anyComponentStyle&quot;,
&quot;maximumWarning&quot;: &quot;2kb&quot;,
&quot;maximumError&quot;: &quot;4kb&quot;
}
]
}
}
},
&quot;serve&quot;: {
&quot;builder&quot;: &quot;@angular-devkit/build-angular:dev-server&quot;,
&quot;options&quot;: {
&quot;browserTarget&quot;: &quot;angular.io-example:build&quot;
},
&quot;configurations&quot;: {
&quot;production&quot;: {
&quot;browserTarget&quot;: &quot;angular.io-example:build:production&quot;
}
}
},
&quot;extract-i18n&quot;: {
&quot;builder&quot;: &quot;@angular-devkit/build-angular:extract-i18n&quot;,
&quot;options&quot;: {
&quot;browserTarget&quot;: &quot;angular.io-example:build&quot;
}
},
&quot;test&quot;: {
&quot;builder&quot;: &quot;@angular-devkit/build-angular:karma&quot;,
&quot;options&quot;: {
&quot;main&quot;: &quot;src/test.ts&quot;,
&quot;polyfills&quot;: &quot;src/polyfills.ts&quot;,
&quot;tsConfig&quot;: &quot;tsconfig.spec.json&quot;,
&quot;karmaConfig&quot;: &quot;karma.conf.js&quot;,
&quot;assets&quot;: [
&quot;src/favicon.ico&quot;,
&quot;src/assets&quot;
],
&quot;styles&quot;: [
&quot;src/styles.css&quot;
],
&quot;scripts&quot;: []
}
},
&quot;lint&quot;: {
&quot;builder&quot;: &quot;@angular-devkit/build-angular:tslint&quot;,
&quot;options&quot;: {
&quot;tsConfig&quot;: [
&quot;tsconfig.app.json&quot;,
&quot;tsconfig.spec.json&quot;,
&quot;e2e/tsconfig.json&quot;
],
&quot;exclude&quot;: [
&quot;**/node_modules/**&quot;
]
}
},
&quot;e2e&quot;: {
&quot;builder&quot;: &quot;@angular-devkit/build-angular:protractor&quot;,
&quot;options&quot;: {
&quot;protractorConfig&quot;: &quot;e2e/protractor.conf.js&quot;,
&quot;devServerTarget&quot;: &quot;angular.io-example:serve&quot;
},
&quot;configurations&quot;: {
&quot;production&quot;: {
&quot;devServerTarget&quot;: &quot;angular.io-example:serve:production&quot;
}
}
}
}
}
},
&quot;defaultProject&quot;: &quot;angular.io-example&quot;
}
"><input type="hidden" name="files[src/polyfills.ts]" value="/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called &quot;evergreen&quot; browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/** IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
*/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch
* requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch
* specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/
/*
Copyright Google LLC. 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
*/"><input type="hidden" name="files[tsconfig.json]" value="{
&quot;compileOnSave&quot;: false,
&quot;compilerOptions&quot;: {
&quot;baseUrl&quot;: &quot;./&quot;,
&quot;outDir&quot;: &quot;./dist/out-tsc&quot;,
&quot;forceConsistentCasingInFileNames&quot;: true,
&quot;noImplicitReturns&quot;: true,
&quot;noFallthroughCasesInSwitch&quot;: true,
&quot;sourceMap&quot;: true,
&quot;declaration&quot;: false,
&quot;downlevelIteration&quot;: true,
&quot;experimentalDecorators&quot;: true,
&quot;moduleResolution&quot;: &quot;node&quot;,
&quot;importHelpers&quot;: true,
&quot;target&quot;: &quot;es2015&quot;,
&quot;module&quot;: &quot;es2020&quot;,
&quot;lib&quot;: [
&quot;es2018&quot;,
&quot;dom&quot;
]
},
&quot;angularCompilerOptions&quot;: {
&quot;strictInjectionParameters&quot;: true,
&quot;strictInputAccessModifiers&quot;: true,
&quot;strictTemplates&quot;: true,
&quot;enableIvy&quot;: true
}
}"><input type="hidden" name="tags[0]" value="angular"><input type="hidden" name="tags[1]" value="example"><input type="hidden" name="tags[2]" value="testing"><input type="hidden" name="description" value="Angular Example - Testing - specs"><input type="hidden" name="dependencies" value="{&quot;@angular/animations&quot;:&quot;~11.0.1&quot;,&quot;@angular/common&quot;:&quot;~11.0.1&quot;,&quot;@angular/compiler&quot;:&quot;~11.0.1&quot;,&quot;@angular/core&quot;:&quot;~11.0.1&quot;,&quot;@angular/forms&quot;:&quot;~11.0.1&quot;,&quot;@angular/platform-browser&quot;:&quot;~11.0.1&quot;,&quot;@angular/platform-browser-dynamic&quot;:&quot;~11.0.1&quot;,&quot;@angular/router&quot;:&quot;~11.0.1&quot;,&quot;angular-in-memory-web-api&quot;:&quot;~0.11.0&quot;,&quot;rxjs&quot;:&quot;~6.6.0&quot;,&quot;tslib&quot;:&quot;^2.0.0&quot;,&quot;zone.js&quot;:&quot;~0.11.4&quot;,&quot;jasmine-core&quot;:&quot;~3.6.0&quot;,&quot;jasmine-marbles&quot;:&quot;~0.6.0&quot;}"></form>
<script>
var embedded = 'ctl=1';
var isEmbedded = window.location.search.indexOf(embedded) > -1;
if (isEmbedded) {
var form = document.getElementById('mainForm');
var action = form.action;
var actionHasParams = action.indexOf('?') > -1;
var symbol = actionHasParams ? '&' : '?'
form.action = form.action + symbol + embedded;
}
document.getElementById("mainForm").submit();
</script>
</body></html>