/
githubmirror
/
angular
Обзор
Документация
Войти
/
githubmirror
/
angular
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/core/test/acceptance/query_spec.ts
3 410 строк
107 KB
splincode
fix(core): handle missing serialized container hydration data
07 апр 2026, 21:22
07 апр 2026, 21:22
77f1ca0
Код
Авторство
О чём код?
/** * @license * Copyright Google LLC 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.dev/license */ import {CommonModule} from '@angular/common'; import { AfterViewInit, Component, ContentChild, ContentChildren, Directive, ElementRef, EventEmitter, forwardRef, InjectionToken, Input, provideZoneChangeDetection, QueryList, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef, ViewRef, ChangeDetectionStrategy, } from '../../src/core'; import {TestBed} from '../../testing'; import {By} from '@angular/platform-browser'; import { createContainerRef, enableLocateOrCreateContainerRefImpl, } from '../../src/linker/view_container_ref'; import {getLContext} from '../../src/render3/context_discovery'; import {DehydratedView} from '../../src/hydration/interfaces'; import { TElementContainerNode, TElementNode, TContainerNode, TNodeType, } from '../../src/render3/interfaces/node'; import {HYDRATION, TVIEW} from '../../src/render3/interfaces/view'; describe('query logic', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [provideZoneChangeDetection()], }); }); beforeEach(() => { TestBed.configureTestingModule({ declarations: [ AppComp, QueryComp, SimpleCompA, SimpleCompB, StaticViewQueryComp, TextDirective, SubclassStaticViewQueryComp, StaticContentQueryComp, SubclassStaticContentQueryComp, QueryCompWithChanges, StaticContentQueryDir, SuperDirectiveQueryTarget, SuperDirective, SubComponent, TestComponentWithToken, TestInjectionTokenContentQueries, TestInjectionTokenQueries, ], }); }); describe('view queries', () => { it('should return Component instances when Components are labeled and retrieved', () => { const template = ` <div><simple-comp-a #viewQuery></simple-comp-a></div> <div><simple-comp-b #viewQuery></simple-comp-b></div> `; const fixture = initWithTemplate(QueryComp, template); const comp = fixture.componentInstance; expect(comp.viewChild).toBeInstanceOf(SimpleCompA); expect(comp.viewChildren.first).toBeInstanceOf(SimpleCompA); expect(comp.viewChildren.last).toBeInstanceOf(SimpleCompB); }); it('should return ElementRef when HTML element is labeled and retrieved', () => { const template = ` <div #viewQuery></div> `; const fixture = initWithTemplate(QueryComp, template); const comp = fixture.componentInstance; expect(comp.viewChild).toBeInstanceOf(ElementRef); expect(comp.viewChildren.first).toBeInstanceOf(ElementRef); }); it('should return ElementRefs when HTML elements are labeled and retrieved', () => { const template = ` <div #viewQuery #first>A</div> <div #viewQuery #second>B</div> `; const fixture = initWithTemplate(QueryComp, template); const comp = fixture.componentInstance; expect(comp.viewChild).toBeInstanceOf(ElementRef); expect(comp.viewChild.nativeElement).toBe(fixture.debugElement.children[0].nativeElement); expect(comp.viewChildren.first).toBeInstanceOf(ElementRef); expect(comp.viewChildren.last).toBeInstanceOf(ElementRef); expect(comp.viewChildren.length).toBe(2); }); it('should return TemplateRef when template is labeled and retrieved', () => { const template = ` <ng-template #viewQuery></ng-template> `; const fixture = initWithTemplate(QueryComp, template); const comp = fixture.componentInstance; expect(comp.viewChildren.first).toBeInstanceOf(TemplateRef); }); it('should support selecting InjectionToken', () => { const fixture = TestBed.createComponent(TestInjectionTokenQueries); const instance = fixture.componentInstance; fixture.detectChanges(); expect(instance.viewFirstOption).toBeDefined(); expect(instance.viewFirstOption instanceof TestComponentWithToken).toBe(true); expect(instance.viewOptions).toBeDefined(); expect(instance.viewOptions.length).toBe(2); expect(instance.contentFirstOption).toBeUndefined(); expect(instance.contentOptions).toBeDefined(); expect(instance.contentOptions.length).toBe(0); }); it('should return TemplateRefs when templates are labeled and retrieved', () => { const template = ` <ng-template #viewQuery></ng-template> <ng-template #viewQuery></ng-template> `; const fixture = initWithTemplate(QueryComp, template); const comp = fixture.componentInstance; expect(comp.viewChild).toBeInstanceOf(TemplateRef); expect(comp.viewChild.elementRef.nativeElement).toBe( fixture.debugElement.childNodes[0].nativeNode, ); expect(comp.viewChildren.first).toBeInstanceOf(TemplateRef); expect(comp.viewChildren.last).toBeInstanceOf(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).toBeInstanceOf(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).toBeInstanceOf(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 = ` <static-view-query-comp></static-view-query-comp> <static-view-query-comp></static-view-query-comp> `; 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).toBeInstanceOf(TextDirective); expect(secondComponent.textDir).toBeInstanceOf(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: '<div #foo></div>', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class SubComp extends MyComp {} TestBed.configureTestingModule({declarations: [SubComp]}); const fixture = TestBed.createComponent(SubComp); fixture.detectChanges(); expect(fixture.componentInstance.foo).toBeInstanceOf(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: '<div #foo></div>', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class SubComp extends MyComp {} TestBed.configureTestingModule({declarations: [SubComp]}); const fixture = TestBed.createComponent(SubComp); fixture.detectChanges(); expect(fixture.componentInstance.foo).toBeInstanceOf(ElementRef); }); it('should support ViewChildren query inherited from undecorated superclasses', () => { @Directive({ selector: '[some-dir]', standalone: false, }) class SomeDir {} class MyComp { @ViewChildren(SomeDir) foo!: QueryList<SomeDir>; } @Component({ selector: 'sub-comp', template: ` <div some-dir></div> <div some-dir></div> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class SubComp extends MyComp {} TestBed.configureTestingModule({declarations: [SubComp, SomeDir]}); const fixture = TestBed.createComponent(SubComp); fixture.detectChanges(); expect(fixture.componentInstance.foo).toBeInstanceOf(QueryList); expect(fixture.componentInstance.foo.length).toBe(2); }); it('should support ViewChildren query inherited from undecorated grand superclasses', () => { @Directive({ selector: '[some-dir]', standalone: false, }) class SomeDir {} class MySuperComp { @ViewChildren(SomeDir) foo!: QueryList<SomeDir>; } class MyComp extends MySuperComp {} @Component({ selector: 'sub-comp', template: ` <div some-dir></div> <div some-dir></div> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class SubComp extends MyComp {} TestBed.configureTestingModule({declarations: [SubComp, SomeDir]}); const fixture = TestBed.createComponent(SubComp); fixture.detectChanges(); expect(fixture.componentInstance.foo).toBeInstanceOf(QueryList); expect(fixture.componentInstance.foo.length).toBe(2); }); it('should support ViewChild query where template is inserted in child component', () => { @Component({ selector: 'required', template: '', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class Required {} @Component({ selector: 'insertion', template: `<ng-container [ngTemplateOutlet]="content"></ng-container>`, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class Insertion { @Input() content!: TemplateRef<{}>; } @Component({ template: ` <ng-template #template> <required></required> </ng-template> <insertion [content]="template"></insertion> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) 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); }); it('should destroy QueryList when the containing view is destroyed', () => { let queryInstance: QueryList<any>; @Component({ selector: 'comp-with-view-query', template: '<div #foo>Content</div>', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class ComponentWithViewQuery { @ViewChildren('foo') set foo(value: any) { queryInstance = value; } get foo() { return queryInstance; } } @Component({ selector: 'root', template: ` <ng-container *ngIf="condition"> <comp-with-view-query></comp-with-view-query> </ng-container> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class Root { condition = true; } TestBed.configureTestingModule({ declarations: [Root, ComponentWithViewQuery], imports: [CommonModule], }); const fixture = TestBed.createComponent(Root); fixture.detectChanges(); expect((queryInstance!.changes as EventEmitter<any>).closed).toBeFalsy(); fixture.componentInstance.condition = false; fixture.detectChanges(); expect((queryInstance!.changes as EventEmitter<any>).closed).toBeTruthy(); }); }); describe('content queries', () => { it('should return Component instance when Component is labeled and retrieved', () => { const template = ` <local-ref-query-component #q> <simple-comp-a #contentQuery></simple-comp-a> </local-ref-query-component> `; const fixture = initWithTemplate(AppComp, template); const comp = fixture.debugElement.children[0].references['q']; expect(comp.contentChild).toBeInstanceOf(SimpleCompA); expect(comp.contentChildren.first).toBeInstanceOf(SimpleCompA); }); it('should support selecting InjectionToken', () => { const fixture = TestBed.createComponent(TestInjectionTokenContentQueries); const instance = fixture.debugElement.query( By.directive(TestInjectionTokenQueries), ).componentInstance; fixture.detectChanges(); expect(instance.contentFirstOption).toBeDefined(); expect(instance.contentFirstOption instanceof TestComponentWithToken).toBe(true); expect(instance.contentOptions).toBeDefined(); expect(instance.contentOptions.length).toBe(2); }); it('should return Component instances when Components are labeled and retrieved', () => { const template = ` <local-ref-query-component #q> <simple-comp-a #contentQuery></simple-comp-a> <simple-comp-b #contentQuery></simple-comp-b> </local-ref-query-component> `; const fixture = initWithTemplate(AppComp, template); const comp = fixture.debugElement.children[0].references['q']; expect(comp.contentChild).toBeInstanceOf(SimpleCompA); expect(comp.contentChildren.first).toBeInstanceOf(SimpleCompA); expect(comp.contentChildren.last).toBeInstanceOf(SimpleCompB); expect(comp.contentChildren.length).toBe(2); }); it('should return ElementRef when HTML element is labeled and retrieved', () => { const template = ` <local-ref-query-component #q> <div #contentQuery></div> </local-ref-query-component> `; const fixture = initWithTemplate(AppComp, template); const comp = fixture.debugElement.children[0].references['q']; expect(comp.contentChildren.first).toBeInstanceOf(ElementRef); }); it('should return ElementRefs when HTML elements are labeled and retrieved', () => { const template = ` <local-ref-query-component #q> <div #contentQuery></div> <div #contentQuery></div> </local-ref-query-component> `; const fixture = initWithTemplate(AppComp, template); const firstChild = fixture.debugElement.children[0]; const comp = firstChild.references['q']; expect(comp.contentChild).toBeInstanceOf(ElementRef); expect(comp.contentChild.nativeElement).toBe(firstChild.children[0].nativeElement); expect(comp.contentChildren.first).toBeInstanceOf(ElementRef); expect(comp.contentChildren.last).toBeInstanceOf(ElementRef); expect(comp.contentChildren.length).toBe(2); }); it('should return TemplateRef when template is labeled and retrieved', () => { const template = ` <local-ref-query-component #q> <ng-template #contentQuery></ng-template> </local-ref-query-component> `; const fixture = initWithTemplate(AppComp, template); const comp = fixture.debugElement.children[0].references['q']; expect(comp.contentChildren.first).toBeInstanceOf(TemplateRef); }); it('should return TemplateRefs when templates are labeled and retrieved', () => { const template = ` <local-ref-query-component #q> <ng-template #contentQuery></ng-template> <ng-template #contentQuery></ng-template> </local-ref-query-component> `; const fixture = initWithTemplate(AppComp, template); const firstChild = fixture.debugElement.children[0]; const comp = firstChild.references['q']; expect(comp.contentChild).toBeInstanceOf(TemplateRef); expect(comp.contentChild.elementRef.nativeElement).toBe(firstChild.childNodes[0].nativeNode); expect(comp.contentChildren.first).toBeInstanceOf(TemplateRef); expect(comp.contentChildren.last).toBeInstanceOf(TemplateRef); expect(comp.contentChildren.length).toBe(2); }); it('should set static content child queries in creation mode (and just in creation mode)', () => { const template = ` <static-content-query-comp> <div [text]="text"></div> <span #foo></span> </static-content-query-comp> `; 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).toBeInstanceOf(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 = ` <subclass-static-content-query-comp> <div [text]="text"></div> <span #foo></span> <div #bar></div> <span #baz></span> </subclass-static-content-query-comp> `; 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).toBeInstanceOf(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 = ` <div staticContentQueryDir> <div [text]="text"></div> <span #foo></span> </div> `; 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).toBeInstanceOf(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 = ` <static-content-query-comp> <div [text]="text"></div> <span #foo></span> </static-content-query-comp> <static-content-query-comp> <div [text]="text"></div> <span #foo></span> </static-content-query-comp> `; 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).toBeInstanceOf(TextDirective); expect(secondComponent.textDir).toBeInstanceOf(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: '<ng-content></ng-content>', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class SubComp extends MyComp {} @Component({ template: '<sub-comp><div #foo></div></sub-comp>', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class App { @ViewChild(SubComp) subComp!: SubComp; } TestBed.configureTestingModule({declarations: [App, SubComp]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(fixture.componentInstance.subComp.foo).toBeInstanceOf(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: '<ng-content></ng-content>', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class SubComp extends MyComp {} @Component({ template: '<sub-comp><div #foo></div></sub-comp>', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class App { @ViewChild(SubComp) subComp!: SubComp; } TestBed.configureTestingModule({declarations: [App, SubComp]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(fixture.componentInstance.subComp.foo).toBeInstanceOf(ElementRef); }); it('should support ContentChildren query inherited from undecorated superclasses', () => { @Directive({ selector: '[some-dir]', standalone: false, }) class SomeDir {} class MyComp { @ContentChildren(SomeDir) foo!: QueryList<SomeDir>; } @Component({ selector: 'sub-comp', template: '<ng-content></ng-content>', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class SubComp extends MyComp {} @Component({ template: ` <sub-comp> <div some-dir></div> <div some-dir></div> </sub-comp> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class App { @ViewChild(SubComp) subComp!: SubComp; } TestBed.configureTestingModule({declarations: [App, SubComp, SomeDir]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(fixture.componentInstance.subComp.foo).toBeInstanceOf(QueryList); expect(fixture.componentInstance.subComp.foo.length).toBe(2); }); it('should support ContentChildren query inherited from undecorated grand superclasses', () => { @Directive({ selector: '[some-dir]', standalone: false, }) class SomeDir {} class MySuperComp { @ContentChildren(SomeDir) foo!: QueryList<SomeDir>; } class MyComp extends MySuperComp {} @Component({ selector: 'sub-comp', template: '<ng-content></ng-content>', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class SubComp extends MyComp {} @Component({ template: ` <sub-comp> <div some-dir></div> <div some-dir></div> </sub-comp> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class App { @ViewChild(SubComp) subComp!: SubComp; } TestBed.configureTestingModule({declarations: [App, SubComp, SomeDir]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(fixture.componentInstance.subComp.foo).toBeInstanceOf(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: ` <shallow-comp> <div *ngIf="showing" #foo></div> </shallow-comp> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class TestComponent { showing = false; } @Component({ selector: 'shallow-comp', template: '', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class ShallowComp { @ContentChildren('foo', {descendants: false}) foos!: QueryList<ElementRef>; } 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); }); it('should support content queries for directives within repeated embedded views', () => { const withContentInstances: DirWithContentQuery[] = []; @Directive({ selector: '[with-content]', standalone: false, }) class DirWithContentQuery { constructor() { withContentInstances.push(this); } @ContentChildren('foo', {descendants: false}) foos!: QueryList<ElementRef>; contentInitQuerySnapshot = 0; contentCheckedQuerySnapshot = 0; ngAfterContentInit() { this.contentInitQuerySnapshot = this.foos ? this.foos.length : 0; } ngAfterContentChecked() { this.contentCheckedQuerySnapshot = this.foos ? this.foos.length : 0; } } @Component({ selector: 'comp', template: ` <ng-container *ngFor="let item of items"> <div with-content> <span #foo></span> </div> </ng-container> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class Root { items = [1, 2, 3]; } TestBed.configureTestingModule({ declarations: [Root, DirWithContentQuery], imports: [CommonModule], }); const fixture = TestBed.createComponent(Root); fixture.detectChanges(); for (let i = 0; i < 3; i++) { expect(withContentInstances[i].foos.length).toBe( 1, `Expected content query to match <span #foo>.`, ); expect(withContentInstances[i].contentInitQuerySnapshot).toBe( 1, `Expected content query results to be available when ngAfterContentInit was called.`, ); expect(withContentInstances[i].contentCheckedQuerySnapshot).toBe( 1, `Expected content query results to be available when ngAfterContentChecked was called.`, ); } }); it('should not match directive host with content queries', () => { @Directive({ selector: '[content-query]', }) class ContentQueryDirective { @ContentChildren('foo', {descendants: true}) foos!: QueryList<ElementRef>; } @Component({ imports: [ContentQueryDirective], template: `<div content-query #foo></div>`, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChild(ContentQueryDirective, {static: true}) contentQueryDirective!: ContentQueryDirective; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const qList = fixture.componentInstance.contentQueryDirective.foos; expect(qList.length).toBe(0); }); it('should report results to appropriate queries where deep content queries are nested', () => { @Directive({selector: '[content-query]', exportAs: 'query'}) class ContentQueryDirective { @ContentChildren('foo, bar, baz', {descendants: true}) qlist!: QueryList<ElementRef>; } @Component({ imports: [ContentQueryDirective], template: ` <div content-query #out="query"> <span #foo></span> <div content-query #in="query"> <span #bar></span> </div> <span #baz></span> </div> `, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChild('in', {static: true}) in!: ContentQueryDirective; @ViewChild('out', {static: true}) out!: ContentQueryDirective; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const inQList = fixture.componentInstance.in.qlist; expect(inQList.length).toBe(1); const outQList = fixture.componentInstance.out.qlist; expect(outQList.length).toBe(3); }); it('should support nested shallow content queries', () => { @Directive({selector: '[content-query]', exportAs: 'query'}) class ContentQueryDirective { @ContentChildren('foo') qlist!: QueryList<ElementRef>; } @Component({ imports: [ContentQueryDirective], template: ` <div content-query #out="query"> <div content-query #in="query" #foo> <span #foo></span> </div> </div> `, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChild('in', {static: true}) in!: ContentQueryDirective; @ViewChild('out', {static: true}) out!: ContentQueryDirective; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const inQList = fixture.componentInstance.in.qlist; expect(inQList.length).toBe(1); const outQList = fixture.componentInstance.out.qlist; expect(outQList.length).toBe(1); }); it('should respect shallow flag on content queries when mixing deep and shallow queries', () => { @Directive({selector: '[shallow-content-query]', exportAs: 'shallow-query'}) class ShallowContentQueryDirective { @ContentChildren('foo') qlist!: QueryList<ElementRef>; } @Directive({selector: '[deep-content-query]', exportAs: 'deep-query'}) class DeepContentQueryDirective { @ContentChildren('foo', {descendants: true}) qlist!: QueryList<ElementRef>; } @Component({ imports: [ShallowContentQueryDirective, DeepContentQueryDirective], template: ` <div shallow-content-query #shallow="shallow-query" deep-content-query #deep="deep-query"> <span #foo></span> <div> <span #foo></span> </div> </div> `, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChild('shallow', {static: true}) shallow!: ShallowContentQueryDirective; @ViewChild('deep', {static: true}) deep!: DeepContentQueryDirective; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const inQList = fixture.componentInstance.shallow.qlist; expect(inQList.length).toBe(1); const outQList = fixture.componentInstance.deep.qlist; expect(outQList.length).toBe(2); }); it('should support shallow ContentChild queries', () => { @Directive({selector: '[query-dir]'}) class ContentQueryDirective { @ContentChild('foo', {descendants: false}) shallow: ElementRef | undefined; // ContentChild queries have {descendants: true} option by default @ContentChild('foo') deep: ElementRef | undefined; } @Component({ imports: [ContentQueryDirective], template: ` <div query-dir> <div> <span #foo></span> </div> </div> `, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChild(ContentQueryDirective, {static: true}) queryDir!: ContentQueryDirective; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); expect(fixture.componentInstance.queryDir.shallow).toBeUndefined(); expect(fixture.componentInstance.queryDir.deep).toBeInstanceOf(ElementRef); }); it('should support view and content queries matching the same element', () => { @Directive({ selector: '[content-query]', }) class ContentQueryDirective { @ContentChildren('foo') foos!: QueryList<ElementRef>; } @Component({ imports: [ContentQueryDirective], template: ` <div content-query> <div id="contentAndView" #foo></div> </div> <div id="contentOnly" #bar></div> `, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChild(ContentQueryDirective, {static: true}) contentQueryDir!: ContentQueryDirective; @ViewChildren('foo, bar') fooBars!: QueryList<ElementRef>; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const contentQList = fixture.componentInstance.contentQueryDir.foos; expect(contentQList.length).toBe(1); expect(contentQList.first.nativeElement.getAttribute('id')).toBe('contentAndView'); const viewQList = fixture.componentInstance.fooBars; expect(viewQList.length).toBe(2); expect(viewQList.first.nativeElement.getAttribute('id')).toBe('contentAndView'); expect(viewQList.last.nativeElement.getAttribute('id')).toBe('contentOnly'); }); }); describe('query order', () => { @Directive({selector: '[text]'}) class TextDirective { @Input() text: string | undefined; } it('should register view query matches from top to bottom', () => { @Component({ imports: [TextDirective], template: ` <span text="A"></span> <div text="B"> <span text="C"> <span text="D"></span> </span> </div> <span text="E"></span>`, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChildren(TextDirective) texts!: QueryList<TextDirective>; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); expect(fixture.componentInstance.texts.map((item) => item.text)).toEqual([ 'A', 'B', 'C', 'D', 'E', ]); }); it('should register content query matches from top to bottom', () => { @Directive({ selector: '[content-query]', }) class ContentQueryDirective { @ContentChildren(TextDirective, {descendants: true}) texts!: QueryList<TextDirective>; } @Component({ imports: [TextDirective, ContentQueryDirective], template: ` <div content-query> <span text="A"></span> <div text="B"> <span text="C"> <span text="D"></span> </span> </div> <span text="E"></span> </div>`, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChild(ContentQueryDirective, {static: true}) contentQueryDirective!: ContentQueryDirective; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); expect( fixture.componentInstance.contentQueryDirective.texts.map((item) => item.text), ).toEqual(['A', 'B', 'C', 'D', 'E']); }); }); // 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).toBeInstanceOf(QueryList); expect(fixture.componentInstance.contentChildren.length).toBe(0); }); describe('descendants: false (default)', () => { /** * A helper function to check if a given object looks like ElementRef. It is used in place of * the `instanceof ElementRef` check since ivy returns a type that looks like ElementRef (have * the same properties but doesn't pass the instanceof ElementRef test) */ function isElementRefLike(result: any): boolean { return result.nativeElement != null; } it('should match directives on elements that used to be wrapped by a required parent in HTML parser', () => { @Directive({ selector: '[myDef]', standalone: false, }) class MyDef {} @Component({ selector: 'my-container', template: ``, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class MyContainer { @ContentChildren(MyDef) myDefs!: QueryList<MyDef>; } @Component({ selector: 'test-cmpt', template: `<my-container><tr myDef></tr></my-container>`, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) 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); }); it('should match elements with local refs inside <ng-container>', () => { @Component({ selector: 'needs-target', template: ``, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class NeedsTarget { @ContentChildren('target') targets!: QueryList<ElementRef>; } @Component({ selector: 'test-cmpt', template: ` <needs-target> <ng-container> <tr #target></tr> </ng-container> </needs-target> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmpt {} TestBed.configureTestingModule({declarations: [TestCmpt, NeedsTarget]}); const fixture = TestBed.createComponent(TestCmpt); const cmptWithQuery = fixture.debugElement.children[0].injector.get(NeedsTarget); fixture.detectChanges(); expect(cmptWithQuery.targets.length).toBe(1); expect(isElementRefLike(cmptWithQuery.targets.first)).toBeTruthy(); }); it('should match elements with local refs inside nested <ng-container>', () => { @Component({ selector: 'needs-target', template: ``, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class NeedsTarget { @ContentChildren('target') targets!: QueryList<ElementRef>; } @Component({ selector: 'test-cmpt', template: ` <needs-target> <ng-container> <ng-container> <ng-container> <tr #target></tr> </ng-container> </ng-container> </ng-container> </needs-target> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmpt {} TestBed.configureTestingModule({declarations: [TestCmpt, NeedsTarget]}); const fixture = TestBed.createComponent(TestCmpt); const cmptWithQuery = fixture.debugElement.children[0].injector.get(NeedsTarget); fixture.detectChanges(); expect(cmptWithQuery.targets.length).toBe(1); expect(isElementRefLike(cmptWithQuery.targets.first)).toBeTruthy(); }); it('should match directives inside <ng-container>', () => { @Directive({ selector: '[targetDir]', standalone: false, }) class TargetDir {} @Component({ selector: 'needs-target', template: ``, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class NeedsTarget { @ContentChildren(TargetDir) targets!: QueryList<HTMLElement>; } @Component({ selector: 'test-cmpt', template: ` <needs-target> <ng-container> <tr targetDir></tr> </ng-container> </needs-target> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmpt {} TestBed.configureTestingModule({declarations: [TestCmpt, NeedsTarget, TargetDir]}); const fixture = TestBed.createComponent(TestCmpt); const cmptWithQuery = fixture.debugElement.children[0].injector.get(NeedsTarget); fixture.detectChanges(); expect(cmptWithQuery.targets.length).toBe(1); expect(cmptWithQuery.targets.first).toBeInstanceOf(TargetDir); }); it('should match directives inside nested <ng-container>', () => { @Directive({ selector: '[targetDir]', standalone: false, }) class TargetDir {} @Component({ selector: 'needs-target', template: ``, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class NeedsTarget { @ContentChildren(TargetDir) targets!: QueryList<HTMLElement>; } @Component({ selector: 'test-cmpt', template: ` <needs-target> <ng-container> <ng-container> <ng-container> <tr targetDir></tr> </ng-container> </ng-container> </ng-container> </needs-target> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmpt {} TestBed.configureTestingModule({declarations: [TestCmpt, NeedsTarget, TargetDir]}); const fixture = TestBed.createComponent(TestCmpt); const cmptWithQuery = fixture.debugElement.children[0].injector.get(NeedsTarget); fixture.detectChanges(); expect(cmptWithQuery.targets.length).toBe(1); expect(cmptWithQuery.targets.first).toBeInstanceOf(TargetDir); }); it('should cross child ng-container when query is declared on ng-container', () => { @Directive({ selector: '[targetDir]', standalone: false, }) class TargetDir {} @Directive({ selector: '[needs-target]', standalone: false, }) class NeedsTarget { @ContentChildren(TargetDir) targets!: QueryList<HTMLElement>; } @Component({ selector: 'test-cmpt', template: ` <ng-container targetDir> <ng-container needs-target> <ng-container> <tr targetDir></tr> </ng-container> </ng-container> </ng-container> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmpt {} TestBed.configureTestingModule({declarations: [TestCmpt, NeedsTarget, TargetDir]}); const fixture = TestBed.createComponent(TestCmpt); const cmptWithQuery = fixture.debugElement.children[0].injector.get(NeedsTarget); fixture.detectChanges(); expect(cmptWithQuery.targets.length).toBe(1); expect(cmptWithQuery.targets.first).toBeInstanceOf(TargetDir); }); it('should match nodes when using structural directives (*syntax) on <ng-container>', () => { @Directive({ selector: '[targetDir]', standalone: false, }) class TargetDir {} @Component({ selector: 'needs-target', template: ``, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class NeedsTarget { @ContentChildren(TargetDir) dirTargets!: QueryList<TargetDir>; @ContentChildren('target') localRefsTargets!: QueryList<ElementRef>; } @Component({ selector: 'test-cmpt', template: ` <needs-target> <ng-container *ngIf="true"> <div targetDir></div> <div #target></div> </ng-container> </needs-target> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmpt {} TestBed.configureTestingModule({declarations: [TestCmpt, NeedsTarget, TargetDir]}); const fixture = TestBed.createComponent(TestCmpt); const cmptWithQuery = fixture.debugElement.children[0].injector.get(NeedsTarget); fixture.detectChanges(); expect(cmptWithQuery.dirTargets.length).toBe(1); expect(cmptWithQuery.dirTargets.first).toBeInstanceOf(TargetDir); expect(cmptWithQuery.localRefsTargets.length).toBe(1); expect(isElementRefLike(cmptWithQuery.localRefsTargets.first)).toBeTruthy(); }); it('should match directives on <ng-container> when crossing nested <ng-container>', () => { @Directive({ selector: '[targetDir]', standalone: false, }) class TargetDir {} @Component({ selector: 'needs-target', template: ``, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class NeedsTarget { @ContentChildren(TargetDir) targets!: QueryList<HTMLElement>; } @Component({ selector: 'test-cmpt', template: ` <needs-target> <ng-container> <ng-container targetDir> <ng-container targetDir> <tr targetDir></tr> </ng-container> </ng-container> </ng-container> </needs-target> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmpt {} TestBed.configureTestingModule({declarations: [TestCmpt, NeedsTarget, TargetDir]}); const fixture = TestBed.createComponent(TestCmpt); const cmptWithQuery = fixture.debugElement.children[0].injector.get(NeedsTarget); fixture.detectChanges(); expect(cmptWithQuery.targets.length).toBe(3); }); }); describe('read option', () => { @Directive({selector: '[child]'}) class Child {} @Directive({selector: '[otherChild]'}) class OtherChild {} it('should query using type predicate and read ElementRef', () => { @Component({ imports: [Child], template: `<div child></div>`, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChildren(Child, {read: ElementRef}) query?: QueryList<ElementRef>; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const qList = fixture.componentInstance.query!; const elToQuery = fixture.nativeElement.querySelector('div'); expect(qList.length).toBe(1); expect(qList.first.nativeElement).toBe(elToQuery); }); it('should query using type predicate and read another directive type', () => { @Component({ imports: [Child, OtherChild], template: `<div child otherChild></div>`, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChildren(Child, {read: OtherChild}) query?: QueryList<OtherChild>; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const qList = fixture.componentInstance.query!; expect(qList.length).toBe(1); expect(qList.first).toBeInstanceOf(OtherChild); }); it('should not add results to query if a requested token cant be read', () => { @Component({ imports: [Child], template: `<div child></div>`, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChildren(Child, {read: OtherChild}) query?: QueryList<OtherChild>; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const qList = fixture.componentInstance.query!; expect(qList.length).toBe(0); }); it('should query using local ref and read ElementRef by default', () => { @Component({ template: ` <div #foo></div> <div></div> `, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChildren('foo') query?: QueryList<ElementRef>; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const qList = fixture.componentInstance.query!; const elToQuery = fixture.nativeElement.querySelector('div'); expect(qList.length).toBe(1); expect(qList.first.nativeElement).toBe(elToQuery); }); it('should query for multiple elements and read ElementRef by default', () => { @Component({ template: ` <div #foo></div> <div></div> <div #bar></div> `, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChildren('foo,bar') query?: QueryList<ElementRef>; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const qList = fixture.componentInstance.query!; const elToQuery = fixture.nativeElement.querySelectorAll('div'); expect(qList.length).toBe(2); expect(qList.first.nativeElement).toBe(elToQuery[0]); expect(qList.last.nativeElement).toBe(elToQuery[2]); }); it('should read ElementRef from an element when explicitly asked for', () => { @Component({ template: ` <div #foo></div> <div></div> `, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChildren('foo', {read: ElementRef}) query?: QueryList<ElementRef>; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const qList = fixture.componentInstance.query!; const elToQuery = fixture.nativeElement.querySelector('div'); expect(qList.length).toBe(1); expect(qList.first.nativeElement).toBe(elToQuery); }); it('should query for <ng-container> and read ElementRef with a native element pointing to comment node', () => { @Component({ template: `<ng-container #foo></ng-container>`, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChildren('foo', {read: ElementRef}) query?: QueryList<ElementRef>; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const qList = fixture.componentInstance.query!; expect(qList.length).toBe(1); expect(qList.first.nativeElement.nodeType).toBe(Node.COMMENT_NODE); }); it('should query for <ng-container> and read ElementRef without explicit read option', () => { @Component({ template: `<ng-container #foo></ng-container>`, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChildren('foo') query?: QueryList<ElementRef>; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const qList = fixture.componentInstance.query!; expect(qList.length).toBe(1); expect(qList.first.nativeElement.nodeType).toBe(Node.COMMENT_NODE); }); it('should read ViewContainerRef from element nodes when explicitly asked for', () => { @Component({ template: `<div #foo></div>`, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChildren('foo', {read: ViewContainerRef}) query?: QueryList<ViewContainerRef>; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const qList = fixture.componentInstance.query!; expect(qList.length).toBe(1); expect(qList.first).toBeInstanceOf(ViewContainerRef); }); it('should read ViewContainerRef from ng-template nodes when explicitly asked for', () => { @Component({ template: `<ng-template #foo></ng-template>`, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChildren('foo', {read: ViewContainerRef}) query?: QueryList<ViewContainerRef>; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const qList = fixture.componentInstance.query!; expect(qList.length).toBe(1); expect(qList.first).toBeInstanceOf(ViewContainerRef); }); it('should not throw when hydration metadata has no serialized container data', () => { @Component({ template: `<div #foo></div>`, changeDetection: ChangeDetectionStrategy.OnPush, }) class TestCmp {} const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const element = fixture.nativeElement.querySelector('div')!; const context = getLContext(element)!; const hostLView = context.lView!; const candidateTNode = hostLView[TVIEW].data[context.nodeIndex] as unknown; expect(candidateTNode).toBeDefined(); expect(!!candidateTNode && typeof candidateTNode === 'object' && 'type' in candidateTNode) .withContext('Expected a TNode with a type field.') .toBeTrue(); const hostTNode = candidateTNode as TElementNode | TContainerNode | TElementContainerNode; expect(!!(hostTNode.type & (TNodeType.AnyContainer | TNodeType.AnyRNode))) .withContext('Expected a host TNode that can create a ViewContainerRef.') .toBeTrue(); hostLView[HYDRATION] = {data: {}, firstChild: null} as DehydratedView; enableLocateOrCreateContainerRefImpl(); const warnSpy = spyOn(console, 'warn'); expect(() => createContainerRef(hostTNode, hostLView)).not.toThrow(); const warningMessages = warnSpy.calls .allArgs() .map(([message]) => String(message)) .join('\n'); expect(warningMessages).toContain( 'Unexpected state: no hydration info available for a given TNode, which represents a view container.', ); }); it('should read ElementRef with a native element pointing to comment DOM node from ng-template', () => { @Component({ template: `<ng-template #foo></ng-template>`, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChildren('foo', {read: ElementRef}) query?: QueryList<ElementRef>; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const qList = fixture.componentInstance.query!; expect(qList.length).toBe(1); expect(qList.first.nativeElement.nodeType).toBe(Node.COMMENT_NODE); }); it('should read TemplateRef from ng-template by default', () => { @Component({ template: `<ng-template #foo></ng-template>`, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChildren('foo') query?: QueryList<TemplateRef<unknown>>; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const qList = fixture.componentInstance.query!; expect(qList.length).toBe(1); expect(qList.first).toBeInstanceOf(TemplateRef); }); it('should read TemplateRef from ng-template when explicitly asked for', () => { @Component({ template: `<ng-template #foo></ng-template>`, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChildren('foo', {read: TemplateRef}) query?: QueryList<TemplateRef<unknown>>; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const qList = fixture.componentInstance.query!; expect(qList.length).toBe(1); expect(qList.first).toBeInstanceOf(TemplateRef); }); it('should read component instance if element queried for is a component host', () => { @Component({ selector: 'child-cmp', template: '', changeDetection: ChangeDetectionStrategy.Eager, }) class ChildCmp {} @Component({ imports: [ChildCmp], template: `<child-cmp #foo></child-cmp>`, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChildren('foo') query?: QueryList<ChildCmp>; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const qList = fixture.componentInstance.query!; expect(qList.length).toBe(1); expect(qList.first).toBeInstanceOf(ChildCmp); }); it('should read component instance with explicit exportAs', () => { @Component({ selector: 'child-cmp', exportAs: 'child', template: '', changeDetection: ChangeDetectionStrategy.Eager, }) class ChildCmp {} @Component({ imports: [ChildCmp], template: `<child-cmp #foo="child"></child-cmp>`, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChildren('foo') query?: QueryList<ChildCmp>; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const qList = fixture.componentInstance.query!; expect(qList.length).toBe(1); expect(qList.first).toBeInstanceOf(ChildCmp); }); it('should read directive instance if element queried for has an exported directive with a matching name', () => { @Directive({selector: '[child]', exportAs: 'child'}) class ChildDirective {} @Component({ imports: [ChildDirective], template: `<div #foo="child" child></div>`, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChildren('foo') query?: QueryList<ChildDirective>; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const qList = fixture.componentInstance.query!; expect(qList.length).toBe(1); expect(qList.first).toBeInstanceOf(ChildDirective); }); it('should read all matching directive instances from a given element', () => { @Directive({selector: '[child1]', exportAs: 'child1'}) class Child1Dir {} @Directive({selector: '[child2]', exportAs: 'child2'}) class Child2Dir {} @Component({ imports: [Child1Dir, Child2Dir], template: `<div #foo="child1" child1 #bar="child2" child2></div>`, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChildren('foo, bar') query?: QueryList<unknown>; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const qList = fixture.componentInstance.query!; expect(qList.length).toBe(2); expect(qList.first).toBeInstanceOf(Child1Dir); expect(qList.last).toBeInstanceOf(Child2Dir); }); it('should read multiple locals exporting the same directive from a given element', () => { @Directive({selector: '[child]', exportAs: 'child'}) class ChildDir {} @Component({ imports: [ChildDir], template: `<div child #foo="child" #bar="child"></div>`, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChildren('foo, bar') query?: QueryList<ChildDir>; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const qList = fixture.componentInstance.query!; expect(qList.length).toBe(2); expect(qList.first).toBeInstanceOf(ChildDir); expect(qList.last).toBeInstanceOf(ChildDir); }); it('should query multiple locals on the same element', () => { @Component({ selector: 'multiple-local-refs', template: ` <div #foo #bar id="target"></div> <div></div> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class MultipleLocalRefsComp { @ViewChildren('foo') fooQuery!: QueryList<any>; @ViewChildren('bar') barQuery!: QueryList<any>; } const fixture = TestBed.createComponent(MultipleLocalRefsComp); fixture.detectChanges(); const cmptInstance = fixture.componentInstance; const targetElement = fixture.nativeElement.querySelector('#target'); const fooList = cmptInstance.fooQuery; expect(fooList.length).toBe(1); expect(fooList.first.nativeElement).toEqual(targetElement); const barList = cmptInstance.barQuery; expect(barList.length).toBe(1); expect(barList.first.nativeElement).toEqual(targetElement); }); it('should match on exported directive name and read a requested token', () => { @Directive({selector: '[child]', exportAs: 'child'}) class ChildDir {} @Component({ imports: [ChildDir], template: `<div child #foo="child"></div>`, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChildren('foo', {read: ElementRef}) query?: QueryList<ElementRef>; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const qList = fixture.componentInstance.query!; expect(qList.length).toBe(1); expect(qList.first).toBeInstanceOf(ElementRef); }); it('should support reading a mix of ElementRef and directive instances', () => { @Directive({selector: '[child]', exportAs: 'child'}) class ChildDir {} @Component({ imports: [ChildDir], template: `<div #foo #bar="child" child></div>`, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChildren('foo, bar') query?: QueryList<unknown>; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const qList = fixture.componentInstance.query!; expect(qList.length).toBe(2); expect(qList.first).toBeInstanceOf(ElementRef); expect(qList.last).toBeInstanceOf(ChildDir); }); it('should not add results to selector-based query if a requested token cant be read', () => { @Directive({selector: '[child]'}) class ChildDir {} @Component({ imports: [], template: `<div #foo></div>`, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChildren('foo', {read: ChildDir}) query?: QueryList<ChildDir>; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const qList = fixture.componentInstance.query!; expect(qList.length).toBe(0); }); it('should not add results to directive-based query if only read token matches', () => { @Directive({selector: '[child]'}) class ChildDir {} @Directive({selector: '[otherChild]'}) class OtherChildDir {} @Component({ imports: [Child], template: `<div child></div>`, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChildren(OtherChild, {read: Child}) query?: QueryList<ChildDir>; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const qList = fixture.componentInstance.query!; expect(qList.length).toBe(0); }); it('should not add results to TemplateRef-based query if only read token matches', () => { @Component({ template: `<div></div>`, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChildren(TemplateRef, {read: ElementRef}) query?: QueryList<ElementRef>; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const qList = fixture.componentInstance.query!; expect(qList.length).toBe(0); }); it('should not add results to the query in case no match found (via TemplateRef)', () => { @Component({ template: `<div></div>`, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChildren(TemplateRef) query?: QueryList<TemplateRef<unknown>>; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const qList = fixture.componentInstance.query!; expect(qList.length).toBe(0); }); it('should query templates if the type is TemplateRef (and respect "read" option)', () => { @Component({ template: ` <ng-template #foo><div>Test</div></ng-template> <ng-template #bar><div>Test</div></ng-template> <ng-template #baz><div>Test</div></ng-template> `, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChildren(TemplateRef) tplQuery?: QueryList<TemplateRef<unknown>>; @ViewChildren(TemplateRef, {read: ElementRef}) elRefQuery?: QueryList<TemplateRef<unknown>>; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const tplQuery = fixture.componentInstance.tplQuery!; const elRefQuery = fixture.componentInstance.elRefQuery!; expect(tplQuery.length).toBe(3); expect(tplQuery.first).toBeInstanceOf(TemplateRef); expect(elRefQuery.length).toBe(3); expect(elRefQuery.first).toBeInstanceOf(ElementRef); }); it('should match using string selector and directive as a read argument', () => { @Component({ imports: [Child], template: `<div child #foo></div>`, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmp { @ViewChildren('foo', {read: Child}) query?: QueryList<Child>; } const fixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); const qList = fixture.componentInstance.query!; expect(qList.length).toBe(1); expect(qList.first).toBeInstanceOf(Child); }); }); 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); }); it('should only fire if the content of the query changes', () => { // When views are inserted/removed the content query need to be recomputed. // Recomputing the query may result in no changes to the query (the item added/removed was // not part of the query). This tests asserts that the query does not fire when no changes // occur. TestBed.configureTestingModule({ declarations: [QueryCompWithStrictChangeEmitParent, QueryCompWithNoChanges], }); const fixture = TestBed.createComponent(QueryCompWithNoChanges); let changesStrict = 0; const componentInstance = fixture.componentInstance.queryComp; fixture.detectChanges(); componentInstance.foos.changes.subscribe((value: any) => { // subscribe to the changes and record when changes occur. changesStrict += 1; }); // First verify that the subscription is working. fixture.componentInstance.innerShowing = false; fixture.detectChanges(); expect(changesStrict).toBe(1); // We detected a change expect(componentInstance.foos.toArray().length).toEqual(1); // now verify that removing a view does not needlessly fire subscription fixture.componentInstance.showing = false; fixture.detectChanges(); expect(changesStrict).toBe(1); // We detected a change expect(componentInstance.foos.toArray().length).toEqual(1); // now verify that adding a view does not needlessly fire subscription fixture.componentInstance.showing = true; fixture.detectChanges(); expect(changesStrict).toBe(1); // We detected a change // Note: even though the `showing` is `true` and the second `<div>` is displayed, the // child element of that <div> is hidden because the `innerShowing` flag is still `false`, // so we expect only one element to be present in the `foos` array. expect(componentInstance.foos.toArray().length).toEqual(1); }); }); describe('view boundaries', () => { describe('ViewContainerRef', () => { @Directive({ selector: '[vc]', exportAs: 'vc', standalone: false, }) 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: ` <ng-template [ngIf]="value"> <div #foo></div> </ng-template> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class TestComponent { value: boolean = false; @ViewChildren('foo') query!: QueryList<any>; } 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: ` <ng-template ngFor let-item [ngForOf]="value"> <div #foo [id]="item"></div> </ng-template> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class TestComponent { value: string[] | undefined; @ViewChildren('foo') query!: QueryList<any>; } 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. */ it('should NOT notify on changes when a given view is removed and re-inserted at the same index', () => { @Component({ selector: 'test-comp', template: ` <ng-template #tpl><div #foo>match</div></ng-template> <ng-template vc></ng-template> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class TestComponent implements AfterViewInit { queryListNotificationCounter = 0; @ViewChild(ViewContainerManipulatorDirective) vc!: ViewContainerManipulatorDirective; @ViewChild('tpl') tpl!: TemplateRef<any>; @ViewChildren('foo') query!: QueryList<any>; 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(1); }); it('should support a mix of content queries from the declaration and embedded view', () => { @Directive({ selector: '[query-for-lots-of-content]', standalone: false, }) class QueryForLotsOfContent { @ContentChildren('foo', {descendants: true}) foos1!: QueryList<ElementRef>; @ContentChildren('foo', {descendants: true}) foos2!: QueryList<ElementRef>; } @Directive({ selector: '[query-for-content]', standalone: false, }) class QueryForContent { @ContentChildren('foo') foos!: QueryList<ElementRef>; } @Component({ selector: 'test-comp', template: ` <div query-for-lots-of-content> <ng-template ngFor let-item [ngForOf]="items"> <div query-for-content> <span #foo></span> </div> </ng-template> </div> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) 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: ` <ng-template #tpl1 let-idx="idx"> <div #foo [id]="'foo1_' + idx"></div> </ng-template> <div #foo id="middle"></div> <ng-template #tpl2 let-idx="idx"> <div #foo [id]="'foo2_' + idx"></div> </ng-template> <ng-template vc></ng-template> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class TestComponent { @ViewChild(ViewContainerManipulatorDirective) vc!: ViewContainerManipulatorDirective; @ViewChild('tpl1') tpl1!: TemplateRef<any>; @ViewChild('tpl2') tpl2!: TemplateRef<any>; @ViewChildren('foo') query!: QueryList<any>; } 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: ` <ng-template #tpl let-idx="idx" let-container_idx="container_idx"> <div #foo [id]="'foo_' + container_idx + '_' + idx"></div> </ng-template> <ng-template vc #vi0="vc"></ng-template> <ng-template vc #vi1="vc"></ng-template> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class TestComponent { @ViewChild('tpl') tpl!: TemplateRef<any>; @ViewChild('vi0') vi0!: ViewContainerManipulatorDirective; @ViewChild('vi1') vi1!: ViewContainerManipulatorDirective; @ViewChildren('foo') query!: QueryList<any>; } 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: ` <ng-template #tpl> <span #foo id="from_tpl"></span> </ng-template> <ng-template [ngTemplateOutlet]="show ? tpl : null"></ng-template> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class MyApp { show = false; @ViewChildren('foo') query!: QueryList<any>; } 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]', standalone: false, }) class Child {} @Directive({ selector: '[parent]', providers: [{provide: Child, useExisting: Parent}], standalone: false, }) class Parent extends Child {} @Component({ selector: 'test-cmpt', template: `<ng-template [ngIf]="true" ><ng-template [ngIf]="true"><div parent></div></ng-template ></ng-template>`, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmpt { @ViewChildren(Child) instances!: QueryList<Child>; } 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}, ], standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class WithMultiProvider {} @Component({ selector: 'test-cmpt', template: `<with-multi-provider></with-multi-provider>`, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmpt { @ViewChildren(MyClass) queryResults!: QueryList<WithMultiProvider>; } TestBed.configureTestingModule({declarations: [TestCmpt, WithMultiProvider]}); const fixture = TestBed.createComponent(TestCmpt); fixture.detectChanges(); expect(fixture.componentInstance.queryResults.length).toBe(1); expect(fixture.componentInstance.queryResults.first).toBeInstanceOf(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}, ], standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class WithMultiProvider {} @Component({ selector: 'test-cmpt', template: ` <ng-template [ngIf]="true"><with-multi-provider></with-multi-provider></ng-template> <with-multi-provider></with-multi-provider> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class TestCmpt { @ViewChildren(MyClass) queryResults!: QueryList<WithMultiProvider>; } TestBed.configureTestingModule({declarations: [TestCmpt, WithMultiProvider]}); const fixture = TestBed.createComponent(TestCmpt); fixture.detectChanges(); expect(fixture.componentInstance.queryResults.length).toBe(2); expect(fixture.componentInstance.queryResults.first).toBeInstanceOf(WithMultiProvider); expect(fixture.componentInstance.queryResults.last).toBeInstanceOf(WithMultiProvider); }); it('should allow undefined provider value in a [View/Content]Child queries', () => { @Directive({ selector: '[group]', standalone: false, }) class GroupDir {} @Directive({ selector: '[undefinedGroup]', providers: [{provide: GroupDir, useValue: undefined}], standalone: false, }) class UndefinedGroup {} @Component({ template: ` <div group></div> <ng-template [ngIf]="true"> <div undefinedGroup></div> </ng-template> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class App { @ViewChild(GroupDir) group!: GroupDir; } TestBed.configureTestingModule({ declarations: [App, GroupDir, UndefinedGroup], imports: [CommonModule], }); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(fixture.componentInstance.group).toBeInstanceOf(GroupDir); }); it('should allow null / undefined provider value in a [View/Content]Children queries', () => { @Directive({ selector: '[group]', standalone: false, }) class GroupDir {} @Directive({ selector: '[nullGroup]', providers: [{provide: GroupDir, useValue: null}], standalone: false, }) class NullGroup {} @Directive({ selector: '[undefinedGroup]', providers: [{provide: GroupDir, useValue: undefined}], standalone: false, }) class UndefinedGroup {} @Component({ template: ` <ng-template [ngIf]="true"> <div nullGroup></div> </ng-template> <div group></div> <ng-template [ngIf]="true"> <div undefinedGroup></div> </ng-template> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class App { @ViewChildren(GroupDir) groups!: QueryList<GroupDir>; } TestBed.configureTestingModule({ declarations: [App, GroupDir, NullGroup, UndefinedGroup], imports: [CommonModule], }); const fixture = TestBed.createComponent(App); fixture.detectChanges(); const queryList = fixture.componentInstance.groups; expect(queryList.length).toBe(3); const groups = queryList.toArray(); expect(groups[0]).toBeNull(); expect(groups[1]).toBeInstanceOf(GroupDir); expect(groups[2]).toBeUndefined(); }); }); describe('querying for string token providers', () => { @Directive({ selector: '[text-token]', providers: [{provide: 'Token', useExisting: TextTokenDirective}], standalone: false, }) class TextTokenDirective {} it('should match string injection token in a ViewChild query', () => { @Component({ template: '<div text-token></div>', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class App { @ViewChild('Token') token: any; } TestBed.configureTestingModule({declarations: [App, TextTokenDirective]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(fixture.componentInstance.token).toBeInstanceOf(TextTokenDirective); }); it('should give precedence to local reference if both a reference and a string injection token provider match a ViewChild query', () => { @Component({ template: '<div text-token #Token></div>', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class App { @ViewChild('Token') token: any; } TestBed.configureTestingModule({declarations: [App, TextTokenDirective]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(fixture.componentInstance.token).toBeInstanceOf(ElementRef); }); it('should match string injection token in a ViewChildren query', () => { @Component({ template: '<div text-token></div>', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class App { @ViewChildren('Token') tokens!: QueryList<any>; } TestBed.configureTestingModule({declarations: [App, TextTokenDirective]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); const tokens = fixture.componentInstance.tokens; expect(tokens.length).toBe(1); expect(tokens.first).toBeInstanceOf(TextTokenDirective); }); it('should match both string injection token and local reference inside a ViewChildren query', () => { @Component({ template: '<div text-token #Token></div>', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class App { @ViewChildren('Token') tokens!: QueryList<any>; } TestBed.configureTestingModule({declarations: [App, TextTokenDirective]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(fixture.componentInstance.tokens.toArray()).toEqual([ jasmine.any(ElementRef), jasmine.any(TextTokenDirective), ]); }); it('should match string injection token in a ContentChild query', () => { @Component({ selector: 'has-query', template: '<ng-content></ng-content>', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class HasQuery { @ContentChild('Token') token: any; } @Component({ template: '<has-query><div text-token></div></has-query>', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class App { @ViewChild(HasQuery) queryComp!: HasQuery; } TestBed.configureTestingModule({declarations: [App, HasQuery, TextTokenDirective]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(fixture.componentInstance.queryComp.token).toBeInstanceOf(TextTokenDirective); }); it('should give precedence to local reference if both a reference and a string injection token provider match a ContentChild query', () => { @Component({ selector: 'has-query', template: '<ng-content></ng-content>', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class HasQuery { @ContentChild('Token') token: any; } @Component({ template: '<has-query><div text-token #Token></div></has-query>', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class App { @ViewChild(HasQuery) queryComp!: HasQuery; } TestBed.configureTestingModule({declarations: [App, HasQuery, TextTokenDirective]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(fixture.componentInstance.queryComp.token).toBeInstanceOf(ElementRef); }); it('should match string injection token in a ContentChildren query', () => { @Component({ selector: 'has-query', template: '<ng-content></ng-content>', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class HasQuery { @ContentChildren('Token') tokens!: QueryList<any>; } @Component({ template: '<has-query><div text-token></div></has-query>', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class App { @ViewChild(HasQuery) queryComp!: HasQuery; } TestBed.configureTestingModule({declarations: [App, HasQuery, TextTokenDirective]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); const tokens = fixture.componentInstance.queryComp.tokens; expect(tokens.length).toBe(1); expect(tokens.first).toBeInstanceOf(TextTokenDirective); }); it('should match both string injection token and local reference inside a ContentChildren query', () => { @Component({ selector: 'has-query', template: '<ng-content></ng-content>', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class HasQuery { @ContentChildren('Token') tokens!: QueryList<any>; } @Component({ template: '<has-query><div text-token #Token></div></has-query>', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class App { @ViewChild(HasQuery) queryComp!: HasQuery; } TestBed.configureTestingModule({declarations: [App, HasQuery, TextTokenDirective]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(fixture.componentInstance.queryComp.tokens.toArray()).toEqual([ jasmine.any(ElementRef), jasmine.any(TextTokenDirective), ]); }); it('should match string token specified through the `read` option of a view query', () => { @Component({ template: '<div text-token #Token></div>', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class App { @ViewChild('Token', {read: 'Token'}) token: any; } TestBed.configureTestingModule({declarations: [App, TextTokenDirective]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(fixture.componentInstance.token).toBeInstanceOf(TextTokenDirective); }); it('should match string token specified through the `read` option of a content query', () => { @Component({ selector: 'has-query', template: '<ng-content></ng-content>', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class HasQuery { @ContentChild('Token', {read: 'Token'}) token: any; } @Component({ template: '<has-query><div text-token #Token></div></has-query>', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class App { @ViewChild(HasQuery) queryComp!: HasQuery; } TestBed.configureTestingModule({declarations: [App, HasQuery, TextTokenDirective]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(fixture.componentInstance.queryComp.token).toBeInstanceOf(TextTokenDirective); }); }); }); function initWithTemplate(compType: Type<any>, 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: '<ng-content></ng-content>', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class QueryComp { @ViewChild('viewQuery') viewChild!: any; @ContentChild('contentQuery') contentChild!: any; @ViewChildren('viewQuery') viewChildren!: QueryList<any>; @ContentChildren('contentQuery') contentChildren!: QueryList<any>; } @Component({ selector: 'app-comp', template: ``, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class AppComp {} @Component({ selector: 'simple-comp-a', template: '', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class SimpleCompA {} @Component({ selector: 'simple-comp-b', template: '', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class SimpleCompB {} @Directive({ selector: '[text]', standalone: false, }) class TextDirective { @Input() text = ''; } @Component({ selector: 'static-view-query-comp', template: ` <div [text]="text"></div> <span #foo></span> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) 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: ` <div [text]="text"></div> <span #foo></span> <div #bar></div> <span #baz></span> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class SubclassStaticViewQueryComp extends StaticViewQueryComp { @ViewChild('bar', {static: true}) bar!: ElementRef; @ViewChild('baz') baz!: ElementRef; } @Component({ selector: 'static-content-query-comp', template: `<ng-content></ng-content>`, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) 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]', standalone: false, }) 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: `<ng-content></ng-content>`, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class SubclassStaticContentQueryComp extends StaticContentQueryComp { @ContentChild('bar', {static: true}) bar!: ElementRef; @ContentChild('baz') baz!: ElementRef; } @Component({ selector: 'query-with-changes', template: ` <div *ngIf="showing" #foo></div> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) export class QueryCompWithChanges { @ViewChildren('foo') foos!: QueryList<any>; showing = false; } @Component({ selector: 'query-with-no-changes', template: ` <query-component> <div *ngIf="true" #foo></div> <div *ngIf="showing"> Showing me should not change the content of the query <div *ngIf="innerShowing" #foo></div> </div> </query-component> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) export class QueryCompWithNoChanges { showing: boolean = true; innerShowing: boolean = true; queryComp!: QueryCompWithStrictChangeEmitParent; } @Component({ selector: 'query-component', template: `<ng-content></ng-content>`, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) export class QueryCompWithStrictChangeEmitParent { @ContentChildren('foo', { descendants: true, emitDistinctChangesOnly: true, }) foos!: QueryList<any>; constructor(public queryCompWithNoChanges: QueryCompWithNoChanges) { queryCompWithNoChanges.queryComp = this; } } @Component({ selector: 'query-target', template: '<ng-content></ng-content>', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class SuperDirectiveQueryTarget {} @Directive({ selector: '[super-directive]', standalone: false, }) class SuperDirective { @ViewChildren(SuperDirectiveQueryTarget) headers!: QueryList<SuperDirectiveQueryTarget>; } @Component({ template: ` <query-target>One</query-target> <query-target>Two</query-target> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class SubComponent extends SuperDirective {} const MY_OPTION_TOKEN = new InjectionToken<TestComponentWithToken>('ComponentWithToken'); @Component({ selector: 'my-option', template: 'Option', providers: [{provide: MY_OPTION_TOKEN, useExisting: TestComponentWithToken}], standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class TestComponentWithToken {} @Component({ selector: 'test-injection-token', template: ` <my-option></my-option> <my-option></my-option> <ng-content></ng-content> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class TestInjectionTokenQueries { @ViewChild(MY_OPTION_TOKEN) viewFirstOption!: TestComponentWithToken; @ViewChildren(MY_OPTION_TOKEN) viewOptions!: QueryList<TestComponentWithToken>; @ContentChild(MY_OPTION_TOKEN) contentFirstOption!: TestComponentWithToken; @ContentChildren(MY_OPTION_TOKEN) contentOptions!: QueryList<TestComponentWithToken>; } @Component({ template: ` <test-injection-token> <my-option></my-option> <my-option></my-option> </test-injection-token> `, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class TestInjectionTokenContentQueries {}