angular-docs-cn/packages/core/test/test_bed_spec.ts
Andrew Kushnir 5a2c3ff8b5 fix(ivy): proper component resolution in case of inheritance (#28439)
Ivy allows Components to extend Directives (but not the other way around) and as a result we may have Component and Directive annotations present at the same time. The logic that resolves annotations to pick the necessary one didn't take this into account and as a result Components were recognized as Directives (and vice versa) in case of inheritance. This change updates the resolution logic by picking known annotation that is the nearest one (in inheritance tree) and compares it with expected type. That should help avoid mis-classification of Components/Directives during resolution.

PR Close #28439
2019-02-05 23:29:04 -05:00

324 lines
11 KiB
TypeScript

/**
* @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 {Component, Directive, Inject, InjectionToken, NgModule, Optional, Pipe} from '@angular/core';
import {TestBed, getTestBed} from '@angular/core/testing/src/test_bed';
import {By} from '@angular/platform-browser';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import {onlyInIvy} from '@angular/private/testing';
const NAME = new InjectionToken<string>('name');
// -- module: HWModule
@Component({
selector: 'hello-world',
template: '<greeting-cmp></greeting-cmp>',
})
export class HelloWorld {
}
// -- module: Greeting
@Component({
selector: 'greeting-cmp',
template: 'Hello {{ name }}',
})
export class GreetingCmp {
name: string;
constructor(@Inject(NAME) @Optional() name: string) { this.name = name || 'nobody!'; }
}
@NgModule({
declarations: [GreetingCmp],
exports: [GreetingCmp],
})
export class GreetingModule {
}
@Component({selector: 'simple-cmp', template: '<b>simple</b>'})
export class SimpleCmp {
}
@Component({selector: 'with-refs-cmp', template: '<div #firstDiv></div>'})
export class WithRefsCmp {
}
@Component({selector: 'inherited-cmp', template: 'inherited'})
export class InheritedCmp extends SimpleCmp {
}
@Directive({selector: '[hostBindingDir]', host: {'[id]': 'id'}})
export class HostBindingDir {
id = 'one';
}
@Component({
selector: 'component-with-prop-bindings',
template: `
<div hostBindingDir [title]="title" [attr.aria-label]="label"></div>
<p title="( {{ label }} - {{ title }} )" [attr.aria-label]="label" id="[ {{ label }} ] [ {{ title }} ]">
</p>
`
})
export class ComponentWithPropBindings {
title = 'some title';
label = 'some label';
}
@Component({
selector: 'simple-app',
template: `
<simple-cmp></simple-cmp> - <inherited-cmp></inherited-cmp>
`
})
export class SimpleApp {
}
@Component({selector: 'inline-template', template: '<p>Hello</p>'})
export class ComponentWithInlineTemplate {
}
@NgModule({
declarations: [
HelloWorld, SimpleCmp, WithRefsCmp, InheritedCmp, SimpleApp, ComponentWithPropBindings,
HostBindingDir
],
imports: [GreetingModule],
providers: [
{provide: NAME, useValue: 'World!'},
]
})
export class HelloWorldModule {
}
describe('TestBed', () => {
beforeEach(() => {
getTestBed().resetTestingModule();
TestBed.configureTestingModule({imports: [HelloWorldModule]});
});
it('should compile and render a component', () => {
const hello = TestBed.createComponent(HelloWorld);
hello.detectChanges();
expect(hello.nativeElement).toHaveText('Hello World!');
});
it('should give access to the component instance', () => {
const hello = TestBed.createComponent(HelloWorld);
hello.detectChanges();
expect(hello.componentInstance).toBeAnInstanceOf(HelloWorld);
});
it('should give the ability to query by css', () => {
const hello = TestBed.createComponent(HelloWorld);
hello.detectChanges();
const greetingByCss = hello.debugElement.query(By.css('greeting-cmp'));
expect(greetingByCss.nativeElement).toHaveText('Hello World!');
expect(greetingByCss.componentInstance).toBeAnInstanceOf(GreetingCmp);
});
it('should give the ability to trigger the change detection', () => {
const hello = TestBed.createComponent(HelloWorld);
hello.detectChanges();
const greetingByCss = hello.debugElement.query(By.css('greeting-cmp'));
expect(greetingByCss.nativeElement).toHaveText('Hello World!');
greetingByCss.componentInstance.name = 'TestBed!';
hello.detectChanges();
expect(greetingByCss.nativeElement).toHaveText('Hello TestBed!');
});
it('should give the ability to access property bindings on a node', () => {
const fixture = TestBed.createComponent(ComponentWithPropBindings);
fixture.detectChanges();
const divElement = fixture.debugElement.query(By.css('div'));
expect(divElement.properties).toEqual({id: 'one', title: 'some title'});
});
it('should give the ability to access interpolated properties on a node', () => {
const fixture = TestBed.createComponent(ComponentWithPropBindings);
fixture.detectChanges();
const paragraphEl = fixture.debugElement.query(By.css('p'));
expect(paragraphEl.properties)
.toEqual({title: '( some label - some title )', id: '[ some label ] [ some title ]'});
});
it('should give access to the node injector', () => {
const fixture = TestBed.createComponent(HelloWorld);
fixture.detectChanges();
const injector = fixture.debugElement.query(By.css('greeting-cmp')).injector;
// from the node injector
const greetingCmp = injector.get(GreetingCmp);
expect(greetingCmp.constructor).toBe(GreetingCmp);
// from the node injector (inherited from a parent node)
const helloWorldCmp = injector.get(HelloWorld);
expect(fixture.componentInstance).toBe(helloWorldCmp);
const nameInjected = injector.get(NAME);
expect(nameInjected).toEqual('World!');
});
it('should give access to the node injector for root node', () => {
const hello = TestBed.createComponent(HelloWorld);
hello.detectChanges();
const injector = hello.debugElement.injector;
// from the node injector
const helloInjected = injector.get(HelloWorld);
expect(helloInjected).toBe(hello.componentInstance);
// from the module injector
const nameInjected = injector.get(NAME);
expect(nameInjected).toEqual('World!');
});
it('should give access to local refs on a node', () => {
const withRefsCmp = TestBed.createComponent(WithRefsCmp);
const firstDivDebugEl = withRefsCmp.debugElement.query(By.css('div'));
// assert that a native element is referenced by a local ref
expect(firstDivDebugEl.references.firstDiv.tagName.toLowerCase()).toBe('div');
});
it('should give the ability to query by directive', () => {
const hello = TestBed.createComponent(HelloWorld);
hello.detectChanges();
const greetingByDirective = hello.debugElement.query(By.directive(GreetingCmp));
expect(greetingByDirective.componentInstance).toBeAnInstanceOf(GreetingCmp);
});
it('allow to override a template', () => {
// use original template when there is no override
let hello = TestBed.createComponent(HelloWorld);
hello.detectChanges();
expect(hello.nativeElement).toHaveText('Hello World!');
// override the template
getTestBed().resetTestingModule();
TestBed.configureTestingModule({imports: [HelloWorldModule]});
TestBed.overrideComponent(GreetingCmp, {set: {template: `Bonjour {{ name }}`}});
hello = TestBed.createComponent(HelloWorld);
hello.detectChanges();
expect(hello.nativeElement).toHaveText('Bonjour World!');
// restore the original template by calling `.resetTestingModule()`
getTestBed().resetTestingModule();
TestBed.configureTestingModule({imports: [HelloWorldModule]});
hello = TestBed.createComponent(HelloWorld);
hello.detectChanges();
expect(hello.nativeElement).toHaveText('Hello World!');
});
it('allow to override a provider', () => {
TestBed.overrideProvider(NAME, {useValue: 'injected World !'});
const hello = TestBed.createComponent(HelloWorld);
hello.detectChanges();
expect(hello.nativeElement).toHaveText('Hello injected World !');
});
it('should resolve components that are extended by other components', () => {
// SimpleApp uses SimpleCmp in its template, which is extended by InheritedCmp
const simpleApp = TestBed.createComponent(SimpleApp);
simpleApp.detectChanges();
expect(simpleApp.nativeElement).toHaveText('simple - inherited');
});
it('should resolve components without async resources synchronously', (done) => {
TestBed
.configureTestingModule({
declarations: [ComponentWithInlineTemplate],
})
.compileComponents()
.then(done)
.catch(error => {
// This should not throw any errors. If an error is thrown, the test will fail.
// Specifically use `catch` here to mark the test as done and *then* throw the error
// so that the test isn't treated as a timeout.
done();
throw error;
});
// Intentionally call `createComponent` before `compileComponents` is resolved. We want this to
// work for components that don't have any async resources (templateUrl, styleUrls).
TestBed.createComponent(ComponentWithInlineTemplate);
});
onlyInIvy('patched ng defs should be removed after resetting TestingModule')
.describe('resetting ng defs', () => {
it('should restore ng defs to their initial states', () => {
@Pipe({name: 'somePipe', pure: true})
class SomePipe {
transform(value: string): string { return `transformed ${value}`; }
}
@Directive({selector: 'someDirective'})
class SomeDirective {
someProp = 'hello';
}
@Component({selector: 'comp', template: 'someText'})
class SomeComponent {
}
@NgModule({declarations: [SomeComponent]})
class SomeModule {
}
TestBed.configureTestingModule({imports: [SomeModule]});
// adding Pipe and Directive via metadata override
TestBed.overrideModule(
SomeModule, {set: {declarations: [SomeComponent, SomePipe, SomeDirective]}});
TestBed.overrideComponent(
SomeComponent,
{set: {template: `<span someDirective>{{'hello' | somePipe}}</span>`}});
TestBed.createComponent(SomeComponent);
const defBeforeReset = (SomeComponent as any).ngComponentDef;
expect(defBeforeReset.pipeDefs().length).toEqual(1);
expect(defBeforeReset.directiveDefs().length).toEqual(2); // directive + component
TestBed.resetTestingModule();
const defAfterReset = (SomeComponent as any).ngComponentDef;
expect(defAfterReset.pipeDefs().length).toEqual(0);
expect(defAfterReset.directiveDefs().length).toEqual(1); // component
});
it('should cleanup ng defs for classes with no ng annotations (in case of inheritance)',
() => {
@Component({selector: 'someDirective', template: '...'})
class SomeComponent {
}
class ComponentWithNoAnnotations extends SomeComponent {}
TestBed.configureTestingModule({declarations: [ComponentWithNoAnnotations]});
TestBed.createComponent(ComponentWithNoAnnotations);
expect(ComponentWithNoAnnotations.hasOwnProperty('ngComponentDef')).toBeTruthy();
expect(SomeComponent.hasOwnProperty('ngComponentDef')).toBeTruthy();
TestBed.resetTestingModule();
expect(ComponentWithNoAnnotations.hasOwnProperty('ngComponentDef')).toBeFalsy();
// ngComponentDef should be preserved on super component
expect(SomeComponent.hasOwnProperty('ngComponentDef')).toBeTruthy();
});
});
});