test(core): replace hand-written instructions in change detection tests with TestBed (#46529)

This commit updates a set of change detection tests to avoid using hand-written instructions and replace them with TestBed APIs.

PR Close #46529
This commit is contained in:
Andrew Kushnir 2022-06-27 14:53:11 -07:00 committed by Dylan Hunn
parent 2e59f9f612
commit de171d8dde

View file

@ -6,54 +6,45 @@
* found in the LICENSE file at https://angular.io/license
*/
import {NgIf} from '@angular/common';
import {CommonModule, DOCUMENT} from '@angular/common';
import {TestBed} from '@angular/core/testing';
import {withBody} from '@angular/private/testing';
import {ChangeDetectionStrategy, DoCheck, OnInit} from '../../src/core';
import {Component, DoCheck, OnInit, Renderer2, RendererFactory2} from '../../src/core';
import {whenRendered} from '../../src/render3/component';
import {AttributeMarker, getRenderedText, LifecycleHooksFeature, ɵɵadvance, ɵɵdefineComponent, ɵɵgetCurrentView, ɵɵproperty, ɵɵtextInterpolate1, ɵɵtextInterpolate2} from '../../src/render3/index';
import {detectChanges, markDirty, tick, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵlistener, ɵɵtemplate, ɵɵtext, ɵɵtextInterpolate} from '../../src/render3/instructions/all';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {Renderer3, RendererFactory3} from '../../src/render3/interfaces/renderer';
import {FLAGS, LViewFlags} from '../../src/render3/interfaces/view';
import {containerEl, createComponent, renderComponent, requestAnimationFrame} from './render_util';
import {getRenderedText} from '../../src/render3/index';
import {detectChanges, markDirty} from '../../src/render3/instructions/all';
describe('change detection', () => {
describe('markDirty, detectChanges, whenRendered, getRenderedText', () => {
let mycompOninit: MyComponentWithOnInit;
@Component({
selector: 'my-comp',
standalone: true,
template: '<span>{{ value }}</span>',
})
class MyComponent implements DoCheck {
value: string = 'works';
doCheckCount = 0;
ngDoCheck(): void {
this.doCheckCount++;
}
static ɵfac = () => new MyComponent();
static ɵcmp = ɵɵdefineComponent({
type: MyComponent,
selectors: [['my-comp']],
decls: 2,
vars: 1,
template:
(rf: RenderFlags, ctx: MyComponent) => {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'span');
ɵɵtext(1);
ɵɵelementEnd();
}
if (rf & RenderFlags.Update) {
ɵɵadvance(1);
ɵɵtextInterpolate(ctx.value);
}
}
});
}
@Component({
selector: 'my-comp-oninit',
standalone: true,
template: '<span>{{ value }}</span>',
})
class MyComponentWithOnInit implements OnInit, DoCheck {
value: string = 'works';
doCheckCount = 0;
constructor() {
mycompOninit = this;
}
ngOnInit() {
markDirty(this);
}
@ -66,28 +57,19 @@ describe('change detection', () => {
this.value = 'click works';
markDirty(this);
}
static ɵfac = () => mycompOninit = new MyComponentWithOnInit();
static ɵcmp = ɵɵdefineComponent({
type: MyComponentWithOnInit,
selectors: [['my-comp-oninit']],
decls: 2,
vars: 1,
template:
(rf: RenderFlags, ctx: MyComponentWithOnInit) => {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'span');
ɵɵtext(1);
ɵɵelementEnd();
}
if (rf & RenderFlags.Update) {
ɵɵadvance(1);
ɵɵtextInterpolate(ctx.value);
}
}
});
}
@Component({
selector: 'my-parent-comp',
standalone: true,
imports: [CommonModule, MyComponentWithOnInit],
template: `
-->
<div *ngIf="show">
<my-comp-oninit></my-comp-oninit>
</div>
`,
})
class MyParentComponent implements OnInit {
show = false;
value = 'parent';
@ -99,47 +81,28 @@ describe('change detection', () => {
this.show = true;
markDirty(this);
}
static ɵfac = () => new MyParentComponent();
static ɵcmp = ɵɵdefineComponent({
type: MyParentComponent,
selectors: [['my-parent-comp']],
decls: 2,
vars: 1,
dependencies: [NgIf, MyComponentWithOnInit],
consts: [[AttributeMarker.Template, 'ngIf']],
template:
(rf: RenderFlags, ctx: MyParentComponent) => {
if (rf & RenderFlags.Create) {
ɵɵtext(0, ' -->\n');
ɵɵtemplate(1, (rf, ctx) => {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'div');
ɵɵelement(1, 'my-comp-oninit');
ɵɵelementEnd();
}
}, 2, 0, 'div', 0);
}
if (rf & RenderFlags.Update) {
ɵɵadvance(1);
ɵɵproperty('ngIf', ctx.show);
}
}
});
}
it('should mark a component dirty and schedule change detection', withBody('my-comp', () => {
const myComp = renderComponent(MyComponent, {hostFeatures: [LifecycleHooksFeature]});
const fixture = TestBed.createComponent(MyComponent);
fixture.detectChanges();
const myComp = fixture.componentInstance;
expect(getRenderedText(myComp)).toEqual('works');
myComp.value = 'updated';
markDirty(myComp);
expect(getRenderedText(myComp)).toEqual('works');
requestAnimationFrame.flush();
fixture.detectChanges();
expect(getRenderedText(myComp)).toEqual('updated');
}));
it('should detectChanges on a component', withBody('my-comp', () => {
const myComp = renderComponent(MyComponent, {hostFeatures: [LifecycleHooksFeature]});
const fixture = TestBed.createComponent(MyComponent);
fixture.detectChanges();
const myComp = fixture.componentInstance;
expect(getRenderedText(myComp)).toEqual('works');
myComp.value = 'updated';
detectChanges(myComp);
@ -148,25 +111,34 @@ describe('change detection', () => {
it('should detectChanges after markDirty is called multiple times within ngOnInit',
withBody('my-comp-oninit', () => {
const myParentComp =
renderComponent(MyParentComponent, {hostFeatures: [LifecycleHooksFeature]});
const fixture = TestBed.createComponent(MyParentComponent);
fixture.detectChanges();
const myParentComp = fixture.componentInstance;
expect(myParentComp.show).toBe(false);
myParentComp.click();
requestAnimationFrame.flush();
fixture.detectChanges();
expect(myParentComp.show).toBe(true);
const myComp = mycompOninit;
expect(getRenderedText(myComp)).toEqual('works');
expect(myComp.doCheckCount).toBe(1);
myComp.click();
expect(getRenderedText(myComp)).toEqual('works');
requestAnimationFrame.flush();
fixture.detectChanges();
expect(getRenderedText(myComp)).toEqual('click works');
expect(myComp.doCheckCount).toBe(2);
}));
it('should detectChanges only once if markDirty is called multiple times',
withBody('my-comp', () => {
const myComp = renderComponent(MyComponent, {hostFeatures: [LifecycleHooksFeature]});
const fixture = TestBed.createComponent(MyComponent);
fixture.detectChanges();
const myComp = fixture.componentInstance;
expect(getRenderedText(myComp)).toEqual('works');
expect(myComp.doCheckCount).toBe(1);
myComp.value = 'ignore';
@ -174,59 +146,75 @@ describe('change detection', () => {
myComp.value = 'updated';
markDirty(myComp);
expect(getRenderedText(myComp)).toEqual('works');
requestAnimationFrame.flush();
fixture.detectChanges();
expect(getRenderedText(myComp)).toEqual('updated');
expect(myComp.doCheckCount).toBe(2);
}));
it('should notify whenRendered', withBody('my-comp', async () => {
const myComp = renderComponent(MyComponent, {hostFeatures: [LifecycleHooksFeature]});
const fixture = TestBed.createComponent(MyComponent);
fixture.detectChanges();
const myComp = fixture.componentInstance;
await whenRendered(myComp);
myComp.value = 'updated';
markDirty(myComp);
setTimeout(requestAnimationFrame.flush, 0);
setTimeout(() => fixture.detectChanges(), 0);
await whenRendered(myComp);
expect(getRenderedText(myComp)).toEqual('updated');
}));
});
it('should call begin and end when the renderer factory implements them', () => {
const log: string[] = [];
it('should call begin and end when the renderer factory implements them',
withBody('<my-comp></my-comp>', () => {
const log: string[] = [];
const testRendererFactory: RendererFactory3 = {
createRenderer: (): Renderer3 => {
return document;
},
begin: () => log.push('begin'),
end: () => log.push('end'),
};
const testRendererFactory: RendererFactory2 = {
createRenderer: (): Renderer2 => {
return document as unknown as Renderer2;
},
begin: () => log.push('begin'),
end: () => log.push('end'),
};
class MyComponent {
get value(): string {
log.push('detect changes');
return 'works';
}
@Component({
selector: 'my-comp',
standalone: true,
template: '{{ value }}',
})
class MyComponent {
get value(): string {
log.push('detect changes');
return 'works';
}
}
static ɵfac = () => new MyComponent();
static ɵcmp = ɵɵdefineComponent({
type: MyComponent,
selectors: [['my-comp']],
decls: 1,
vars: 1,
template:
(rf: RenderFlags, ctx: MyComponent) => {
if (rf & RenderFlags.Create) {
ɵɵtext(0);
}
if (rf & RenderFlags.Update) {
ɵɵtextInterpolate(ctx.value);
}
}
});
}
TestBed.configureTestingModule({
providers: [
{
provide: DOCUMENT,
useFactory: () => document,
},
{
provide: RendererFactory2,
useValue: testRendererFactory,
}
]
});
const myComp = renderComponent(MyComponent, {rendererFactory: testRendererFactory});
expect(getRenderedText(myComp)).toEqual('works');
expect(log).toEqual(['begin', 'detect changes', 'end']);
});
const fixture = TestBed.createComponent(MyComponent);
fixture.detectChanges();
const myComp = fixture.componentInstance;
expect(getRenderedText(myComp)).toEqual('works');
expect(log).toEqual([
'begin',
'detect changes', // regular change detection cycle
'end',
'detect changes' // check no changes cycle
]);
}));
});