test(ivy): add onDestroy acceptance tests (#30445)
Ports render3 onDestroy tests over to be acceptance tests. Removes old render3 tests if possible. Note the onlyInIvy test for one area where Ivy has a different behavior than ViewEngine PR Close #30445
This commit is contained in:
parent
257e9646d0
commit
222dde129d
|
@ -10,6 +10,7 @@ import {CommonModule} from '@angular/common';
|
||||||
import {Component, ComponentFactoryResolver, Directive, Input, NgModule, OnChanges, SimpleChanges, ViewChild, ViewContainerRef} from '@angular/core';
|
import {Component, ComponentFactoryResolver, Directive, Input, NgModule, OnChanges, SimpleChanges, ViewChild, ViewContainerRef} from '@angular/core';
|
||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
import {By} from '@angular/platform-browser';
|
import {By} from '@angular/platform-browser';
|
||||||
|
import {onlyInIvy} from '@angular/private/testing';
|
||||||
|
|
||||||
describe('ngOnChanges', () => {
|
describe('ngOnChanges', () => {
|
||||||
it('should correctly support updating one Input among many', () => {
|
it('should correctly support updating one Input among many', () => {
|
||||||
|
@ -1656,7 +1657,7 @@ describe('afterViewInit', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('ngAfterViewChecked', () => {
|
describe('afterViewChecked', () => {
|
||||||
it('should call ngAfterViewChecked every update', () => {
|
it('should call ngAfterViewChecked every update', () => {
|
||||||
let afterViewCheckedCalls = 0;
|
let afterViewCheckedCalls = 0;
|
||||||
|
|
||||||
|
@ -1887,3 +1888,566 @@ describe('ngAfterViewChecked', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('onDestroy', () => {
|
||||||
|
|
||||||
|
|
||||||
|
it('should call destroy when view is removed', () => {
|
||||||
|
let destroyCalled = 0;
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'comp',
|
||||||
|
template: `<p>test</p>`,
|
||||||
|
})
|
||||||
|
class Comp {
|
||||||
|
ngOnDestroy() { destroyCalled++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: `<comp *ngIf="show"></comp>`,
|
||||||
|
})
|
||||||
|
class App {
|
||||||
|
show = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [App, Comp],
|
||||||
|
imports: [CommonModule],
|
||||||
|
});
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(destroyCalled).toBe(0);
|
||||||
|
|
||||||
|
fixture.componentInstance.show = false;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(destroyCalled).toBe(1);
|
||||||
|
|
||||||
|
fixture.componentInstance.show = true;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(destroyCalled).toBe(1);
|
||||||
|
|
||||||
|
fixture.componentInstance.show = false;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(destroyCalled).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call destroy when multiple views are removed', () => {
|
||||||
|
const events: string[] = [];
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'comp',
|
||||||
|
template: `<p>test</p>`,
|
||||||
|
})
|
||||||
|
class Comp {
|
||||||
|
@Input()
|
||||||
|
name = '';
|
||||||
|
|
||||||
|
ngOnDestroy() { events.push('comp ' + this.name); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: `
|
||||||
|
<div *ngIf="show">
|
||||||
|
<comp name="1"></comp>
|
||||||
|
<comp name="2"></comp>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
class App {
|
||||||
|
show = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [App, Comp],
|
||||||
|
imports: [CommonModule],
|
||||||
|
});
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(events).toEqual([]);
|
||||||
|
|
||||||
|
fixture.componentInstance.show = false;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(events).toEqual(['comp 1', 'comp 2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be called in child components before parent components', () => {
|
||||||
|
const events: string[] = [];
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'child',
|
||||||
|
template: `<p>test</p>`,
|
||||||
|
})
|
||||||
|
class Child {
|
||||||
|
@Input()
|
||||||
|
name = '';
|
||||||
|
|
||||||
|
ngOnDestroy() { events.push('child of parent ' + this.name); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'parent',
|
||||||
|
template: `<child [name]="name"></child>`,
|
||||||
|
})
|
||||||
|
class Parent {
|
||||||
|
@Input()
|
||||||
|
name = '';
|
||||||
|
ngOnDestroy() { events.push('parent ' + this.name); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: `
|
||||||
|
<div *ngIf="show">
|
||||||
|
<parent name="1"></parent>
|
||||||
|
<parent name="2"></parent>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
class App {
|
||||||
|
show = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [App, Parent, Child],
|
||||||
|
imports: [CommonModule],
|
||||||
|
});
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(events).toEqual([]);
|
||||||
|
|
||||||
|
fixture.componentInstance.show = false;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(events).toEqual([
|
||||||
|
'child of parent 1',
|
||||||
|
'child of parent 2',
|
||||||
|
'parent 1',
|
||||||
|
'parent 2',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be called bottom up with children nested 2 levels deep', () => {
|
||||||
|
const events: string[] = [];
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'child',
|
||||||
|
template: `<p>test</p>`,
|
||||||
|
})
|
||||||
|
class Child {
|
||||||
|
@Input()
|
||||||
|
name = '';
|
||||||
|
|
||||||
|
ngOnDestroy() { events.push('child ' + this.name); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'parent',
|
||||||
|
template: `<child [name]="name"></child>`,
|
||||||
|
})
|
||||||
|
class Parent {
|
||||||
|
@Input()
|
||||||
|
name = '';
|
||||||
|
ngOnDestroy() { events.push('parent ' + this.name); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'grandparent',
|
||||||
|
template: `<parent [name]="name"></parent>`,
|
||||||
|
})
|
||||||
|
class Grandparent {
|
||||||
|
@Input()
|
||||||
|
name = '';
|
||||||
|
|
||||||
|
ngOnDestroy() { events.push('grandparent ' + this.name); }
|
||||||
|
}
|
||||||
|
@Component({
|
||||||
|
template: `
|
||||||
|
<div *ngIf="show">
|
||||||
|
<grandparent name="1"></grandparent>
|
||||||
|
<grandparent name="2"></grandparent>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
class App {
|
||||||
|
show = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [App, Grandparent, Parent, Child],
|
||||||
|
imports: [CommonModule],
|
||||||
|
});
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(events).toEqual([]);
|
||||||
|
|
||||||
|
fixture.componentInstance.show = false;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(events).toEqual([
|
||||||
|
'child 1',
|
||||||
|
'parent 1',
|
||||||
|
'child 2',
|
||||||
|
'parent 2',
|
||||||
|
'grandparent 1',
|
||||||
|
'grandparent 2',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be called in projected components before their hosts', () => {
|
||||||
|
const events: string[] = [];
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'projected',
|
||||||
|
template: `<p>test</p>`,
|
||||||
|
})
|
||||||
|
class Projected {
|
||||||
|
@Input()
|
||||||
|
name = '';
|
||||||
|
|
||||||
|
ngOnDestroy() { events.push('projected ' + this.name); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'comp',
|
||||||
|
template: `<div><ng-content></ng-content></div>`,
|
||||||
|
})
|
||||||
|
class Comp {
|
||||||
|
@Input()
|
||||||
|
name = '';
|
||||||
|
|
||||||
|
ngOnDestroy() { events.push('comp ' + this.name); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: `
|
||||||
|
<div *ngIf="show">
|
||||||
|
<comp name="1">
|
||||||
|
<projected name="1"></projected>
|
||||||
|
</comp>
|
||||||
|
<comp name="2">
|
||||||
|
<projected name="2"></projected>
|
||||||
|
</comp>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
class App {
|
||||||
|
show = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [App, Comp, Projected],
|
||||||
|
});
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(events).toEqual([]);
|
||||||
|
|
||||||
|
fixture.componentInstance.show = false;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(events).toEqual([
|
||||||
|
'projected 1',
|
||||||
|
'comp 1',
|
||||||
|
'projected 2',
|
||||||
|
'comp 2',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should be called in consistent order if views are removed and re-added', () => {
|
||||||
|
const events: string[] = [];
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'comp',
|
||||||
|
template: `<p>test</p>`,
|
||||||
|
})
|
||||||
|
class Comp {
|
||||||
|
@Input()
|
||||||
|
name = '';
|
||||||
|
|
||||||
|
ngOnDestroy() { events.push('comp ' + this.name); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: `
|
||||||
|
<div *ngIf="showAll">
|
||||||
|
<comp name="1"></comp>
|
||||||
|
<comp *ngIf="showMiddle" name="2"></comp>
|
||||||
|
<comp name="3"></comp>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
class App {
|
||||||
|
showAll = true;
|
||||||
|
showMiddle = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [App, Comp],
|
||||||
|
imports: [CommonModule],
|
||||||
|
});
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(events).toEqual([]);
|
||||||
|
|
||||||
|
fixture.componentInstance.showMiddle = false;
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(events).toEqual(['comp 2']);
|
||||||
|
|
||||||
|
fixture.componentInstance.showAll = false;
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(events).toEqual([
|
||||||
|
'comp 2',
|
||||||
|
'comp 1',
|
||||||
|
'comp 3',
|
||||||
|
]);
|
||||||
|
|
||||||
|
fixture.componentInstance.showAll = true;
|
||||||
|
fixture.componentInstance.showMiddle = true;
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(events).toEqual([
|
||||||
|
'comp 2',
|
||||||
|
'comp 1',
|
||||||
|
'comp 3',
|
||||||
|
]);
|
||||||
|
|
||||||
|
fixture.componentInstance.showAll = false;
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(events).toEqual([
|
||||||
|
'comp 2',
|
||||||
|
'comp 1',
|
||||||
|
'comp 3',
|
||||||
|
'comp 2',
|
||||||
|
'comp 1',
|
||||||
|
'comp 3',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be called on every iteration of a destroyed for loop', () => {
|
||||||
|
const events: string[] = [];
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'comp',
|
||||||
|
template: `<p>test</p>`,
|
||||||
|
})
|
||||||
|
class Comp {
|
||||||
|
@Input()
|
||||||
|
name = '';
|
||||||
|
|
||||||
|
ngOnDestroy() { events.push('comp ' + this.name); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: `
|
||||||
|
<div *ngIf="show">
|
||||||
|
<comp *ngFor="let number of numbers" [name]="number"></comp>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
class App {
|
||||||
|
show = true;
|
||||||
|
numbers = [0, 1, 2, 3];
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [App, Comp],
|
||||||
|
});
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(events).toEqual([]);
|
||||||
|
|
||||||
|
fixture.componentInstance.show = false;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(events).toEqual([
|
||||||
|
'comp 0',
|
||||||
|
'comp 1',
|
||||||
|
'comp 2',
|
||||||
|
'comp 3',
|
||||||
|
]);
|
||||||
|
|
||||||
|
fixture.componentInstance.show = true;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(events).toEqual([
|
||||||
|
'comp 0',
|
||||||
|
'comp 1',
|
||||||
|
'comp 2',
|
||||||
|
'comp 3',
|
||||||
|
]);
|
||||||
|
|
||||||
|
fixture.componentInstance.numbers.splice(1, 1);
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(events).toEqual([
|
||||||
|
'comp 0',
|
||||||
|
'comp 1',
|
||||||
|
'comp 2',
|
||||||
|
'comp 3',
|
||||||
|
'comp 1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
fixture.componentInstance.show = false;
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(events).toEqual([
|
||||||
|
'comp 0',
|
||||||
|
'comp 1',
|
||||||
|
'comp 2',
|
||||||
|
'comp 3',
|
||||||
|
'comp 1',
|
||||||
|
'comp 0',
|
||||||
|
'comp 2',
|
||||||
|
'comp 3',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call destroy properly if view also has listeners', () => {
|
||||||
|
const events: string[] = [];
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'comp',
|
||||||
|
template: `<p>test</p>`,
|
||||||
|
})
|
||||||
|
class Comp {
|
||||||
|
ngOnDestroy() { events.push('comp'); }
|
||||||
|
}
|
||||||
|
@Component({
|
||||||
|
template: `
|
||||||
|
<div *ngIf="show">
|
||||||
|
<button (click)="handleClick1()">test 1</button>
|
||||||
|
<comp></comp>
|
||||||
|
<button (click)="handleClick2()">test 2</button>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
class App {
|
||||||
|
show = true;
|
||||||
|
|
||||||
|
clicksToButton1 = 0;
|
||||||
|
|
||||||
|
clicksToButton2 = 0;
|
||||||
|
|
||||||
|
handleClick1() { this.clicksToButton1++; }
|
||||||
|
|
||||||
|
handleClick2() { this.clicksToButton2++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [App, Comp],
|
||||||
|
});
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const buttons = fixture.debugElement.queryAll(By.css('button'));
|
||||||
|
buttons.forEach(button => button.nativeElement.click());
|
||||||
|
|
||||||
|
expect(fixture.componentInstance.clicksToButton1).toBe(1);
|
||||||
|
expect(fixture.componentInstance.clicksToButton2).toBe(1);
|
||||||
|
expect(events).toEqual([]);
|
||||||
|
|
||||||
|
fixture.componentInstance.show = false;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
buttons.forEach(button => button.nativeElement.click());
|
||||||
|
expect(fixture.componentInstance.clicksToButton1).toBe(1);
|
||||||
|
expect(fixture.componentInstance.clicksToButton2).toBe(1);
|
||||||
|
|
||||||
|
expect(events).toEqual(['comp']);
|
||||||
|
});
|
||||||
|
|
||||||
|
onlyInIvy(
|
||||||
|
'View Engine has the opposite behavior, where it calls destroy on the directives first, then the components')
|
||||||
|
.it('should be called on directives after component', () => {
|
||||||
|
const events: string[] = [];
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[dir]',
|
||||||
|
})
|
||||||
|
class Dir {
|
||||||
|
@Input('dir')
|
||||||
|
name = '';
|
||||||
|
|
||||||
|
ngOnDestroy() { events.push('dir ' + this.name); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'comp',
|
||||||
|
template: `<p>test</p>`,
|
||||||
|
})
|
||||||
|
class Comp {
|
||||||
|
@Input()
|
||||||
|
name = '';
|
||||||
|
|
||||||
|
ngOnDestroy() { events.push('comp ' + this.name); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: `
|
||||||
|
<div *ngIf="show">
|
||||||
|
<comp name="1" dir="1"></comp>
|
||||||
|
<comp name="2" dir="2"></comp>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
class App {
|
||||||
|
show = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [App, Dir, Comp],
|
||||||
|
imports: [CommonModule],
|
||||||
|
});
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(events).toEqual([]);
|
||||||
|
|
||||||
|
fixture.componentInstance.show = false;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(events).toEqual([
|
||||||
|
'comp 1',
|
||||||
|
'dir 1',
|
||||||
|
'comp 2',
|
||||||
|
'dir 2',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be called on directives on an element', () => {
|
||||||
|
const events: string[] = [];
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[dir]',
|
||||||
|
})
|
||||||
|
class Dir {
|
||||||
|
ngOnDestroy() { events.push('dir'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({template: `<p *ngIf="show" dir></p>`})
|
||||||
|
class App {
|
||||||
|
show = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [App, Dir],
|
||||||
|
imports: [CommonModule],
|
||||||
|
});
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(events).toEqual([]);
|
||||||
|
|
||||||
|
fixture.componentInstance.show = false;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(events).toEqual(['dir']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -116,573 +116,6 @@ describe('lifecycles', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onDestroy', () => {
|
|
||||||
let events: string[];
|
|
||||||
|
|
||||||
beforeEach(() => { events = []; });
|
|
||||||
|
|
||||||
let Comp = createOnDestroyComponent('comp', (rf: RenderFlags, ctx: any) => {
|
|
||||||
if (rf & RenderFlags.Create) {
|
|
||||||
ΔprojectionDef();
|
|
||||||
Δprojection(0);
|
|
||||||
}
|
|
||||||
}, 1);
|
|
||||||
let Parent = createOnDestroyComponent('parent', getParentTemplate('comp'), 1, 1, [Comp]);
|
|
||||||
|
|
||||||
function createOnDestroyComponent(
|
|
||||||
name: string, template: ComponentTemplate<any>, consts: number = 0, vars: number = 0,
|
|
||||||
directives: any[] = []) {
|
|
||||||
return class Component {
|
|
||||||
val: string = '';
|
|
||||||
ngOnDestroy() { events.push(`${name}${this.val}`); }
|
|
||||||
|
|
||||||
static ngComponentDef = ΔdefineComponent({
|
|
||||||
type: Component,
|
|
||||||
selectors: [[name]],
|
|
||||||
factory: () => new Component(),
|
|
||||||
consts: consts,
|
|
||||||
vars: vars,
|
|
||||||
inputs: {val: 'val'},
|
|
||||||
template: template,
|
|
||||||
directives: directives
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let Grandparent = createOnDestroyComponent('grandparent', function(rf: RenderFlags, ctx: any) {
|
|
||||||
if (rf & RenderFlags.Create) {
|
|
||||||
Δelement(0, 'parent');
|
|
||||||
}
|
|
||||||
}, 1, 0, [Parent]);
|
|
||||||
|
|
||||||
const ProjectedComp = createOnDestroyComponent('projected', (rf: RenderFlags, ctx: any) => {});
|
|
||||||
|
|
||||||
class Directive {
|
|
||||||
ngOnDestroy() { events.push('dir'); }
|
|
||||||
|
|
||||||
static ngDirectiveDef = ΔdefineDirective(
|
|
||||||
{type: Directive, selectors: [['', 'dir', '']], factory: () => new Directive()});
|
|
||||||
}
|
|
||||||
|
|
||||||
const defs = [Comp, Parent, Grandparent, ProjectedComp, Directive];
|
|
||||||
|
|
||||||
it('should call destroy when view is removed', () => {
|
|
||||||
/**
|
|
||||||
* % if (!skip) {
|
|
||||||
* <comp></comp>
|
|
||||||
* % }
|
|
||||||
*/
|
|
||||||
|
|
||||||
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
||||||
if (rf & RenderFlags.Create) {
|
|
||||||
Δcontainer(0);
|
|
||||||
}
|
|
||||||
if (rf & RenderFlags.Update) {
|
|
||||||
ΔcontainerRefreshStart(0);
|
|
||||||
{
|
|
||||||
if (!ctx.skip) {
|
|
||||||
let rf1 = ΔembeddedViewStart(0, 1, 0);
|
|
||||||
if (rf1 & RenderFlags.Create) {
|
|
||||||
Δelement(0, 'comp');
|
|
||||||
}
|
|
||||||
ΔembeddedViewEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ΔcontainerRefreshEnd();
|
|
||||||
}
|
|
||||||
}, 1, 0, defs);
|
|
||||||
|
|
||||||
const fixture = new ComponentFixture(App);
|
|
||||||
|
|
||||||
fixture.component.skip = true;
|
|
||||||
fixture.update();
|
|
||||||
expect(events).toEqual(['comp']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call destroy when multiple views are removed', () => {
|
|
||||||
/**
|
|
||||||
* % if (!skip) {
|
|
||||||
* <comp [val]="1"></comp>
|
|
||||||
* <comp [val]="2"></comp>
|
|
||||||
* % }
|
|
||||||
*/
|
|
||||||
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
||||||
if (rf & RenderFlags.Create) {
|
|
||||||
Δcontainer(0);
|
|
||||||
}
|
|
||||||
if (rf & RenderFlags.Update) {
|
|
||||||
ΔcontainerRefreshStart(0);
|
|
||||||
{
|
|
||||||
if (!ctx.skip) {
|
|
||||||
let rf1 = ΔembeddedViewStart(0, 2, 2);
|
|
||||||
if (rf1 & RenderFlags.Create) {
|
|
||||||
Δelement(0, 'comp');
|
|
||||||
Δelement(1, 'comp');
|
|
||||||
}
|
|
||||||
if (rf1 & RenderFlags.Update) {
|
|
||||||
ΔelementProperty(0, 'val', Δbind('1'));
|
|
||||||
Δselect(1);
|
|
||||||
ΔelementProperty(1, 'val', Δbind('2'));
|
|
||||||
}
|
|
||||||
ΔembeddedViewEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ΔcontainerRefreshEnd();
|
|
||||||
}
|
|
||||||
}, 1, 0, defs);
|
|
||||||
|
|
||||||
const fixture = new ComponentFixture(App);
|
|
||||||
|
|
||||||
fixture.component.skip = true;
|
|
||||||
fixture.update();
|
|
||||||
expect(events).toEqual(['comp1', 'comp2']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be called in child components before parent components', () => {
|
|
||||||
/**
|
|
||||||
* % if (!skip) {
|
|
||||||
* <parent></parent>
|
|
||||||
* % }
|
|
||||||
*
|
|
||||||
* parent template: <comp></comp>
|
|
||||||
*/
|
|
||||||
|
|
||||||
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
||||||
if (rf & RenderFlags.Create) {
|
|
||||||
Δcontainer(0);
|
|
||||||
}
|
|
||||||
if (rf & RenderFlags.Update) {
|
|
||||||
ΔcontainerRefreshStart(0);
|
|
||||||
{
|
|
||||||
if (!ctx.skip) {
|
|
||||||
let rf1 = ΔembeddedViewStart(0, 1, 0);
|
|
||||||
if (rf1 & RenderFlags.Create) {
|
|
||||||
Δelement(0, 'parent');
|
|
||||||
}
|
|
||||||
ΔembeddedViewEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ΔcontainerRefreshEnd();
|
|
||||||
}
|
|
||||||
}, 1, 0, defs);
|
|
||||||
|
|
||||||
const fixture = new ComponentFixture(App);
|
|
||||||
|
|
||||||
fixture.component.skip = true;
|
|
||||||
fixture.update();
|
|
||||||
expect(events).toEqual(['comp', 'parent']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be called bottom up with children nested 2 levels deep', () => {
|
|
||||||
/**
|
|
||||||
* % if (!skip) {
|
|
||||||
* <grandparent></grandparent>
|
|
||||||
* % }
|
|
||||||
*
|
|
||||||
* grandparent template: <parent></parent>
|
|
||||||
* parent template: <comp></comp>
|
|
||||||
*/
|
|
||||||
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
||||||
if (rf & RenderFlags.Create) {
|
|
||||||
Δcontainer(0);
|
|
||||||
}
|
|
||||||
if (rf & RenderFlags.Update) {
|
|
||||||
ΔcontainerRefreshStart(0);
|
|
||||||
{
|
|
||||||
if (!ctx.skip) {
|
|
||||||
let rf1 = ΔembeddedViewStart(0, 1, 0);
|
|
||||||
if (rf1 & RenderFlags.Create) {
|
|
||||||
Δelement(0, 'grandparent');
|
|
||||||
}
|
|
||||||
ΔembeddedViewEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ΔcontainerRefreshEnd();
|
|
||||||
}
|
|
||||||
}, 1, 0, defs);
|
|
||||||
|
|
||||||
const fixture = new ComponentFixture(App);
|
|
||||||
|
|
||||||
fixture.component.skip = true;
|
|
||||||
fixture.update();
|
|
||||||
expect(events).toEqual(['comp', 'parent', 'grandparent']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be called in projected components before their hosts', () => {
|
|
||||||
/**
|
|
||||||
* % if (!skip) {
|
|
||||||
* <comp [val]="1">
|
|
||||||
* <projected [val]="1"></projected>
|
|
||||||
* </comp>
|
|
||||||
* <comp [val]="2">
|
|
||||||
* <projected [val]="2"></projected>
|
|
||||||
* </comp>
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
||||||
if (rf & RenderFlags.Create) {
|
|
||||||
Δcontainer(0);
|
|
||||||
}
|
|
||||||
if (rf & RenderFlags.Update) {
|
|
||||||
ΔcontainerRefreshStart(0);
|
|
||||||
{
|
|
||||||
if (!ctx.skip) {
|
|
||||||
let rf1 = ΔembeddedViewStart(0, 4, 0);
|
|
||||||
if (rf1 & RenderFlags.Create) {
|
|
||||||
ΔelementStart(0, 'comp');
|
|
||||||
{ Δelement(1, 'projected'); }
|
|
||||||
ΔelementEnd();
|
|
||||||
ΔelementStart(2, 'comp');
|
|
||||||
{ Δelement(3, 'projected'); }
|
|
||||||
ΔelementEnd();
|
|
||||||
}
|
|
||||||
if (rf1 & RenderFlags.Update) {
|
|
||||||
ΔelementProperty(0, 'val', 1);
|
|
||||||
Δselect(1);
|
|
||||||
ΔelementProperty(1, 'val', 1);
|
|
||||||
Δselect(2);
|
|
||||||
ΔelementProperty(2, 'val', 2);
|
|
||||||
Δselect(3);
|
|
||||||
ΔelementProperty(3, 'val', 2);
|
|
||||||
}
|
|
||||||
ΔembeddedViewEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ΔcontainerRefreshEnd();
|
|
||||||
}
|
|
||||||
}, 1, 0, defs);
|
|
||||||
|
|
||||||
const fixture = new ComponentFixture(App);
|
|
||||||
|
|
||||||
fixture.component.skip = true;
|
|
||||||
fixture.update();
|
|
||||||
expect(events).toEqual(['projected1', 'comp1', 'projected2', 'comp2']);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should be called in consistent order if views are removed and re-added', () => {
|
|
||||||
/**
|
|
||||||
* % if (condition) {
|
|
||||||
* <comp [val]="1"></comp>
|
|
||||||
* % if (condition2) {
|
|
||||||
* <comp [val]="2"></comp>
|
|
||||||
* % }
|
|
||||||
* <comp [val]="3"></comp>
|
|
||||||
* % }
|
|
||||||
*/
|
|
||||||
|
|
||||||
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
||||||
if (rf & RenderFlags.Create) {
|
|
||||||
Δcontainer(0);
|
|
||||||
}
|
|
||||||
if (rf & RenderFlags.Update) {
|
|
||||||
ΔcontainerRefreshStart(0);
|
|
||||||
{
|
|
||||||
if (ctx.condition) {
|
|
||||||
let rf1 = ΔembeddedViewStart(0, 3, 2);
|
|
||||||
if (rf1 & RenderFlags.Create) {
|
|
||||||
Δelement(0, 'comp');
|
|
||||||
Δcontainer(1);
|
|
||||||
Δelement(2, 'comp');
|
|
||||||
}
|
|
||||||
if (rf1 & RenderFlags.Update) {
|
|
||||||
ΔelementProperty(0, 'val', Δbind('1'));
|
|
||||||
Δselect(2);
|
|
||||||
ΔelementProperty(2, 'val', Δbind('3'));
|
|
||||||
ΔcontainerRefreshStart(1);
|
|
||||||
{
|
|
||||||
if (ctx.condition2) {
|
|
||||||
let rf2 = ΔembeddedViewStart(0, 1, 1);
|
|
||||||
if (rf2 & RenderFlags.Create) {
|
|
||||||
Δelement(0, 'comp');
|
|
||||||
}
|
|
||||||
if (rf2 & RenderFlags.Update) {
|
|
||||||
ΔelementProperty(0, 'val', Δbind('2'));
|
|
||||||
}
|
|
||||||
ΔembeddedViewEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ΔcontainerRefreshEnd();
|
|
||||||
}
|
|
||||||
ΔembeddedViewEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ΔcontainerRefreshEnd();
|
|
||||||
}
|
|
||||||
}, 1, 0, defs);
|
|
||||||
|
|
||||||
const fixture = new ComponentFixture(App);
|
|
||||||
fixture.component.condition = true;
|
|
||||||
fixture.component.condition2 = true;
|
|
||||||
fixture.update();
|
|
||||||
|
|
||||||
// remove all views
|
|
||||||
fixture.component.condition = false;
|
|
||||||
fixture.update();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current angular will process in this same order (root is the top-level removed view):
|
|
||||||
*
|
|
||||||
* root.child (comp1 view) onDestroy: null
|
|
||||||
* root.child.next (container) -> embeddedView
|
|
||||||
* embeddedView.child (comp2 view) onDestroy: null
|
|
||||||
* embeddedView onDestroy: [comp2]
|
|
||||||
* root.child.next.next (comp3 view) onDestroy: null
|
|
||||||
* root onDestroy: [comp1, comp3]
|
|
||||||
*/
|
|
||||||
expect(events).toEqual(['comp2', 'comp1', 'comp3']);
|
|
||||||
|
|
||||||
events = [];
|
|
||||||
// remove inner view
|
|
||||||
fixture.component.condition = true;
|
|
||||||
fixture.component.condition2 = false;
|
|
||||||
fixture.update();
|
|
||||||
|
|
||||||
// remove outer view
|
|
||||||
fixture.component.condition = false;
|
|
||||||
fixture.update();
|
|
||||||
expect(events).toEqual(['comp1', 'comp3']);
|
|
||||||
|
|
||||||
events = [];
|
|
||||||
// restore both views
|
|
||||||
fixture.component.condition = true;
|
|
||||||
fixture.component.condition2 = true;
|
|
||||||
fixture.update();
|
|
||||||
|
|
||||||
// remove both views
|
|
||||||
fixture.component.condition = false;
|
|
||||||
fixture.update();
|
|
||||||
expect(events).toEqual(['comp2', 'comp1', 'comp3']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be called in every iteration of a destroyed for loop', () => {
|
|
||||||
/**
|
|
||||||
* % if (condition) {
|
|
||||||
* <comp [val]="1"></comp>
|
|
||||||
* % for (let i = 2; i < len; i++) {
|
|
||||||
* <comp [val]="i"></comp>
|
|
||||||
* % }
|
|
||||||
* <comp [val]="5"></comp>
|
|
||||||
* % }
|
|
||||||
*/
|
|
||||||
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
||||||
if (rf & RenderFlags.Create) {
|
|
||||||
Δcontainer(0);
|
|
||||||
}
|
|
||||||
if (rf & RenderFlags.Update) {
|
|
||||||
ΔcontainerRefreshStart(0);
|
|
||||||
{
|
|
||||||
if (ctx.condition) {
|
|
||||||
let rf1 = ΔembeddedViewStart(0, 3, 2);
|
|
||||||
if (rf1 & RenderFlags.Create) {
|
|
||||||
Δelement(0, 'comp');
|
|
||||||
Δcontainer(1);
|
|
||||||
Δelement(2, 'comp');
|
|
||||||
}
|
|
||||||
if (rf1 & RenderFlags.Update) {
|
|
||||||
ΔelementProperty(0, 'val', Δbind('1'));
|
|
||||||
Δselect(2);
|
|
||||||
ΔelementProperty(2, 'val', Δbind('5'));
|
|
||||||
ΔcontainerRefreshStart(1);
|
|
||||||
{
|
|
||||||
for (let j = 2; j < ctx.len; j++) {
|
|
||||||
let rf2 = ΔembeddedViewStart(0, 1, 1);
|
|
||||||
if (rf2 & RenderFlags.Create) {
|
|
||||||
Δelement(0, 'comp');
|
|
||||||
}
|
|
||||||
if (rf2 & RenderFlags.Update) {
|
|
||||||
ΔelementProperty(0, 'val', Δbind(j));
|
|
||||||
}
|
|
||||||
ΔembeddedViewEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ΔcontainerRefreshEnd();
|
|
||||||
}
|
|
||||||
ΔembeddedViewEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ΔcontainerRefreshEnd();
|
|
||||||
}
|
|
||||||
}, 1, 0, defs);
|
|
||||||
|
|
||||||
const fixture = new ComponentFixture(App);
|
|
||||||
fixture.component.condition = true;
|
|
||||||
fixture.component.len = 5;
|
|
||||||
fixture.update();
|
|
||||||
|
|
||||||
fixture.component.condition = false;
|
|
||||||
fixture.update();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current angular will process in this same order (root is the top-level removed view):
|
|
||||||
*
|
|
||||||
* root.child (comp1 view) onDestroy: null
|
|
||||||
* root.child.next (container) -> embeddedView (children[0].data)
|
|
||||||
* embeddedView.child (comp2 view) onDestroy: null
|
|
||||||
* embeddedView onDestroy: [comp2]
|
|
||||||
* embeddedView.next.child (comp3 view) onDestroy: null
|
|
||||||
* embeddedView.next onDestroy: [comp3]
|
|
||||||
* embeddedView.next.next.child (comp4 view) onDestroy: null
|
|
||||||
* embeddedView.next.next onDestroy: [comp4]
|
|
||||||
* embeddedView.next.next -> container -> root
|
|
||||||
* root onDestroy: [comp1, comp5]
|
|
||||||
*/
|
|
||||||
expect(events).toEqual(['comp2', 'comp3', 'comp4', 'comp1', 'comp5']);
|
|
||||||
|
|
||||||
events = [];
|
|
||||||
fixture.component.condition = true;
|
|
||||||
fixture.component.len = 4;
|
|
||||||
fixture.update();
|
|
||||||
|
|
||||||
fixture.component.condition = false;
|
|
||||||
fixture.update();
|
|
||||||
expect(events).toEqual(['comp2', 'comp3', 'comp1', 'comp5']);
|
|
||||||
|
|
||||||
events = [];
|
|
||||||
fixture.component.condition = true;
|
|
||||||
fixture.component.len = 5;
|
|
||||||
fixture.update();
|
|
||||||
|
|
||||||
fixture.component.condition = false;
|
|
||||||
fixture.update();
|
|
||||||
expect(events).toEqual(['comp2', 'comp3', 'comp4', 'comp1', 'comp5']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call destroy properly if view also has listeners', () => {
|
|
||||||
/**
|
|
||||||
* % if (condition) {
|
|
||||||
* <button (click)="onClick()">Click me</button>
|
|
||||||
* <comp></comp>
|
|
||||||
* <button (click)="onClick()">Click me</button>
|
|
||||||
* % }
|
|
||||||
*/
|
|
||||||
function Template(rf: RenderFlags, ctx: any) {
|
|
||||||
if (rf & RenderFlags.Create) {
|
|
||||||
Δcontainer(0);
|
|
||||||
}
|
|
||||||
if (rf & RenderFlags.Update) {
|
|
||||||
ΔcontainerRefreshStart(0);
|
|
||||||
{
|
|
||||||
if (ctx.condition) {
|
|
||||||
let rf1 = ΔembeddedViewStart(0, 5, 0);
|
|
||||||
if (rf1 & RenderFlags.Create) {
|
|
||||||
ΔelementStart(0, 'button');
|
|
||||||
{
|
|
||||||
Δlistener('click', ctx.onClick.bind(ctx));
|
|
||||||
Δtext(1, 'Click me');
|
|
||||||
}
|
|
||||||
ΔelementEnd();
|
|
||||||
Δelement(2, 'comp');
|
|
||||||
ΔelementStart(3, 'button');
|
|
||||||
{
|
|
||||||
Δlistener('click', ctx.onClick.bind(ctx));
|
|
||||||
Δtext(4, 'Click me');
|
|
||||||
}
|
|
||||||
ΔelementEnd();
|
|
||||||
}
|
|
||||||
ΔembeddedViewEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ΔcontainerRefreshEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class App {
|
|
||||||
counter = 0;
|
|
||||||
condition = true;
|
|
||||||
onClick() { this.counter++; }
|
|
||||||
}
|
|
||||||
|
|
||||||
const ctx: {counter: number} = new App();
|
|
||||||
renderToHtml(Template, ctx, 1, 0, defs);
|
|
||||||
|
|
||||||
const buttons = containerEl.querySelectorAll('button') !;
|
|
||||||
buttons[0].click();
|
|
||||||
expect(ctx.counter).toEqual(1);
|
|
||||||
buttons[1].click();
|
|
||||||
expect(ctx.counter).toEqual(2);
|
|
||||||
|
|
||||||
renderToHtml(Template, {condition: false}, 1, 0, defs);
|
|
||||||
|
|
||||||
buttons[0].click();
|
|
||||||
buttons[1].click();
|
|
||||||
expect(events).toEqual(['comp']);
|
|
||||||
expect(ctx.counter).toEqual(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be called on directives after component', () => {
|
|
||||||
/**
|
|
||||||
* % if (condition) {
|
|
||||||
* <comp></comp>
|
|
||||||
* % }
|
|
||||||
*/
|
|
||||||
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
||||||
if (rf & RenderFlags.Create) {
|
|
||||||
Δcontainer(0);
|
|
||||||
}
|
|
||||||
if (rf & RenderFlags.Update) {
|
|
||||||
ΔcontainerRefreshStart(0);
|
|
||||||
{
|
|
||||||
if (ctx.condition) {
|
|
||||||
let rf1 = ΔembeddedViewStart(0, 1, 0);
|
|
||||||
if (rf1 & RenderFlags.Create) {
|
|
||||||
Δelement(0, 'comp', ['dir', '']);
|
|
||||||
}
|
|
||||||
ΔembeddedViewEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ΔcontainerRefreshEnd();
|
|
||||||
}
|
|
||||||
}, 1, 0, defs);
|
|
||||||
|
|
||||||
const fixture = new ComponentFixture(App);
|
|
||||||
fixture.component.condition = true;
|
|
||||||
fixture.update();
|
|
||||||
expect(events).toEqual([]);
|
|
||||||
|
|
||||||
fixture.component.condition = false;
|
|
||||||
fixture.update();
|
|
||||||
expect(events).toEqual(['comp', 'dir']);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be called on directives on an element', () => {
|
|
||||||
/**
|
|
||||||
* % if (condition) {
|
|
||||||
* <div directive></div>
|
|
||||||
* % }
|
|
||||||
*/
|
|
||||||
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
||||||
if (rf & RenderFlags.Create) {
|
|
||||||
Δcontainer(0);
|
|
||||||
}
|
|
||||||
if (rf & RenderFlags.Update) {
|
|
||||||
ΔcontainerRefreshStart(0);
|
|
||||||
{
|
|
||||||
if (ctx.condition) {
|
|
||||||
let rf1 = ΔembeddedViewStart(0, 1, 0);
|
|
||||||
if (rf1 & RenderFlags.Create) {
|
|
||||||
Δelement(0, 'div', ['dir', '']);
|
|
||||||
}
|
|
||||||
ΔembeddedViewEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ΔcontainerRefreshEnd();
|
|
||||||
}
|
|
||||||
}, 1, 0, defs);
|
|
||||||
|
|
||||||
const fixture = new ComponentFixture(App);
|
|
||||||
fixture.component.condition = true;
|
|
||||||
fixture.update();
|
|
||||||
expect(events).toEqual([]);
|
|
||||||
|
|
||||||
fixture.component.condition = false;
|
|
||||||
fixture.update();
|
|
||||||
expect(events).toEqual(['dir']);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('onChanges', () => {
|
describe('onChanges', () => {
|
||||||
let events: ({type: string, name: string, [key: string]: any})[];
|
let events: ({type: string, name: string, [key: string]: any})[];
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue