/
githubmirror
/
angular
Обзор
Документация
Войти
/
githubmirror
/
angular
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/core/test/linker/integration_spec.ts
3 082 строки
101 KB
Matthieu Riegler
refactor(core): remove `ComponentFactoryResolver` usages
13 апр 2026, 16:00
13 апр 2026, 16:00
df9eed4
Код
Авторство
О чём код?
/** * @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, DOCUMENT, ɵgetDOM as getDOM} from '@angular/common'; import {createMouseEvent, dispatchEvent, el, isCommentNode} from '@angular/private/testing'; import {expect} from '@angular/private/testing/matchers'; import { ChangeDetectionStrategy, ChangeDetectorRef, PipeTransform, } from '../../src/change_detection/change_detection'; import { Attribute, Compiler, Component, ComponentRef, ContentChildren, createComponent, Directive, EnvironmentInjector, EventEmitter, Host, HostBinding, HostListener, Inject, Injectable, InjectionToken, Injector, Input, NgModule, NgModuleRef, NO_ERRORS_SCHEMA, OnDestroy, Output, Pipe, provideNgReflectAttributes, reflectComponentType, signal, SkipSelf, ViewChild, ViewRef, ɵsetClassDebugInfo, } from '../../src/core'; import {ElementRef} from '../../src/linker/element_ref'; import {QueryList} from '../../src/linker/query_list'; import {TemplateRef} from '../../src/linker/template_ref'; import {ViewContainerRef} from '../../src/linker/view_container_ref'; import {EmbeddedViewRef} from '../../src/linker/view_ref'; import {fakeAsync, getTestBed, TestBed, tick, waitForAsync} from '../../testing'; import {stringify} from '../../src/util/stringify'; const ANCHOR_ELEMENT = new InjectionToken('AnchorElement'); describe('integration tests', function () { describe('react to record changes', function () { it('should consume text node changes', () => { TestBed.configureTestingModule({declarations: [MyComp]}); const template = '<div>{{ctxProp()}}</div>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); fixture.componentInstance.ctxProp.set('Hello World!'); fixture.detectChanges(); expect(fixture.nativeElement).toHaveText('Hello World!'); }); it('should update text node with a blank string when interpolation evaluates to null', () => { TestBed.configureTestingModule({declarations: [MyComp]}); const template = '<div>{{null}}{{ctxProp()}}</div>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); fixture.componentInstance.ctxProp.set(null!); fixture.detectChanges(); expect(fixture.nativeElement).toHaveText(''); }); it('should allow both null and undefined in expressions', () => { const template = '<div>{{null == undefined}}|{{null === undefined}}</div>'; const fixture = TestBed.configureTestingModule({declarations: [MyComp]}) .overrideComponent(MyComp, {set: {template}}) .createComponent(MyComp); fixture.detectChanges(); expect(fixture.nativeElement).toHaveText('true|false'); }); it('should support an arbitrary number of interpolations in an element', () => { TestBed.configureTestingModule({declarations: [MyComp]}); const template = `<div>before{{'0'}}a{{'1'}}b{{'2'}}c{{'3'}}d{{'4'}}e{{'5'}}f{{'6'}}g{{'7'}}h{{'8'}}i{{'9'}}j{{'10'}}after</div>`; const fixture = TestBed.overrideComponent(MyComp, {set: {template}}).createComponent(MyComp); fixture.detectChanges(); expect(fixture.nativeElement).toHaveText('before0a1b2c3d4e5f6g7h8i9j10after'); }); it('should use a blank string when interpolation evaluates to null or undefined with an arbitrary number of interpolations', () => { TestBed.configureTestingModule({declarations: [MyComp]}); const template = `<div>0{{null}}a{{undefined}}b{{null}}c{{undefined}}d{{null}}e{{undefined}}f{{null}}g{{undefined}}h{{null}}i{{undefined}}j{{null}}1</div>`; const fixture = TestBed.overrideComponent(MyComp, {set: {template}}).createComponent(MyComp); fixture.detectChanges(); expect(fixture.nativeElement).toHaveText('0abcdefghij1'); }); it('should consume element binding changes', () => { TestBed.configureTestingModule({declarations: [MyComp]}); const template = '<div [id]="ctxProp()"></div>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); fixture.componentInstance.ctxProp.set('Hello World!'); fixture.detectChanges(); expect(fixture.debugElement.children[0].nativeElement.id).toEqual('Hello World!'); }); it('should consume binding to aria-* attributes', () => { TestBed.configureTestingModule({declarations: [MyComp]}); const template = '<div [attr.aria-label]="ctxProp()"></div>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); fixture.componentInstance.ctxProp.set('Initial aria label'); fixture.detectChanges(); expect(fixture.debugElement.children[0].nativeElement.getAttribute('aria-label')).toEqual( 'Initial aria label', ); fixture.componentInstance.ctxProp.set('Changed aria label'); fixture.detectChanges(); expect(fixture.debugElement.children[0].nativeElement.getAttribute('aria-label')).toEqual( 'Changed aria label', ); }); it('should remove an attribute when attribute expression evaluates to null', () => { TestBed.configureTestingModule({declarations: [MyComp]}); const template = '<div [attr.foo]="ctxProp()"></div>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); fixture.componentInstance.ctxProp.set('bar'); fixture.detectChanges(); expect(fixture.debugElement.children[0].nativeElement.getAttribute('foo')).toEqual('bar'); fixture.componentInstance.ctxProp.set(null!); fixture.detectChanges(); expect(fixture.debugElement.children[0].nativeElement.hasAttribute('foo')).toBeFalsy(); }); it('should remove style when when style expression evaluates to null', () => { TestBed.configureTestingModule({declarations: [MyComp]}); const template = '<div [style.height.px]="ctxProp()"></div>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); fixture.componentInstance.ctxProp.set('10'); fixture.detectChanges(); expect(fixture.debugElement.children[0].nativeElement.style['height']).toEqual('10px'); fixture.componentInstance.ctxProp.set(null!); fixture.detectChanges(); expect(fixture.debugElement.children[0].nativeElement.style['height']).toEqual(''); }); it('should consume binding to property names where attr name and property name do not match', () => { TestBed.configureTestingModule({declarations: [MyComp]}); const template = '<div [tabindex]="ctxNumProp()"></div>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); fixture.detectChanges(); expect(fixture.debugElement.children[0].nativeElement.tabIndex).toEqual(0); fixture.componentInstance.ctxNumProp.set(5); fixture.detectChanges(); expect(fixture.debugElement.children[0].nativeElement.tabIndex).toEqual(5); }); it('should consume binding to camel-cased properties', () => { TestBed.configureTestingModule({declarations: [MyComp]}); const template = '<input [readOnly]="ctxBoolProp()">'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); fixture.detectChanges(); expect(fixture.debugElement.children[0].nativeElement.readOnly).toBeFalsy(); fixture.componentInstance.ctxBoolProp.set(true); fixture.detectChanges(); expect(fixture.debugElement.children[0].nativeElement.readOnly).toBeTruthy(); }); it('should consume binding to innerHtml', () => { TestBed.configureTestingModule({declarations: [MyComp]}); const template = '<div innerHtml="{{ctxProp()}}"></div>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); fixture.componentInstance.ctxProp.set('Some <span>HTML</span>'); fixture.detectChanges(); expect(fixture.debugElement.children[0].nativeElement.innerHTML).toEqual( 'Some <span>HTML</span>', ); fixture.componentInstance.ctxProp.set('Some other <div>HTML</div>'); fixture.detectChanges(); expect(fixture.debugElement.children[0].nativeElement.innerHTML).toEqual( 'Some other <div>HTML</div>', ); }); it('should consume binding to htmlFor using for alias', () => { const template = '<label [for]="ctxProp()"></label>'; const fixture = TestBed.configureTestingModule({declarations: [MyComp]}) .overrideComponent(MyComp, {set: {template}}) .createComponent(MyComp); const nativeEl = fixture.debugElement.children[0].nativeElement; fixture.debugElement.componentInstance.ctxProp.set('foo'); fixture.detectChanges(); expect(nativeEl.htmlFor).toBe('foo'); }); it('should consume directive watch expression change.', () => { TestBed.configureTestingModule({declarations: [MyComp, MyDir]}); const template = '<span>' + '<div my-dir [elprop]="ctxProp()"></div>' + '<div my-dir elprop="Hi there!"></div>' + '<div my-dir elprop="Hi {{\'there!\'}}"></div>' + '<div my-dir elprop="One more {{ctxProp()}}"></div>' + '</span>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); fixture.componentInstance.ctxProp.set('Hello World!'); fixture.detectChanges(); const containerSpan = fixture.debugElement.children[0]; expect(containerSpan.children[0].injector.get(MyDir).dirProp).toEqual('Hello World!'); expect(containerSpan.children[1].injector.get(MyDir).dirProp).toEqual('Hi there!'); expect(containerSpan.children[2].injector.get(MyDir).dirProp).toEqual('Hi there!'); expect(containerSpan.children[3].injector.get(MyDir).dirProp).toEqual( 'One more Hello World!', ); }); describe('pipes', () => { it('should support pipes in bindings', () => { TestBed.configureTestingModule({declarations: [MyComp, MyDir, DoublePipe]}); const template = '<div my-dir #dir="mydir" [elprop]="ctxProp() | double"></div>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); fixture.componentInstance.ctxProp.set('a'); fixture.detectChanges(); const dir = fixture.debugElement.children[0].references!['dir']; expect(dir.dirProp).toEqual('aa'); }); }); it('should support nested components.', () => { TestBed.configureTestingModule({declarations: [MyComp, ChildComp]}); const template = '<child-cmp></child-cmp>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); fixture.detectChanges(); expect(fixture.nativeElement).toHaveText('hello'); }); // GH issue 328 - https://github.com/angular/angular/issues/328 it('should support different directive types on a single node', () => { TestBed.configureTestingModule({declarations: [MyComp, ChildComp, MyDir]}); const template = '<child-cmp my-dir [elprop]="ctxProp()"></child-cmp>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); fixture.componentInstance.ctxProp.set('Hello World!'); fixture.detectChanges(); const tc = fixture.debugElement.children[0]; expect(tc.injector.get(MyDir).dirProp).toEqual('Hello World!'); expect(tc.injector.get(ChildComp).dirProp()).toEqual(null); }); it('should support directives where a binding attribute is not given', () => { TestBed.configureTestingModule({declarations: [MyComp, MyDir]}); const template = '<p my-dir></p>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); }); it('should execute a given directive once, even if specified multiple times', () => { TestBed.configureTestingModule({ declarations: [MyComp, DuplicateDir, DuplicateDir, [DuplicateDir, [DuplicateDir]]], }); const template = '<p no-duplicate></p>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); expect(fixture.nativeElement).toHaveText('noduplicate'); }); it('should support directives where a selector matches property binding', () => { TestBed.configureTestingModule({declarations: [MyComp, IdDir]}); const template = '<p [id]="ctxProp()"></p>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); const tc = fixture.debugElement.children[0]; const idDir = tc.injector.get(IdDir); fixture.componentInstance.ctxProp.set('some_id'); fixture.detectChanges(); expect(idDir.id).toEqual('some_id'); fixture.componentInstance.ctxProp.set('other_id'); fixture.detectChanges(); expect(idDir.id).toEqual('other_id'); }); it('should support directives where a selector matches event binding', () => { TestBed.configureTestingModule({declarations: [MyComp, EventDir]}); const template = '<p (customEvent)="doNothing()"></p>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); const tc = fixture.debugElement.children[0]; expect(tc.injector.get(EventDir)).not.toBeNull(); }); it('should display correct error message for uninitialized @Output', () => { @Component({ selector: 'my-uninitialized-output', template: '<p>It works!</p>', standalone: false, }) class UninitializedOutputComp { @Output() customEvent!: EventEmitter<any>; } const template = '<my-uninitialized-output (customEvent)="doNothing()"></my-uninitialized-output>'; TestBed.overrideComponent(MyComp, {set: {template}}); TestBed.configureTestingModule({declarations: [MyComp, UninitializedOutputComp]}); expect(() => TestBed.createComponent(MyComp)).toThrowError( "@Output customEvent not initialized in 'UninitializedOutputComp'.", ); }); it('should read directives metadata from their binding token', () => { TestBed.configureTestingModule({declarations: [MyComp, PrivateImpl, NeedsPublicApi]}); const template = '<div public-api><div needs-public-api></div></div>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); }); it('should not share empty context for template directives - issue #10045', () => { TestBed.configureTestingModule({declarations: [MyComp, PollutedContext, NoContext]}); const template = '<ng-template pollutedContext let-foo="bar">{{foo}}</ng-template><ng-template noContext let-foo="bar">{{foo}}</ng-template>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); fixture.detectChanges(); expect(fixture.nativeElement).toHaveText('baz'); }); it('should not detach views in ViewContainers when the parent view is destroyed.', () => { TestBed.configureTestingModule({declarations: [MyComp, SomeViewport]}); const template = '<div *ngIf="ctxBoolProp()"><ng-template some-viewport let-greeting="someTmpl"><span>{{greeting}}</span></ng-template></div>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); fixture.componentInstance.ctxBoolProp.set(true); fixture.detectChanges(); const ngIfEl = fixture.debugElement.children[0]; const someViewport: SomeViewport = ngIfEl.childNodes .find((debugElement) => debugElement.nativeNode.nodeType === Node.COMMENT_NODE)! .injector.get(SomeViewport); expect(someViewport.container.length).toBe(2); expect(ngIfEl.children.length).toBe(2); fixture.componentInstance.ctxBoolProp.set(false); fixture.detectChanges(); expect(someViewport.container.length).toBe(2); expect(fixture.debugElement.children.length).toBe(0); }); it('should use a comment while stamping out `<ng-template>` elements.', () => { const fixture = TestBed.configureTestingModule({declarations: [MyComp]}) .overrideComponent(MyComp, {set: {template: '<ng-template></ng-template>'}}) .createComponent(MyComp); const childNodesOfWrapper = fixture.nativeElement.childNodes; expect(childNodesOfWrapper.length).toBe(1); expect(isCommentNode(childNodesOfWrapper[0])).toBe(true); }); it('should allow to transplant TemplateRefs into other ViewContainers', () => { TestBed.configureTestingModule({ declarations: [ MyComp, SomeDirective, CompWithHost, ToolbarComponent, ToolbarViewContainer, ToolbarPart, ], imports: [CommonModule], schemas: [NO_ERRORS_SCHEMA], }); const template = '<some-directive><toolbar><ng-template toolbarpart let-toolbarProp="toolbarProp">{{ctxProp()}},{{toolbarProp}},<cmp-with-host></cmp-with-host></ng-template></toolbar></some-directive>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); fixture.componentInstance.ctxProp.set('From myComp'); fixture.detectChanges(); expect(fixture.nativeElement).toHaveText( 'TOOLBAR(From myComp,From toolbar,Component with an injected host)', ); }); describe('reference bindings', () => { it('should assign a component to a ref-', () => { TestBed.configureTestingModule({declarations: [MyComp, ChildComp]}); const template = '<p><child-cmp ref-alice></child-cmp></p>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); expect(fixture.debugElement.children[0].children[0].references!['alice']).toBeInstanceOf( ChildComp, ); }); it('should assign a directive to a ref-', () => { TestBed.configureTestingModule({declarations: [MyComp, ExportDir]}); const template = '<div><div export-dir #localdir="dir"></div></div>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); expect(fixture.debugElement.children[0].children[0].references!['localdir']).toBeInstanceOf( ExportDir, ); }); it('should assign a directive to a ref when it has multiple exportAs names', () => { TestBed.configureTestingModule({ declarations: [MyComp, DirectiveWithMultipleExportAsNames], }); const template = '<div multiple-export-as #x="dirX" #y="dirY"></div>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); expect(fixture.debugElement.children[0].references!['x']).toBeInstanceOf( DirectiveWithMultipleExportAsNames, ); expect(fixture.debugElement.children[0].references!['y']).toBeInstanceOf( DirectiveWithMultipleExportAsNames, ); }); it('should make the assigned component accessible in property bindings, even if they were declared before the component', () => { TestBed.configureTestingModule({declarations: [MyComp, ChildComp]}); const template = '<ng-template [ngIf]="true">{{alice.ctxProp()}}</ng-template>|{{alice.ctxProp()}}|<child-cmp ref-alice></child-cmp>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); fixture.detectChanges(); expect(fixture.nativeElement).toHaveText('hello|hello|hello'); }); it('should assign two component instances each with a ref-', () => { TestBed.configureTestingModule({declarations: [MyComp, ChildComp]}); const template = '<p><child-cmp ref-alice></child-cmp><child-cmp ref-bob></child-cmp></p>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); const pEl = fixture.debugElement.children[0]; const alice = pEl.children[0].references!['alice']; const bob = pEl.children[1].references!['bob']; expect(alice).toBeInstanceOf(ChildComp); expect(bob).toBeInstanceOf(ChildComp); expect(alice).not.toBe(bob); }); it('should assign the component instance to a ref- with shorthand syntax', () => { TestBed.configureTestingModule({declarations: [MyComp, ChildComp]}); const template = '<child-cmp #alice></child-cmp>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); expect(fixture.debugElement.children[0].references!['alice']).toBeInstanceOf(ChildComp); }); it('should assign the element instance to a user-defined variable', () => { TestBed.configureTestingModule({declarations: [MyComp]}); const template = '<div><div ref-alice><i>Hello</i></div></div>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); const value = fixture.debugElement.children[0].children[0].references!['alice']; expect(value).not.toBe(null); expect(value.tagName.toLowerCase()).toEqual('div'); }); it('should assign the TemplateRef to a user-defined variable', () => { const fixture = TestBed.configureTestingModule({declarations: [MyComp]}) .overrideComponent(MyComp, {set: {template: '<ng-template ref-alice></ng-template>'}}) .createComponent(MyComp); const value = fixture.debugElement.childNodes[0].references!['alice']; expect(value.createEmbeddedView).toBeTruthy(); }); it('should preserve case', () => { TestBed.configureTestingModule({declarations: [MyComp, ChildComp]}); const template = '<p><child-cmp ref-superAlice></child-cmp></p>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); expect( fixture.debugElement.children[0].children[0].references!['superAlice'], ).toBeInstanceOf(ChildComp); }); }); describe('OnPush components', () => { it('should use ChangeDetectorRef to manually request a check', () => { TestBed.configureTestingModule({declarations: [MyComp, [[PushCmpWithRef]]]}); const template = '<push-cmp-with-ref #cmp></push-cmp-with-ref>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); const cmp = fixture.debugElement.children[0].references!['cmp']; fixture.detectChanges(); expect(cmp.numberOfChecks).toEqual(1); fixture.detectChanges(); expect(cmp.numberOfChecks).toEqual(1); cmp.propagate(); fixture.detectChanges(); expect(cmp.numberOfChecks).toEqual(2); }); it('should be checked when its bindings got updated', () => { TestBed.configureTestingModule({ declarations: [MyComp, PushCmp, EventCmp], imports: [CommonModule], }); const template = '<push-cmp [prop]="ctxProp()" #cmp></push-cmp>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); const cmp = fixture.debugElement.children[0].references!['cmp']; fixture.componentInstance.ctxProp.set('one'); fixture.detectChanges(); expect(cmp.numberOfChecks).toEqual(1); fixture.componentInstance.ctxProp.set('two'); fixture.detectChanges(); expect(cmp.numberOfChecks).toEqual(2); }); if (getDOM().supportsDOMEvents) { it('should allow to destroy a component from within a host event handler', fakeAsync(() => { TestBed.configureTestingModule({declarations: [MyComp, [[PushCmpWithHostEvent]]]}); const template = '<push-cmp-with-host-event></push-cmp-with-host-event>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); tick(); fixture.detectChanges(); const cmpEl = fixture.debugElement.children[0]; const cmp: PushCmpWithHostEvent = cmpEl.injector.get(PushCmpWithHostEvent); cmp.ctxCallback = (_: any) => fixture.destroy(); expect(() => cmpEl.triggerEventHandler('click', <Event>{})).not.toThrow(); })); } it('should be checked when an event is fired', () => { TestBed.configureTestingModule({ declarations: [MyComp, PushCmp, EventCmp], imports: [CommonModule], }); const template = '<push-cmp [prop]="ctxProp()" #cmp></push-cmp>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); const cmpEl = fixture.debugElement.children[0]; const cmp = cmpEl.componentInstance; fixture.detectChanges(); fixture.detectChanges(); expect(cmp.numberOfChecks).toEqual(1); // regular element cmpEl.children[0].triggerEventHandler('click', <Event>{}); fixture.detectChanges(); fixture.detectChanges(); expect(cmp.numberOfChecks).toEqual(2); // element inside of an *ngIf cmpEl.children[1].triggerEventHandler('click', <Event>{}); fixture.detectChanges(); fixture.detectChanges(); expect(cmp.numberOfChecks).toEqual(3); // element inside a nested component cmpEl.children[2].children[0].triggerEventHandler('click', <Event>{}); fixture.detectChanges(); fixture.detectChanges(); expect(cmp.numberOfChecks).toEqual(4); // host element cmpEl.triggerEventHandler('click', <Event>{}); fixture.detectChanges(); fixture.detectChanges(); expect(cmp.numberOfChecks).toEqual(5); }); it('should not affect updating properties on the component', () => { TestBed.configureTestingModule({declarations: [MyComp, [[PushCmpWithRef]]]}); const template = '<push-cmp-with-ref [prop]="ctxProp()" #cmp></push-cmp-with-ref>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); const cmp = fixture.debugElement.children[0].references!['cmp']; fixture.componentInstance.ctxProp.set('one'); fixture.detectChanges(); expect(cmp.prop).toEqual('one'); fixture.componentInstance.ctxProp.set('two'); fixture.detectChanges(); expect(cmp.prop).toEqual('two'); }); it('should be checked when an async pipe requests a check', fakeAsync(() => { TestBed.configureTestingModule({ declarations: [MyComp, PushCmpWithAsyncPipe], imports: [CommonModule], }); const template = '<push-cmp-with-async #cmp></push-cmp-with-async>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); tick(); const cmp: PushCmpWithAsyncPipe = fixture.debugElement.children[0].references!['cmp']; fixture.detectChanges(); expect(cmp.numberOfChecks).toEqual(1); fixture.detectChanges(); fixture.detectChanges(); expect(cmp.numberOfChecks).toEqual(1); cmp.resolve(2); tick(); fixture.detectChanges(); expect(cmp.numberOfChecks).toEqual(2); })); }); it('should create a component that injects an @Host', () => { TestBed.configureTestingModule({ declarations: [MyComp, SomeDirective, CompWithHost], schemas: [NO_ERRORS_SCHEMA], }); const template = ` <some-directive> <p> <cmp-with-host #child></cmp-with-host> </p> </some-directive>`; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); const childComponent = fixture.debugElement.children[0].children[0].children[0].references!['child']; expect(childComponent.myHost).toBeInstanceOf(SomeDirective); }); it('should create a component that injects an @Host through viewcontainer directive', () => { TestBed.configureTestingModule({ declarations: [MyComp, SomeDirective, CompWithHost], schemas: [NO_ERRORS_SCHEMA], }); const template = ` <some-directive> <p *ngIf="true"> <cmp-with-host #child></cmp-with-host> </p> </some-directive>`; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); fixture.detectChanges(); const tc = fixture.debugElement.children[0].children[0].children[0]; const childComponent = tc.references!['child']; expect(childComponent.myHost).toBeInstanceOf(SomeDirective); }); it('should support events via EventEmitter on regular elements', waitForAsync(() => { TestBed.configureTestingModule({ declarations: [MyComp, DirectiveEmittingEvent, DirectiveListeningEvent], }); const template = '<div emitter listener></div>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); const tc = fixture.debugElement.children[0]; const emitter = tc.injector.get(DirectiveEmittingEvent); const listener = tc.injector.get(DirectiveListeningEvent); expect(listener.msg).toEqual(''); let eventCount = 0; emitter.event.subscribe({ next: () => { eventCount++; if (eventCount === 1) { expect(listener.msg).toEqual('fired !'); fixture.destroy(); emitter.fireEvent('fired again !'); } else { expect(listener.msg).toEqual('fired !'); } }, }); emitter.fireEvent('fired !'); })); it('should support events via EventEmitter on template elements', waitForAsync(() => { const fixture = TestBed.configureTestingModule({ declarations: [MyComp, DirectiveEmittingEvent, DirectiveListeningEvent], }) .overrideComponent(MyComp, { set: { template: '<ng-template emitter listener (event)="ctxProp.set($event)"></ng-template>', }, }) .createComponent(MyComp); const tc = fixture.debugElement.childNodes.find( (debugElement) => debugElement.nativeNode.nodeType === Node.COMMENT_NODE, )!; const emitter = tc.injector.get(DirectiveEmittingEvent); const myComp = fixture.debugElement.injector.get(MyComp); const listener = tc.injector.get(DirectiveListeningEvent); myComp.ctxProp.set(''); expect(listener.msg).toEqual(''); emitter.event.subscribe({ next: () => { expect(listener.msg).toEqual('fired !'); expect(myComp.ctxProp()).toEqual('fired !'); }, }); emitter.fireEvent('fired !'); })); it('should support [()] syntax', waitForAsync(() => { TestBed.configureTestingModule({declarations: [MyComp, DirectiveWithTwoWayBinding]}); const template = '<div [(control)]="ctxProp" two-way></div>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); const tc = fixture.debugElement.children[0]; const dir = tc.injector.get(DirectiveWithTwoWayBinding); fixture.componentInstance.ctxProp.set('one'); fixture.detectChanges(); expect(dir.control).toEqual('one'); dir.controlChange.subscribe({ next: () => { expect(fixture.componentInstance.ctxProp()).toEqual('two'); }, }); dir.triggerChange('two'); })); it('should support render events', () => { TestBed.configureTestingModule({declarations: [MyComp, DirectiveListeningDomEvent]}); const template = '<div listener></div>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); const tc = fixture.debugElement.children[0]; const listener = tc.injector.get(DirectiveListeningDomEvent); dispatchEvent(tc.nativeElement, 'domEvent'); expect(listener.eventTypes).toEqual([ 'domEvent', 'body_domEvent', 'document_domEvent', 'window_domEvent', ]); fixture.destroy(); listener.eventTypes = []; dispatchEvent(tc.nativeElement, 'domEvent'); expect(listener.eventTypes).toEqual([]); }); it('should support render global events', () => { TestBed.configureTestingModule({declarations: [MyComp, DirectiveListeningDomEvent]}); const template = '<div listener></div>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); const doc = TestBed.inject(DOCUMENT); const tc = fixture.debugElement.children[0]; const listener = tc.injector.get(DirectiveListeningDomEvent); dispatchEvent(getDOM().getGlobalEventTarget(doc, 'window'), 'domEvent'); expect(listener.eventTypes).toEqual(['window_domEvent']); listener.eventTypes = []; dispatchEvent(getDOM().getGlobalEventTarget(doc, 'document'), 'domEvent'); expect(listener.eventTypes).toEqual(['document_domEvent', 'window_domEvent']); fixture.destroy(); listener.eventTypes = []; dispatchEvent(getDOM().getGlobalEventTarget(doc, 'body'), 'domEvent'); expect(listener.eventTypes).toEqual([]); }); it('should support updating host element via hostAttributes on root elements', () => { @Component({ host: {'role': 'button'}, template: '', standalone: false, }) class ComponentUpdatingHostAttributes {} TestBed.configureTestingModule({declarations: [ComponentUpdatingHostAttributes]}); const fixture = TestBed.createComponent(ComponentUpdatingHostAttributes); fixture.detectChanges(); expect(fixture.debugElement.nativeElement.getAttribute('role')).toEqual('button'); }); it('should support updating host element via hostAttributes on host elements', () => { TestBed.configureTestingModule({declarations: [MyComp, DirectiveUpdatingHostAttributes]}); const template = '<div update-host-attributes></div>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); fixture.detectChanges(); expect(fixture.debugElement.children[0].nativeElement.getAttribute('role')).toEqual('button'); }); it('should support updating host element via hostProperties', () => { TestBed.configureTestingModule({declarations: [MyComp, DirectiveUpdatingHostProperties]}); const template = '<div update-host-properties></div>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); const tc = fixture.debugElement.children[0]; const updateHost = tc.injector.get(DirectiveUpdatingHostProperties); updateHost.id = 'newId'; fixture.detectChanges(); expect(tc.nativeElement.id).toEqual('newId'); }); it('should not use template variables for expressions in hostProperties', () => { @Directive({ selector: '[host-properties]', host: {'[id]': 'id', '[title]': 'unknownProp'}, standalone: false, }) class DirectiveWithHostProps { id = 'one'; unknownProp = 'unknownProp'; } const fixture = TestBed.configureTestingModule({ declarations: [MyComp, DirectiveWithHostProps], }) .overrideComponent(MyComp, { set: {template: `<div *ngFor="let id of ['forId']" host-properties></div>`}, }) .createComponent(MyComp); fixture.detectChanges(); const tc = fixture.debugElement.children[0]; expect(tc.properties['id']).toBe('one'); expect(tc.properties['title']).toBe('unknownProp'); }); it('should not allow pipes in hostProperties', () => { @Directive({ selector: '[host-properties]', host: {'[id]': 'id | uppercase'}, standalone: false, }) class DirectiveWithHostProps {} TestBed.configureTestingModule({declarations: [MyComp, DirectiveWithHostProps]}); const template = '<div host-properties></div>'; TestBed.overrideComponent(MyComp, {set: {template}}); expect(() => TestBed.createComponent(MyComp)).toThrowError( /Host binding expression cannot contain pipes/, ); }); it('should not use template variables for expressions in hostListeners', () => { @Directive({ selector: '[host-listener]', host: {'(click)': 'doIt(id, unknownProp)'}, standalone: false, }) class DirectiveWithHostListener { id = 'one'; receivedArgs: any[] = []; doIt(...args: any[]) { this.receivedArgs = args; } } const fixture = TestBed.configureTestingModule({ declarations: [MyComp, DirectiveWithHostListener], }) .overrideComponent(MyComp, { set: {template: `<div *ngFor="let id of ['forId']" host-listener></div>`}, }) .createComponent(MyComp); fixture.detectChanges(); const tc = fixture.debugElement.children[0]; tc.triggerEventHandler('click', {}); const dir: DirectiveWithHostListener = tc.injector.get(DirectiveWithHostListener); expect(dir.receivedArgs).toEqual(['one', undefined]); }); it('should not allow pipes in hostListeners', () => { @Directive({ selector: '[host-listener]', host: {'(click)': 'doIt() | somePipe'}, standalone: false, }) class DirectiveWithHostListener {} TestBed.configureTestingModule({declarations: [MyComp, DirectiveWithHostListener]}); const template = '<div host-listener></div>'; TestBed.overrideComponent(MyComp, {set: {template}}); expect(() => TestBed.createComponent(MyComp)).toThrowError( /Cannot have a pipe in an action expression/, ); }); if (getDOM().supportsDOMEvents) { it('should support preventing default on render events', () => { TestBed.configureTestingModule({ declarations: [ MyComp, DirectiveListeningDomEventPrevent, DirectiveListeningDomEventNoPrevent, ], }); const template = '<input type="checkbox" listenerprevent><input type="checkbox" listenernoprevent>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); const dispatchedEvent = createMouseEvent('click'); const dispatchedEvent2 = createMouseEvent('click'); getDOM().dispatchEvent(fixture.debugElement.children[0].nativeElement, dispatchedEvent); getDOM().dispatchEvent(fixture.debugElement.children[1].nativeElement, dispatchedEvent2); expect(isPrevented(dispatchedEvent)).toBe(true); expect(isPrevented(dispatchedEvent2)).toBe(false); expect(fixture.debugElement.children[0].nativeElement.checked).toBeFalsy(); expect(fixture.debugElement.children[1].nativeElement.checked).toBeTruthy(); }); } it('should support render global events from multiple directives', () => { TestBed.configureTestingModule({ declarations: [MyComp, DirectiveListeningDomEvent, DirectiveListeningDomEventOther], }); const template = '<div *ngIf="ctxBoolProp()" listener listenerother></div>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); const doc = TestBed.inject(DOCUMENT); globalCounter = 0; fixture.componentInstance.ctxBoolProp.set(true); fixture.detectChanges(); const tc = fixture.debugElement.children[0]; const listener = tc.injector.get(DirectiveListeningDomEvent); const listenerother = tc.injector.get(DirectiveListeningDomEventOther); dispatchEvent(getDOM().getGlobalEventTarget(doc, 'window'), 'domEvent'); expect(listener.eventTypes).toEqual(['window_domEvent']); expect(listenerother.eventType).toEqual('other_domEvent'); expect(globalCounter).toEqual(1); fixture.componentInstance.ctxBoolProp.set(false); fixture.detectChanges(); dispatchEvent(getDOM().getGlobalEventTarget(doc, 'window'), 'domEvent'); expect(globalCounter).toEqual(1); fixture.componentInstance.ctxBoolProp.set(true); fixture.detectChanges(); dispatchEvent(getDOM().getGlobalEventTarget(doc, 'window'), 'domEvent'); expect(globalCounter).toEqual(2); // need to destroy to release all remaining global event listeners fixture.destroy(); }); describe('ViewContainerRef', () => { beforeEach(() => { TestBed.configureTestingModule({ declarations: [MyComp, DynamicViewport, ChildCompUsingService], }); TestBed.overrideComponent(MyComp, { add: {template: '<div><dynamic-vp #dynamic></dynamic-vp></div>'}, }); }); describe('.createComponent', () => { it('should allow to create a component at any bound location', waitForAsync(() => { const fixture = TestBed.configureTestingModule({ schemas: [NO_ERRORS_SCHEMA], }).createComponent(MyComp); const tc = fixture.debugElement.children[0].children[0]; const dynamicVp: DynamicViewport = tc.injector.get(DynamicViewport); dynamicVp.create(); fixture.detectChanges(); expect(fixture.debugElement.children[0].children[1].nativeElement).toHaveText( 'dynamic greet', ); })); it('should allow to create multiple components at a location', waitForAsync(() => { const fixture = TestBed.configureTestingModule({ schemas: [NO_ERRORS_SCHEMA], }).createComponent(MyComp); const tc = fixture.debugElement.children[0].children[0]; const dynamicVp: DynamicViewport = tc.injector.get(DynamicViewport); dynamicVp.create(); dynamicVp.create(); fixture.detectChanges(); expect(fixture.debugElement.children[0].children[1].nativeElement).toHaveText( 'dynamic greet', ); expect(fixture.debugElement.children[0].children[2].nativeElement).toHaveText( 'dynamic greet', ); })); it('should create a component with the passed NgModuleRef', () => { @Component({ template: '', standalone: false, }) class RootComp { constructor(public vc: ViewContainerRef) {} } @Component({ template: '', standalone: false, }) class MyComp { constructor(@Inject('someToken') public someToken: string) {} } @NgModule({ declarations: [RootComp, MyComp], providers: [{provide: 'someToken', useValue: 'someRootValue'}], }) class RootModule {} @NgModule({providers: [{provide: 'someToken', useValue: 'someValue'}]}) class MyModule {} const compFixture = TestBed.configureTestingModule({ imports: [RootModule], }).createComponent(RootComp); const compiler = TestBed.inject(Compiler); const myModule = compiler .compileModuleSync(MyModule) .create(TestBed.inject(NgModuleRef).injector); // Note: MyComp was declared as entryComponent in the RootModule, // but we pass MyModule to the createComponent call. // -> expect the providers of MyModule! const compRef = compFixture.componentInstance.vc.createComponent(MyComp, { ngModuleRef: myModule, }); expect(compRef.instance.someToken).toBe('someValue'); }); }); describe('.insert', () => { it('should throw with destroyed views', waitForAsync(() => { const fixture = TestBed.configureTestingModule({ schemas: [NO_ERRORS_SCHEMA], }).createComponent(MyComp); const tc = fixture.debugElement.children[0].children[0]; const dynamicVp: DynamicViewport = tc.injector.get(DynamicViewport); const ref = dynamicVp.create(); fixture.detectChanges(); ref.destroy(); expect(() => { dynamicVp.insert(ref.hostView); }).toThrowError(/Cannot insert a destroyed View in a ViewContainer!/); })); }); describe('.move', () => { it('should throw with destroyed views', waitForAsync(() => { const fixture = TestBed.configureTestingModule({ schemas: [NO_ERRORS_SCHEMA], }).createComponent(MyComp); const tc = fixture.debugElement.children[0].children[0]; const dynamicVp: DynamicViewport = tc.injector.get(DynamicViewport); const ref = dynamicVp.create(); fixture.detectChanges(); ref.destroy(); expect(() => { dynamicVp.move(ref.hostView, 1); }).toThrowError(/Cannot move a destroyed View in a ViewContainer!/); })); }); }); it('should support static attributes', () => { TestBed.configureTestingModule({declarations: [MyComp, NeedsAttribute]}); const template = '<input static type="text" title>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); const tc = fixture.debugElement.children[0]; const needsAttribute = tc.injector.get(NeedsAttribute); expect(needsAttribute.typeAttribute).toEqual('text'); expect(needsAttribute.staticAttribute).toEqual(''); expect(needsAttribute.fooAttribute).toBeNull(); }); }); describe('dependency injection', () => { it('should support bindings', () => { TestBed.configureTestingModule({ declarations: [MyComp, DirectiveProvidingInjectable, DirectiveConsumingInjectable], schemas: [NO_ERRORS_SCHEMA], }); const template = ` <directive-providing-injectable > <directive-consuming-injectable #consuming> </directive-consuming-injectable> </directive-providing-injectable> `; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); const comp = fixture.debugElement.children[0].children[0].references['consuming']; expect(comp.injectable).toBeInstanceOf(InjectableService); }); it('should support viewProviders', () => { TestBed.configureTestingModule({ declarations: [MyComp, DirectiveProvidingInjectableInView, DirectiveConsumingInjectable], schemas: [NO_ERRORS_SCHEMA], }); const template = ` <directive-consuming-injectable #consuming> </directive-consuming-injectable> `; TestBed.overrideComponent(DirectiveProvidingInjectableInView, {set: {template}}); const fixture = TestBed.createComponent(DirectiveProvidingInjectableInView); const comp = fixture.debugElement.children[0].references['consuming']; expect(comp.injectable).toBeInstanceOf(InjectableService); }); it('should support unbounded lookup', () => { TestBed.configureTestingModule({ declarations: [ MyComp, DirectiveProvidingInjectable, DirectiveContainingDirectiveConsumingAnInjectable, DirectiveConsumingInjectableUnbounded, ], schemas: [NO_ERRORS_SCHEMA], }); const template = ` <directive-providing-injectable> <directive-containing-directive-consuming-an-injectable #dir> </directive-containing-directive-consuming-an-injectable> </directive-providing-injectable> `; TestBed.overrideComponent(MyComp, {set: {template}}); TestBed.overrideComponent(DirectiveContainingDirectiveConsumingAnInjectable, { set: { template: ` <directive-consuming-injectable-unbounded></directive-consuming-injectable-unbounded> `, }, }); const fixture = TestBed.createComponent(MyComp); const comp = fixture.debugElement.children[0].children[0].references['dir']; expect(comp.directive.injectable).toBeInstanceOf(InjectableService); }); it('should support the event-bus scenario', () => { TestBed.configureTestingModule({ declarations: [ MyComp, GrandParentProvidingEventBus, ParentProvidingEventBus, ChildConsumingEventBus, ], schemas: [NO_ERRORS_SCHEMA], }); const template = ` <grand-parent-providing-event-bus> <parent-providing-event-bus> <child-consuming-event-bus> </child-consuming-event-bus> </parent-providing-event-bus> </grand-parent-providing-event-bus> `; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); const gpComp = fixture.debugElement.children[0]; const parentComp = gpComp.children[0]; const childComp = parentComp.children[0]; const grandParent = gpComp.injector.get(GrandParentProvidingEventBus); const parent = parentComp.injector.get(ParentProvidingEventBus); const child = childComp.injector.get(ChildConsumingEventBus); expect(grandParent.bus.name).toEqual('grandparent'); expect(parent.bus.name).toEqual('parent'); expect(parent.grandParentBus).toBe(grandParent.bus); expect(child.bus).toBe(parent.bus); }); it('should instantiate bindings lazily', () => { TestBed.configureTestingModule({ declarations: [MyComp, DirectiveConsumingInjectable, ComponentProvidingLoggingInjectable], schemas: [NO_ERRORS_SCHEMA], }); const template = ` <component-providing-logging-injectable #providing> <directive-consuming-injectable *ngIf="ctxBoolProp()"> </directive-consuming-injectable> </component-providing-logging-injectable> `; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); const providing = fixture.debugElement.children[0].references['providing']; expect(providing.created).toBe(false); fixture.componentInstance.ctxBoolProp.set(true); fixture.detectChanges(); expect(providing.created).toBe(true); }); }); describe('corner cases', () => { it('should remove script tags from templates', () => { TestBed.configureTestingModule({declarations: [MyComp]}); const template = ` <script>alert("Ooops");</script> <div>before<script>alert("Ooops");</script><span>inside</span>after</div>`; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); expect(fixture.nativeElement.querySelectorAll('script').length).toEqual(0); }); it('should throw when using directives without selector in NgModule declarations', () => { @Directive({ standalone: false, }) class SomeDirective {} @Component({ selector: 'comp', template: '', standalone: false, }) class SomeComponent {} TestBed.configureTestingModule({declarations: [MyComp, SomeDirective, SomeComponent]}); expect(() => TestBed.createComponent(MyComp)).toThrowError( `Directive ${stringify(SomeDirective)} has no selector, please add it!`, ); }); it('should not throw when using directives without selector as base class not in declarations', () => { @Directive({ standalone: false, }) abstract class Base { constructor(readonly injector: Injector) {} } @Directive() abstract class EmptyDir {} @Directive({ inputs: ['a', 'b'], standalone: false, }) class TestDirWithInputs {} @Component({ selector: 'comp', template: '', standalone: false, }) class SomeComponent extends Base {} @Component({ selector: 'comp2', template: '', standalone: false, }) class SomeComponent2 extends EmptyDir {} @Component({ selector: 'comp3', template: '', standalone: false, }) class SomeComponent3 extends TestDirWithInputs {} TestBed.configureTestingModule({ declarations: [MyComp, SomeComponent, SomeComponent2, SomeComponent3], }); expect(() => TestBed.createComponent(MyComp)).not.toThrowError(); }); it('should throw when using directives with empty string selector', () => { @Directive({ selector: '', standalone: false, }) class SomeDirective {} @Component({ selector: 'comp', template: '', standalone: false, }) class SomeComponent {} TestBed.configureTestingModule({declarations: [MyComp, SomeDirective, SomeComponent]}); expect(() => TestBed.createComponent(MyComp)).toThrowError( `Directive ${stringify(SomeDirective)} has no selector, please add it!`, ); }); it('should use a default element name for components without selectors', () => { @Component({ template: '----', standalone: false, }) class NoSelectorComponent {} expect(reflectComponentType(NoSelectorComponent)?.selector).toBe('ng-component'); expect( createComponent(NoSelectorComponent, { environmentInjector: TestBed.inject(EnvironmentInjector), }).location.nativeElement.nodeName.toLowerCase(), ).toEqual('ng-component'); }); }); describe('error handling', () => { it('should report a meaningful error when a directive is missing annotation', () => { TestBed.configureTestingModule({declarations: [MyComp, SomeDirectiveMissingAnnotation]}); expect(() => TestBed.createComponent(MyComp)).toThrowError( `Unexpected value '${stringify( SomeDirectiveMissingAnnotation, )}' declared by the module 'DynamicTestModule'. Please add a @Pipe/@Directive/@Component annotation.`, ); }); it('should report a meaningful error when a component is missing view annotation', () => { TestBed.configureTestingModule({declarations: [MyComp, ComponentWithoutView]}); try { TestBed.createComponent(ComponentWithoutView); } catch (e) { expect((e as Error).message).toContain( `No template specified for component ${stringify(ComponentWithoutView)}`, ); } }); }); it('should support imperative views', () => { TestBed.configureTestingModule({declarations: [MyComp, SimpleImperativeViewComponent]}); const template = '<simple-imp-cmp></simple-imp-cmp>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); expect(fixture.nativeElement).toHaveText('hello imp view'); }); it('should support moving embedded views around', () => { TestBed.configureTestingModule({ declarations: [MyComp, SomeImperativeViewport], providers: [{provide: ANCHOR_ELEMENT, useValue: el('<div></div>')}], }); const template = '<div><div *someImpvp="ctxBoolProp()">hello</div></div>'; TestBed.overrideComponent(MyComp, {set: {template}}); const anchorElement = getTestBed().inject(ANCHOR_ELEMENT); const fixture = TestBed.createComponent(MyComp); fixture.detectChanges(); expect(anchorElement).toHaveText(''); fixture.componentInstance.ctxBoolProp.set(true); fixture.detectChanges(); expect(anchorElement).toHaveText('hello'); fixture.componentInstance.ctxBoolProp.set(false); fixture.detectChanges(); expect(fixture.nativeElement).toHaveText(''); }); describe('moving embedded views of projectable nodes in a dynamic component', () => { @Component({ selector: 'menu-item', template: '', standalone: false, }) class DynamicMenuItem { @ViewChild('templateRef', {static: true}) templateRef!: TemplateRef<any>; itemContent: string | undefined; } @Component({ selector: 'test', template: `<ng-container #menuItemsContainer></ng-container>`, standalone: false, }) class TestCmp { @ViewChild('menuItemsContainer', {static: true, read: ViewContainerRef}) menuItemsContainer!: ViewContainerRef; } beforeEach(() => { TestBed.configureTestingModule({declarations: [TestCmp, DynamicMenuItem]}); }); const createElWithContent = (content: string, tagName = 'span') => { const element = document.createElement(tagName); element.textContent = content; return element; }; it('should support moving embedded views of projectable nodes', () => { TestBed.overrideTemplate( DynamicMenuItem, `<ng-template #templateRef><ng-content></ng-content></ng-template>`, ); const fixture = TestBed.createComponent(TestCmp); const menuItemsContainer = fixture.componentInstance.menuItemsContainer; const cmptRefWithAa = createComponent(DynamicMenuItem, { environmentInjector: TestBed.inject(EnvironmentInjector), projectableNodes: [[createElWithContent('Aa')]], }); const cmptRefWithBb = createComponent(DynamicMenuItem, { environmentInjector: TestBed.inject(EnvironmentInjector), projectableNodes: [[createElWithContent('Bb')]], }); const cmptRefWithCc = createComponent(DynamicMenuItem, { environmentInjector: TestBed.inject(EnvironmentInjector), projectableNodes: [[createElWithContent('Cc')]], }); menuItemsContainer.insert(cmptRefWithAa.instance.templateRef.createEmbeddedView({})); menuItemsContainer.insert(cmptRefWithBb.instance.templateRef.createEmbeddedView({})); menuItemsContainer.insert(cmptRefWithCc.instance.templateRef.createEmbeddedView({})); menuItemsContainer.move(menuItemsContainer.get(0)!, 1); expect(fixture.nativeElement.textContent).toBe('BbAaCc'); menuItemsContainer.move(menuItemsContainer.get(2)!, 1); expect(fixture.nativeElement.textContent).toBe('BbCcAa'); }); it('should support moving embedded views of projectable nodes in multiple slots', () => { TestBed.overrideTemplate( DynamicMenuItem, `<ng-template #templateRef><ng-content select="span"></ng-content><ng-content select="button"></ng-content></ng-template>`, ); const fixture = TestBed.createComponent(TestCmp); const menuItemsContainer = fixture.componentInstance.menuItemsContainer; const cmptRefWithAa = createComponent(DynamicMenuItem, { environmentInjector: TestBed.inject(EnvironmentInjector), projectableNodes: [[createElWithContent('A')], [createElWithContent('a', 'button')]], }); const cmptRefWithBb = createComponent(DynamicMenuItem, { environmentInjector: TestBed.inject(EnvironmentInjector), projectableNodes: [[createElWithContent('B')], [createElWithContent('b', 'button')]], }); const cmptRefWithCc = createComponent(DynamicMenuItem, { environmentInjector: TestBed.inject(EnvironmentInjector), projectableNodes: [[createElWithContent('C')], [createElWithContent('c', 'button')]], }); menuItemsContainer.insert(cmptRefWithAa.instance.templateRef.createEmbeddedView({})); menuItemsContainer.insert(cmptRefWithBb.instance.templateRef.createEmbeddedView({})); menuItemsContainer.insert(cmptRefWithCc.instance.templateRef.createEmbeddedView({})); menuItemsContainer.move(menuItemsContainer.get(0)!, 1); expect(fixture.nativeElement.textContent).toBe('BbAaCc'); menuItemsContainer.move(menuItemsContainer.get(2)!, 1); expect(fixture.nativeElement.textContent).toBe('BbCcAa'); }); it('should support moving embedded views of projectable nodes in multiple slots and interpolations', () => { TestBed.overrideTemplate( DynamicMenuItem, `<ng-template #templateRef><ng-content select="span"></ng-content>{{itemContent}}<ng-content select="button"></ng-content></ng-template>`, ); TestBed.configureTestingModule({declarations: [TestCmp, DynamicMenuItem]}); const fixture = TestBed.createComponent(TestCmp); const menuItemsContainer = fixture.componentInstance.menuItemsContainer; const cmptRefWithAa = createComponent(DynamicMenuItem, { environmentInjector: TestBed.inject(EnvironmentInjector), projectableNodes: [[createElWithContent('A')], [createElWithContent('a', 'button')]], }); const cmptRefWithBb = createComponent(DynamicMenuItem, { environmentInjector: TestBed.inject(EnvironmentInjector), projectableNodes: [[createElWithContent('B')], [createElWithContent('b', 'button')]], }); const cmptRefWithCc = createComponent(DynamicMenuItem, { environmentInjector: TestBed.inject(EnvironmentInjector), projectableNodes: [[createElWithContent('C')], [createElWithContent('c', 'button')]], }); menuItemsContainer.insert(cmptRefWithAa.instance.templateRef.createEmbeddedView({})); menuItemsContainer.insert(cmptRefWithBb.instance.templateRef.createEmbeddedView({})); menuItemsContainer.insert(cmptRefWithCc.instance.templateRef.createEmbeddedView({})); cmptRefWithAa.instance.itemContent = '0'; cmptRefWithBb.instance.itemContent = '1'; cmptRefWithCc.instance.itemContent = '2'; fixture.detectChanges(); menuItemsContainer.move(menuItemsContainer.get(0)!, 1); expect(fixture.nativeElement.textContent).toBe('B1bA0aC2c'); menuItemsContainer.move(menuItemsContainer.get(2)!, 1); expect(fixture.nativeElement.textContent).toBe('B1bC2cA0a'); }); it('should support moving embedded views with empty projectable slots', () => { TestBed.overrideTemplate( DynamicMenuItem, `<ng-template #templateRef><ng-content></ng-content></ng-template>`, ); const fixture = TestBed.createComponent(TestCmp); const menuItemsContainer = fixture.componentInstance.menuItemsContainer; const cmptRefWithAa = createComponent(DynamicMenuItem, { environmentInjector: TestBed.inject(EnvironmentInjector), projectableNodes: [[]], }); const cmptRefWithBb = createComponent(DynamicMenuItem, { environmentInjector: TestBed.inject(EnvironmentInjector), projectableNodes: [[createElWithContent('Bb')]], }); const cmptRefWithCc = createComponent(DynamicMenuItem, { environmentInjector: TestBed.inject(EnvironmentInjector), projectableNodes: [[createElWithContent('Cc')]], }); menuItemsContainer.insert(cmptRefWithAa.instance.templateRef.createEmbeddedView({})); menuItemsContainer.insert(cmptRefWithBb.instance.templateRef.createEmbeddedView({})); menuItemsContainer.insert(cmptRefWithCc.instance.templateRef.createEmbeddedView({})); menuItemsContainer.move(menuItemsContainer.get(0)!, 1); // [ Bb, NULL, Cc] expect(fixture.nativeElement.textContent).toBe('BbCc'); menuItemsContainer.move(menuItemsContainer.get(2)!, 1); // [ Bb, Cc, NULL] expect(fixture.nativeElement.textContent).toBe('BbCc'); menuItemsContainer.move(menuItemsContainer.get(0)!, 1); // [ Cc, Bb, NULL] expect(fixture.nativeElement.textContent).toBe('CcBb'); }); }); describe('Property bindings', () => { it('should throw on bindings to unknown properties', () => { TestBed.configureTestingModule({declarations: [MyComp]}); const template = '<div unknown="{{ctxProp()}}"></div>'; TestBed.overrideComponent(MyComp, {set: {template}}); const spy = spyOn(console, 'error'); const fixture = TestBed.createComponent(MyComp); fixture.detectChanges(); expect(spy.calls.mostRecent().args[0]).toMatch( /Can't bind to 'unknown' since it isn't a known property of 'div'./, ); }); it('should throw on bindings to unknown properties (micro-syntax)', () => { TestBed.configureTestingModule({imports: [CommonModule], declarations: [MyComp]}); const template = '<div *ngFor="let item in ctxArrProp">{{item}}</div>'; TestBed.overrideComponent(MyComp, {set: {template}}); const spy = spyOn(console, 'error'); const fixture = TestBed.createComponent(MyComp); fixture.detectChanges(); expect(spy.calls.mostRecent().args[0]).toMatch( /Can't bind to 'ngForIn' since it isn't a known property of 'div'./, ); }); it('should not throw for property binding to a non-existing property when there is a matching directive property', () => { TestBed.configureTestingModule({declarations: [MyComp, MyDir]}); const template = '<div my-dir [elprop]="ctxProp()"></div>'; TestBed.overrideComponent(MyComp, {set: {template}}); expect(() => TestBed.createComponent(MyComp)).not.toThrow(); }); it('should not be created when there is a directive with the same property', () => { TestBed.configureTestingModule({declarations: [MyComp, DirectiveWithTitle]}); const template = '<span [title]="ctxProp()"></span>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); fixture.componentInstance.ctxProp.set('TITLE'); fixture.detectChanges(); const el = fixture.nativeElement.querySelector('span'); expect(el.title).toBeFalsy(); }); it('should work when a directive uses hostProperty to update the DOM element', () => { TestBed.configureTestingModule({declarations: [MyComp, DirectiveWithTitleAndHostProperty]}); const template = '<span [title]="ctxProp()"></span>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); fixture.componentInstance.ctxProp.set('TITLE'); fixture.detectChanges(); const el = fixture.nativeElement.querySelector('span'); expect(el.title).toEqual('TITLE'); }); }); describe('logging property updates', () => { describe('by default, when provideNgReflectAttributes() is not provided', () => { it('should not reflect properties', () => { TestBed.configureTestingModule({ declarations: [MyComp, MyDir], }); TestBed.overrideComponent(MyComp, { set: {template: `<div my-dir [elprop]="ctxProp()"></div>`}, }); const fixture = TestBed.createComponent(MyComp); fixture.componentInstance.ctxProp.set('hello'); fixture.detectChanges(); const html = fixture.nativeElement.innerHTML; expect(html).not.toContain('ng-reflect'); }); it('should not reflect property values on template comments', () => { const fixture = TestBed.configureTestingModule({ declarations: [MyComp], }) .overrideComponent(MyComp, { set: {template: `<ng-template [ngIf]="ctxBoolProp()"></ng-template>`}, }) .createComponent(MyComp); fixture.componentInstance.ctxBoolProp.set(true); fixture.detectChanges(); const html = fixture.nativeElement.innerHTML; expect(html).not.toContain('ng-reflect'); }); }); it('should reflect property values as attributes', () => { TestBed.configureTestingModule({ declarations: [MyComp, MyDir], providers: [provideNgReflectAttributes()], }); TestBed.overrideComponent(MyComp, { set: {template: `<div my-dir [elprop]="ctxProp()"></div>`}, }); const fixture = TestBed.createComponent(MyComp); fixture.componentInstance.ctxProp.set('hello'); fixture.detectChanges(); const html = fixture.nativeElement.innerHTML; expect(html).toContain('ng-reflect-dir-prop="hello"'); }); it('should reflect property values on unbound inputs', () => { TestBed.configureTestingModule({ declarations: [MyComp, MyDir], providers: [provideNgReflectAttributes()], }); TestBed.overrideComponent(MyComp, { set: {template: `<div my-dir elprop="hello" title="Reflect test"></div>`}, }); const fixture = TestBed.createComponent(MyComp); fixture.detectChanges(); const html = fixture.nativeElement.innerHTML; expect(html).toContain('ng-reflect-dir-prop="hello"'); expect(html).not.toContain('ng-reflect-title'); }); it(`should work with prop names containing '$'`, () => { TestBed.configureTestingModule({ declarations: [ParentCmp, SomeCmpWithInput], providers: [provideNgReflectAttributes()], }); const fixture = TestBed.createComponent(ParentCmp); fixture.detectChanges(); const html = fixture.nativeElement.innerHTML; expect(html).toContain('ng-reflect-test_="hello"'); }); it('should reflect property values on template comments', () => { const fixture = TestBed.configureTestingModule({ declarations: [MyComp], providers: [provideNgReflectAttributes()], }) .overrideComponent(MyComp, { set: {template: `<ng-template [ngIf]="ctxBoolProp()"></ng-template>`}, }) .createComponent(MyComp); fixture.componentInstance.ctxBoolProp.set(true); fixture.detectChanges(); const html = fixture.nativeElement.innerHTML; expect(html).toContain('"ng-reflect-ng-if": "true"'); }); it('should reflect property values on ng-containers', () => { const fixture = TestBed.configureTestingModule({ declarations: [MyComp], providers: [provideNgReflectAttributes()], }) .overrideComponent(MyComp, { set: {template: `<ng-container *ngIf="ctxBoolProp()">content</ng-container>`}, }) .createComponent(MyComp); fixture.componentInstance.ctxBoolProp.set(true); fixture.detectChanges(); const html = fixture.nativeElement.innerHTML; expect(html).toContain('"ng-reflect-ng-if": "true"'); }); it('should reflect property values of multiple directive bound to the same input name', () => { TestBed.configureTestingModule({ declarations: [MyComp, MyDir, MyDir2], providers: [provideNgReflectAttributes()], }); TestBed.overrideComponent(MyComp, { set: {template: `<div my-dir my-dir2 [elprop]="ctxProp()"></div>`}, }); const fixture = TestBed.createComponent(MyComp); fixture.componentInstance.ctxProp.set('hello'); fixture.detectChanges(); const html = fixture.nativeElement.innerHTML; expect(html).toContain('ng-reflect-dir-prop="hello"'); expect(html).toContain('ng-reflect-dir-prop2="hello"'); }); it('should indicate when toString() throws', () => { TestBed.configureTestingModule({ declarations: [MyComp, MyDir], providers: [provideNgReflectAttributes()], }); const template = '<div my-dir [elprop]="toStringThrow"></div>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toContain('[ERROR]'); }); it('should not reflect undefined values', () => { TestBed.configureTestingModule({ declarations: [MyComp, MyDir, MyDir2], providers: [provideNgReflectAttributes()], }); TestBed.overrideComponent(MyComp, { set: {template: `<div my-dir [elprop]="ctxProp()"></div>`}, }); const fixture = TestBed.createComponent(MyComp); fixture.componentInstance.ctxProp.set('hello'); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toContain('ng-reflect-dir-prop="hello"'); fixture.componentInstance.ctxProp.set(undefined!); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).not.toContain('ng-reflect-'); }); it('should not reflect null values', () => { TestBed.configureTestingModule({ declarations: [MyComp, MyDir, MyDir2], providers: [provideNgReflectAttributes()], }); TestBed.overrideComponent(MyComp, { set: {template: `<div my-dir [elprop]="ctxProp()"></div>`}, }); const fixture = TestBed.createComponent(MyComp); fixture.componentInstance.ctxProp.set('hello'); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toContain('ng-reflect-dir-prop="hello"'); fixture.componentInstance.ctxProp.set(null!); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).not.toContain('ng-reflect-'); }); it('should reflect empty strings', () => { TestBed.configureTestingModule({ declarations: [MyComp, MyDir, MyDir2], providers: [provideNgReflectAttributes()], }); TestBed.overrideComponent(MyComp, { set: {template: `<div my-dir [elprop]="ctxProp()"></div>`}, }); const fixture = TestBed.createComponent(MyComp); fixture.componentInstance.ctxProp.set(''); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toContain('ng-reflect-dir-prop=""'); }); it('should not reflect in comment nodes when the value changes to undefined', () => { const fixture = TestBed.configureTestingModule({ declarations: [MyComp], providers: [provideNgReflectAttributes()], }) .overrideComponent(MyComp, { set: {template: `<ng-template [ngIf]="ctxBoolProp()"></ng-template>`}, }) .createComponent(MyComp); fixture.componentInstance.ctxBoolProp.set(true); fixture.detectChanges(); let html = fixture.nativeElement.innerHTML; expect(html).toContain('bindings={'); expect(html).toContain('"ng-reflect-ng-if": "true"'); fixture.componentInstance.ctxBoolProp.set(undefined!); fixture.detectChanges(); html = fixture.nativeElement.innerHTML; expect(html).toContain('bindings={'); expect(html).not.toContain('ng-reflect'); }); it('should reflect in comment nodes when the value changes to null', () => { const fixture = TestBed.configureTestingModule({ declarations: [MyComp], providers: [provideNgReflectAttributes()], }) .overrideComponent(MyComp, { set: {template: `<ng-template [ngIf]="ctxBoolProp()"></ng-template>`}, }) .createComponent(MyComp); fixture.componentInstance.ctxBoolProp.set(true); fixture.detectChanges(); let html = fixture.nativeElement.innerHTML; expect(html).toContain('bindings={'); expect(html).toContain('"ng-reflect-ng-if": "true"'); fixture.componentInstance.ctxBoolProp.set(null!); fixture.detectChanges(); html = fixture.nativeElement.innerHTML; expect(html).toContain('bindings={'); expect(html).toContain('"ng-reflect-ng-if": null'); }); }); describe('property decorators', () => { it('should support property decorators', () => { TestBed.configureTestingModule({ declarations: [MyComp, DirectiveWithPropDecorators], schemas: [NO_ERRORS_SCHEMA], }); const template = '<with-prop-decorators elProp="aaa"></with-prop-decorators>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); fixture.detectChanges(); const dir = fixture.debugElement.children[0].injector.get(DirectiveWithPropDecorators); expect(dir.dirProp).toEqual('aaa'); }); it('should support host binding decorators', () => { TestBed.configureTestingModule({ declarations: [MyComp, DirectiveWithPropDecorators], schemas: [NO_ERRORS_SCHEMA], }); const template = '<with-prop-decorators></with-prop-decorators>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); fixture.detectChanges(); const dir = fixture.debugElement.children[0].injector.get(DirectiveWithPropDecorators); dir.myAttr = 'aaa'; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.debugElement.children[0].nativeElement.outerHTML).toContain('my-attr="aaa"'); }); if (getDOM().supportsDOMEvents) { it('should support event decorators', fakeAsync(() => { TestBed.configureTestingModule({ declarations: [MyComp, DirectiveWithPropDecorators], schemas: [NO_ERRORS_SCHEMA], }); const template = `<with-prop-decorators (elEvent)="ctxProp.set('called')">`; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); tick(); const emitter = fixture.debugElement.children[0].injector.get(DirectiveWithPropDecorators); emitter.fireEvent('fired !'); tick(); expect(fixture.componentInstance.ctxProp()).toEqual('called'); })); it('should support host listener decorators', () => { TestBed.configureTestingModule({ declarations: [MyComp, DirectiveWithPropDecorators], schemas: [NO_ERRORS_SCHEMA], }); const template = '<with-prop-decorators></with-prop-decorators>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); fixture.detectChanges(); const dir = fixture.debugElement.children[0].injector.get(DirectiveWithPropDecorators); const native = fixture.debugElement.children[0].nativeElement; getDOM().dispatchEvent(native, createMouseEvent('click')); expect(dir.target).toBe(native); }); } it('should support defining views in the component decorator', () => { TestBed.configureTestingModule({ declarations: [MyComp, ComponentWithTemplate], imports: [CommonModule], schemas: [NO_ERRORS_SCHEMA], }); const template = '<component-with-template></component-with-template>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); fixture.detectChanges(); const native = fixture.debugElement.children[0].nativeElement; expect(native).toHaveText('No View Decorator: 123'); }); }); describe('whitespaces in templates', () => { it('should not remove whitespaces by default', waitForAsync(() => { @Component({ selector: 'comp', template: '<span>foo</span> <span>bar</span>', standalone: false, }) class MyCmp {} const f = TestBed.configureTestingModule({declarations: [MyCmp]}).createComponent(MyCmp); f.detectChanges(); expect(f.nativeElement.childNodes.length).toBe(2); })); it('should not remove whitespaces when explicitly requested not to do so', waitForAsync(() => { @Component({ selector: 'comp', template: '<span>foo</span> <span>bar</span>', preserveWhitespaces: true, standalone: false, }) class MyCmp {} const f = TestBed.configureTestingModule({declarations: [MyCmp]}).createComponent(MyCmp); f.detectChanges(); expect(f.nativeElement.childNodes.length).toBe(3); })); it('should remove whitespaces when explicitly requested to do so', waitForAsync(() => { @Component({ selector: 'comp', template: '<span>foo</span> <span>bar</span>', preserveWhitespaces: false, standalone: false, }) class MyCmp {} const f = TestBed.configureTestingModule({declarations: [MyCmp]}).createComponent(MyCmp); f.detectChanges(); expect(f.nativeElement.childNodes.length).toBe(2); })); }); describe('orphan components', () => { it('should display correct error message for orphan component if forbidOrphanRendering option is set', () => { @Component({ template: '...', standalone: false, }) class MainComp {} ɵsetClassDebugInfo(MainComp, { className: 'MainComp', filePath: 'test.ts', lineNumber: 11, forbidOrphanRendering: true, }); TestBed.configureTestingModule({declarations: [MainComp]}); expect(() => TestBed.createComponent(MainComp)).toThrowError( /^NG0981: Orphan component found\! Trying to render the component MainComp \(at test\.ts:11\) without first loading the NgModule that declares it/, ); }); it('should not throw error for orphan component if forbidOrphanRendering option is not set', () => { @Component({ template: '...', standalone: false, }) class MainComp {} ɵsetClassDebugInfo(MainComp, { className: 'MainComp', filePath: 'test.ts', lineNumber: 11, }); TestBed.configureTestingModule({declarations: [MainComp]}); expect(() => TestBed.createComponent(MainComp)).not.toThrow(); }); }); if (getDOM().supportsDOMEvents) { describe('svg', () => { it('should support svg elements', () => { TestBed.configureTestingModule({declarations: [MyComp]}); const template = '<svg><use xlink:href="Port" /></svg>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); const el = fixture.nativeElement; const svg = el.childNodes[0]; const use = svg.childNodes[0]; expect(svg.namespaceURI).toEqual('http://www.w3.org/2000/svg'); expect(use.namespaceURI).toEqual('http://www.w3.org/2000/svg'); const firstAttribute = use.attributes[0]; expect(firstAttribute.name).toEqual('xlink:href'); expect(firstAttribute.namespaceURI).toEqual('http://www.w3.org/1999/xlink'); }); it('should support foreignObjects with document fragments', () => { TestBed.configureTestingModule({declarations: [MyComp]}); const template = '<svg><foreignObject><xhtml:div><p>Test</p></xhtml:div></foreignObject></svg>'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); const el = fixture.nativeElement; const svg = el.childNodes[0]; const foreignObject = svg.childNodes[0]; const p = foreignObject.childNodes[0]; expect(svg.namespaceURI).toEqual('http://www.w3.org/2000/svg'); expect(foreignObject.namespaceURI).toEqual('http://www.w3.org/2000/svg'); expect(p.namespaceURI).toEqual('http://www.w3.org/1999/xhtml'); }); }); describe('attributes', () => { it('should support attributes with namespace', () => { TestBed.configureTestingModule({declarations: [MyComp, SomeCmp]}); const template = '<svg:use xlink:href="#id" />'; TestBed.overrideComponent(SomeCmp, {set: {template}}); const fixture = TestBed.createComponent(SomeCmp); const useEl = fixture.nativeElement.firstChild; expect(useEl.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toEqual('#id'); }); it('should support binding to attributes with namespace', () => { TestBed.configureTestingModule({declarations: [MyComp, SomeCmp]}); const template = '<svg:use [attr.xlink:href]="value" />'; TestBed.overrideComponent(SomeCmp, {set: {template}}); const fixture = TestBed.createComponent(SomeCmp); const cmp = fixture.componentInstance; const useEl = fixture.nativeElement.firstChild; cmp.value = '#id'; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(useEl.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toEqual('#id'); cmp.value = null; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(useEl.hasAttributeNS('http://www.w3.org/1999/xlink', 'href')).toEqual(false); }); }); } }); @Component({ selector: 'cmp-with-default-interpolation', template: `{{ text }}`, standalone: false, }) class ComponentWithDefaultInterpolation { text = 'Default Interpolation'; } @Injectable() class MyService { greeting: string; constructor() { this.greeting = 'hello'; } } @Component({ selector: 'simple-imp-cmp', template: '', standalone: false, }) class SimpleImperativeViewComponent { done: any; constructor(self: ElementRef) { const hostElement = self.nativeElement; hostElement.appendChild(el('hello imp view')); } } @Directive({ selector: 'dynamic-vp', standalone: false, }) class DynamicViewport { private injector: Injector; constructor(private vc: ViewContainerRef) { const myService = new MyService(); myService.greeting = 'dynamic greet'; this.injector = Injector.create({ providers: [{provide: MyService, useValue: myService}], parent: vc.injector, }); } create(): ComponentRef<ChildCompUsingService> { return this.vc.createComponent(ChildCompUsingService, { index: this.vc.length, injector: this.injector, }); } insert(viewRef: ViewRef, index?: number): ViewRef { return this.vc.insert(viewRef, index); } move(viewRef: ViewRef, currentIndex: number): ViewRef { return this.vc.move(viewRef, currentIndex); } } @Directive({ selector: '[my-dir]', inputs: ['dirProp: elprop'], exportAs: 'mydir', standalone: false, }) class MyDir { dirProp: string; constructor() { this.dirProp = ''; } } @Directive({ selector: '[my-dir2]', inputs: ['dirProp2: elprop'], exportAs: 'mydir2', standalone: false, }) class MyDir2 { dirProp2: string; constructor() { this.dirProp2 = ''; } } @Directive({ selector: '[title]', inputs: ['title'], standalone: false, }) class DirectiveWithTitle { title: string | undefined; } @Directive({ selector: '[title]', inputs: ['title'], host: {'[title]': 'title'}, standalone: false, }) class DirectiveWithTitleAndHostProperty { title: string | undefined; } @Component({ selector: 'event-cmp', template: '<div (click)="noop()"></div>', standalone: false, }) class EventCmp { noop() {} } @Component({ selector: 'push-cmp', inputs: ['prop'], host: {'(click)': 'true'}, changeDetection: ChangeDetectionStrategy.OnPush, template: '{{field}}<div (click)="noop()"></div><div *ngIf="true" (click)="noop()"></div><event-cmp></event-cmp>', standalone: false, }) class PushCmp { numberOfChecks: number; prop: any; constructor() { this.numberOfChecks = 0; } noop() {} get field() { this.numberOfChecks++; return 'fixed'; } } @Component({ selector: 'push-cmp-with-ref', inputs: ['prop'], changeDetection: ChangeDetectionStrategy.OnPush, template: '{{field}}', standalone: false, }) class PushCmpWithRef { numberOfChecks: number; ref: ChangeDetectorRef; prop: any; constructor(ref: ChangeDetectorRef) { this.numberOfChecks = 0; this.ref = ref; } get field() { this.numberOfChecks++; return 'fixed'; } propagate() { this.ref.markForCheck(); } } @Component({ selector: 'push-cmp-with-host-event', host: {'(click)': 'ctxCallback($event)'}, changeDetection: ChangeDetectionStrategy.OnPush, template: '', standalone: false, }) class PushCmpWithHostEvent { ctxCallback: Function = (_: any) => {}; } @Component({ selector: 'push-cmp-with-async', changeDetection: ChangeDetectionStrategy.OnPush, template: '{{field | async}}', standalone: false, }) class PushCmpWithAsyncPipe { numberOfChecks: number = 0; resolve!: (result: any) => void; promise: Promise<any>; constructor() { this.promise = new Promise((resolve) => { this.resolve = resolve; }); } get field() { this.numberOfChecks++; return this.promise; } } @Component({ selector: 'my-comp', template: '', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class MyComp { readonly ctxProp = signal<string | undefined>(undefined); readonly ctxNumProp = signal<number | undefined>(undefined); readonly ctxBoolProp = signal<boolean | undefined>(undefined); readonly ctxArrProp = signal<number[] | undefined>(undefined); readonly toStringThrow = { toString: function () { throw 'boom'; }, }; constructor() { this.ctxProp.set('initial value'); this.ctxNumProp.set(0); this.ctxBoolProp.set(false); this.ctxArrProp.set([0, 1, 2]); } throwError() { throw 'boom'; } } @Component({ selector: 'child-cmp', inputs: ['dirProp'], viewProviders: [MyService], template: '{{ctxProp()}}', standalone: false, }) class ChildComp { ctxProp = signal<string | undefined>(undefined); dirProp = signal<string | null>(null); constructor(service: MyService) { this.ctxProp.set(service.greeting); this.dirProp.set(null); } } @Component({ selector: 'child-cmp-no-template', template: '', standalone: false, }) class ChildCompNoTemplate { ctxProp: string = 'hello'; } @Component({ selector: 'child-cmp-svc', template: '{{ctxProp()}}', standalone: false, }) class ChildCompUsingService { ctxProp = signal<string | undefined>(undefined); constructor(service: MyService) { this.ctxProp.set(service.greeting); } } @Directive({ selector: 'some-directive', standalone: false, }) class SomeDirective {} class SomeDirectiveMissingAnnotation {} @Component({ selector: 'cmp-with-host', template: '<p>Component with an injected host</p>', standalone: false, }) class CompWithHost { myHost: SomeDirective; constructor(@Host() someComp: SomeDirective) { this.myHost = someComp; } } @Component({ selector: '[child-cmp2]', viewProviders: [MyService], standalone: false, }) class ChildComp2 { ctxProp: string; dirProp: string | null; constructor(service: MyService) { this.ctxProp = service.greeting; this.dirProp = null; } } class SomeViewportContext { constructor(public someTmpl: string) {} } @Directive({ selector: '[some-viewport]', standalone: false, }) class SomeViewport { constructor( public container: ViewContainerRef, templateRef: TemplateRef<SomeViewportContext>, ) { container.createEmbeddedView(templateRef, new SomeViewportContext('hello')); container.createEmbeddedView(templateRef, new SomeViewportContext('again')); } } @Directive({ selector: '[pollutedContext]', standalone: false, }) class PollutedContext { constructor( private tplRef: TemplateRef<any>, private vcRef: ViewContainerRef, ) { const evRef = this.vcRef.createEmbeddedView(this.tplRef); evRef.context.bar = 'baz'; } } @Directive({ selector: '[noContext]', standalone: false, }) class NoContext { constructor( private tplRef: TemplateRef<any>, private vcRef: ViewContainerRef, ) { this.vcRef.createEmbeddedView(this.tplRef); } } @Pipe({ name: 'double', standalone: false, }) class DoublePipe implements PipeTransform, OnDestroy { ngOnDestroy() {} transform(value: any) { return `${value}${value}`; } } @Directive({ selector: '[emitter]', outputs: ['event'], standalone: false, }) class DirectiveEmittingEvent { msg: string; event: EventEmitter<any>; constructor() { this.msg = ''; this.event = new EventEmitter(); } fireEvent(msg: string) { this.event.emit(msg); } } @Directive({ selector: '[update-host-attributes]', host: {'role': 'button'}, standalone: false, }) class DirectiveUpdatingHostAttributes {} @Directive({ selector: '[update-host-properties]', host: {'[id]': 'id'}, standalone: false, }) class DirectiveUpdatingHostProperties { id: string; constructor() { this.id = 'one'; } } @Directive({ selector: '[listener]', host: {'(event)': 'onEvent($event)'}, standalone: false, }) class DirectiveListeningEvent { msg: string; constructor() { this.msg = ''; } onEvent(msg: string) { this.msg = msg; } } @Directive({ selector: '[listener]', host: { '(domEvent)': 'onEvent($event.type)', '(window:domEvent)': 'onWindowEvent($event.type)', '(document:domEvent)': 'onDocumentEvent($event.type)', '(body:domEvent)': 'onBodyEvent($event.type)', }, standalone: false, }) class DirectiveListeningDomEvent { eventTypes: string[] = []; onEvent(eventType: string) { this.eventTypes.push(eventType); } onWindowEvent(eventType: string) { this.eventTypes.push('window_' + eventType); } onDocumentEvent(eventType: string) { this.eventTypes.push('document_' + eventType); } onBodyEvent(eventType: string) { this.eventTypes.push('body_' + eventType); } } let globalCounter = 0; @Directive({ selector: '[listenerother]', host: {'(window:domEvent)': 'onEvent($event.type)'}, standalone: false, }) class DirectiveListeningDomEventOther { eventType: string; constructor() { this.eventType = ''; } onEvent(eventType: string) { globalCounter++; this.eventType = 'other_' + eventType; } } @Directive({ selector: '[listenerprevent]', host: {'(click)': 'onEvent($event)'}, standalone: false, }) class DirectiveListeningDomEventPrevent { onEvent(event: any) { return false; } } @Directive({ selector: '[listenernoprevent]', host: {'(click)': 'onEvent($event)'}, standalone: false, }) class DirectiveListeningDomEventNoPrevent { onEvent(event: any) { return true; } } @Directive({ selector: '[id]', inputs: ['id'], standalone: false, }) class IdDir { id: string | undefined; } @Directive({ selector: '[customEvent]', standalone: false, }) class EventDir { @Output() customEvent = new EventEmitter(); doSomething() {} } @Directive({ selector: '[static]', standalone: false, }) class NeedsAttribute { typeAttribute: string; staticAttribute: string; fooAttribute: string; constructor( @Attribute('type') typeAttribute: string, @Attribute('static') staticAttribute: string, @Attribute('foo') fooAttribute: string, ) { this.typeAttribute = typeAttribute; this.staticAttribute = staticAttribute; this.fooAttribute = fooAttribute; } } @Injectable() class PublicApi {} @Directive({ selector: '[public-api]', providers: [{provide: PublicApi, useExisting: PrivateImpl, deps: []}], standalone: false, }) class PrivateImpl extends PublicApi {} @Directive({ selector: '[needs-public-api]', standalone: false, }) class NeedsPublicApi { constructor(@Host() api: PublicApi) { expect(api instanceof PrivateImpl).toBe(true); } } class ToolbarContext { constructor(public toolbarProp: string) {} } @Directive({ selector: '[toolbarpart]', standalone: false, }) class ToolbarPart { templateRef: TemplateRef<ToolbarContext>; constructor(templateRef: TemplateRef<ToolbarContext>) { this.templateRef = templateRef; } } @Directive({ selector: '[toolbarVc]', inputs: ['toolbarVc'], standalone: false, }) class ToolbarViewContainer { constructor(public vc: ViewContainerRef) {} set toolbarVc(part: ToolbarPart) { this.vc.createEmbeddedView(part.templateRef, new ToolbarContext('From toolbar'), 0); } } @Component({ selector: 'toolbar', template: 'TOOLBAR(<div *ngFor="let part of query" [toolbarVc]="part"></div>)', standalone: false, }) class ToolbarComponent { @ContentChildren(ToolbarPart) query!: QueryList<ToolbarPart>; ctxProp: string = 'hello world'; constructor() {} } @Directive({ selector: '[two-way]', inputs: ['control'], outputs: ['controlChange'], standalone: false, }) class DirectiveWithTwoWayBinding { controlChange = new EventEmitter(); control: any = null; triggerChange(value: any) { this.controlChange.emit(value); } } @Injectable() class InjectableService {} function createInjectableWithLogging(inj: Injector) { inj.get(ComponentProvidingLoggingInjectable).created = true; return new InjectableService(); } @Component({ selector: 'component-providing-logging-injectable', providers: [ {provide: InjectableService, useFactory: createInjectableWithLogging, deps: [Injector]}, ], template: '', standalone: false, }) class ComponentProvidingLoggingInjectable { created: boolean = false; } @Directive({ selector: 'directive-providing-injectable', providers: [[InjectableService]], standalone: false, }) class DirectiveProvidingInjectable {} @Component({ selector: 'directive-providing-injectable', viewProviders: [[InjectableService]], template: '', standalone: false, }) class DirectiveProvidingInjectableInView {} @Component({ selector: 'directive-providing-injectable', providers: [{provide: InjectableService, useValue: 'host'}], viewProviders: [{provide: InjectableService, useValue: 'view'}], template: '', standalone: false, }) class DirectiveProvidingInjectableInHostAndView {} @Component({ selector: 'directive-consuming-injectable', template: '', standalone: false, }) class DirectiveConsumingInjectable { injectable: any; constructor(@Host() @Inject(InjectableService) injectable: any) { this.injectable = injectable; } } @Component({ selector: 'directive-containing-directive-consuming-an-injectable', standalone: false, }) class DirectiveContainingDirectiveConsumingAnInjectable { directive: any; } @Component({ selector: 'directive-consuming-injectable-unbounded', template: '', standalone: false, }) class DirectiveConsumingInjectableUnbounded { injectable: any; constructor( injectable: InjectableService, @SkipSelf() parent: DirectiveContainingDirectiveConsumingAnInjectable, ) { this.injectable = injectable; parent.directive = this; } } class EventBus { parentEventBus: EventBus; name: string; constructor(parentEventBus: EventBus, name: string) { this.parentEventBus = parentEventBus; this.name = name; } } @Directive({ selector: 'grand-parent-providing-event-bus', providers: [{provide: EventBus, useValue: new EventBus(null!, 'grandparent')}], standalone: false, }) class GrandParentProvidingEventBus { bus: EventBus; constructor(bus: EventBus) { this.bus = bus; } } function createParentBus(peb: EventBus) { return new EventBus(peb, 'parent'); } @Component({ selector: 'parent-providing-event-bus', providers: [{provide: EventBus, useFactory: createParentBus, deps: [[EventBus, new SkipSelf()]]}], template: `<child-consuming-event-bus></child-consuming-event-bus>`, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class ParentProvidingEventBus { bus: EventBus; grandParentBus: EventBus; constructor(bus: EventBus, @SkipSelf() grandParentBus: EventBus) { this.bus = bus; this.grandParentBus = grandParentBus; } } @Directive({ selector: 'child-consuming-event-bus', standalone: false, }) class ChildConsumingEventBus { bus: EventBus; constructor(@SkipSelf() bus: EventBus) { this.bus = bus; } } @Directive({ selector: '[someImpvp]', inputs: ['someImpvp'], standalone: false, }) class SomeImperativeViewport { view: EmbeddedViewRef<Object> | null; anchor: any; constructor( public vc: ViewContainerRef, public templateRef: TemplateRef<Object>, @Inject(ANCHOR_ELEMENT) anchor: any, ) { this.view = null; this.anchor = anchor; } set someImpvp(value: boolean) { if (this.view) { this.vc.clear(); this.view = null; } if (value) { this.view = this.vc.createEmbeddedView(this.templateRef); const nodes = this.view.rootNodes; for (let i = 0; i < nodes.length; i++) { this.anchor.appendChild(nodes[i]); } } } } @Directive({ selector: '[export-dir]', exportAs: 'dir', standalone: false, }) class ExportDir {} @Directive({ selector: '[multiple-export-as]', exportAs: 'dirX, dirY', standalone: false, }) export class DirectiveWithMultipleExportAsNames {} @Component({ selector: 'comp', standalone: false, }) class ComponentWithoutView {} @Directive({ selector: '[no-duplicate]', standalone: false, }) class DuplicateDir { constructor(elRef: ElementRef) { elRef.nativeElement.textContent += 'noduplicate'; } } @Directive({ selector: '[no-duplicate]', standalone: false, }) class OtherDuplicateDir { constructor(elRef: ElementRef) { elRef.nativeElement.textContent += 'othernoduplicate'; } } @Directive({ selector: 'directive-throwing-error', standalone: false, }) class DirectiveThrowingAnError { constructor() { throw new Error('BOOM'); } } @Component({ selector: 'component-with-template', template: `No View Decorator: <div *ngFor="let item of items">{{ item }}</div>`, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class ComponentWithTemplate { items = [1, 2, 3]; } @Directive({ selector: 'with-prop-decorators', standalone: false, }) class DirectiveWithPropDecorators { target: any; @Input('elProp') dirProp: string | undefined; @Output('elEvent') event = new EventEmitter(); @HostBinding('attr.my-attr') myAttr: string | undefined; @HostListener('click', ['$event.target']) onClick(target: any) { this.target = target; } fireEvent(msg: any) { this.event.emit(msg); } } @Component({ selector: 'some-cmp', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class SomeCmp { value: any; } @Component({ selector: 'parent-cmp', template: `<cmp [test$]="name"></cmp>`, standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) export class ParentCmp { name: string = 'hello'; } @Component({ selector: 'cmp', template: '', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) class SomeCmpWithInput { @Input() test$: any; } function isPrevented(evt: Event): boolean { return evt.defaultPrevented || (evt.returnValue != null && !evt.returnValue); }