test(ivy): move change detection tests into acceptance (#30425)

Moves most of the r3 change detection tests into `acceptance`. Notes:
* A handful of tests weren't migrated, because they were testing an API that isn't exposed publicly yet.
* The `should throw if bindings in children of current view have changed` and `should NOT throw if bindings in ancestors of current view have changed` tests were removed, because there's not nice way of hitting the same code path with `TestBed` and doing the same assertion as with the raw instructions. I'm open to ideas on how we could do them.
* There were a few tests that assert that the `innerHTML` looks in a particular way. I've switched them to use `textContent`, because Ivy and ViewEngine produce slightly different DOM. The tests were only checking whether the text has changed anyway.

PR Close #30425
This commit is contained in:
Kristiyan Kostadinov 2019-05-12 18:52:41 +02:00 committed by Alex Rickabaugh
parent 090eac068a
commit 96baff3a85
2 changed files with 863 additions and 1170 deletions

View File

@ -7,7 +7,8 @@
*/
import {ApplicationRef, ChangeDetectionStrategy, Component, ComponentFactoryResolver, ComponentRef, Directive, EmbeddedViewRef, NgModule, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core';
import {CommonModule} from '@angular/common';
import {ApplicationRef, ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, Directive, DoCheck, EmbeddedViewRef, ErrorHandler, Input, NgModule, OnInit, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers';
@ -127,4 +128,853 @@ describe('change detection', () => {
});
});
});
describe('OnPush', () => {
@Component({
selector: 'my-comp',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `{{ doCheckCount }} - {{ name }} <button (click)="onClick()"></button>`
})
class MyComponent implements DoCheck {
@Input()
name = 'Nancy';
doCheckCount = 0;
ngDoCheck(): void { this.doCheckCount++; }
onClick() {}
}
@Component({selector: 'my-app', template: '<my-comp [name]="name"></my-comp>'})
class MyApp {
@ViewChild(MyComponent) comp !: MyComponent;
name: string = 'Nancy';
}
it('should check OnPush components on initialization', () => {
TestBed.configureTestingModule({declarations: [MyComponent, MyApp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
expect(fixture.nativeElement.textContent.trim()).toEqual('1 - Nancy');
});
it('should call doCheck even when OnPush components are not dirty', () => {
TestBed.configureTestingModule({declarations: [MyComponent, MyApp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
fixture.detectChanges();
expect(fixture.componentInstance.comp.doCheckCount).toEqual(2);
fixture.detectChanges();
expect(fixture.componentInstance.comp.doCheckCount).toEqual(3);
});
it('should skip OnPush components in update mode when they are not dirty', () => {
TestBed.configureTestingModule({declarations: [MyComponent, MyApp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
// doCheckCount is 2, but 1 should be rendered since it has not been marked dirty.
expect(fixture.nativeElement.textContent.trim()).toEqual('1 - Nancy');
fixture.detectChanges();
// doCheckCount is 3, but 1 should be rendered since it has not been marked dirty.
expect(fixture.nativeElement.textContent.trim()).toEqual('1 - Nancy');
});
it('should check OnPush components in update mode when inputs change', () => {
TestBed.configureTestingModule({declarations: [MyComponent, MyApp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
fixture.componentInstance.name = 'Bess';
fixture.detectChanges();
expect(fixture.componentInstance.comp.doCheckCount).toEqual(2);
// View should update, as changed input marks view dirty
expect(fixture.nativeElement.textContent.trim()).toEqual('2 - Bess');
fixture.componentInstance.name = 'George';
fixture.detectChanges();
// View should update, as changed input marks view dirty
expect(fixture.componentInstance.comp.doCheckCount).toEqual(3);
expect(fixture.nativeElement.textContent.trim()).toEqual('3 - George');
fixture.detectChanges();
expect(fixture.componentInstance.comp.doCheckCount).toEqual(4);
// View should not be updated to "4", as inputs have not changed.
expect(fixture.nativeElement.textContent.trim()).toEqual('3 - George');
});
it('should check OnPush components in update mode when component events occur', () => {
TestBed.configureTestingModule({declarations: [MyComponent, MyApp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
expect(fixture.componentInstance.comp.doCheckCount).toEqual(1);
expect(fixture.nativeElement.textContent.trim()).toEqual('1 - Nancy');
const button = fixture.nativeElement.querySelector('button') !;
button.click();
// No ticks should have been scheduled.
expect(fixture.componentInstance.comp.doCheckCount).toEqual(1);
expect(fixture.nativeElement.textContent.trim()).toEqual('1 - Nancy');
fixture.detectChanges();
// Because the onPush comp should be dirty, it should update once CD runs
expect(fixture.componentInstance.comp.doCheckCount).toEqual(2);
expect(fixture.nativeElement.textContent.trim()).toEqual('2 - Nancy');
});
it('should not check OnPush components in update mode when parent events occur', () => {
@Component({
selector: 'button-parent',
template: '<my-comp></my-comp><button id="parent" (click)="noop()"></button>'
})
class ButtonParent {
@ViewChild(MyComponent) comp !: MyComponent;
noop() {}
}
TestBed.configureTestingModule({declarations: [MyComponent, ButtonParent]});
const fixture = TestBed.createComponent(ButtonParent);
fixture.detectChanges();
expect(fixture.nativeElement.textContent.trim()).toEqual('1 - Nancy');
const button: HTMLButtonElement = fixture.nativeElement.querySelector('button#parent');
button.click();
fixture.detectChanges();
// The comp should still be clean. So doCheck will run, but the view should display 1.
expect(fixture.componentInstance.comp.doCheckCount).toEqual(2);
expect(fixture.nativeElement.textContent.trim()).toEqual('1 - Nancy');
});
it('should check parent OnPush components in update mode when child events occur', () => {
@Component({
selector: 'button-parent',
template: '{{ doCheckCount }} - <my-comp></my-comp>',
changeDetection: ChangeDetectionStrategy.OnPush
})
class ButtonParent implements DoCheck {
@ViewChild(MyComponent) comp !: MyComponent;
noop() {}
doCheckCount = 0;
ngDoCheck(): void { this.doCheckCount++; }
}
@Component({selector: 'my-button-app', template: '<button-parent></button-parent>'})
class MyButtonApp {
@ViewChild(ButtonParent) parent !: ButtonParent;
}
TestBed.configureTestingModule({declarations: [MyButtonApp, MyComponent, ButtonParent]});
const fixture = TestBed.createComponent(MyButtonApp);
fixture.detectChanges();
const parent = fixture.componentInstance.parent;
const comp = parent.comp;
expect(parent.doCheckCount).toEqual(1);
expect(comp.doCheckCount).toEqual(1);
expect(fixture.nativeElement.textContent.trim()).toEqual('1 - 1 - Nancy');
fixture.detectChanges();
expect(parent.doCheckCount).toEqual(2);
// parent isn't checked, so child doCheck won't run
expect(comp.doCheckCount).toEqual(1);
expect(fixture.nativeElement.textContent.trim()).toEqual('1 - 1 - Nancy');
const button = fixture.nativeElement.querySelector('button');
button.click();
// No ticks should have been scheduled.
expect(parent.doCheckCount).toEqual(2);
expect(comp.doCheckCount).toEqual(1);
fixture.detectChanges();
expect(parent.doCheckCount).toEqual(3);
expect(comp.doCheckCount).toEqual(2);
expect(fixture.nativeElement.textContent.trim()).toEqual('3 - 2 - Nancy');
});
});
describe('ChangeDetectorRef', () => {
describe('detectChanges()', () => {
@Component({
selector: 'my-comp',
template: '{{ name }}',
changeDetection: ChangeDetectionStrategy.OnPush
})
class MyComp implements DoCheck {
doCheckCount = 0;
name = 'Nancy';
constructor(public cdr: ChangeDetectorRef) {}
ngDoCheck() { this.doCheckCount++; }
}
@Component({selector: 'parent-comp', template: `{{ doCheckCount}} - <my-comp></my-comp>`})
class ParentComp implements DoCheck {
@ViewChild(MyComp) myComp !: MyComp;
doCheckCount = 0;
constructor(public cdr: ChangeDetectorRef) {}
ngDoCheck() { this.doCheckCount++; }
}
@Directive({selector: '[dir]'})
class Dir {
constructor(public cdr: ChangeDetectorRef) {}
}
it('should check the component view when called by component (even when OnPush && clean)',
() => {
TestBed.configureTestingModule({declarations: [MyComp]});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('Nancy');
fixture.componentInstance.name =
'Bess'; // as this is not an Input, the component stays clean
fixture.componentInstance.cdr.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('Bess');
});
it('should NOT call component doCheck when called by a component', () => {
TestBed.configureTestingModule({declarations: [MyComp]});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
expect(fixture.componentInstance.doCheckCount).toEqual(1);
// NOTE: in current Angular, detectChanges does not itself trigger doCheck, but you
// may see doCheck called in some cases bc of the extra CD run triggered by zone.js.
// It's important not to call doCheck to allow calls to detectChanges in that hook.
fixture.componentInstance.cdr.detectChanges();
expect(fixture.componentInstance.doCheckCount).toEqual(1);
});
it('should NOT check the component parent when called by a child component', () => {
TestBed.configureTestingModule({declarations: [MyComp, ParentComp]});
const fixture = TestBed.createComponent(ParentComp);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('1 - Nancy');
fixture.componentInstance.doCheckCount = 100;
fixture.componentInstance.myComp.cdr.detectChanges();
expect(fixture.componentInstance.doCheckCount).toEqual(100);
expect(fixture.nativeElement.textContent).toEqual('1 - Nancy');
});
it('should check component children when called by component if dirty or check-always',
() => {
TestBed.configureTestingModule({declarations: [MyComp, ParentComp]});
const fixture = TestBed.createComponent(ParentComp);
fixture.detectChanges();
expect(fixture.componentInstance.doCheckCount).toEqual(1);
fixture.componentInstance.myComp.name = 'Bess';
fixture.componentInstance.cdr.detectChanges();
expect(fixture.componentInstance.doCheckCount).toEqual(1);
expect(fixture.componentInstance.myComp.doCheckCount).toEqual(2);
// OnPush child is not dirty, so its change isn't rendered.
expect(fixture.nativeElement.textContent).toEqual('1 - Nancy');
});
it('should not group detectChanges calls (call every time)', () => {
TestBed.configureTestingModule({declarations: [MyComp, ParentComp]});
const fixture = TestBed.createComponent(ParentComp);
fixture.detectChanges();
expect(fixture.componentInstance.doCheckCount).toEqual(1);
fixture.componentInstance.cdr.detectChanges();
fixture.componentInstance.cdr.detectChanges();
expect(fixture.componentInstance.myComp.doCheckCount).toEqual(3);
});
it('should check component view when called by directive on component node', () => {
@Component({template: '<my-comp dir></my-comp>'})
class MyApp {
@ViewChild(MyComp) myComp !: MyComp;
@ViewChild(Dir) dir !: Dir;
}
TestBed.configureTestingModule({declarations: [MyComp, Dir, MyApp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('Nancy');
fixture.componentInstance.myComp.name = 'George';
fixture.componentInstance.dir.cdr.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('George');
});
it('should check host component when called by directive on element node', () => {
@Component({template: '{{ value }}<div dir></div>'})
class MyApp {
@ViewChild(MyComp) myComp !: MyComp;
@ViewChild(Dir) dir !: Dir;
value = '';
}
TestBed.configureTestingModule({declarations: [Dir, MyApp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
fixture.componentInstance.value = 'Frank';
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('Frank');
fixture.componentInstance.value = 'Joe';
fixture.componentInstance.dir.cdr.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('Joe');
});
it('should check the host component when called from EmbeddedViewRef', () => {
@Component({template: '{{ name }}<div *ngIf="showing" dir></div>'})
class MyApp {
@ViewChild(Dir) dir !: Dir;
showing = true;
name = 'Amelia';
}
TestBed.configureTestingModule({declarations: [Dir, MyApp], imports: [CommonModule]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('Amelia');
fixture.componentInstance.name = 'Emerson';
fixture.componentInstance.dir.cdr.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('Emerson');
});
it('should support call in ngOnInit', () => {
@Component({template: '{{ value }}'})
class DetectChangesComp implements OnInit {
value = 0;
constructor(public cdr: ChangeDetectorRef) {}
ngOnInit() {
this.value++;
this.cdr.detectChanges();
}
}
TestBed.configureTestingModule({declarations: [DetectChangesComp]});
const fixture = TestBed.createComponent(DetectChangesComp);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('1');
});
['OnInit', 'AfterContentInit', 'AfterViewInit', 'OnChanges'].forEach(hook => {
it(`should not go infinite loop when recursively called from children's ng${hook}`, () => {
@Component({template: '<child-comp [inp]="true"></child-comp>'})
class ParentComp {
constructor(public cdr: ChangeDetectorRef) {}
triggerChangeDetection() { this.cdr.detectChanges(); }
}
@Component({template: '{{inp}}', selector: 'child-comp'})
class ChildComp {
@Input()
inp: any = '';
count = 0;
constructor(public parentComp: ParentComp) {}
ngOnInit() { this.check('OnInit'); }
ngAfterContentInit() { this.check('AfterContentInit'); }
ngAfterViewInit() { this.check('AfterViewInit'); }
ngOnChanges() { this.check('OnChanges'); }
check(h: string) {
if (h === hook) {
this.count++;
if (this.count > 1) throw new Error(`ng${hook} should be called only once!`);
this.parentComp.triggerChangeDetection();
}
}
}
TestBed.configureTestingModule({declarations: [ParentComp, ChildComp]});
expect(() => {
const fixture = TestBed.createComponent(ParentComp);
fixture.detectChanges();
}).not.toThrow();
});
});
it('should support call in ngDoCheck', () => {
@Component({template: '{{doCheckCount}}'})
class DetectChangesComp {
doCheckCount = 0;
constructor(public cdr: ChangeDetectorRef) {}
ngDoCheck() {
this.doCheckCount++;
this.cdr.detectChanges();
}
}
TestBed.configureTestingModule({declarations: [DetectChangesComp]});
const fixture = TestBed.createComponent(DetectChangesComp);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('1');
});
describe('dynamic views', () => {
@Component({selector: 'structural-comp', template: '{{ value }}'})
class StructuralComp {
@Input()
tmp !: TemplateRef<any>;
value = 'one';
constructor(public vcr: ViewContainerRef) {}
create() { return this.vcr.createEmbeddedView(this.tmp, {ctx: this}); }
}
it('should support ViewRef.detectChanges()', () => {
@Component({
template:
'<ng-template #foo let-ctx="ctx">{{ ctx.value }}</ng-template><structural-comp [tmp]="foo"></structural-comp>'
})
class App {
@ViewChild(StructuralComp) structuralComp !: StructuralComp;
}
TestBed.configureTestingModule({declarations: [App, StructuralComp]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('one');
const viewRef: EmbeddedViewRef<any> = fixture.componentInstance.structuralComp.create();
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('oneone');
// check embedded view update
fixture.componentInstance.structuralComp.value = 'two';
viewRef.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('onetwo');
// check root view update
fixture.componentInstance.structuralComp.value = 'three';
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('threethree');
});
it('should support ViewRef.detectChanges() directly after creation', () => {
@Component({
template: '<ng-template #foo>Template text</ng-template><structural-comp [tmp]="foo">'
})
class App {
@ViewChild(StructuralComp) structuralComp !: StructuralComp;
}
TestBed.configureTestingModule({declarations: [App, StructuralComp]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('one');
const viewRef: EmbeddedViewRef<any> = fixture.componentInstance.structuralComp.create();
viewRef.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('oneTemplate text');
});
});
});
describe('attach/detach', () => {
@Component({selector: 'detached-comp', template: '{{ value }}'})
class DetachedComp implements DoCheck {
value = 'one';
doCheckCount = 0;
constructor(public cdr: ChangeDetectorRef) {}
ngDoCheck() { this.doCheckCount++; }
}
@Component({template: '<detached-comp></detached-comp>'})
class MyApp {
@ViewChild(DetachedComp) comp !: DetachedComp;
constructor(public cdr: ChangeDetectorRef) {}
}
it('should not check detached components', () => {
TestBed.configureTestingModule({declarations: [MyApp, DetachedComp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('one');
fixture.componentInstance.comp.cdr.detach();
fixture.componentInstance.comp.value = 'two';
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('one');
});
it('should check re-attached components', () => {
TestBed.configureTestingModule({declarations: [MyApp, DetachedComp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('one');
fixture.componentInstance.comp.cdr.detach();
fixture.componentInstance.comp.value = 'two';
fixture.componentInstance.comp.cdr.reattach();
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('two');
});
it('should call lifecycle hooks on detached components', () => {
TestBed.configureTestingModule({declarations: [MyApp, DetachedComp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
expect(fixture.componentInstance.comp.doCheckCount).toEqual(1);
fixture.componentInstance.comp.cdr.detach();
fixture.detectChanges();
expect(fixture.componentInstance.comp.doCheckCount).toEqual(2);
});
it('should check detached component when detectChanges is called', () => {
TestBed.configureTestingModule({declarations: [MyApp, DetachedComp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('one');
fixture.componentInstance.comp.cdr.detach();
fixture.componentInstance.comp.value = 'two';
fixture.componentInstance.comp.cdr.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('two');
});
it('should not check detached component when markDirty is called', () => {
TestBed.configureTestingModule({declarations: [MyApp, DetachedComp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
const comp = fixture.componentInstance.comp;
comp.cdr.detach();
comp.value = 'two';
comp.cdr.markForCheck();
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('one');
});
it('should detach any child components when parent is detached', () => {
TestBed.configureTestingModule({declarations: [MyApp, DetachedComp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('one');
fixture.componentInstance.cdr.detach();
fixture.componentInstance.comp.value = 'two';
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('one');
fixture.componentInstance.cdr.reattach();
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('two');
});
it('should detach OnPush components properly', () => {
@Component({
selector: 'on-push-comp',
template: '{{ value }}',
changeDetection: ChangeDetectionStrategy.OnPush
})
class OnPushComp {
@Input()
value !: string;
constructor(public cdr: ChangeDetectorRef) {}
}
@Component({template: '<on-push-comp [value]="value"></on-push-comp>'})
class OnPushApp {
@ViewChild(OnPushComp) onPushComp !: OnPushComp;
value = '';
}
TestBed.configureTestingModule({declarations: [OnPushApp, OnPushComp]});
const fixture = TestBed.createComponent(OnPushApp);
fixture.detectChanges();
fixture.componentInstance.value = 'one';
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('one');
fixture.componentInstance.onPushComp.cdr.detach();
fixture.componentInstance.value = 'two';
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('one');
fixture.componentInstance.onPushComp.cdr.reattach();
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('two');
});
});
describe('markForCheck()', () => {
@Component({
selector: 'on-push-comp',
template: '{{ value }}',
changeDetection: ChangeDetectionStrategy.OnPush
})
class OnPushComp implements DoCheck {
value = 'one';
doCheckCount = 0;
constructor(public cdr: ChangeDetectorRef) {}
ngDoCheck() { this.doCheckCount++; }
}
@Component({
template: '{{ value }} - <on-push-comp></on-push-comp>',
changeDetection: ChangeDetectionStrategy.OnPush
})
class OnPushParent {
@ViewChild(OnPushComp) comp !: OnPushComp;
value = 'one';
}
it('should ensure OnPush components are checked', () => {
TestBed.configureTestingModule({declarations: [OnPushParent, OnPushComp]});
const fixture = TestBed.createComponent(OnPushParent);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('one - one');
fixture.componentInstance.comp.value = 'two';
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('one - one');
fixture.componentInstance.comp.cdr.markForCheck();
// Change detection should not have run yet, since markForCheck
// does not itself schedule change detection.
expect(fixture.nativeElement.textContent).toEqual('one - one');
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('one - two');
});
it('should never schedule change detection on its own', () => {
TestBed.configureTestingModule({declarations: [OnPushParent, OnPushComp]});
const fixture = TestBed.createComponent(OnPushParent);
fixture.detectChanges();
const comp = fixture.componentInstance.comp;
expect(comp.doCheckCount).toEqual(1);
comp.cdr.markForCheck();
comp.cdr.markForCheck();
expect(comp.doCheckCount).toEqual(1);
});
it('should ensure ancestor OnPush components are checked', () => {
TestBed.configureTestingModule({declarations: [OnPushParent, OnPushComp]});
const fixture = TestBed.createComponent(OnPushParent);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('one - one');
fixture.componentInstance.value = 'two';
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('one - one');
fixture.componentInstance.comp.cdr.markForCheck();
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('two - one');
});
it('should ensure OnPush components in embedded views are checked', () => {
@Component({
template: '{{ value }} - <on-push-comp *ngIf="showing"></on-push-comp>',
changeDetection: ChangeDetectionStrategy.OnPush
})
class EmbeddedViewParent {
@ViewChild(OnPushComp) comp !: OnPushComp;
value = 'one';
showing = true;
}
TestBed.configureTestingModule(
{declarations: [EmbeddedViewParent, OnPushComp], imports: [CommonModule]});
const fixture = TestBed.createComponent(EmbeddedViewParent);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('one - one');
fixture.componentInstance.comp.value = 'two';
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('one - one');
fixture.componentInstance.comp.cdr.markForCheck();
// markForCheck should not trigger change detection on its own.
expect(fixture.nativeElement.textContent).toEqual('one - one');
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('one - two');
fixture.componentInstance.value = 'two';
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('one - two');
fixture.componentInstance.comp.cdr.markForCheck();
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('two - two');
});
// TODO(kara): add test for dynamic views once bug fix is in
});
describe('checkNoChanges', () => {
let comp: NoChangesComp;
@Component({selector: 'no-changes-comp', template: '{{ value }}'})
class NoChangesComp {
value = 1;
doCheckCount = 0;
contentCheckCount = 0;
viewCheckCount = 0;
ngDoCheck() { this.doCheckCount++; }
ngAfterContentChecked() { this.contentCheckCount++; }
ngAfterViewChecked() { this.viewCheckCount++; }
constructor(public cdr: ChangeDetectorRef) { comp = this; }
}
@Component({template: '{{ value }} - <no-changes-comp></no-changes-comp>'})
class AppComp {
value = 1;
constructor(public cdr: ChangeDetectorRef) {}
}
// Custom error handler that just rethrows all the errors from the
// view, rather than logging them out. Used to keep our logs clean.
class RethrowErrorHandler extends ErrorHandler {
handleError(error: any) { throw error; }
}
it('should throw if bindings in current view have changed', () => {
TestBed.configureTestingModule({
declarations: [NoChangesComp],
providers: [{provide: ErrorHandler, useClass: RethrowErrorHandler}]
});
const fixture = TestBed.createComponent(NoChangesComp);
expect(() => { fixture.componentInstance.cdr.checkNoChanges(); })
.toThrowError(
/ExpressionChangedAfterItHasBeenCheckedError: .+ Previous value: '.*undefined'. Current value: '.*1'/gi);
});
it('should throw if interpolations in current view have changed', () => {
TestBed.configureTestingModule({
declarations: [AppComp, NoChangesComp],
providers: [{provide: ErrorHandler, useClass: RethrowErrorHandler}]
});
const fixture = TestBed.createComponent(AppComp);
expect(() => fixture.componentInstance.cdr.checkNoChanges())
.toThrowError(
/ExpressionChangedAfterItHasBeenCheckedError: .+ Previous value: '.*undefined'. Current value: '.*1'/gi);
});
it('should throw if bindings in embedded view have changed', () => {
@Component({template: '<span *ngIf="showing">{{ showing }}</span>'})
class EmbeddedViewApp {
showing = true;
constructor(public cdr: ChangeDetectorRef) {}
}
TestBed.configureTestingModule({
declarations: [EmbeddedViewApp],
imports: [CommonModule],
providers: [{provide: ErrorHandler, useClass: RethrowErrorHandler}]
});
const fixture = TestBed.createComponent(EmbeddedViewApp);
expect(() => fixture.componentInstance.cdr.checkNoChanges())
.toThrowError(
/ExpressionChangedAfterItHasBeenCheckedError: .+ Previous value: '.*undefined'. Current value: '.*true'/gi);
});
it('should NOT call lifecycle hooks', () => {
TestBed.configureTestingModule({
declarations: [AppComp, NoChangesComp],
providers: [{provide: ErrorHandler, useClass: RethrowErrorHandler}]
});
const fixture = TestBed.createComponent(AppComp);
fixture.detectChanges();
expect(comp.doCheckCount).toEqual(1);
expect(comp.contentCheckCount).toEqual(1);
expect(comp.viewCheckCount).toEqual(1);
comp.value = 2;
expect(() => fixture.componentInstance.cdr.checkNoChanges()).toThrow();
expect(comp.doCheckCount).toEqual(1);
expect(comp.contentCheckCount).toEqual(1);
expect(comp.viewCheckCount).toEqual(1);
});
});
});
});

File diff suppressed because it is too large Load Diff