Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ivy): support providing components and dirs in tests #29945

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion integration/_payload-limits.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"master": {
"uncompressed": {
"runtime": 1440,
"main": 14287,
"main": 14487,
"polyfills": 43567
}
}
Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/render3/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import '../util/ng_dev_mode';

import {ChangeDetectionStrategy} from '../change_detection/constants';
import {NG_INJECTABLE_DEF, ɵɵdefineInjectable} from '../di/interface/defs';
import {Mutable, Type} from '../interface/type';
import {NgModuleDef} from '../metadata/ng_module';
import {SchemaMetadata} from '../metadata/schema';
Expand Down Expand Up @@ -300,7 +301,17 @@ export function ɵɵdefineComponent<T>(componentDefinition: {
def.pipeDefs = pipeTypes ?
() => (typeof pipeTypes === 'function' ? pipeTypes() : pipeTypes).map(extractPipeDef) :
null;

// Add ngInjectableDef so components are reachable through the module injector by default
// (unless it has already been set by the @Injectable decorator). This is mostly to
// support injecting components in tests. In real application code, components should
// be retrieved through the node injector, so this isn't a problem.
if (!type.hasOwnProperty(NG_INJECTABLE_DEF)) {
(type as any)[NG_INJECTABLE_DEF] =
ɵɵdefineInjectable<T>({factory: componentDefinition.factory as() => T});
}
}) as never;

return def as never;
}

Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/render3/jit/directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import {R3DirectiveMetadataFacade, getCompilerFacade} from '../../compiler/compiler_facade';
import {R3ComponentMetadataFacade, R3QueryMetadataFacade} from '../../compiler/compiler_facade_interface';
import {resolveForwardRef} from '../../di/forward_ref';
import {compileInjectable} from '../../di/jit/injectable';
import {getReflect, reflectDependencies} from '../../di/jit/util';
import {Type} from '../../interface/type';
import {Query} from '../../metadata/di';
Expand Down Expand Up @@ -93,6 +94,12 @@ export function compileComponent(type: Type<any>, metadata: Component): void {
// Make the property configurable in dev mode to allow overriding in tests
configurable: !!ngDevMode,
});


// Add ngInjectableDef so components are reachable through the module injector by default
// This is mostly to support injecting components in tests. In real application code,
// components should be retrieved through the node injector, so this isn't a problem.
compileInjectable(type);
}

function hasSelectorScope<T>(component: Type<T>): component is Type<T>&
Expand Down Expand Up @@ -125,6 +132,11 @@ export function compileDirective(type: Type<any>, directive: Directive): void {
// Make the property configurable in dev mode to allow overriding in tests
configurable: !!ngDevMode,
});

// Add ngInjectableDef so directives are reachable through the module injector by default
// This is mostly to support injecting directives in tests. In real application code,
// directives should be retrieved through the node injector, so this isn't a problem.
compileInjectable(type);
}

export function extendsDirectlyFromObject(type: Type<any>): boolean {
Expand Down
95 changes: 93 additions & 2 deletions packages/core/test/acceptance/providers_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,54 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Component, Injectable} from '@angular/core';
import {Component, Directive, Inject, Injectable, InjectionToken} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {onlyInIvy} from '@angular/private/testing';


