diff --git a/packages/core/test/acceptance/change_detection_spec.ts b/packages/core/test/acceptance/change_detection_spec.ts
index 340a224321..596e67e2b7 100644
--- a/packages/core/test/acceptance/change_detection_spec.ts
+++ b/packages/core/test/acceptance/change_detection_spec.ts
@@ -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', () => {
});
});
-});
\ No newline at end of file
+ describe('OnPush', () => {
+ @Component({
+ selector: 'my-comp',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `{{ doCheckCount }} - {{ name }} `
+ })
+ class MyComponent implements DoCheck {
+ @Input()
+ name = 'Nancy';
+ doCheckCount = 0;
+
+ ngDoCheck(): void { this.doCheckCount++; }
+
+ onClick() {}
+ }
+
+ @Component({selector: 'my-app', template: ''})
+ 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: ''
+ })
+ 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 }} - ',
+ changeDetection: ChangeDetectionStrategy.OnPush
+ })
+ class ButtonParent implements DoCheck {
+ @ViewChild(MyComponent) comp !: MyComponent;
+ noop() {}
+
+ doCheckCount = 0;
+ ngDoCheck(): void { this.doCheckCount++; }
+ }
+
+ @Component({selector: 'my-button-app', template: ''})
+ 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}} - `})
+ 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: ''})
+ 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 }}
'})
+ 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 }}'})
+ 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: ''})
+ 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;
+ value = 'one';
+
+ constructor(public vcr: ViewContainerRef) {}
+
+ create() { return this.vcr.createEmbeddedView(this.tmp, {ctx: this}); }
+ }
+
+ it('should support ViewRef.detectChanges()', () => {
+ @Component({
+ template:
+ '{{ ctx.value }}'
+ })
+ 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 = 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: 'Template text'
+ })
+ 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 = 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: ''})
+ 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: ''})
+ 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 }} - ',
+ 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 }} - ',
+ 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 }} - '})
+ 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: '{{ showing }}'})
+ 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);
+ });
+
+ });
+
+ });
+
+});
diff --git a/packages/core/test/render3/change_detection_spec.ts b/packages/core/test/render3/change_detection_spec.ts
index c5e38d76d5..52629da45b 100644
--- a/packages/core/test/render3/change_detection_spec.ts
+++ b/packages/core/test/render3/change_detection_spec.ts
@@ -6,18 +6,17 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {EmbeddedViewRef, TemplateRef, ViewContainerRef} from '@angular/core';
import {withBody} from '@angular/private/testing';
-import {ChangeDetectionStrategy, ChangeDetectorRef, DoCheck, RendererType2} from '../../src/core';
+import {ChangeDetectionStrategy, DoCheck} from '../../src/core';
import {whenRendered} from '../../src/render3/component';
-import {LifecycleHooksFeature, getRenderedText, ɵɵNgOnChangesFeature, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵgetCurrentView, ɵɵtemplateRefExtractor} from '../../src/render3/index';
-import {detectChanges, markDirty, tick, ɵɵbind, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵdirectiveInject, ɵɵelement, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵinterpolation1, ɵɵinterpolation2, ɵɵlistener, ɵɵreference, ɵɵtemplate, ɵɵtext, ɵɵtextBinding} from '../../src/render3/instructions/all';
+import {LifecycleHooksFeature, getRenderedText, ɵɵdefineComponent, ɵɵgetCurrentView} from '../../src/render3/index';
+import {detectChanges, markDirty, tick, ɵɵbind, ɵɵelement, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵinterpolation1, ɵɵinterpolation2, ɵɵlistener, ɵɵtext, ɵɵtextBinding} from '../../src/render3/instructions/all';
import {RenderFlags} from '../../src/render3/interfaces/definition';
-import {RElement, Renderer3, RendererFactory3} from '../../src/render3/interfaces/renderer';
+import {Renderer3, RendererFactory3} from '../../src/render3/interfaces/renderer';
import {FLAGS, LViewFlags} from '../../src/render3/interfaces/view';
-import {ComponentFixture, containerEl, createComponent, renderComponent, requestAnimationFrame} from './render_util';
+import {containerEl, createComponent, renderComponent, requestAnimationFrame} from './render_util';
describe('change detection', () => {
describe('markDirty, detectChanges, whenRendered, getRenderedText', () => {
@@ -129,175 +128,6 @@ describe('change detection', () => {
});
}
- class MyApp {
- name: string = 'Nancy';
-
- static ngComponentDef = ɵɵdefineComponent({
- type: MyApp,
- selectors: [['my-app']],
- factory: () => new MyApp(),
- consts: 1,
- vars: 1,
- /** */
- template: (rf: RenderFlags, ctx: MyApp) => {
- if (rf & RenderFlags.Create) {
- ɵɵelement(0, 'my-comp');
- }
- if (rf & RenderFlags.Update) {
- ɵɵelementProperty(0, 'name', ɵɵbind(ctx.name));
- }
- },
- directives: () => [MyComponent]
- });
- }
-
- it('should check OnPush components on initialization', () => {
- const myApp = renderComponent(MyApp);
- expect(getRenderedText(myApp)).toEqual('1 - Nancy');
- });
-
- it('should call doCheck even when OnPush components are not dirty', () => {
- const myApp = renderComponent(MyApp);
-
- tick(myApp);
- expect(comp.doCheckCount).toEqual(2);
-
- tick(myApp);
- expect(comp.doCheckCount).toEqual(3);
- });
-
- it('should skip OnPush components in update mode when they are not dirty', () => {
- const myApp = renderComponent(MyApp);
-
- tick(myApp);
- // doCheckCount is 2, but 1 should be rendered since it has not been marked dirty.
- expect(getRenderedText(myApp)).toEqual('1 - Nancy');
-
- tick(myApp);
- // doCheckCount is 3, but 1 should be rendered since it has not been marked dirty.
- expect(getRenderedText(myApp)).toEqual('1 - Nancy');
- });
-
- it('should check OnPush components in update mode when inputs change', () => {
- const myApp = renderComponent(MyApp);
-
- myApp.name = 'Bess';
- tick(myApp);
- expect(comp.doCheckCount).toEqual(2);
- // View should update, as changed input marks view dirty
- expect(getRenderedText(myApp)).toEqual('2 - Bess');
-
- myApp.name = 'George';
- tick(myApp);
- // View should update, as changed input marks view dirty
- expect(comp.doCheckCount).toEqual(3);
- expect(getRenderedText(myApp)).toEqual('3 - George');
-
- tick(myApp);
- expect(comp.doCheckCount).toEqual(4);
- // View should not be updated to "4", as inputs have not changed.
- expect(getRenderedText(myApp)).toEqual('3 - George');
- });
-
- it('should check OnPush components in update mode when component events occur', () => {
- const myApp = renderComponent(MyApp);
- expect(comp.doCheckCount).toEqual(1);
- expect(getRenderedText(myApp)).toEqual('1 - Nancy');
-
- const button = containerEl.querySelector('button') !;
- button.click();
- requestAnimationFrame.flush();
- // No ticks should have been scheduled.
- expect(comp.doCheckCount).toEqual(1);
- expect(getRenderedText(myApp)).toEqual('1 - Nancy');
-
- tick(myApp);
- // Because the onPush comp should be dirty, it should update once CD runs
- expect(comp.doCheckCount).toEqual(2);
- expect(getRenderedText(myApp)).toEqual('2 - Nancy');
- });
-
- it('should not check OnPush components in update mode when parent events occur', () => {
- function noop() {}
-
- const ButtonParent = createComponent('button-parent', function(rf: RenderFlags, ctx: any) {
- if (rf & RenderFlags.Create) {
- ɵɵelement(0, 'my-comp');
- ɵɵelementStart(1, 'button', ['id', 'parent']);
- { ɵɵlistener('click', () => noop()); }
- ɵɵelementEnd();
- }
- }, 2, 0, [MyComponent]);
-
- const buttonParent = renderComponent(ButtonParent);
- expect(getRenderedText(buttonParent)).toEqual('1 - Nancy');
-
- const button = containerEl.querySelector('button#parent') !;
- (button as HTMLButtonElement).click();
- tick(buttonParent);
- // The comp should still be clean. So doCheck will run, but the view should display 1.
- expect(comp.doCheckCount).toEqual(2);
- expect(getRenderedText(buttonParent)).toEqual('1 - Nancy');
- });
-
- it('should check parent OnPush components in update mode when child events occur', () => {
- let parent: ButtonParent;
-
- class ButtonParent implements DoCheck {
- doCheckCount = 0;
- ngDoCheck(): void { this.doCheckCount++; }
-
- static ngComponentDef = ɵɵdefineComponent({
- type: ButtonParent,
- selectors: [['button-parent']],
- factory: () => parent = new ButtonParent(),
- consts: 2,
- vars: 1,
- /** {{ doCheckCount }} - */
- template: (rf: RenderFlags, ctx: ButtonParent) => {
- if (rf & RenderFlags.Create) {
- ɵɵtext(0);
- ɵɵelement(1, 'my-comp');
- }
- if (rf & RenderFlags.Update) {
- ɵɵtextBinding(0, ɵɵinterpolation1('', ctx.doCheckCount, ' - '));
- }
- },
- directives: () => [MyComponent],
- changeDetection: ChangeDetectionStrategy.OnPush
- });
- }
-
- const MyButtonApp = createComponent('my-button-app', function(rf: RenderFlags, ctx: any) {
- if (rf & RenderFlags.Create) {
- ɵɵelement(0, 'button-parent');
- }
- }, 1, 0, [ButtonParent]);
-
- const myButtonApp = renderComponent(MyButtonApp);
- expect(parent !.doCheckCount).toEqual(1);
- expect(comp !.doCheckCount).toEqual(1);
- expect(getRenderedText(myButtonApp)).toEqual('1 - 1 - Nancy');
-
- tick(myButtonApp);
- expect(parent !.doCheckCount).toEqual(2);
- // parent isn't checked, so child doCheck won't run
- expect(comp !.doCheckCount).toEqual(1);
- expect(getRenderedText(myButtonApp)).toEqual('1 - 1 - Nancy');
-
- const button = containerEl.querySelector('button');
- button !.click();
- requestAnimationFrame.flush();
- // No ticks should have been scheduled.
- expect(parent !.doCheckCount).toEqual(2);
- expect(comp !.doCheckCount).toEqual(1);
-
- tick(myButtonApp);
- expect(parent !.doCheckCount).toEqual(3);
- expect(comp !.doCheckCount).toEqual(2);
- expect(getRenderedText(myButtonApp)).toEqual('3 - 2 - Nancy');
- });
-
describe('Manual mode', () => {
class ManualComponent implements DoCheck {
/* @Input() */
@@ -420,12 +250,11 @@ describe('change detection', () => {
});
}
- const MyButtonApp =
- createComponent('my-button-app', function(rf: RenderFlags, ctx: any) {
- if (rf & RenderFlags.Create) {
- ɵɵelement(0, 'button-parent');
- }
- }, 1, 0, [ButtonParent]);
+ const MyButtonApp = createComponent('my-button-app', function(rf: RenderFlags) {
+ if (rf & RenderFlags.Create) {
+ ɵɵelement(0, 'button-parent');
+ }
+ }, 1, 0, [ButtonParent]);
const myButtonApp = renderComponent(MyButtonApp);
expect(parent !.doCheckCount).toEqual(1);
@@ -462,997 +291,11 @@ describe('change detection', () => {
});
});
- describe('ChangeDetectorRef', () => {
-
- describe('detectChanges()', () => {
- let myComp: MyComp;
- let dir: Dir;
-
- class MyComp {
- doCheckCount = 0;
- name = 'Nancy';
-
- constructor(public cdr: ChangeDetectorRef) {}
-
- ngDoCheck() { this.doCheckCount++; }
-
- static ngComponentDef = ɵɵdefineComponent({
- type: MyComp,
- selectors: [['my-comp']],
- factory: () => myComp = new MyComp(ɵɵdirectiveInject(ChangeDetectorRef as any)),
- consts: 1,
- vars: 1,
- /** {{ name }} */
- template: (rf: RenderFlags, ctx: MyComp) => {
- if (rf & RenderFlags.Create) {
- ɵɵtext(0);
- }
- if (rf & RenderFlags.Update) {
- ɵɵtextBinding(0, ɵɵbind(ctx.name));
- }
- },
- changeDetection: ChangeDetectionStrategy.OnPush
- });
- }
-
- class ParentComp {
- doCheckCount = 0;
-
- constructor(public cdr: ChangeDetectorRef) {}
-
- ngDoCheck() { this.doCheckCount++; }
-
- static ngComponentDef = ɵɵdefineComponent({
- type: ParentComp,
- selectors: [['parent-comp']],
- factory: () => new ParentComp(ɵɵdirectiveInject(ChangeDetectorRef as any)),
- consts: 2,
- vars: 1,
- /**
- * {{ doCheckCount}} -
- *
- */
- template: (rf: RenderFlags, ctx: ParentComp) => {
- if (rf & RenderFlags.Create) {
- ɵɵtext(0);
- ɵɵelement(1, 'my-comp');
- }
- if (rf & RenderFlags.Update) {
- ɵɵtextBinding(0, ɵɵinterpolation1('', ctx.doCheckCount, ' - '));
- }
- },
- directives: () => [MyComp]
- });
- }
-
- class Dir {
- constructor(public cdr: ChangeDetectorRef) {}
-
- static ngDirectiveDef = ɵɵdefineDirective({
- type: Dir,
- selectors: [['', 'dir', '']],
- factory: () => dir = new Dir(ɵɵdirectiveInject(ChangeDetectorRef as any))
- });
- }
-
-
- it('should check the component view when called by component (even when OnPush && clean)',
- () => {
- const comp = renderComponent(MyComp, {hostFeatures: [LifecycleHooksFeature]});
- expect(getRenderedText(comp)).toEqual('Nancy');
-
- comp.name = 'Bess'; // as this is not an Input, the component stays clean
- comp.cdr.detectChanges();
- expect(getRenderedText(comp)).toEqual('Bess');
- });
-
- it('should NOT call component doCheck when called by a component', () => {
- const comp = renderComponent(MyComp, {hostFeatures: [LifecycleHooksFeature]});
- expect(comp.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.
- comp.cdr.detectChanges();
- expect(comp.doCheckCount).toEqual(1);
- });
-
- it('should NOT check the component parent when called by a child component', () => {
- const parentComp = renderComponent(ParentComp, {hostFeatures: [LifecycleHooksFeature]});
- expect(getRenderedText(parentComp)).toEqual('1 - Nancy');
-
- parentComp.doCheckCount = 100;
- myComp.cdr.detectChanges();
- expect(parentComp.doCheckCount).toEqual(100);
- expect(getRenderedText(parentComp)).toEqual('1 - Nancy');
- });
-
- it('should check component children when called by component if dirty or check-always',
- () => {
- const parentComp = renderComponent(ParentComp, {hostFeatures: [LifecycleHooksFeature]});
- expect(parentComp.doCheckCount).toEqual(1);
-
- myComp.name = 'Bess';
- parentComp.cdr.detectChanges();
- expect(parentComp.doCheckCount).toEqual(1);
- expect(myComp.doCheckCount).toEqual(2);
- // OnPush child is not dirty, so its change isn't rendered.
- expect(getRenderedText(parentComp)).toEqual('1 - Nancy');
- });
-
- it('should not group detectChanges calls (call every time)', () => {
- const parentComp = renderComponent(ParentComp, {hostFeatures: [LifecycleHooksFeature]});
- expect(myComp.doCheckCount).toEqual(1);
-
- parentComp.cdr.detectChanges();
- parentComp.cdr.detectChanges();
- expect(myComp.doCheckCount).toEqual(3);
- });
-
- it('should check component view when called by directive on component node', () => {
- /** */
- const MyApp = createComponent('my-app', function(rf: RenderFlags, ctx: any) {
- if (rf & RenderFlags.Create) {
- ɵɵelement(0, 'my-comp', ['dir', '']);
- }
- }, 1, 0, [MyComp, Dir]);
-
- const app = renderComponent(MyApp);
- expect(getRenderedText(app)).toEqual('Nancy');
-
- myComp.name = 'George';
- dir !.cdr.detectChanges();
- expect(getRenderedText(app)).toEqual('George');
- });
-
- it('should check host component when called by directive on element node', () => {
- /**
- * {{ name }}
- *
- */
- const MyApp = createComponent('my-app', function(rf: RenderFlags, ctx: any) {
- if (rf & RenderFlags.Create) {
- ɵɵtext(0);
- ɵɵelement(1, 'div', ['dir', '']);
- }
- if (rf & RenderFlags.Update) {
- ɵɵtextBinding(1, ɵɵbind(ctx.value));
- }
- }, 2, 1, [Dir]);
-
- const app = renderComponent(MyApp);
- app.value = 'Frank';
- tick(app);
- expect(getRenderedText(app)).toEqual('Frank');
-
- app.value = 'Joe';
- dir !.cdr.detectChanges();
- expect(getRenderedText(app)).toEqual('Joe');
- });
-
- it('should check the host component when called from EmbeddedViewRef', () => {
- class MyApp {
- showing = true;
- name = 'Amelia';
-
- constructor(public cdr: ChangeDetectorRef) {}
-
- static ngComponentDef = ɵɵdefineComponent({
- type: MyApp,
- selectors: [['my-app']],
- factory: () => new MyApp(ɵɵdirectiveInject(ChangeDetectorRef as any)),
- consts: 2,
- vars: 1,
- /**
- * {{ name}}
- * % if (showing) {
- *
- * % }
- */
- template: function(rf: RenderFlags, ctx: MyApp) {
- if (rf & RenderFlags.Create) {
- ɵɵtext(0);
- ɵɵcontainer(1);
- }
- if (rf & RenderFlags.Update) {
- ɵɵtextBinding(0, ɵɵbind(ctx.name));
- ɵɵcontainerRefreshStart(1);
- {
- if (ctx.showing) {
- let rf0 = ɵɵembeddedViewStart(0, 1, 0);
- if (rf0 & RenderFlags.Create) {
- ɵɵelement(0, 'div', ['dir', '']);
- }
- }
- ɵɵembeddedViewEnd();
- }
- ɵɵcontainerRefreshEnd();
- }
- },
- directives: [Dir]
- });
- }
-
- const app = renderComponent(MyApp);
- expect(getRenderedText(app)).toEqual('Amelia');
-
- app.name = 'Emerson';
- dir !.cdr.detectChanges();
- expect(getRenderedText(app)).toEqual('Emerson');
- });
-
- it('should support call in ngOnInit', () => {
- class DetectChangesComp {
- value = 0;
-
- constructor(public cdr: ChangeDetectorRef) {}
-
- ngOnInit() {
- this.value++;
- this.cdr.detectChanges();
- }
-
- static ngComponentDef = ɵɵdefineComponent({
- type: DetectChangesComp,
- selectors: [['detect-changes-comp']],
- factory: () => new DetectChangesComp(ɵɵdirectiveInject(ChangeDetectorRef as any)),
- consts: 1,
- vars: 1,
- /** {{ value }} */
- template: (rf: RenderFlags, ctx: DetectChangesComp) => {
- if (rf & RenderFlags.Create) {
- ɵɵtext(0);
- }
- if (rf & RenderFlags.Update) {
- ɵɵtextBinding(0, ɵɵbind(ctx.value));
- }
- }
- });
- }
-
- const comp = renderComponent(DetectChangesComp, {hostFeatures: [LifecycleHooksFeature]});
- expect(getRenderedText(comp)).toEqual('1');
- });
-
-
- ['OnInit', 'AfterContentInit', 'AfterViewInit', 'OnChanges'].forEach(hook => {
- it(`should not go infinite loop when recursively called from children's ng${hook}`, () => {
- class ChildComp {
- // @Input
- inp = '';
-
- 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();
- }
- }
-
- static ngComponentDef = ɵɵdefineComponent({
- type: ChildComp,
- selectors: [['child-comp']],
- factory: () => new ChildComp(ɵɵdirectiveInject(ParentComp as any)),
- consts: 1,
- vars: 0,
- template: (rf: RenderFlags, ctx: ChildComp) => {
- if (rf & RenderFlags.Create) {
- ɵɵtext(0, 'foo');
- }
- },
- inputs: {inp: 'inp'},
- features: [ɵɵNgOnChangesFeature]
- });
- }
-
- class ParentComp {
- constructor(public cdr: ChangeDetectorRef) {}
-
- triggerChangeDetection() { this.cdr.detectChanges(); }
-
- static ngComponentDef = ɵɵdefineComponent({
- type: ParentComp,
- selectors: [['parent-comp']],
- factory: () => new ParentComp(ɵɵdirectiveInject(ChangeDetectorRef as any)),
- consts: 1,
- vars: 1,
- /** {{ value }} */
- template: (rf: RenderFlags, ctx: ParentComp) => {
- if (rf & RenderFlags.Create) {
- ɵɵelement(0, 'child-comp');
- }
- if (rf & RenderFlags.Update) {
- ɵɵelementProperty(0, 'inp', ɵɵbind(true));
- }
- },
- directives: [ChildComp]
- });
- }
-
- expect(() => renderComponent(ParentComp)).not.toThrow();
- });
- });
-
- it('should support call in ngDoCheck', () => {
- class DetectChangesComp {
- doCheckCount = 0;
-
- constructor(public cdr: ChangeDetectorRef) {}
-
- ngDoCheck() {
- this.doCheckCount++;
- this.cdr.detectChanges();
- }
-
- static ngComponentDef = ɵɵdefineComponent({
- type: DetectChangesComp,
- selectors: [['detect-changes-comp']],
- factory: () => new DetectChangesComp(ɵɵdirectiveInject(ChangeDetectorRef as any)),
- consts: 1,
- vars: 1,
- /** {{ doCheckCount }} */
- template: (rf: RenderFlags, ctx: DetectChangesComp) => {
- if (rf & RenderFlags.Create) {
- ɵɵtext(0);
- }
- if (rf & RenderFlags.Update) {
- ɵɵtextBinding(0, ɵɵbind(ctx.doCheckCount));
- }
- }
- });
- }
-
- const comp = renderComponent(DetectChangesComp, {hostFeatures: [LifecycleHooksFeature]});
- expect(getRenderedText(comp)).toEqual('1');
- });
-
- describe('dynamic views', () => {
- let structuralComp: StructuralComp|null = null;
-
- beforeEach(() => structuralComp = null);
-
- class StructuralComp {
- tmp !: TemplateRef;
- value = 'one';
-
- constructor(public vcr: ViewContainerRef) {}
-
- create() { return this.vcr.createEmbeddedView(this.tmp, this); }
-
- static ngComponentDef = ɵɵdefineComponent({
- type: StructuralComp,
- selectors: [['structural-comp']],
- factory: () => structuralComp =
- new StructuralComp(ɵɵdirectiveInject(ViewContainerRef as any)),
- inputs: {tmp: 'tmp'},
- consts: 1,
- vars: 1,
- template: function(rf: RenderFlags, ctx: any) {
- if (rf & RenderFlags.Create) {
- ɵɵtext(0);
- }
- if (rf & RenderFlags.Update) {
- ɵɵtextBinding(0, ɵɵbind(ctx.value));
- }
- }
- });
- }
-
- it('should support ViewRef.detectChanges()', () => {
- function FooTemplate(rf: RenderFlags, ctx: any) {
- if (rf & RenderFlags.Create) {
- ɵɵtext(0);
- }
- if (rf & RenderFlags.Update) {
- ɵɵtextBinding(0, ɵɵbind(ctx.value));
- }
- }
-
- /**
- * {{ value }}
- *
- */
- const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
- if (rf & RenderFlags.Create) {
- ɵɵtemplate(
- 0, FooTemplate, 1, 1, 'ng-template', null, ['foo', ''], ɵɵtemplateRefExtractor);
- ɵɵelement(2, 'structural-comp');
- }
- if (rf & RenderFlags.Update) {
- const foo = ɵɵreference(1) as any;
- ɵɵelementProperty(2, 'tmp', ɵɵbind(foo));
- }
- }, 3, 1, [StructuralComp]);
-
- const fixture = new ComponentFixture(App);
- fixture.update();
- expect(fixture.html).toEqual('one');
-
- const viewRef: EmbeddedViewRef = structuralComp !.create();
- fixture.update();
- expect(fixture.html).toEqual('oneone');
-
- // check embedded view update
- structuralComp !.value = 'two';
- viewRef.detectChanges();
- expect(fixture.html).toEqual('onetwo');
-
- // check root view update
- structuralComp !.value = 'three';
- fixture.update();
- expect(fixture.html).toEqual('threethree');
- });
-
- it('should support ViewRef.detectChanges() directly after creation', () => {
- function FooTemplate(rf: RenderFlags, ctx: any) {
- if (rf & RenderFlags.Create) {
- ɵɵtext(0, 'Template text');
- }
- }
-
- /**
- * Template text
- *
- */
- const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
- if (rf & RenderFlags.Create) {
- ɵɵtemplate(
- 0, FooTemplate, 1, 0, 'ng-template', null, ['foo', ''], ɵɵtemplateRefExtractor);
- ɵɵelement(2, 'structural-comp');
- }
- if (rf & RenderFlags.Update) {
- const foo = ɵɵreference(1) as any;
- ɵɵelementProperty(2, 'tmp', ɵɵbind(foo));
- }
- }, 3, 1, [StructuralComp]);
-
- const fixture = new ComponentFixture(App);
- fixture.update();
- expect(fixture.html).toEqual('one');
-
- const viewRef: EmbeddedViewRef = structuralComp !.create();
- viewRef.detectChanges();
- expect(fixture.html).toEqual('oneTemplate text');
- });
-
- });
-
- });
-
- describe('attach/detach', () => {
- let comp: DetachedComp;
-
- class MyApp {
- constructor(public cdr: ChangeDetectorRef) {}
-
- static ngComponentDef = ɵɵdefineComponent({
- type: MyApp,
- selectors: [['my-app']],
- factory: () => new MyApp(ɵɵdirectiveInject(ChangeDetectorRef as any)),
- consts: 1,
- vars: 0,
- /** */
- template: (rf: RenderFlags, ctx: MyApp) => {
- if (rf & RenderFlags.Create) {
- ɵɵelement(0, 'detached-comp');
- }
- },
- directives: () => [DetachedComp]
- });
- }
-
- class DetachedComp {
- value = 'one';
- doCheckCount = 0;
-
- constructor(public cdr: ChangeDetectorRef) {}
-
- ngDoCheck() { this.doCheckCount++; }
-
- static ngComponentDef = ɵɵdefineComponent({
- type: DetachedComp,
- selectors: [['detached-comp']],
- factory: () => comp = new DetachedComp(ɵɵdirectiveInject(ChangeDetectorRef as any)),
- consts: 1,
- vars: 1,
- /** {{ value }} */
- template: (rf: RenderFlags, ctx: DetachedComp) => {
- if (rf & RenderFlags.Create) {
- ɵɵtext(0);
- }
- if (rf & RenderFlags.Update) {
- ɵɵtextBinding(0, ɵɵbind(ctx.value));
- }
- }
- });
- }
-
- it('should not check detached components', () => {
- const app = renderComponent(MyApp);
- expect(getRenderedText(app)).toEqual('one');
-
- comp.cdr.detach();
-
- comp.value = 'two';
- tick(app);
- expect(getRenderedText(app)).toEqual('one');
- });
-
- it('should check re-attached components', () => {
- const app = renderComponent(MyApp);
- expect(getRenderedText(app)).toEqual('one');
-
- comp.cdr.detach();
- comp.value = 'two';
-
- comp.cdr.reattach();
- tick(app);
- expect(getRenderedText(app)).toEqual('two');
- });
-
- it('should call lifecycle hooks on detached components', () => {
- const app = renderComponent(MyApp);
- expect(comp.doCheckCount).toEqual(1);
-
- comp.cdr.detach();
-
- tick(app);
- expect(comp.doCheckCount).toEqual(2);
- });
-
- it('should check detached component when detectChanges is called', () => {
- const app = renderComponent(MyApp);
- expect(getRenderedText(app)).toEqual('one');
-
- comp.cdr.detach();
-
- comp.value = 'two';
- detectChanges(comp);
- expect(getRenderedText(app)).toEqual('two');
- });
-
- it('should not check detached component when markDirty is called', () => {
- const app = renderComponent(MyApp);
- expect(getRenderedText(app)).toEqual('one');
-
- comp.cdr.detach();
-
- comp.value = 'two';
- markDirty(comp);
- requestAnimationFrame.flush();
-
- expect(getRenderedText(app)).toEqual('one');
- });
-
- it('should detach any child components when parent is detached', () => {
- const app = renderComponent(MyApp);
- expect(getRenderedText(app)).toEqual('one');
-
- app.cdr.detach();
-
- comp.value = 'two';
- tick(app);
- expect(getRenderedText(app)).toEqual('one');
-
- app.cdr.reattach();
-
- tick(app);
- expect(getRenderedText(app)).toEqual('two');
- });
-
- it('should detach OnPush components properly', () => {
- let onPushComp: OnPushComp;
-
- class OnPushComp {
- /** @Input() */
- // TODO(issue/24571): remove '!'.
- value !: string;
-
- constructor(public cdr: ChangeDetectorRef) {}
-
- static ngComponentDef = ɵɵdefineComponent({
- type: OnPushComp,
- selectors: [['on-push-comp']],
- factory: () => onPushComp = new OnPushComp(ɵɵdirectiveInject(ChangeDetectorRef as any)),
- consts: 1,
- vars: 1,
- /** {{ value }} */
- template: (rf: RenderFlags, ctx: any) => {
- if (rf & RenderFlags.Create) {
- ɵɵtext(0);
- }
- if (rf & RenderFlags.Update) {
- ɵɵtextBinding(0, ɵɵbind(ctx.value));
- }
- },
- changeDetection: ChangeDetectionStrategy.OnPush,
- inputs: {value: 'value'}
- });
- }
-
- /** */
- const OnPushApp = createComponent('on-push-app', function(rf: RenderFlags, ctx: any) {
- if (rf & RenderFlags.Create) {
- ɵɵelement(0, 'on-push-comp');
- }
- if (rf & RenderFlags.Update) {
- ɵɵelementProperty(0, 'value', ɵɵbind(ctx.value));
- }
- }, 1, 1, [OnPushComp]);
-
- const app = renderComponent(OnPushApp);
- app.value = 'one';
- tick(app);
- expect(getRenderedText(app)).toEqual('one');
-
- onPushComp !.cdr.detach();
-
- app.value = 'two';
- tick(app);
- expect(getRenderedText(app)).toEqual('one');
-
- onPushComp !.cdr.reattach();
-
- tick(app);
- expect(getRenderedText(app)).toEqual('two');
- });
-
- });
-
- describe('markForCheck()', () => {
- let comp: OnPushComp;
-
- class OnPushComp {
- value = 'one';
-
- doCheckCount = 0;
-
- constructor(public cdr: ChangeDetectorRef) {}
-
- ngDoCheck() { this.doCheckCount++; }
-
- static ngComponentDef = ɵɵdefineComponent({
- type: OnPushComp,
- selectors: [['on-push-comp']],
- factory: () => comp = new OnPushComp(ɵɵdirectiveInject(ChangeDetectorRef as any)),
- consts: 1,
- vars: 1,
- /** {{ value }} */
- template: (rf: RenderFlags, ctx: OnPushComp) => {
- if (rf & RenderFlags.Create) {
- ɵɵtext(0);
- }
- if (rf & RenderFlags.Update) {
- ɵɵtextBinding(0, ɵɵbind(ctx.value));
- }
- },
- changeDetection: ChangeDetectionStrategy.OnPush
- });
- }
-
- class OnPushParent {
- value = 'one';
-
- static ngComponentDef = ɵɵdefineComponent({
- type: OnPushParent,
- selectors: [['on-push-parent']],
- factory: () => new OnPushParent(),
- consts: 2,
- vars: 1,
- /**
- * {{ value }} -
- *
- */
- template: (rf: RenderFlags, ctx: OnPushParent) => {
- if (rf & RenderFlags.Create) {
- ɵɵtext(0);
- ɵɵelement(1, 'on-push-comp');
- }
- if (rf & RenderFlags.Update) {
- ɵɵtextBinding(0, ɵɵinterpolation1('', ctx.value, ' - '));
- }
- },
- directives: () => [OnPushComp],
- changeDetection: ChangeDetectionStrategy.OnPush
- });
- }
-
- it('should ensure OnPush components are checked', () => {
- const fixture = new ComponentFixture(OnPushParent);
- expect(fixture.hostElement.textContent).toEqual('one - one');
-
- comp.value = 'two';
- tick(fixture.component);
- expect(fixture.hostElement.textContent).toEqual('one - one');
-
- comp.cdr.markForCheck();
-
- // Change detection should not have run yet, since markForCheck
- // does not itself schedule change detection.
- expect(fixture.hostElement.textContent).toEqual('one - one');
-
- tick(fixture.component);
- expect(fixture.hostElement.textContent).toEqual('one - two');
- });
-
- it('should never schedule change detection on its own', () => {
- const fixture = new ComponentFixture(OnPushParent);
- expect(comp.doCheckCount).toEqual(1);
-
- comp.cdr.markForCheck();
- comp.cdr.markForCheck();
- requestAnimationFrame.flush();
-
- expect(comp.doCheckCount).toEqual(1);
- });
-
- it('should ensure ancestor OnPush components are checked', () => {
- const fixture = new ComponentFixture(OnPushParent);
- expect(fixture.hostElement.textContent).toEqual('one - one');
-
- fixture.component.value = 'two';
- tick(fixture.component);
- expect(fixture.hostElement.textContent).toEqual('one - one');
-
- comp.cdr.markForCheck();
- tick(fixture.component);
- expect(fixture.hostElement.textContent).toEqual('two - one');
-
- });
-
- it('should ensure OnPush components in embedded views are checked', () => {
- class EmbeddedViewParent {
- value = 'one';
- showing = true;
-
- static ngComponentDef = ɵɵdefineComponent({
- type: EmbeddedViewParent,
- selectors: [['embedded-view-parent']],
- factory: () => new EmbeddedViewParent(),
- consts: 2,
- vars: 1,
- /**
- * {{ value }} -
- * % if (ctx.showing) {
- *
- * % }
- */
- template: (rf: RenderFlags, ctx: any) => {
- if (rf & RenderFlags.Create) {
- ɵɵtext(0);
- ɵɵcontainer(1);
- }
- if (rf & RenderFlags.Update) {
- ɵɵtextBinding(0, ɵɵinterpolation1('', ctx.value, ' - '));
- ɵɵcontainerRefreshStart(1);
- {
- if (ctx.showing) {
- let rf0 = ɵɵembeddedViewStart(0, 1, 0);
- if (rf0 & RenderFlags.Create) {
- ɵɵelement(0, 'on-push-comp');
- }
- ɵɵembeddedViewEnd();
- }
- }
- ɵɵcontainerRefreshEnd();
- }
- },
- directives: () => [OnPushComp],
- changeDetection: ChangeDetectionStrategy.OnPush
- });
- }
-
- const fixture = new ComponentFixture(EmbeddedViewParent);
- expect(fixture.hostElement.textContent).toEqual('one - one');
-
- comp.value = 'two';
- tick(fixture.component);
- expect(fixture.hostElement.textContent).toEqual('one - one');
-
- comp.cdr.markForCheck();
- // markForCheck should not trigger change detection on its own.
- expect(fixture.hostElement.textContent).toEqual('one - one');
-
- tick(fixture.component);
- expect(fixture.hostElement.textContent).toEqual('one - two');
-
- fixture.component.value = 'two';
- tick(fixture.component);
- expect(fixture.hostElement.textContent).toEqual('one - two');
-
- comp.cdr.markForCheck();
- tick(fixture.component);
- expect(fixture.hostElement.textContent).toEqual('two - two');
- });
-
- // TODO(kara): add test for dynamic views once bug fix is in
- });
-
- describe('checkNoChanges', () => {
- let comp: NoChangesComp;
-
- class NoChangesComp {
- value = 1;
- doCheckCount = 0;
- contentCheckCount = 0;
- viewCheckCount = 0;
-
- ngDoCheck() { this.doCheckCount++; }
-
- ngAfterContentChecked() { this.contentCheckCount++; }
-
- ngAfterViewChecked() { this.viewCheckCount++; }
-
- constructor(public cdr: ChangeDetectorRef) {}
-
- static ngComponentDef = ɵɵdefineComponent({
- type: NoChangesComp,
- selectors: [['no-changes-comp']],
- factory: () => comp = new NoChangesComp(ɵɵdirectiveInject(ChangeDetectorRef as any)),
- consts: 1,
- vars: 1,
- template: (rf: RenderFlags, ctx: NoChangesComp) => {
- if (rf & RenderFlags.Create) {
- ɵɵtext(0);
- }
- if (rf & RenderFlags.Update) {
- ɵɵtextBinding(0, ɵɵbind(ctx.value));
- }
- }
- });
- }
-
- class AppComp {
- value = 1;
-
- constructor(public cdr: ChangeDetectorRef) {}
-
- static ngComponentDef = ɵɵdefineComponent({
- type: AppComp,
- selectors: [['app-comp']],
- factory: () => new AppComp(ɵɵdirectiveInject(ChangeDetectorRef as any)),
- consts: 2,
- vars: 1,
- /**
- * {{ value }} -
- *
- */
- template: (rf: RenderFlags, ctx: AppComp) => {
- if (rf & RenderFlags.Create) {
- ɵɵtext(0);
- ɵɵelement(1, 'no-changes-comp');
- }
- if (rf & RenderFlags.Update) {
- ɵɵtextBinding(0, ɵɵinterpolation1('', ctx.value, ' - '));
- }
- },
- directives: () => [NoChangesComp]
- });
- }
-
- it('should throw if bindings in current view have changed', () => {
- const comp = renderComponent(NoChangesComp, {hostFeatures: [LifecycleHooksFeature]});
-
- expect(() => comp.cdr.checkNoChanges()).not.toThrow();
-
- comp.value = 2;
- expect(() => comp.cdr.checkNoChanges())
- .toThrowError(
- /ExpressionChangedAfterItHasBeenCheckedError: .+ Previous value: '1'. Current value: '2'/gi);
- });
-
- it('should throw if interpolations in current view have changed', () => {
- const app = renderComponent(AppComp);
-
- expect(() => app.cdr.checkNoChanges()).not.toThrow();
-
- app.value = 2;
- expect(() => app.cdr.checkNoChanges())
- .toThrowError(
- /ExpressionChangedAfterItHasBeenCheckedError: .+ Previous value: '1'. Current value: '2'/gi);
- });
-
- it('should throw if bindings in children of current view have changed', () => {
- const app = renderComponent(AppComp);
-
- expect(() => app.cdr.checkNoChanges()).not.toThrow();
-
- comp.value = 2;
- expect(() => app.cdr.checkNoChanges())
- .toThrowError(
- /ExpressionChangedAfterItHasBeenCheckedError: .+ Previous value: '1'. Current value: '2'/gi);
- });
-
- it('should throw if bindings in embedded view have changed', () => {
- class EmbeddedViewApp {
- value = 1;
- showing = true;
-
- constructor(public cdr: ChangeDetectorRef) {}
-
- static ngComponentDef = ɵɵdefineComponent({
- type: EmbeddedViewApp,
- selectors: [['embedded-view-app']],
- factory: () => new EmbeddedViewApp(ɵɵdirectiveInject(ChangeDetectorRef as any)),
- consts: 1,
- vars: 0,
- /**
- * % if (showing) {
- * {{ value }}
- * %}
- */
- template: (rf: RenderFlags, ctx: EmbeddedViewApp) => {
- if (rf & RenderFlags.Create) {
- ɵɵcontainer(0);
- }
- if (rf & RenderFlags.Update) {
- ɵɵcontainerRefreshStart(0);
- {
- if (ctx.showing) {
- let rf0 = ɵɵembeddedViewStart(0, 1, 1);
- if (rf0 & RenderFlags.Create) {
- ɵɵtext(0);
- }
- if (rf0 & RenderFlags.Update) {
- ɵɵtextBinding(0, ɵɵbind(ctx.value));
- }
- ɵɵembeddedViewEnd();
- }
- }
- ɵɵcontainerRefreshEnd();
- }
- }
- });
- }
-
- const app = renderComponent(EmbeddedViewApp);
-
- expect(() => app.cdr.checkNoChanges()).not.toThrow();
-
- app.value = 2;
- expect(() => app.cdr.checkNoChanges())
- .toThrowError(
- /ExpressionChangedAfterItHasBeenCheckedError: .+ Previous value: '1'. Current value: '2'/gi);
- });
-
- it('should NOT call lifecycle hooks', () => {
- const app = renderComponent(AppComp);
- expect(comp.doCheckCount).toEqual(1);
- expect(comp.contentCheckCount).toEqual(1);
- expect(comp.viewCheckCount).toEqual(1);
-
- comp.value = 2;
- expect(() => app.cdr.checkNoChanges()).toThrow();
- expect(comp.doCheckCount).toEqual(1);
- expect(comp.contentCheckCount).toEqual(1);
- expect(comp.viewCheckCount).toEqual(1);
- });
-
- it('should NOT throw if bindings in ancestors of current view have changed', () => {
- const app = renderComponent(AppComp);
-
- app.value = 2;
- expect(() => comp.cdr.checkNoChanges()).not.toThrow();
- });
-
- });
-
- });
-
it('should call begin and end when the renderer factory implements them', () => {
const log: string[] = [];
const testRendererFactory: RendererFactory3 = {
- createRenderer: (hostElement: RElement | null, rendererType: RendererType2 | null):
- Renderer3 => { return document; },
+ createRenderer: (): Renderer3 => { return document; },
begin: () => log.push('begin'),
end: () => log.push('end'),
};