/** * @license * Copyright Google Inc. All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {CommonModule} from '@angular/common'; import {AfterViewInit, Component, ContentChild, ContentChildren, Directive, ElementRef, Input, QueryList, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef, ViewRef, forwardRef} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {expect} from '@angular/platform-browser/testing/src/matchers'; import {onlyInIvy} from '@angular/private/testing'; describe('query logic', () => { beforeEach(() => { TestBed.configureTestingModule({ declarations: [ AppComp, QueryComp, SimpleCompA, SimpleCompB, StaticViewQueryComp, TextDirective, SubclassStaticViewQueryComp, StaticContentQueryComp, SubclassStaticContentQueryComp, QueryCompWithChanges, StaticContentQueryDir, SuperDirectiveQueryTarget, SuperDirective, SubComponent ] }); }); describe('view queries', () => { it('should return Component instances when Components are labeled and retrieved', () => { const template = `
`; const fixture = initWithTemplate(QueryComp, template); const comp = fixture.componentInstance; expect(comp.viewChild).toBeAnInstanceOf(SimpleCompA); expect(comp.viewChildren.first).toBeAnInstanceOf(SimpleCompA); expect(comp.viewChildren.last).toBeAnInstanceOf(SimpleCompB); }); it('should return ElementRef when HTML element is labeled and retrieved', () => { const template = `
`; const fixture = initWithTemplate(QueryComp, template); const comp = fixture.componentInstance; expect(comp.viewChild).toBeAnInstanceOf(ElementRef); expect(comp.viewChildren.first).toBeAnInstanceOf(ElementRef); }); onlyInIvy('multiple local refs are supported in Ivy') .it('should return ElementRefs when HTML elements are labeled and retrieved', () => { const template = `
A
B
`; const fixture = initWithTemplate(QueryComp, template); const comp = fixture.componentInstance; expect(comp.viewChild).toBeAnInstanceOf(ElementRef); expect(comp.viewChild.nativeElement).toBe(fixture.debugElement.children[0].nativeElement); expect(comp.viewChildren.first).toBeAnInstanceOf(ElementRef); expect(comp.viewChildren.last).toBeAnInstanceOf(ElementRef); expect(comp.viewChildren.length).toBe(2); }); it('should return TemplateRef when template is labeled and retrieved', () => { const template = ` `; const fixture = initWithTemplate(QueryComp, template); const comp = fixture.componentInstance; expect(comp.viewChildren.first).toBeAnInstanceOf(TemplateRef); }); onlyInIvy('multiple local refs are supported in Ivy') .it('should return TemplateRefs when templates are labeled and retrieved', () => { const template = ` `; const fixture = initWithTemplate(QueryComp, template); const comp = fixture.componentInstance; expect(comp.viewChild).toBeAnInstanceOf(TemplateRef); expect(comp.viewChild.elementRef.nativeElement) .toBe(fixture.debugElement.childNodes[0].nativeNode); expect(comp.viewChildren.first).toBeAnInstanceOf(TemplateRef); expect(comp.viewChildren.last).toBeAnInstanceOf(TemplateRef); expect(comp.viewChildren.length).toBe(2); }); it('should set static view child queries in creation mode (and just in creation mode)', () => { const fixture = TestBed.createComponent(StaticViewQueryComp); const component = fixture.componentInstance; // static ViewChild query should be set in creation mode, before CD runs expect(component.textDir).toBeAnInstanceOf(TextDirective); expect(component.textDir.text).toEqual(''); expect(component.setEvents).toEqual(['textDir set']); // dynamic ViewChild query should not have been resolved yet expect(component.foo).not.toBeDefined(); const span = fixture.nativeElement.querySelector('span'); fixture.detectChanges(); expect(component.textDir.text).toEqual('some text'); expect(component.foo.nativeElement).toBe(span); expect(component.setEvents).toEqual(['textDir set', 'foo set']); }); it('should support static view child queries inherited from superclasses', () => { const fixture = TestBed.createComponent(SubclassStaticViewQueryComp); const component = fixture.componentInstance; const divs = fixture.nativeElement.querySelectorAll('div'); const spans = fixture.nativeElement.querySelectorAll('span'); // static ViewChild queries should be set in creation mode, before CD runs expect(component.textDir).toBeAnInstanceOf(TextDirective); expect(component.textDir.text).toEqual(''); expect(component.bar.nativeElement).toEqual(divs[1]); // dynamic ViewChild queries should not have been resolved yet expect(component.foo).not.toBeDefined(); expect(component.baz).not.toBeDefined(); fixture.detectChanges(); expect(component.textDir.text).toEqual('some text'); expect(component.foo.nativeElement).toBe(spans[0]); expect(component.baz.nativeElement).toBe(spans[1]); }); it('should support multiple static view queries (multiple template passes)', () => { const template = ` `; TestBed.overrideComponent(AppComp, {set: new Component({template})}); const fixture = TestBed.createComponent(AppComp); const firstComponent = fixture.debugElement.children[0].injector.get(StaticViewQueryComp); const secondComponent = fixture.debugElement.children[1].injector.get(StaticViewQueryComp); // static ViewChild query should be set in creation mode, before CD runs expect(firstComponent.textDir).toBeAnInstanceOf(TextDirective); expect(secondComponent.textDir).toBeAnInstanceOf(TextDirective); expect(firstComponent.textDir.text).toEqual(''); expect(secondComponent.textDir.text).toEqual(''); expect(firstComponent.setEvents).toEqual(['textDir set']); expect(secondComponent.setEvents).toEqual(['textDir set']); // dynamic ViewChild query should not have been resolved yet expect(firstComponent.foo).not.toBeDefined(); expect(secondComponent.foo).not.toBeDefined(); const spans = fixture.nativeElement.querySelectorAll('span'); fixture.detectChanges(); expect(firstComponent.textDir.text).toEqual('some text'); expect(secondComponent.textDir.text).toEqual('some text'); expect(firstComponent.foo.nativeElement).toBe(spans[0]); expect(secondComponent.foo.nativeElement).toBe(spans[1]); expect(firstComponent.setEvents).toEqual(['textDir set', 'foo set']); expect(secondComponent.setEvents).toEqual(['textDir set', 'foo set']); }); it('should allow for view queries to be inherited from a directive', () => { const fixture = TestBed.createComponent(SubComponent); const comp = fixture.componentInstance; fixture.detectChanges(); expect(comp.headers).toBeTruthy(); expect(comp.headers.length).toBe(2); expect(comp.headers.toArray().every(result => result instanceof SuperDirectiveQueryTarget)) .toBe(true); }); it('should support ViewChild query inherited from undecorated superclasses', () => { class MyComp { @ViewChild('foo') foo: any; } @Component({selector: 'sub-comp', template: '
'}) class SubComp extends MyComp { } TestBed.configureTestingModule({declarations: [SubComp]}); const fixture = TestBed.createComponent(SubComp); fixture.detectChanges(); expect(fixture.componentInstance.foo).toBeAnInstanceOf(ElementRef); }); it('should support ViewChild query inherited from undecorated grand superclasses', () => { class MySuperComp { @ViewChild('foo') foo: any; } class MyComp extends MySuperComp {} @Component({selector: 'sub-comp', template: '
'}) class SubComp extends MyComp { } TestBed.configureTestingModule({declarations: [SubComp]}); const fixture = TestBed.createComponent(SubComp); fixture.detectChanges(); expect(fixture.componentInstance.foo).toBeAnInstanceOf(ElementRef); }); it('should support ViewChildren query inherited from undecorated superclasses', () => { @Directive({selector: '[some-dir]'}) class SomeDir { } class MyComp { @ViewChildren(SomeDir) foo !: QueryList; } @Component({ selector: 'sub-comp', template: `
` }) class SubComp extends MyComp { } TestBed.configureTestingModule({declarations: [SubComp, SomeDir]}); const fixture = TestBed.createComponent(SubComp); fixture.detectChanges(); expect(fixture.componentInstance.foo).toBeAnInstanceOf(QueryList); expect(fixture.componentInstance.foo.length).toBe(2); }); it('should support ViewChildren query inherited from undecorated grand superclasses', () => { @Directive({selector: '[some-dir]'}) class SomeDir { } class MySuperComp { @ViewChildren(SomeDir) foo !: QueryList; } class MyComp extends MySuperComp {} @Component({ selector: 'sub-comp', template: `
` }) class SubComp extends MyComp { } TestBed.configureTestingModule({declarations: [SubComp, SomeDir]}); const fixture = TestBed.createComponent(SubComp); fixture.detectChanges(); expect(fixture.componentInstance.foo).toBeAnInstanceOf(QueryList); expect(fixture.componentInstance.foo.length).toBe(2); }); it('should support ViewChild query where template is inserted in child component', () => { @Component({selector: 'required', template: ''}) class Required { } @Component({ selector: 'insertion', template: `` }) class Insertion { @Input() content !: TemplateRef<{}>; } @Component({ template: ` ` }) class App { @ViewChild(Required) requiredEl !: Required; viewChildAvailableInAfterViewInit?: boolean; ngAfterViewInit() { this.viewChildAvailableInAfterViewInit = this.requiredEl !== undefined; } } const fixture = TestBed.configureTestingModule({declarations: [App, Insertion, Required]}) .createComponent(App); fixture.detectChanges(); expect(fixture.componentInstance.viewChildAvailableInAfterViewInit).toBe(true); }); }); describe('content queries', () => { it('should return Component instance when Component is labeled and retrieved', () => { const template = ` `; const fixture = initWithTemplate(AppComp, template); const comp = fixture.debugElement.children[0].references['q']; expect(comp.contentChild).toBeAnInstanceOf(SimpleCompA); expect(comp.contentChildren.first).toBeAnInstanceOf(SimpleCompA); }); onlyInIvy('multiple local refs are supported in Ivy') .it('should return Component instances when Components are labeled and retrieved', () => { const template = ` `; const fixture = initWithTemplate(AppComp, template); const comp = fixture.debugElement.children[0].references['q']; expect(comp.contentChild).toBeAnInstanceOf(SimpleCompA); expect(comp.contentChildren.first).toBeAnInstanceOf(SimpleCompA); expect(comp.contentChildren.last).toBeAnInstanceOf(SimpleCompB); expect(comp.contentChildren.length).toBe(2); }); it('should return ElementRef when HTML element is labeled and retrieved', () => { const template = `
`; const fixture = initWithTemplate(AppComp, template); const comp = fixture.debugElement.children[0].references['q']; expect(comp.contentChildren.first).toBeAnInstanceOf(ElementRef); }); onlyInIvy('multiple local refs are supported in Ivy') .it('should return ElementRefs when HTML elements are labeled and retrieved', () => { const template = `
`; const fixture = initWithTemplate(AppComp, template); const firstChild = fixture.debugElement.children[0]; const comp = firstChild.references['q']; expect(comp.contentChild).toBeAnInstanceOf(ElementRef); expect(comp.contentChild.nativeElement).toBe(firstChild.children[0].nativeElement); expect(comp.contentChildren.first).toBeAnInstanceOf(ElementRef); expect(comp.contentChildren.last).toBeAnInstanceOf(ElementRef); expect(comp.contentChildren.length).toBe(2); }); it('should return TemplateRef when template is labeled and retrieved', () => { const template = ` `; const fixture = initWithTemplate(AppComp, template); const comp = fixture.debugElement.children[0].references['q']; expect(comp.contentChildren.first).toBeAnInstanceOf(TemplateRef); }); onlyInIvy('multiple local refs are supported in Ivy') .it('should return TemplateRefs when templates are labeled and retrieved', () => { const template = ` `; const fixture = initWithTemplate(AppComp, template); const firstChild = fixture.debugElement.children[0]; const comp = firstChild.references['q']; expect(comp.contentChild).toBeAnInstanceOf(TemplateRef); expect(comp.contentChild.elementRef.nativeElement) .toBe(firstChild.childNodes[0].nativeNode); expect(comp.contentChildren.first).toBeAnInstanceOf(TemplateRef); expect(comp.contentChildren.last).toBeAnInstanceOf(TemplateRef); expect(comp.contentChildren.length).toBe(2); }); it('should set static content child queries in creation mode (and just in creation mode)', () => { const template = `
`; TestBed.overrideComponent(AppComp, {set: new Component({template})}); const fixture = TestBed.createComponent(AppComp); const component = fixture.debugElement.children[0].injector.get(StaticContentQueryComp); // static ContentChild query should be set in creation mode, before CD runs expect(component.textDir).toBeAnInstanceOf(TextDirective); expect(component.textDir.text).toEqual(''); expect(component.setEvents).toEqual(['textDir set']); // dynamic ContentChild query should not have been resolved yet expect(component.foo).not.toBeDefined(); const span = fixture.nativeElement.querySelector('span'); (fixture.componentInstance as any).text = 'some text'; fixture.detectChanges(); expect(component.textDir.text).toEqual('some text'); expect(component.foo.nativeElement).toBe(span); expect(component.setEvents).toEqual(['textDir set', 'foo set']); }); it('should support static content child queries inherited from superclasses', () => { const template = `
`; TestBed.overrideComponent(AppComp, {set: new Component({template})}); const fixture = TestBed.createComponent(AppComp); const component = fixture.debugElement.children[0].injector.get(SubclassStaticContentQueryComp); const divs = fixture.nativeElement.querySelectorAll('div'); const spans = fixture.nativeElement.querySelectorAll('span'); // static ContentChild queries should be set in creation mode, before CD runs expect(component.textDir).toBeAnInstanceOf(TextDirective); expect(component.textDir.text).toEqual(''); expect(component.bar.nativeElement).toEqual(divs[1]); // dynamic ContentChild queries should not have been resolved yet expect(component.foo).not.toBeDefined(); expect(component.baz).not.toBeDefined(); (fixture.componentInstance as any).text = 'some text'; fixture.detectChanges(); expect(component.textDir.text).toEqual('some text'); expect(component.foo.nativeElement).toBe(spans[0]); expect(component.baz.nativeElement).toBe(spans[1]); }); it('should set static content child queries on directives', () => { const template = `
`; TestBed.overrideComponent(AppComp, {set: new Component({template})}); const fixture = TestBed.createComponent(AppComp); const component = fixture.debugElement.children[0].injector.get(StaticContentQueryDir); // static ContentChild query should be set in creation mode, before CD runs expect(component.textDir).toBeAnInstanceOf(TextDirective); expect(component.textDir.text).toEqual(''); expect(component.setEvents).toEqual(['textDir set']); // dynamic ContentChild query should not have been resolved yet expect(component.foo).not.toBeDefined(); const span = fixture.nativeElement.querySelector('span'); (fixture.componentInstance as any).text = 'some text'; fixture.detectChanges(); expect(component.textDir.text).toEqual('some text'); expect(component.foo.nativeElement).toBe(span); expect(component.setEvents).toEqual(['textDir set', 'foo set']); }); it('should support multiple content query components (multiple template passes)', () => { const template = `
`; TestBed.overrideComponent(AppComp, {set: new Component({template})}); const fixture = TestBed.createComponent(AppComp); const firstComponent = fixture.debugElement.children[0].injector.get(StaticContentQueryComp); const secondComponent = fixture.debugElement.children[1].injector.get(StaticContentQueryComp); // static ContentChild query should be set in creation mode, before CD runs expect(firstComponent.textDir).toBeAnInstanceOf(TextDirective); expect(secondComponent.textDir).toBeAnInstanceOf(TextDirective); expect(firstComponent.textDir.text).toEqual(''); expect(secondComponent.textDir.text).toEqual(''); expect(firstComponent.setEvents).toEqual(['textDir set']); expect(secondComponent.setEvents).toEqual(['textDir set']); // dynamic ContentChild query should not have been resolved yet expect(firstComponent.foo).not.toBeDefined(); expect(secondComponent.foo).not.toBeDefined(); const spans = fixture.nativeElement.querySelectorAll('span'); (fixture.componentInstance as any).text = 'some text'; fixture.detectChanges(); expect(firstComponent.textDir.text).toEqual('some text'); expect(secondComponent.textDir.text).toEqual('some text'); expect(firstComponent.foo.nativeElement).toBe(spans[0]); expect(secondComponent.foo.nativeElement).toBe(spans[1]); expect(firstComponent.setEvents).toEqual(['textDir set', 'foo set']); expect(secondComponent.setEvents).toEqual(['textDir set', 'foo set']); }); it('should support ContentChild query inherited from undecorated superclasses', () => { class MyComp { @ContentChild('foo') foo: any; } @Component({selector: 'sub-comp', template: ''}) class SubComp extends MyComp { } @Component({template: '
'}) class App { @ViewChild(SubComp) subComp !: SubComp; } TestBed.configureTestingModule({declarations: [App, SubComp]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(fixture.componentInstance.subComp.foo).toBeAnInstanceOf(ElementRef); }); it('should support ContentChild query inherited from undecorated grand superclasses', () => { class MySuperComp { @ContentChild('foo') foo: any; } class MyComp extends MySuperComp {} @Component({selector: 'sub-comp', template: ''}) class SubComp extends MyComp { } @Component({template: '
'}) class App { @ViewChild(SubComp) subComp !: SubComp; } TestBed.configureTestingModule({declarations: [App, SubComp]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(fixture.componentInstance.subComp.foo).toBeAnInstanceOf(ElementRef); }); it('should support ContentChildren query inherited from undecorated superclasses', () => { @Directive({selector: '[some-dir]'}) class SomeDir { } class MyComp { @ContentChildren(SomeDir) foo !: QueryList; } @Component({selector: 'sub-comp', template: ''}) class SubComp extends MyComp { } @Component({ template: `
` }) class App { @ViewChild(SubComp) subComp !: SubComp; } TestBed.configureTestingModule({declarations: [App, SubComp, SomeDir]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(fixture.componentInstance.subComp.foo).toBeAnInstanceOf(QueryList); expect(fixture.componentInstance.subComp.foo.length).toBe(2); }); it('should support ContentChildren query inherited from undecorated grand superclasses', () => { @Directive({selector: '[some-dir]'}) class SomeDir { } class MySuperComp { @ContentChildren(SomeDir) foo !: QueryList; } class MyComp extends MySuperComp {} @Component({selector: 'sub-comp', template: ''}) class SubComp extends MyComp { } @Component({ template: `
` }) class App { @ViewChild(SubComp) subComp !: SubComp; } TestBed.configureTestingModule({declarations: [App, SubComp, SomeDir]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(fixture.componentInstance.subComp.foo).toBeAnInstanceOf(QueryList); expect(fixture.componentInstance.subComp.foo.length).toBe(2); }); it('should match shallow content queries in views inserted / removed by ngIf', () => { @Component({ selector: 'test-comp', template: `
` }) class TestComponent { showing = false; } @Component({ selector: 'shallow-comp', template: '', }) class ShallowComp { @ContentChildren('foo', {descendants: false}) foos !: QueryList; } TestBed.configureTestingModule( {declarations: [TestComponent, ShallowComp], imports: [CommonModule]}); const fixture = TestBed.createComponent(TestComponent); fixture.detectChanges(); const shallowComp = fixture.debugElement.query(By.directive(ShallowComp)).componentInstance; const queryList = shallowComp !.foos; expect(queryList.length).toBe(0); fixture.componentInstance.showing = true; fixture.detectChanges(); expect(queryList.length).toBe(1); fixture.componentInstance.showing = false; fixture.detectChanges(); expect(queryList.length).toBe(0); }); }); // Some root components may have ContentChildren queries if they are also // usable as a child component. We should still generate an empty QueryList // for these queries when they are at root for backwards compatibility with // ViewEngine. it('should generate an empty QueryList for root components', () => { const fixture = TestBed.createComponent(QueryComp); fixture.detectChanges(); expect(fixture.componentInstance.contentChildren).toBeAnInstanceOf(QueryList); expect(fixture.componentInstance.contentChildren.length).toBe(0); }); describe('descendants', () => { it('should match directives on elements that used to be wrapped by a required parent in HTML parser', () => { @Directive({selector: '[myDef]'}) class MyDef { } @Component({selector: 'my-container', template: ``}) class MyContainer { @ContentChildren(MyDef) myDefs !: QueryList; } @Component( {selector: 'test-cmpt', template: ``}) class TestCmpt { } TestBed.configureTestingModule({declarations: [TestCmpt, MyContainer, MyDef]}); const fixture = TestBed.createComponent(TestCmpt); const cmptWithQuery = fixture.debugElement.children[0].injector.get(MyContainer); fixture.detectChanges(); expect(cmptWithQuery.myDefs.length).toBe(1); }); }); describe('observable interface', () => { it('should allow observing changes to query list', () => { const fixture = TestBed.createComponent(QueryCompWithChanges); let changes = 0; fixture.detectChanges(); fixture.componentInstance.foos.changes.subscribe((value: any) => { changes += 1; expect(value).toBe(fixture.componentInstance.foos); }); // refresh without setting dirty - no emit fixture.detectChanges(); expect(changes).toBe(0); // refresh with setting dirty - emit fixture.componentInstance.showing = true; fixture.detectChanges(); expect(changes).toBe(1); }); }); describe('view boundaries', () => { describe('ViewContainerRef', () => { @Directive({selector: '[vc]', exportAs: 'vc'}) class ViewContainerManipulatorDirective { constructor(private _vcRef: ViewContainerRef) {} insertTpl(tpl: TemplateRef<{}>, ctx: {}, idx?: number): ViewRef { return this._vcRef.createEmbeddedView(tpl, ctx, idx); } remove(index?: number) { this._vcRef.remove(index); } move(viewRef: ViewRef, index: number) { this._vcRef.move(viewRef, index); } } it('should report results in views inserted / removed by ngIf', () => { @Component({ selector: 'test-comp', template: `
` }) class TestComponent { value: boolean = false; @ViewChildren('foo') query !: QueryList; } TestBed.configureTestingModule({declarations: [TestComponent]}); const fixture = TestBed.createComponent(TestComponent); fixture.detectChanges(); const queryList = fixture.componentInstance.query; expect(queryList.length).toBe(0); fixture.componentInstance.value = true; fixture.detectChanges(); expect(queryList.length).toBe(1); fixture.componentInstance.value = false; fixture.detectChanges(); expect(queryList.length).toBe(0); }); it('should report results in views inserted / removed by ngFor', () => { @Component({ selector: 'test-comp', template: `
`, }) class TestComponent { value: string[]|undefined; @ViewChildren('foo') query !: QueryList; } TestBed.configureTestingModule({declarations: [TestComponent]}); const fixture = TestBed.createComponent(TestComponent); fixture.detectChanges(); const queryList = fixture.componentInstance.query; expect(queryList.length).toBe(0); fixture.componentInstance.value = ['a', 'b', 'c']; fixture.detectChanges(); expect(queryList.length).toBe(3); // Remove the "b" element from the value. fixture.componentInstance.value.splice(1, 1); fixture.detectChanges(); expect(queryList.length).toBe(2); // make sure that the "b" element has been removed from query results expect(queryList.first.nativeElement.id).toBe('a'); expect(queryList.last.nativeElement.id).toBe('c'); }); /** * ViewContainerRef API allows "moving" a view to the same (previous) index. Such operation * has no observable effect on the rendered UI (displays stays the same) but internally we've * got 2 implementation choices when it comes to "moving" a view: * - systematically detach and insert a view - this would result in unnecessary processing * when the previous and new indexes for the move operation are the same; * - detect the situation where the indexes are the same and do no processing in such case. * * This tests asserts on the implementation choices done by the VE (detach and insert) so we * can replicate the same behaviour in ivy. */ it('should notify on changes when a given view is removed and re-inserted at the same index', () => { @Component({ selector: 'test-comp', template: `
match
`, }) class TestComponent implements AfterViewInit { queryListNotificationCounter = 0; @ViewChild(ViewContainerManipulatorDirective) vc !: ViewContainerManipulatorDirective; @ViewChild('tpl') tpl !: TemplateRef; @ViewChildren('foo') query !: QueryList; ngAfterViewInit() { this.query.changes.subscribe(() => this.queryListNotificationCounter++); } } TestBed.configureTestingModule( {declarations: [ViewContainerManipulatorDirective, TestComponent]}); const fixture = TestBed.createComponent(TestComponent); fixture.detectChanges(); const queryList = fixture.componentInstance.query; const {tpl, vc} = fixture.componentInstance; const viewRef = vc.insertTpl(tpl, {}, 0); fixture.detectChanges(); expect(queryList.length).toBe(1); expect(fixture.componentInstance.queryListNotificationCounter).toBe(1); vc.move(viewRef, 0); fixture.detectChanges(); expect(queryList.length).toBe(1); expect(fixture.componentInstance.queryListNotificationCounter).toBe(2); }); it('should support a mix of content queries from the declaration and embedded view', () => { @Directive({selector: '[query-for-lots-of-content]'}) class QueryForLotsOfContent { @ContentChildren('foo', {descendants: true}) foos1 !: QueryList; @ContentChildren('foo', {descendants: true}) foos2 !: QueryList; } @Directive({selector: '[query-for-content]'}) class QueryForContent { @ContentChildren('foo') foos !: QueryList; } @Component({ selector: 'test-comp', template: `
` }) class TestComponent { items = [1, 2]; } TestBed.configureTestingModule( {declarations: [TestComponent, QueryForContent, QueryForLotsOfContent]}); const fixture = TestBed.createComponent(TestComponent); fixture.detectChanges(); const lotsOfContentEl = fixture.debugElement.query(By.directive(QueryForLotsOfContent)); const lotsOfContentInstance = lotsOfContentEl.injector.get(QueryForLotsOfContent); const contentEl = fixture.debugElement.query(By.directive(QueryForContent)); const contentInstance = contentEl.injector.get(QueryForContent); expect(lotsOfContentInstance.foos1.length).toBe(2); expect(lotsOfContentInstance.foos2.length).toBe(2); expect(contentInstance.foos.length).toBe(1); fixture.componentInstance.items = []; fixture.detectChanges(); expect(lotsOfContentInstance.foos1.length).toBe(0); expect(lotsOfContentInstance.foos2.length).toBe(0); }); // https://stackblitz.com/edit/angular-rrmmuf?file=src/app/app.component.ts it('should report results when different instances of TemplateRef are inserted into one ViewContainerRefs', () => { @Component({ selector: 'test-comp', template: `
`, }) class TestComponent { @ViewChild(ViewContainerManipulatorDirective) vc !: ViewContainerManipulatorDirective; @ViewChild('tpl1') tpl1 !: TemplateRef; @ViewChild('tpl2') tpl2 !: TemplateRef; @ViewChildren('foo') query !: QueryList; } TestBed.configureTestingModule( {declarations: [ViewContainerManipulatorDirective, TestComponent]}); const fixture = TestBed.createComponent(TestComponent); fixture.detectChanges(); const queryList = fixture.componentInstance.query; const {tpl1, tpl2, vc} = fixture.componentInstance; expect(queryList.length).toBe(1); expect(queryList.first.nativeElement.getAttribute('id')).toBe('middle'); vc.insertTpl(tpl1 !, {idx: 0}, 0); vc.insertTpl(tpl2 !, {idx: 1}, 1); fixture.detectChanges(); expect(queryList.length).toBe(3); let qListArr = queryList.toArray(); expect(qListArr[0].nativeElement.getAttribute('id')).toBe('foo1_0'); expect(qListArr[1].nativeElement.getAttribute('id')).toBe('middle'); expect(qListArr[2].nativeElement.getAttribute('id')).toBe('foo2_1'); vc.insertTpl(tpl1 !, {idx: 1}, 1); fixture.detectChanges(); expect(queryList.length).toBe(4); qListArr = queryList.toArray(); expect(qListArr[0].nativeElement.getAttribute('id')).toBe('foo1_0'); expect(qListArr[1].nativeElement.getAttribute('id')).toBe('foo1_1'); expect(qListArr[2].nativeElement.getAttribute('id')).toBe('middle'); expect(qListArr[3].nativeElement.getAttribute('id')).toBe('foo2_1'); vc.remove(1); fixture.detectChanges(); expect(queryList.length).toBe(3); qListArr = queryList.toArray(); expect(qListArr[0].nativeElement.getAttribute('id')).toBe('foo1_0'); expect(qListArr[1].nativeElement.getAttribute('id')).toBe('middle'); expect(qListArr[2].nativeElement.getAttribute('id')).toBe('foo2_1'); vc.remove(1); fixture.detectChanges(); expect(queryList.length).toBe(2); qListArr = queryList.toArray(); expect(qListArr[0].nativeElement.getAttribute('id')).toBe('foo1_0'); expect(qListArr[1].nativeElement.getAttribute('id')).toBe('middle'); }); // https://stackblitz.com/edit/angular-7vvo9j?file=src%2Fapp%2Fapp.component.ts // https://stackblitz.com/edit/angular-xzwp6n it('should report results when the same TemplateRef is inserted into different ViewContainerRefs', () => { @Component({ selector: 'test-comp', template: `
`, }) class TestComponent { @ViewChild('tpl') tpl !: TemplateRef; @ViewChild('vi0') vi0 !: ViewContainerManipulatorDirective; @ViewChild('vi1') vi1 !: ViewContainerManipulatorDirective; @ViewChildren('foo') query !: QueryList; } TestBed.configureTestingModule( {declarations: [ViewContainerManipulatorDirective, TestComponent]}); const fixture = TestBed.createComponent(TestComponent); fixture.detectChanges(); const queryList = fixture.componentInstance.query; const {tpl, vi0, vi1} = fixture.componentInstance; expect(queryList.length).toBe(0); vi0.insertTpl(tpl !, {idx: 0, container_idx: 0}, 0); vi1.insertTpl(tpl !, {idx: 0, container_idx: 1}, 0); fixture.detectChanges(); expect(queryList.length).toBe(2); let qListArr = queryList.toArray(); expect(qListArr[0].nativeElement.getAttribute('id')).toBe('foo_0_0'); expect(qListArr[1].nativeElement.getAttribute('id')).toBe('foo_1_0'); vi0.remove(); fixture.detectChanges(); expect(queryList.length).toBe(1); qListArr = queryList.toArray(); expect(qListArr[0].nativeElement.getAttribute('id')).toBe('foo_1_0'); vi1.remove(); fixture.detectChanges(); expect(queryList.length).toBe(0); }); // https://stackblitz.com/edit/angular-wpd6gv?file=src%2Fapp%2Fapp.component.ts it('should report results from views inserted in a lifecycle hook', () => { @Component({ selector: 'my-app', template: ` `, }) class MyApp { show = false; @ViewChildren('foo') query !: QueryList; } TestBed.configureTestingModule({declarations: [MyApp], imports: [CommonModule]}); const fixture = TestBed.createComponent(MyApp); fixture.detectChanges(); const queryList = fixture.componentInstance.query; expect(queryList.length).toBe(0); fixture.componentInstance.show = true; fixture.detectChanges(); expect(queryList.length).toBe(1); expect(queryList.first.nativeElement.id).toBe('from_tpl'); fixture.componentInstance.show = false; fixture.detectChanges(); expect(queryList.length).toBe(0); }); }); }); describe('non-regression', () => { it('should query by provider super-type in an embedded view', () => { @Directive({selector: '[child]'}) class Child { } @Directive({selector: '[parent]', providers: [{provide: Child, useExisting: Parent}]}) class Parent extends Child { } @Component({ selector: 'test-cmpt', template: `
` }) class TestCmpt { @ViewChildren(Child) instances !: QueryList; } TestBed.configureTestingModule({declarations: [TestCmpt, Parent, Child]}); const fixture = TestBed.createComponent(TestCmpt); fixture.detectChanges(); expect(fixture.componentInstance.instances.length).toBe(1); }); it('should flatten multi-provider results', () => { class MyClass {} @Component({ selector: 'with-multi-provider', template: '', providers: [{provide: MyClass, useExisting: forwardRef(() => WithMultiProvider), multi: true}] }) class WithMultiProvider { } @Component({selector: 'test-cmpt', template: ``}) class TestCmpt { @ViewChildren(MyClass) queryResults !: QueryList; } TestBed.configureTestingModule({declarations: [TestCmpt, WithMultiProvider]}); const fixture = TestBed.createComponent(TestCmpt); fixture.detectChanges(); expect(fixture.componentInstance.queryResults.length).toBe(1); expect(fixture.componentInstance.queryResults.first).toBeAnInstanceOf(WithMultiProvider); }); it('should flatten multi-provider results when crossing ng-template', () => { class MyClass {} @Component({ selector: 'with-multi-provider', template: '', providers: [{provide: MyClass, useExisting: forwardRef(() => WithMultiProvider), multi: true}] }) class WithMultiProvider { } @Component({ selector: 'test-cmpt', template: ` ` }) class TestCmpt { @ViewChildren(MyClass) queryResults !: QueryList; } TestBed.configureTestingModule({declarations: [TestCmpt, WithMultiProvider]}); const fixture = TestBed.createComponent(TestCmpt); fixture.detectChanges(); expect(fixture.componentInstance.queryResults.length).toBe(2); expect(fixture.componentInstance.queryResults.first).toBeAnInstanceOf(WithMultiProvider); expect(fixture.componentInstance.queryResults.last).toBeAnInstanceOf(WithMultiProvider); }); }); }); function initWithTemplate(compType: Type, template: string) { TestBed.overrideComponent(compType, {set: new Component({template})}); const fixture = TestBed.createComponent(compType); fixture.detectChanges(); return fixture; } @Component({selector: 'local-ref-query-component', template: ''}) class QueryComp { @ViewChild('viewQuery') viewChild !: any; @ContentChild('contentQuery') contentChild !: any; @ViewChildren('viewQuery') viewChildren !: QueryList; @ContentChildren('contentQuery') contentChildren !: QueryList; } @Component({selector: 'app-comp', template: ``}) class AppComp { } @Component({selector: 'simple-comp-a', template: ''}) class SimpleCompA { } @Component({selector: 'simple-comp-b', template: ''}) class SimpleCompB { } @Directive({selector: '[text]'}) class TextDirective { @Input() text = ''; } @Component({ selector: 'static-view-query-comp', template: `
` }) class StaticViewQueryComp { private _textDir !: TextDirective; private _foo !: ElementRef; setEvents: string[] = []; @ViewChild(TextDirective, {static: true}) get textDir(): TextDirective { return this._textDir; } set textDir(value: TextDirective) { this.setEvents.push('textDir set'); this._textDir = value; } @ViewChild('foo') get foo(): ElementRef { return this._foo; } set foo(value: ElementRef) { this.setEvents.push('foo set'); this._foo = value; } text = 'some text'; } @Component({ selector: 'subclass-static-view-query-comp', template: `
` }) class SubclassStaticViewQueryComp extends StaticViewQueryComp { @ViewChild('bar', {static: true}) bar !: ElementRef; @ViewChild('baz') baz !: ElementRef; } @Component({selector: 'static-content-query-comp', template: ``}) class StaticContentQueryComp { private _textDir !: TextDirective; private _foo !: ElementRef; setEvents: string[] = []; @ContentChild(TextDirective, {static: true}) get textDir(): TextDirective { return this._textDir; } set textDir(value: TextDirective) { this.setEvents.push('textDir set'); this._textDir = value; } @ContentChild('foo') get foo(): ElementRef { return this._foo; } set foo(value: ElementRef) { this.setEvents.push('foo set'); this._foo = value; } } @Directive({selector: '[staticContentQueryDir]'}) class StaticContentQueryDir { private _textDir !: TextDirective; private _foo !: ElementRef; setEvents: string[] = []; @ContentChild(TextDirective, {static: true}) get textDir(): TextDirective { return this._textDir; } set textDir(value: TextDirective) { this.setEvents.push('textDir set'); this._textDir = value; } @ContentChild('foo') get foo(): ElementRef { return this._foo; } set foo(value: ElementRef) { this.setEvents.push('foo set'); this._foo = value; } } @Component({selector: 'subclass-static-content-query-comp', template: ``}) class SubclassStaticContentQueryComp extends StaticContentQueryComp { @ContentChild('bar', {static: true}) bar !: ElementRef; @ContentChild('baz') baz !: ElementRef; } @Component({ selector: 'query-with-changes', template: `
` }) export class QueryCompWithChanges { @ViewChildren('foo') foos !: QueryList; showing = false; } @Component({selector: 'query-target', template: ''}) class SuperDirectiveQueryTarget { } @Directive({selector: '[super-directive]'}) class SuperDirective { @ViewChildren(SuperDirectiveQueryTarget) headers !: QueryList; } @Component({ template: ` One Two ` }) class SubComponent extends SuperDirective { }