describe('providers', () => {

describe('inheritance', () => {

it('should NOT inherit providers', () => {
const SOME_DIRS = new InjectionToken('someDirs');

@Directive({
selector: '[super-dir]',
providers: [{provide: SOME_DIRS, useClass: SuperDirective, multi: true}]
})
class SuperDirective {
}

@Directive({
selector: '[sub-dir]',
providers: [{provide: SOME_DIRS, useClass: SubDirective, multi: true}]
})
class SubDirective extends SuperDirective {
}

@Directive({selector: '[other-dir]'})
class OtherDirective {
constructor(@Inject(SOME_DIRS) public dirs: any) {}
}

@Component({selector: 'app-comp', template: `<div other-dir sub-dir></div>`})
class App {
}

TestBed.configureTestingModule(
{declarations: [SuperDirective, SubDirective, OtherDirective, App]});

const fixture = TestBed.createComponent(App);
fixture.detectChanges();

const otherDir = fixture.debugElement.query(By.css('div')).injector.get(OtherDirective);
expect(otherDir.dirs.length).toEqual(1);
expect(otherDir.dirs[0] instanceof SubDirective).toBe(true);
});

});

describe('lifecycles', () => {
it('should inherit ngOnDestroy hooks on providers', () => {
const logs: string[] = [];
Expand Down Expand Up @@ -181,4 +223,53 @@ describe('providers', () => {
});

});

describe('components and directives', () => {

class MyService {
value = 'some value';
}

@Component({selector: 'my-comp', template: ``})
class MyComp {
constructor(public svc: MyService) {}
}

@Directive({selector: '[some-dir]'})
class MyDir {
constructor(public svc: MyService) {}
}

it('should support providing components in tests without @Injectable', () => {
@Component({selector: 'test-comp', template: '<my-comp></my-comp>'})
class TestComp {
}

TestBed.configureTestingModule({
declarations: [TestComp, MyComp],
// providing MyComp is unnecessary but it shouldn't throw
providers: [MyComp, MyService],
});

const fixture = TestBed.createComponent(TestComp);
const myCompInstance = fixture.debugElement.query(By.css('my-comp')).injector.get(MyComp);
expect(myCompInstance.svc.value).toEqual('some value');
});

it('should support providing directives in tests without @Injectable', () => {
@Component({selector: 'test-comp', template: '<div some-dir></div>'})
class TestComp {
}

TestBed.configureTestingModule({
declarations: [TestComp, MyDir],
// providing MyDir is unnecessary but it shouldn't throw
providers: [MyDir, MyService],
});

const fixture = TestBed.createComponent(TestComp);
const myCompInstance = fixture.debugElement.query(By.css('div')).injector.get(MyDir);
expect(myCompInstance.svc.value).toEqual('some value');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@
{
"name": "NG_ELEMENT_ID"
},
{
"name": "NG_INJECTABLE_DEF"
},
{
"name": "NG_PIPE_DEF"
},
Expand Down Expand Up @@ -680,6 +683,9 @@
{
"name": "ɵɵdefineComponent"
},
{
"name": "ɵɵdefineInjectable"
},
{
"name": "ɵɵdefineInjector"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@
{
"name": "NG_ELEMENT_ID"
},
{
"name": "NG_INJECTABLE_DEF"
},
{
"name": "NG_PIPE_DEF"
},
Expand Down Expand Up @@ -491,6 +494,9 @@
{
"name": "ɵɵdefineComponent"
},
{
"name": "ɵɵdefineInjectable"
},
{
"name": "ɵɵnamespaceHTML"
},
Expand Down
50 changes: 0 additions & 50 deletions packages/core/test/render3/inherit_definition_feature_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -636,54 +636,4 @@ describe('InheritDefinitionFeature', () => {
}).toThrowError('Directives cannot inherit Components');
});

it('should NOT inherit providers', () => {
let otherDir !: OtherDirective;

const SOME_DIRS = new InjectionToken('someDirs');

// providers: [{ provide: SOME_DIRS, useClass: SuperDirective, multi: true }]
class SuperDirective {
static ngDirectiveDef = ɵɵdefineDirective({
type: SuperDirective,
selectors: [['', 'superDir', '']],
factory: () => new SuperDirective(),
features:
[ɵɵProvidersFeature([{provide: SOME_DIRS, useClass: SuperDirective, multi: true}])],
});
}

// providers: [{ provide: SOME_DIRS, useClass: SubDirective, multi: true }]
class SubDirective extends SuperDirective {
static ngDirectiveDef = ɵɵdefineDirective({
type: SubDirective,
selectors: [['', 'subDir', '']],
factory: () => new SubDirective(),
features: [
ɵɵProvidersFeature([{provide: SOME_DIRS, useClass: SubDirective, multi: true}]),
ɵɵInheritDefinitionFeature
],
});
}

class OtherDirective {
constructor(@Inject(SOME_DIRS) public dirs: any) {}

static ngDirectiveDef = ɵɵdefineDirective({
type: OtherDirective,
selectors: [['', 'otherDir', '']],
factory: () => otherDir = new OtherDirective(ɵɵdirectiveInject(SOME_DIRS)),
});
}

/** <div otherDir subDir></div> */
const App = createComponent('app', (rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) {
ɵɵelement(0, 'div', ['otherDir', '', 'subDir', '']);
}
}, 1, 0, [OtherDirective, SubDirective, SuperDirective]);

const fixture = new ComponentFixture(App);
expect(otherDir.dirs.length).toEqual(1);
expect(otherDir.dirs[0] instanceof SubDirective).toBe(true);
});
});