// #docplaster 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 ///////////// // #docregion FancyService 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. // #docregion testbed-get service = TestBed.get(FancyService, null); // #enddocregion testbed-get }); 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') ); })); // #enddocregion FancyService // 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') ); })); // #docregion FancyService 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'); })); }); // #enddocregion FancyService describe('use inject within `it`', () => { // #docregion getTimeoutValue beforeEach(() => { TestBed.configureTestingModule({ providers: [FancyService] }); }); // #enddocregion getTimeoutValue it('should use modified providers', inject([FancyService], (service: FancyService) => { service.setValue('value modified in beforeEach'); expect(service.getValue()).toBe('value modified in beforeEach'); }) ); // #docregion getTimeoutValue it('test should wait for FancyService.getTimeoutValue', async(inject([FancyService], (service: FancyService) => { service.getTimeoutValue().then( value => expect(value).toBe('timeout value') ); }))); // #enddocregion getTimeoutValue }); 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'); }); // #docregion ButtonComp 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'); }); // #enddocregion ButtonComp // 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 = 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 = 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} `); })); // #docregion ReversePipeComp 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'); })); // #enddocregion ReversePipeComp // 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'); }); // #docregion debug-dom-renderer it('DebugDomRender 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.properties['customProperty']).toBe(true, 'customProperty'); expect(el.styles['color']).toBe(comp.color, 'color style'); expect(el.styles['width']).toBe(comp.width + 'px', 'width style'); }); // #enddocregion debug-dom-renderer }); describe('TestBed Component Overrides:', () => { it('should override ChildComp\'s template', () => { const fixture = TestBed.configureTestingModule({ declarations: [Child1Component], }) .overrideComponent(Child1Component, { set: { template: 'Fake' } }) .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: '' }) 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: `
!
` } }) .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'); // #docregion custom-predicate // Filter for DebugElements with a #content reference const contentRefs = el.queryAll( de => de.references['content']); // #enddocregion custom-predicate 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; 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()` }) class FakeChildWithGrandchildComponent { } @Component({ selector: 'grandchild-1', template: `Fake Grandchild` }) class FakeGrandchildComponent { } @Injectable() class FakeFancyService extends FancyService { value: string = 'faked value'; }