1828 lines
55 KiB
HTML
1828 lines
55 KiB
HTML
<html lang="en"><head></head><body><form id="mainForm" method="post" action="http://plnkr.co/edit/?p=preview" target="_self"><input type="hidden" name="files[browser-test-shim.js]" value="// BROWSER TESTING SHIM
|
|
// Keep it in-sync with what karma-test-shim does
|
|
/*global jasmine, __karma__, window*/
|
|
(function () {
|
|
|
|
Error.stackTraceLimit = 0; // "No stacktrace"" is usually best for app testing.
|
|
|
|
// Uncomment to get full stacktrace output. Sometimes helpful, usually not.
|
|
// Error.stackTraceLimit = Infinity; //
|
|
|
|
jasmine.DEFAULT_TIMEOUT_INTERVAL = 3000;
|
|
|
|
var baseURL = document.baseURI;
|
|
baseURL = baseURL + baseURL[baseURL.length-1] ? '' : '/';
|
|
|
|
System.config({
|
|
baseURL: baseURL,
|
|
// Extend usual application package list with test folder
|
|
packages: { 'testing': { main: 'index.js', defaultExtension: 'js' } },
|
|
|
|
// Assume npm: is set in `paths` in systemjs.config
|
|
// Map the angular testing umd bundles
|
|
map: {
|
|
'@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js',
|
|
'@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js',
|
|
'@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js',
|
|
'@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js',
|
|
'@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js',
|
|
'@angular/http/testing': 'npm:@angular/http/bundles/http-testing.umd.js',
|
|
'@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js',
|
|
'@angular/forms/testing': 'npm:@angular/forms/bundles/forms-testing.umd.js',
|
|
},
|
|
});
|
|
|
|
System.import('systemjs.config.js')
|
|
.then(importSystemJsExtras)
|
|
.then(initTestBed)
|
|
.then(initTesting);
|
|
|
|
/** Optional SystemJS configuration extras. Keep going w/o it */
|
|
function importSystemJsExtras(){
|
|
return System.import('systemjs.config.extras.js')
|
|
.catch(function(reason) {
|
|
console.log(
|
|
'Note: System.import could not load "systemjs.config.extras.js" where you might have added more configuration. It is an optional file so we will continue without it.'
|
|
);
|
|
console.log(reason);
|
|
});
|
|
}
|
|
|
|
function initTestBed(){
|
|
return Promise.all([
|
|
System.import('@angular/core/testing'),
|
|
System.import('@angular/platform-browser-dynamic/testing')
|
|
])
|
|
|
|
.then(function (providers) {
|
|
var coreTesting = providers[0];
|
|
var browserTesting = providers[1];
|
|
|
|
coreTesting.TestBed.initTestEnvironment(
|
|
browserTesting.BrowserDynamicTestingModule,
|
|
browserTesting.platformBrowserDynamicTesting());
|
|
})
|
|
}
|
|
|
|
// Import all spec files defined in the html (__spec_files__)
|
|
// and start Jasmine testrunner
|
|
function initTesting () {
|
|
console.log('loading spec files: '+__spec_files__.join(', '));
|
|
return Promise.all(
|
|
__spec_files__.map(function(spec) {
|
|
return System.import(spec);
|
|
})
|
|
)
|
|
// After all imports load, re-execute `window.onload` which
|
|
// triggers the Jasmine test-runner start or explain what went wrong
|
|
.then(success, console.error.bind(console));
|
|
|
|
function success () {
|
|
console.log('Spec files loaded; starting Jasmine testrunner');
|
|
window.onload();
|
|
}
|
|
}
|
|
|
|
})();
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[systemjs.config.extras.js]" value="/** App specific SystemJS configuration */
|
|
System.config({
|
|
packages: {
|
|
// barrels
|
|
'app/model': {main:'index.js', defaultExtension:'js'},
|
|
'app/model/testing': {main:'index.js', defaultExtension:'js'}
|
|
}
|
|
});
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[styles.css]" value="/* Master Styles */
|
|
h1 {
|
|
color: #369;
|
|
font-family: Arial, Helvetica, sans-serif;
|
|
font-size: 250%;
|
|
}
|
|
h2, h3 {
|
|
color: #444;
|
|
font-family: Arial, Helvetica, sans-serif;
|
|
font-weight: lighter;
|
|
}
|
|
body {
|
|
margin: 2em;
|
|
}
|
|
body, input[text], button {
|
|
color: #888;
|
|
font-family: Cambria, Georgia;
|
|
}
|
|
a {
|
|
cursor: pointer;
|
|
cursor: hand;
|
|
}
|
|
button {
|
|
font-family: Arial;
|
|
background-color: #eee;
|
|
border: none;
|
|
padding: 5px 10px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
cursor: hand;
|
|
}
|
|
button:hover {
|
|
background-color: #cfd8dc;
|
|
}
|
|
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: #eee;
|
|
border-radius: 4px;
|
|
}
|
|
nav a:visited, a:link {
|
|
color: #607D8B;
|
|
}
|
|
nav a:hover {
|
|
color: #039be5;
|
|
background-color: #CFD8DC;
|
|
}
|
|
nav a.active {
|
|
color: #039be5;
|
|
}
|
|
|
|
/* items class */
|
|
.items {
|
|
margin: 0 0 2em 0;
|
|
list-style-type: none;
|
|
padding: 0;
|
|
width: 24em;
|
|
}
|
|
.items li {
|
|
cursor: pointer;
|
|
position: relative;
|
|
left: 0;
|
|
background-color: #EEE;
|
|
margin: .5em;
|
|
padding: .3em 0;
|
|
height: 1.6em;
|
|
border-radius: 4px;
|
|
}
|
|
.items li:hover {
|
|
color: #607D8B;
|
|
background-color: #DDD;
|
|
left: .1em;
|
|
}
|
|
.items li.selected {
|
|
background-color: #CFD8DC;
|
|
color: white;
|
|
}
|
|
.items li.selected:hover {
|
|
background-color: #BBD8DC;
|
|
}
|
|
.items .text {
|
|
position: relative;
|
|
top: -3px;
|
|
}
|
|
.items .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;
|
|
}
|
|
/* everywhere else */
|
|
* {
|
|
font-family: Arial, Helvetica, sans-serif;
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/bag/bag-external-template.html]" value="<span>from external template</span>
|
|
|
|
|
|
<!--
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
-->"><input type="hidden" name="files[app/bag/async-helper.spec.ts]" value="import { async, fakeAsync, tick } from '@angular/core/testing';
|
|
|
|
import { Observable } from 'rxjs/Observable';
|
|
|
|
describe('Angular async helper', () => {
|
|
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',
|
|
async(() => { setTimeout(() => { actuallyDone = true; }, 0); }));
|
|
|
|
it('should run async test with successful promise', async(() => {
|
|
const p = new Promise(resolve => { setTimeout(resolve, 10); });
|
|
p.then(() => { actuallyDone = true; });
|
|
}));
|
|
|
|
it('should run async test with failed promise', async(() => {
|
|
const p = new Promise((resolve, reject) => { setTimeout(reject, 10); });
|
|
p.catch(() => { actuallyDone = true; });
|
|
}));
|
|
|
|
// Fail message: Cannot use setInterval from within an async zone test
|
|
// See https://github.com/angular/angular/issues/10127
|
|
xit('should run async test with successful delayed Observable', async(() => {
|
|
const source = Observable.of(true).delay(10);
|
|
source.subscribe(
|
|
val => actuallyDone = true,
|
|
err => fail(err)
|
|
);
|
|
}));
|
|
|
|
// Fail message: Error: 1 periodic timer(s) still in the queue
|
|
// See https://github.com/angular/angular/issues/10127
|
|
xit('should run async test with successful delayed Observable', fakeAsync(() => {
|
|
const source = Observable.of(true).delay(10);
|
|
source.subscribe(
|
|
val => actuallyDone = true,
|
|
err => fail(err)
|
|
);
|
|
|
|
tick();
|
|
}));
|
|
|
|
});
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/bag/bag.no-testbed.spec.ts]" value="import { DependentService, FancyService } from './bag';
|
|
|
|
///////// Fakes /////////
|
|
export class FakeFancyService extends FancyService {
|
|
value: string = 'faked value';
|
|
}
|
|
////////////////////////
|
|
// Straight Jasmine - no imports from Angular test libraries
|
|
|
|
describe('FancyService without the TestBed', () => {
|
|
let service: FancyService;
|
|
|
|
beforeEach(() => { service = new FancyService(); });
|
|
|
|
it('#getValue should return real value', () => {
|
|
expect(service.getValue()).toBe('real value');
|
|
});
|
|
|
|
it('#getAsyncValue should return async value', done => {
|
|
service.getAsyncValue().then(value => {
|
|
expect(value).toBe('async value');
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('#getTimeoutValue should return timeout value', done => {
|
|
service = new FancyService();
|
|
service.getTimeoutValue().then(value => {
|
|
expect(value).toBe('timeout value');
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('#getObservableValue should return observable value', done => {
|
|
service.getObservableValue().subscribe(value => {
|
|
expect(value).toBe('observable value');
|
|
done();
|
|
});
|
|
});
|
|
|
|
});
|
|
|
|
// DependentService requires injection of a FancyService
|
|
describe('DependentService without the TestBed', () => {
|
|
let service: DependentService;
|
|
|
|
it('#getValue should return real value by way of the real FancyService', () => {
|
|
service = new DependentService(new FancyService());
|
|
expect(service.getValue()).toBe('real value');
|
|
});
|
|
|
|
it('#getValue should return faked value by way of a fakeService', () => {
|
|
service = new DependentService(new FakeFancyService());
|
|
expect(service.getValue()).toBe('faked value');
|
|
});
|
|
|
|
it('#getValue should return faked value from a fake object', () => {
|
|
const fake = { getValue: () => 'fake value' };
|
|
service = new DependentService(fake as FancyService);
|
|
expect(service.getValue()).toBe('fake value');
|
|
});
|
|
|
|
it('#getValue should return stubbed value from a FancyService spy', () => {
|
|
const fancy = new FancyService();
|
|
const stubValue = 'stub value';
|
|
const spy = spyOn(fancy, 'getValue').and.returnValue(stubValue);
|
|
service = new DependentService(fancy);
|
|
|
|
expect(service.getValue()).toBe(stubValue, 'service returned stub value');
|
|
expect(spy.calls.count()).toBe(1, 'stubbed method was called once');
|
|
expect(spy.calls.mostRecent().returnValue).toBe(stubValue);
|
|
});
|
|
});
|
|
|
|
import { ReversePipe } from './bag';
|
|
|
|
describe('ReversePipe', () => {
|
|
let pipe: ReversePipe;
|
|
|
|
beforeEach(() => { pipe = new ReversePipe(); });
|
|
|
|
it('transforms "abc" to "cba"', () => {
|
|
expect(pipe.transform('abc')).toBe('cba');
|
|
});
|
|
|
|
it('no change to palindrome: "able was I ere I saw elba"', () => {
|
|
const palindrome = 'able was I ere I saw elba';
|
|
expect(pipe.transform(palindrome)).toBe(palindrome);
|
|
});
|
|
|
|
});
|
|
|
|
|
|
import { ButtonComponent } from './bag';
|
|
describe('ButtonComp', () => {
|
|
let comp: ButtonComponent;
|
|
beforeEach(() => comp = new ButtonComponent());
|
|
|
|
it('#isOn should be false initially', () => {
|
|
expect(comp.isOn).toBe(false);
|
|
});
|
|
|
|
it('#clicked() should set #isOn to true', () => {
|
|
comp.clicked();
|
|
expect(comp.isOn).toBe(true);
|
|
});
|
|
|
|
it('#clicked() should set #message to "is on"', () => {
|
|
comp.clicked();
|
|
expect(comp.message).toMatch(/is on/i);
|
|
});
|
|
|
|
it('#clicked() should toggle #isOn', () => {
|
|
comp.clicked();
|
|
expect(comp.isOn).toBe(true);
|
|
comp.clicked();
|
|
expect(comp.isOn).toBe(false);
|
|
});
|
|
});
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/bag/bag.spec.ts]" value="import {
|
|
BagModule,
|
|
BankAccountComponent, BankAccountParentComponent,
|
|
ButtonComponent,
|
|
Child1Component, Child2Component, Child3Component,
|
|
FancyService,
|
|
ExternalTemplateComponent,
|
|
InputComponent,
|
|
IoComponent, IoParentComponent,
|
|
MyIfComponent, MyIfChildComponent, MyIfParentComponent,
|
|
NeedsContentComponent, ParentComponent,
|
|
TestProvidersComponent, TestViewProvidersComponent,
|
|
ReversePipeComponent, ShellComponent
|
|
} from './bag';
|
|
|
|
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 { async, ComponentFixture, fakeAsync, inject, TestBed, tick
|
|
} from '@angular/core/testing';
|
|
|
|
import { addMatchers, newEvent, click } from '../../testing';
|
|
|
|
beforeEach( addMatchers );
|
|
|
|
//////// Service Tests /////////////
|
|
describe('use inject helper in beforeEach', () => {
|
|
let service: FancyService;
|
|
|
|
beforeEach(() => {
|
|
TestBed.configureTestingModule({ providers: [FancyService] });
|
|
|
|
// `TestBed.get` returns the injectable or an
|
|
// alternative object (including null) if the service provider is not found.
|
|
// Of course it will be found in this case because we're providing it.
|
|
service = TestBed.get(FancyService, null);
|
|
});
|
|
|
|
it('should use FancyService', () => {
|
|
expect(service.getValue()).toBe('real value');
|
|
});
|
|
|
|
it('should use FancyService', () => {
|
|
expect(service.getValue()).toBe('real value');
|
|
});
|
|
|
|
it('test should wait for FancyService.getAsyncValue', async(() => {
|
|
service.getAsyncValue().then(
|
|
value => expect(value).toBe('async value')
|
|
);
|
|
}));
|
|
|
|
it('test should wait for FancyService.getTimeoutValue', async(() => {
|
|
service.getTimeoutValue().then(
|
|
value => expect(value).toBe('timeout value')
|
|
);
|
|
}));
|
|
|
|
it('test should wait for FancyService.getObservableValue', async(() => {
|
|
service.getObservableValue().subscribe(
|
|
value => expect(value).toBe('observable value')
|
|
);
|
|
}));
|
|
|
|
// See https://github.com/angular/angular/issues/10127
|
|
xit('test should wait for FancyService.getObservableDelayValue', async(() => {
|
|
service.getObservableDelayValue().subscribe(
|
|
value => expect(value).toBe('observable delay value')
|
|
);
|
|
}));
|
|
it('should allow the use of fakeAsync', fakeAsync(() => {
|
|
let value: any;
|
|
service.getAsyncValue().then((val: any) => value = val);
|
|
tick(); // Trigger JS engine cycle until all promises resolve.
|
|
expect(value).toBe('async value');
|
|
}));
|
|
});
|
|
|
|
describe('use inject within `it`', () => {
|
|
beforeEach(() => {
|
|
TestBed.configureTestingModule({ providers: [FancyService] });
|
|
});
|
|
|
|
|
|
it('should use modified providers',
|
|
inject([FancyService], (service: FancyService) => {
|
|
service.setValue('value modified in beforeEach');
|
|
expect(service.getValue()).toBe('value modified in beforeEach');
|
|
})
|
|
);
|
|
|
|
it('test should wait for FancyService.getTimeoutValue',
|
|
async(inject([FancyService], (service: FancyService) => {
|
|
|
|
service.getTimeoutValue().then(
|
|
value => expect(value).toBe('timeout value')
|
|
);
|
|
})));
|
|
});
|
|
|
|
describe('using async(inject) within beforeEach', () => {
|
|
let serviceValue: string;
|
|
|
|
beforeEach(() => {
|
|
TestBed.configureTestingModule({ providers: [FancyService] });
|
|
});
|
|
|
|
beforeEach( async(inject([FancyService], (service: FancyService) => {
|
|
service.getAsyncValue().then(value => serviceValue = value);
|
|
})));
|
|
|
|
it('should use asynchronously modified value ... in synchronous test', () => {
|
|
expect(serviceValue).toBe('async value');
|
|
});
|
|
});
|
|
|
|
|
|
/////////// Component Tests //////////////////
|
|
|
|
describe('TestBed Component Tests', () => {
|
|
|
|
beforeEach( async(() => {
|
|
TestBed
|
|
.configureTestingModule({
|
|
imports: [BagModule],
|
|
})
|
|
// Compile everything in BagModule
|
|
.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', () => {
|
|
const fixture = TestBed.createComponent(IoParentComponent);
|
|
const comp = fixture.componentInstance;
|
|
|
|
fixture.detectChanges();
|
|
const heroEl = fixture.debugElement.query(By.css('.hero')); // first hero
|
|
|
|
const ngForRow = heroEl.parent; // Angular's NgForRow wrapper element
|
|
|
|
// jasmine.any is instance-of-type test.
|
|
expect(ngForRow.componentInstance).toEqual(jasmine.any(IoComponent), 'component is IoComp');
|
|
|
|
const hero = ngForRow.context['$implicit']; // the hero object
|
|
expect(hero.name).toBe(comp.heroes[0].name, '1st hero\'s name');
|
|
});
|
|
|
|
|
|
it('should support clicking a button', () => {
|
|
const fixture = TestBed.createComponent(ButtonComponent);
|
|
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)', async(() => {
|
|
const expectedOrigName = 'John';
|
|
const expectedNewName = 'Sally';
|
|
|
|
const fixture = TestBed.createComponent(InputComponent);
|
|
fixture.detectChanges();
|
|
|
|
const comp = fixture.componentInstance;
|
|
const input = <HTMLInputElement> fixture.debugElement.query(By.css('input')).nativeElement;
|
|
|
|
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
|
|
input.dispatchEvent(newEvent('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 = <HTMLInputElement> fixture.debugElement.query(By.css('input')).nativeElement;
|
|
|
|
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
|
|
input.dispatchEvent(newEvent('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
|
|
input.dispatchEvent(newEvent('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(comp, 'context is the parent 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']).toBe(false, '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 FancyService provider', () => {
|
|
const fixture = TestBed.configureTestingModule({
|
|
declarations: [TestProvidersComponent],
|
|
})
|
|
.overrideComponent(TestProvidersComponent, {
|
|
remove: { providers: [FancyService]},
|
|
add: { providers: [{ provide: FancyService, useClass: FakeFancyService }] },
|
|
|
|
// Or replace them all (this component has only one provider)
|
|
// set: { providers: [{ provide: FancyService, useClass: FakeFancyService }] },
|
|
})
|
|
.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(FancyService, 'FancyService');
|
|
});
|
|
|
|
it('should override TestViewProvidersComp\'s FancyService viewProvider', () => {
|
|
const fixture = TestBed.configureTestingModule({
|
|
declarations: [TestViewProvidersComponent],
|
|
})
|
|
.overrideComponent(TestViewProvidersComponent, {
|
|
// remove: { viewProviders: [FancyService]},
|
|
// add: { viewProviders: [{ provide: FancyService, useClass: FakeFancyService }] },
|
|
|
|
// Or replace them all (this component has only one viewProvider)
|
|
set: { viewProviders: [{ provide: FancyService, useClass: FakeFancyService }] },
|
|
})
|
|
.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 FancyService provider: module, TestCompomponent, TestProvidersComponent
|
|
const fixture = TestBed.configureTestingModule({
|
|
declarations: [TestComponent, TestProvidersComponent],
|
|
providers: [FancyService]
|
|
})
|
|
.overrideComponent(TestComponent, {
|
|
set: { providers: [{ provide: FancyService, useValue: {} }] }
|
|
})
|
|
.overrideComponent(TestProvidersComponent, {
|
|
set: { providers: [{ provide: FancyService, useClass: FakeFancyService }] }
|
|
})
|
|
.createComponent(TestComponent);
|
|
|
|
let testBedProvider: FancyService;
|
|
let tcProvider: {};
|
|
let tpcProvider: FakeFancyService;
|
|
|
|
// `inject` uses TestBed's injector
|
|
inject([FancyService], (s: FancyService) => testBedProvider = s)();
|
|
tcProvider = fixture.debugElement.injector.get(FancyService);
|
|
tpcProvider = fixture.debugElement.children[0].injector.get(FancyService);
|
|
|
|
expect(testBedProvider).not.toBe(tcProvider, 'testBed/tc not same providers');
|
|
expect(testBedProvider).not.toBe(tpcProvider, 'testBed/tpc not same providers');
|
|
|
|
expect(testBedProvider instanceof FancyService).toBe(true, 'testBedProvider is FancyService');
|
|
expect(tcProvider).toEqual({}, 'tcProvider is {}');
|
|
expect(tpcProvider instanceof FakeFancyService).toBe(true, 'tpcProvider is FakeFancyService');
|
|
});
|
|
|
|
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="My"></child-1>
|
|
<child-2 #content text="dog"></child-2>
|
|
<child-2 text="has"></child-2>
|
|
<child-3 #content text="fleas"></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( async(() => {
|
|
TestBed.configureTestingModule({
|
|
declarations: [ParentComponent, FakeChildComponent]
|
|
})
|
|
.compileComponents();
|
|
}));
|
|
|
|
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( async(() => {
|
|
TestBed.configureTestingModule({
|
|
declarations: [ParentComponent, FakeChildWithGrandchildComponent, FakeGrandchildComponent]
|
|
})
|
|
.compileComponents();
|
|
}));
|
|
|
|
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( async(() => {
|
|
TestBed.configureTestingModule({
|
|
imports: [FormsModule],
|
|
declarations: [MyIfChildComponent, MyIfParentComponent]
|
|
})
|
|
.compileComponents().then(() => {
|
|
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', async(() => {
|
|
fixture.detectChanges();
|
|
getChild();
|
|
|
|
child.childValue = 'bar';
|
|
|
|
return new Promise(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 "Close Child" 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(function (de) { return de.componentInstance instanceof MyIfChildComponent; })[0];
|
|
|
|
// WE'LL USE THIS APPROACH !
|
|
// DebugElement.query: find first instance (if any)
|
|
childDe = fixture.debugElement
|
|
.query(function (de) { return de.componentInstance instanceof MyIfChildComponent; });
|
|
|
|
if (childDe && 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 FakeFancyService extends FancyService {
|
|
value: string = 'faked value';
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/bag/bag.ts]" value="/* tslint:disable:forin */
|
|
import { Component, ContentChildren, Directive, EventEmitter,
|
|
Injectable, Input, Output, Optional,
|
|
HostBinding, HostListener,
|
|
OnInit, OnChanges, OnDestroy,
|
|
Pipe, PipeTransform,
|
|
SimpleChange } from '@angular/core';
|
|
|
|
import { Observable } from 'rxjs/Observable';
|
|
import 'rxjs/add/observable/of';
|
|
import 'rxjs/add/operator/delay';
|
|
|
|
////////// The App: Services and Components for the tests. //////////////
|
|
|
|
export class Hero {
|
|
name: string;
|
|
}
|
|
|
|
////////// Services ///////////////
|
|
@Injectable()
|
|
export class FancyService {
|
|
protected value: string = 'real value';
|
|
|
|
getValue() { return this.value; }
|
|
setValue(value: string) { this.value = value; }
|
|
|
|
getAsyncValue() { return Promise.resolve('async value'); }
|
|
|
|
getObservableValue() { return Observable.of('observable value'); }
|
|
|
|
getTimeoutValue() {
|
|
return new Promise((resolve) => {
|
|
setTimeout(() => { resolve('timeout value'); }, 10);
|
|
});
|
|
}
|
|
|
|
getObservableDelayValue() {
|
|
return Observable.of('observable delay value').delay(10);
|
|
}
|
|
}
|
|
|
|
@Injectable()
|
|
export class DependentService {
|
|
constructor(private dependentService: FancyService) { }
|
|
getValue() { return this.dependentService.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="RBC"
|
|
account="4747"
|
|
[style.width.px]="width"
|
|
[style.color]="color"
|
|
[class.closed]="isClosed"
|
|
[class.open]="!isClosed">
|
|
</bank-account>
|
|
`
|
|
})
|
|
export class BankAccountParentComponent {
|
|
width = 200;
|
|
color = 'red';
|
|
isClosed = true;
|
|
}
|
|
|
|
@Component({
|
|
selector: 'button-comp',
|
|
template: `
|
|
<button (click)="clicked()">Click me!</button>
|
|
<span>{{message}}</span>`
|
|
})
|
|
export class ButtonComponent {
|
|
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)]="name">`
|
|
})
|
|
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 style-guide 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)]="name"> {{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="hero" (click)="click()">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="!selectedHero"><i>Click to select a hero</i></p>
|
|
<p *ngIf="selectedHero">The selected hero is {{selectedHero.name}}</p>
|
|
<io-comp
|
|
*ngFor="let hero of heroes"
|
|
[hero]=hero
|
|
(selected)="onSelect($event)">
|
|
</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="showMore">More</span>)`
|
|
})
|
|
export class MyIfComponent {
|
|
showMore = false;
|
|
}
|
|
|
|
@Component({
|
|
selector: 'my-service-comp',
|
|
template: `injected value: {{fancyService.value}}`,
|
|
providers: [FancyService]
|
|
})
|
|
export class TestProvidersComponent {
|
|
constructor(private fancyService: FancyService) {}
|
|
}
|
|
|
|
|
|
@Component({
|
|
selector: 'my-service-comp',
|
|
template: `injected value: {{fancyService.value}}`,
|
|
viewProviders: [FancyService]
|
|
})
|
|
export class TestViewProvidersComponent {
|
|
constructor(private fancyService: FancyService) {}
|
|
}
|
|
|
|
@Component({
|
|
moduleId: module.id,
|
|
selector: 'external-template-comp',
|
|
templateUrl: './bag-external-template.html'
|
|
})
|
|
export class ExternalTemplateComponent implements OnInit {
|
|
serviceValue: string;
|
|
|
|
constructor(@Optional() private service: FancyService) { }
|
|
|
|
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({
|
|
moduleId: module.id,
|
|
selector: 'bad-template-comp',
|
|
templateUrl: './non-existant.html'
|
|
})
|
|
export class BadTemplateUrlComponent { }
|
|
|
|
|
|
|
|
@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)]="childValue"> </label>
|
|
</div>
|
|
<p><i>Change log:</i></p>
|
|
<div *ngFor="let log of changeLog; let i=index">{{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: {[propertyName: string]: SimpleChange}) {
|
|
for (let propName in changes) {
|
|
this.ngOnChangesCounter += 1;
|
|
let prop = changes[propName];
|
|
let cur = JSON.stringify(prop.currentValue);
|
|
let 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)]="parentValue">
|
|
</label>
|
|
<button (click)="clicked()">{{toggleLabel}} Child</button><br>
|
|
<div *ngIf="showChild"
|
|
style="margin: 4px; padding: 4px; background-color: aliceblue;">
|
|
<my-if-child-1 [(value)]="parentValue"></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)]="text">
|
|
<span>{{text | reverse}}</span>
|
|
`
|
|
})
|
|
export class ReversePipeComponent {
|
|
text = 'my dog has fleas.';
|
|
}
|
|
|
|
@Component({template: '<div>Replace Me</div>'})
|
|
export class ShellComponent { }
|
|
|
|
@Component({
|
|
selector: 'bag-comp',
|
|
template: `
|
|
<h1>Specs Bag</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>
|
|
<button-comp></button-comp>
|
|
<hr>
|
|
<h3>Needs Content</h3>
|
|
<needs-content #nc>
|
|
<child-1 #content text="My"></child-1>
|
|
<child-2 #content text="dog"></child-2>
|
|
<child-2 text="has"></child-2>
|
|
<child-3 #content text="fleas"></child-3>
|
|
<div #content>!</div>
|
|
</needs-content>
|
|
`
|
|
})
|
|
export class BagComponent { }
|
|
//////// Aggregations ////////////
|
|
|
|
export const bagDeclarations = [
|
|
BagComponent,
|
|
BankAccountComponent, BankAccountParentComponent,
|
|
ButtonComponent,
|
|
Child1Component, Child2Component, Child3Component,
|
|
ExternalTemplateComponent, InnerCompWithExternalTemplateComponent,
|
|
InputComponent,
|
|
InputValueBinderDirective, InputValueBinderComponent,
|
|
IoComponent, IoParentComponent,
|
|
MyIfComponent, MyIfChildComponent, MyIfParentComponent,
|
|
NeedsContentComponent, ParentComponent,
|
|
TestProvidersComponent, TestViewProvidersComponent,
|
|
ReversePipe, ReversePipeComponent, ShellComponent
|
|
];
|
|
|
|
export const bagProviders = [DependentService, FancyService];
|
|
|
|
////////////////////
|
|
////////////
|
|
import { NgModule } from '@angular/core';
|
|
import { BrowserModule } from '@angular/platform-browser';
|
|
import { FormsModule } from '@angular/forms';
|
|
|
|
@NgModule({
|
|
imports: [BrowserModule, FormsModule],
|
|
declarations: bagDeclarations,
|
|
providers: bagProviders,
|
|
entryComponents: [BagComponent],
|
|
bootstrap: [BagComponent]
|
|
})
|
|
export class BagModule { }
|
|
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[testing/index.ts]" value="import { DebugElement } from '@angular/core';
|
|
import { tick, ComponentFixture } from '@angular/core/testing';
|
|
|
|
export * from './jasmine-matchers';
|
|
export * from './router-stubs';
|
|
|
|
///// Short utilities /////
|
|
|
|
/** Wait a tick, then detect changes */
|
|
export function advance(f: ComponentFixture<any>): void {
|
|
tick();
|
|
f.detectChanges();
|
|
}
|
|
|
|
/**
|
|
* Create custom DOM event the old fashioned way
|
|
*
|
|
* https://developer.mozilla.org/en-US/docs/Web/API/Event/initEvent
|
|
* Although officially deprecated, some browsers (phantom) don't accept the preferred "new Event(eventName)"
|
|
*/
|
|
export function newEvent(eventName: string, bubbles = false, cancelable = false) {
|
|
let evt = document.createEvent('CustomEvent'); // MUST be 'CustomEvent'
|
|
evt.initCustomEvent(eventName, bubbles, cancelable, null);
|
|
return evt;
|
|
}
|
|
|
|
// 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 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[testing/jasmine-matchers.d.ts]" value="declare namespace jasmine {
|
|
interface Matchers {
|
|
toHaveText(actual: any, expectationFailOutput?: any): jasmine.CustomMatcher;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[testing/jasmine-matchers.ts]" value="/// <reference path="./jasmine-matchers.d.ts" />
|
|
|
|
//// Jasmine Custom Matchers ////
|
|
// Be sure to extend jasmine-matchers.d.ts when adding matchers
|
|
|
|
export function addMatchers(): void {
|
|
jasmine.addMatchers({
|
|
toHaveText: toHaveText
|
|
});
|
|
}
|
|
|
|
function toHaveText(): jasmine.CustomMatcher {
|
|
return {
|
|
compare: function (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 && n.hasChildNodes()) {
|
|
return elementText(Array.prototype.slice.call(n.childNodes));
|
|
}
|
|
|
|
if (n.nativeElement) { n = n.nativeElement; }
|
|
|
|
return n.textContent;
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[testing/router-stubs.ts]" value=" // export for convenience.
|
|
export { ActivatedRoute, Router, RouterLink, RouterOutlet} from '@angular/router';
|
|
|
|
import { Component, Directive, Injectable, Input } from '@angular/core';
|
|
import { NavigationExtras } from '@angular/router';
|
|
|
|
@Directive({
|
|
selector: '[routerLink]',
|
|
host: {
|
|
'(click)': 'onClick()'
|
|
}
|
|
})
|
|
export class RouterLinkStubDirective {
|
|
@Input('routerLink') linkParams: any;
|
|
navigatedTo: any = null;
|
|
|
|
onClick() {
|
|
this.navigatedTo = this.linkParams;
|
|
}
|
|
}
|
|
|
|
@Component({selector: 'router-outlet', template: ''})
|
|
export class RouterOutletStubComponent { }
|
|
|
|
@Injectable()
|
|
export class RouterStub {
|
|
navigate(commands: any[], extras?: NavigationExtras) { }
|
|
}
|
|
|
|
|
|
// Only implements params and part of snapshot.params
|
|
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
|
|
|
@Injectable()
|
|
export class ActivatedRouteStub {
|
|
|
|
// ActivatedRoute.params is Observable
|
|
private subject = new BehaviorSubject(this.testParams);
|
|
params = this.subject.asObservable();
|
|
|
|
// Test parameters
|
|
private _testParams: {};
|
|
get testParams() { return this._testParams; }
|
|
set testParams(params: {}) {
|
|
this._testParams = params;
|
|
this.subject.next(params);
|
|
}
|
|
|
|
// ActivatedRoute.snapshot.params
|
|
get snapshot() {
|
|
return { params: this.testParams };
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[index.html]" value="<!-- Run the "bag" specs in a browser -->
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<script>document.write('<base href="' + document.location + '" />');</script>
|
|
<title>Specs Bag</title>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<link rel="stylesheet" href="styles.css">
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine.css">
|
|
|
|
</head>
|
|
<body>
|
|
<!-- Polyfills -->
|
|
<script src="https://unpkg.com/core-js/client/shim.min.js"></script>
|
|
|
|
<script src="https://unpkg.com/systemjs@0.19.39/dist/system.src.js"></script>
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine-html.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/boot.js"></script>
|
|
|
|
<script src="https://unpkg.com/zone.js@0.7.4?main=browser"></script>
|
|
<script src="https://unpkg.com/zone.js/dist/long-stack-trace-zone.js?main=browser"></script>
|
|
<script src="https://unpkg.com/zone.js/dist/proxy.js?main=browser"></script>
|
|
<script src="https://unpkg.com/zone.js/dist/sync-test.js?main=browser"></script>
|
|
<script src="https://unpkg.com/zone.js/dist/jasmine-patch.js?main=browser"></script>
|
|
<script src="https://unpkg.com/zone.js/dist/async-test.js?main=browser"></script>
|
|
<script src="https://unpkg.com/zone.js/dist/fake-async-test.js?main=browser"></script>
|
|
|
|
<script>
|
|
var __spec_files__ = [
|
|
'app/bag/bag.spec',
|
|
'app/bag/bag.no-testbed.spec',
|
|
'app/bag/async-helper.spec'
|
|
];
|
|
</script>
|
|
<script src="browser-test-shim.js"></script>
|
|
</body>
|
|
|
|
</html>
|
|
|
|
|
|
<!--
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
-->"><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="private" value="true"><input type="hidden" name="description" value="Angular Example - Testing - bag.specs"><input type="hidden" name="files[systemjs.config.js]" value="/**
|
|
* WEB ANGULAR VERSION
|
|
* (based on systemjs.config.js in angular.io)
|
|
* System configuration for Angular samples
|
|
* Adjust as necessary for your application needs.
|
|
*/
|
|
(function (global) {
|
|
System.config({
|
|
// DEMO ONLY! REAL CODE SHOULD NOT TRANSPILE IN THE BROWSER
|
|
transpiler: 'ts',
|
|
typescriptOptions: {
|
|
// Copy of compiler options in standard tsconfig.json
|
|
"target": "es5",
|
|
"module": "commonjs",
|
|
"moduleResolution": "node",
|
|
"sourceMap": true,
|
|
"emitDecoratorMetadata": true,
|
|
"experimentalDecorators": true,
|
|
"lib": ["es2015", "dom"],
|
|
"noImplicitAny": true,
|
|
"suppressImplicitAnyIndexErrors": true
|
|
},
|
|
meta: {
|
|
'typescript': {
|
|
"exports": "ts"
|
|
}
|
|
},
|
|
paths: {
|
|
// paths serve as alias
|
|
'npm:': 'https://unpkg.com/'
|
|
},
|
|
// map tells the System loader where to look for things
|
|
map: {
|
|
// our app is within the app folder
|
|
app: 'app',
|
|
|
|
// angular bundles
|
|
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
|
|
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
|
|
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
|
|
'@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
|
|
'@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
|
|
'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
|
|
'@angular/router': 'npm:@angular/router/bundles/router.umd.js',
|
|
'@angular/router/upgrade': 'npm:@angular/router/bundles/router-upgrade.umd.js',
|
|
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
|
|
'@angular/upgrade': 'npm:@angular/upgrade/bundles/upgrade.umd.js',
|
|
'@angular/upgrade/static': 'npm:@angular/upgrade/bundles/upgrade-static.umd.js',
|
|
|
|
// other libraries
|
|
'rxjs': 'npm:rxjs@5.0.1',
|
|
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js',
|
|
'ts': 'npm:plugin-typescript@5.2.7/lib/plugin.js',
|
|
'typescript': 'npm:typescript@2.0.10/lib/typescript.js',
|
|
|
|
},
|
|
// packages tells the System loader how to load when no filename and/or no extension
|
|
packages: {
|
|
app: {
|
|
main: './main.ts',
|
|
defaultExtension: 'ts'
|
|
},
|
|
rxjs: {
|
|
defaultExtension: 'js'
|
|
}
|
|
}
|
|
});
|
|
|
|
})(this);
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/
|
|
"></form><script>document.getElementById("mainForm").submit();</script></body></html> |