/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {CommonModule} from '@angular/common';
import {Compiler, ComponentFactory, ComponentRef, ErrorHandler, EventEmitter, Host, Inject, Injectable, InjectionToken, Injector, NO_ERRORS_SCHEMA, NgModule, NgModuleRef, OnDestroy, SkipSelf, ViewRef, ɵivyEnabled as ivyEnabled} from '@angular/core';
import {ChangeDetectionStrategy, ChangeDetectorRef, PipeTransform} from '@angular/core/src/change_detection/change_detection';
import {getDebugContext} from '@angular/core/src/errors';
import {ComponentFactoryResolver} from '@angular/core/src/linker/component_factory_resolver';
import {ElementRef} from '@angular/core/src/linker/element_ref';
import {QueryList} from '@angular/core/src/linker/query_list';
import {TemplateRef} from '@angular/core/src/linker/template_ref';
import {ViewContainerRef} from '@angular/core/src/linker/view_container_ref';
import {EmbeddedViewRef} from '@angular/core/src/linker/view_ref';
import {Attribute, Component, ContentChildren, Directive, HostBinding, HostListener, Input, Output, Pipe} from '@angular/core/src/metadata';
import {TestBed, async, fakeAsync, getTestBed, tick} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {DOCUMENT} from '@angular/platform-browser/src/dom/dom_tokens';
import {dispatchEvent, el} from '@angular/platform-browser/testing/src/browser_util';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import {fixmeIvy, modifiedInIvy} from '@angular/private/testing';
import {stringify} from '../../src/util';
const ANCHOR_ELEMENT = new InjectionToken('AnchorElement');
if (ivyEnabled) {
describe('ivy', () => { declareTests(); });
} else {
describe('jit', () => { declareTests({useJit: true}); });
describe('no jit', () => { declareTests({useJit: false}); });
}
function declareTests(config?: {useJit: boolean}) {
describe('integration tests', function() {
beforeEach(() => { TestBed.configureCompiler({...config}); });
describe('react to record changes', function() {
it('should consume text node changes', () => {
TestBed.configureTestingModule({declarations: [MyComp]});
const template = '
{{ctxProp}}
';
TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp);
fixture.componentInstance.ctxProp = '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 = '
{{null}}{{ctxProp}}
';
TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp);
fixture.componentInstance.ctxProp = null !;
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('');
});
it('should allow both null and undefined in expressions', () => {
const template = '
{{null == undefined}}|{{null === undefined}}
';
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 =
`
`;
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 =
`
`;
TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp);
expect(getDOM().querySelectorAll(fixture.nativeElement, 'script').length).toEqual(0);
});
fixmeIvy('FW-662: Components without selector are not supported')
.it('should throw when using directives without selector', () => {
@Directive({})
class SomeDirective {
}
@Component({selector: 'comp', template: ''})
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', () => {
let noSelectorComponentFactory: ComponentFactory = undefined !;
@Component({template: '----'})
class NoSelectorComponent {
}
@Component({selector: 'some-comp', template: '', entryComponents: [NoSelectorComponent]})
class SomeComponent {
constructor(componentFactoryResolver: ComponentFactoryResolver) {
// grab its own component factory
noSelectorComponentFactory =
componentFactoryResolver.resolveComponentFactory(NoSelectorComponent) !;
}
}
TestBed.configureTestingModule({declarations: [SomeComponent, NoSelectorComponent]});
// get the factory
TestBed.createComponent(SomeComponent);
expect(noSelectorComponentFactory.selector).toBe('ng-component');
expect(
getDOM()
.nodeName(noSelectorComponentFactory.create(Injector.NULL).location.nativeElement)
.toLowerCase())
.toEqual('ng-component');
});
});
describe('error handling', () => {
fixmeIvy('FW-682: TestBed: tests assert that compilation produces specific error')
.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.`);
});
fixmeIvy('FW-682: TestBed: tests assert that compilation produces specific error')
.it('should report a meaningful error when a component is missing view annotation',
() => {
TestBed.configureTestingModule({declarations: [MyComp, ComponentWithoutView]});
try {
TestBed.createComponent(ComponentWithoutView);
expect(true).toBe(false);
} catch (e) {
expect(e.message).toContain(
`No template specified for component ${stringify(ComponentWithoutView)}`);
}
});
fixmeIvy('FW-722: getDebugContext needs to be replaced / re-implemented')
.it('should provide an error context when an error happens in DI', () => {
TestBed.configureTestingModule({
declarations: [MyComp, DirectiveThrowingAnError],
schemas: [NO_ERRORS_SCHEMA],
});
const template = ``;
TestBed.overrideComponent(MyComp, {set: {template}});
try {
TestBed.createComponent(MyComp);
throw 'Should throw';
} catch (e) {
const c = getDebugContext(e);
expect(getDOM().nodeName(c.componentRenderElement).toUpperCase()).toEqual('DIV');
expect((c.injector).get).toBeTruthy();
}
});
fixmeIvy('FW-722: getDebugContext needs to be replaced / re-implemented')
.it('should provide an error context when an error happens in change detection', () => {
TestBed.configureTestingModule({declarations: [MyComp, DirectiveThrowingAnError]});
const template = ``;
TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp);
try {
fixture.detectChanges();
throw 'Should throw';
} catch (e) {
const c = getDebugContext(e);
expect(getDOM().nodeName(c.renderNode).toUpperCase()).toEqual('INPUT');
expect(getDOM().nodeName(c.componentRenderElement).toUpperCase()).toEqual('DIV');
expect((c.injector).get).toBeTruthy();
expect(c.context).toBe(fixture.componentInstance);
expect(c.references['local']).toBeDefined();
}
});
fixmeIvy('FW-722: getDebugContext needs to be replaced / re-implemented')
.it('should provide an error context when an error happens in change detection (text node)',
() => {
TestBed.configureTestingModule({declarations: [MyComp]});
const template = `
{{one.two.three}}
`;
TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp);
try {
fixture.detectChanges();
throw 'Should throw';
} catch (e) {
const c = getDebugContext(e);
expect(c.renderNode).toBeTruthy();
}
});
if (getDOM().supportsDOMEvents()) { // this is required to use fakeAsync
fixmeIvy('FW-722: getDebugContext needs to be replaced / re-implemented')
.it('should provide an error context when an error happens in an event handler',
fakeAsync(() => {
TestBed.configureTestingModule({
declarations: [MyComp, DirectiveEmittingEvent, DirectiveListeningEvent],
schemas: [NO_ERRORS_SCHEMA],
});
const template = ``;
TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp);
tick();
const tc = fixture.debugElement.children[0];
const errorHandler = tc.injector.get(ErrorHandler);
let err: any;
spyOn(errorHandler, 'handleError').and.callFake((e: any) => err = e);
tc.injector.get(DirectiveEmittingEvent).fireEvent('boom');
expect(err).toBeTruthy();
const c = getDebugContext(err);
expect(getDOM().nodeName(c.renderNode).toUpperCase()).toEqual('SPAN');
expect(getDOM().nodeName(c.componentRenderElement).toUpperCase()).toEqual('DIV');
expect((c.injector).get).toBeTruthy();
expect(c.context).toBe(fixture.componentInstance);
expect(c.references['local']).toBeDefined();
}));
}
});
it('should support imperative views', () => {
TestBed.configureTestingModule({declarations: [MyComp, SimpleImperativeViewComponent]});
const template = '';
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('')}],
});
const template = '
hello
';
TestBed.overrideComponent(MyComp, {set: {template}});
const anchorElement = getTestBed().get(ANCHOR_ELEMENT);
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
expect(anchorElement).toHaveText('');
fixture.componentInstance.ctxBoolProp = true;
fixture.detectChanges();
expect(anchorElement).toHaveText('hello');
fixture.componentInstance.ctxBoolProp = false;
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('');
});
describe('Property bindings', () => {
fixmeIvy('FW-721: Bindings to unknown properties are not reported as errors')
.it('should throw on bindings to unknown properties', () => {
TestBed.configureTestingModule({declarations: [MyComp]});
const template = '';
TestBed.overrideComponent(MyComp, {set: {template}});
try {
TestBed.createComponent(MyComp);
throw 'Should throw';
} catch (e) {
expect(e.message).toMatch(
/Template parse errors:\nCan't bind to 'unknown' since it isn't a known property of 'div'. \("
\]unknown="{{ctxProp}}"><\/div>"\): .*MyComp.html@0:5/);
}
});
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 = '';
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 = '';
TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp);
fixture.componentInstance.ctxProp = 'TITLE';
fixture.detectChanges();
const el = getDOM().querySelector(fixture.nativeElement, 'span');
expect(el.title).toBeFalsy();
});
fixmeIvy('FW-711: elementProperty instruction should not be used in host bindings')
.it('should work when a directive uses hostProperty to update the DOM element', () => {
TestBed.configureTestingModule(
{declarations: [MyComp, DirectiveWithTitleAndHostProperty]});
const template = '';
TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp);
fixture.componentInstance.ctxProp = 'TITLE';
fixture.detectChanges();
const el = getDOM().querySelector(fixture.nativeElement, 'span');
expect(getDOM().getProperty(el, 'title')).toEqual('TITLE');
});
});
describe('logging property updates', () => {
fixmeIvy('FW-664: ng-reflect-* is not supported')
.it('should reflect property values as attributes', () => {
TestBed.configureTestingModule({declarations: [MyComp, MyDir]});
const template = '