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:
parent
090eac068a
commit
96baff3a85
|
@ -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
Loading…
Reference in New Issue