feat(compiler-cli): report error if undecorated class with Angular features is discovered (#36921)
Previously in v9, we deprecated the pattern of undecorated base classes that rely on Angular features. We ran a migration for this in version 9 and will run the same on in version 10 again. To ensure that projects do not regress and start using the unsupported pattern again, we report an error in ngtsc if such undecorated classes are discovered. We keep the compatibility code enabled in ngcc so that libraries can be still be consumed, even if they have not been migrated yet. Resolves FW-2130. PR Close #36921
This commit is contained in:

committed by
Alex Rickabaugh

parent
c6ecdc9a81
commit
4c92cf43cf
@ -3276,574 +3276,4 @@ describe('compiler compliance', () => {
|
||||
expectEmit(result.source, MyAppDeclaration, 'Invalid component definition');
|
||||
});
|
||||
});
|
||||
|
||||
describe('inherited base classes', () => {
|
||||
const directive = {
|
||||
'some.directive.ts': `
|
||||
import {Directive} from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[someDir]',
|
||||
})
|
||||
export class SomeDirective { }
|
||||
`
|
||||
};
|
||||
|
||||
it('should add an abstract directive if one or more @Input is present', () => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Component, NgModule, Input} from '@angular/core';
|
||||
export class BaseClass {
|
||||
@Input()
|
||||
input1 = 'test';
|
||||
|
||||
@Input('alias2')
|
||||
input2 = 'whatever';
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
template: \`<div>{{input1}} {{input2}}</div>\`
|
||||
})
|
||||
export class MyComponent extends BaseClass {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [MyComponent]
|
||||
})
|
||||
export class MyModule {}
|
||||
`
|
||||
}
|
||||
};
|
||||
const expectedOutput = `
|
||||
// ...
|
||||
BaseClass.ɵdir = $r3$.ɵɵdefineDirective({
|
||||
type: BaseClass,
|
||||
inputs: {
|
||||
input1: "input1",
|
||||
input2: ["alias2", "input2"]
|
||||
}
|
||||
});
|
||||
// ...
|
||||
`;
|
||||
const result = compile(files, angularFiles);
|
||||
expectEmit(result.source, expectedOutput, 'Invalid directive definition');
|
||||
});
|
||||
|
||||
it('should add an abstract directive if one or more @Output is present', () => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Component, NgModule, Output, EventEmitter} from '@angular/core';
|
||||
export class BaseClass {
|
||||
@Output()
|
||||
output1 = new EventEmitter<string>();
|
||||
|
||||
@Output()
|
||||
output2 = new EventEmitter<string>();
|
||||
|
||||
clicked() {
|
||||
this.output1.emit('test');
|
||||
this.output2.emit('test');
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
template: \`<button (click)="clicked()">Click Me</button>\`
|
||||
})
|
||||
export class MyComponent extends BaseClass {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [MyComponent]
|
||||
})
|
||||
export class MyModule {}
|
||||
`
|
||||
}
|
||||
};
|
||||
const expectedOutput = `
|
||||
// ...
|
||||
BaseClass.ɵdir = $r3$.ɵɵdefineDirective({
|
||||
type: BaseClass,
|
||||
outputs: {
|
||||
output1: "output1",
|
||||
output2: "output2"
|
||||
}
|
||||
});
|
||||
// ...
|
||||
`;
|
||||
const result = compile(files, angularFiles);
|
||||
expectEmit(result.source, expectedOutput, 'Invalid directive definition');
|
||||
});
|
||||
|
||||
it('should add an abstract directive if a mixture of @Input and @Output props are present',
|
||||
() => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Component, NgModule, Input, Output, EventEmitter} from '@angular/core';
|
||||
export class BaseClass {
|
||||
@Output()
|
||||
output1 = new EventEmitter<string>();
|
||||
|
||||
@Output()
|
||||
output2 = new EventEmitter<string>();
|
||||
|
||||
@Input()
|
||||
input1 = 'test';
|
||||
|
||||
@Input('whatever')
|
||||
input2 = 'blah';
|
||||
|
||||
clicked() {
|
||||
this.output1.emit('test');
|
||||
this.output2.emit('test');
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
template: \`<button (click)="clicked()">Click Me</button>\`
|
||||
})
|
||||
export class MyComponent extends BaseClass {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [MyComponent]
|
||||
})
|
||||
export class MyModule {}
|
||||
`
|
||||
}
|
||||
};
|
||||
const expectedOutput = `
|
||||
// ...
|
||||
BaseClass.ɵdir = $r3$.ɵɵdefineDirective({
|
||||
type: BaseClass,
|
||||
inputs: {
|
||||
input1: "input1",
|
||||
input2: ["whatever", "input2"]
|
||||
},
|
||||
outputs: {
|
||||
output1: "output1",
|
||||
output2: "output2"
|
||||
}
|
||||
});
|
||||
// ...
|
||||
`;
|
||||
const result = compile(files, angularFiles);
|
||||
expectEmit(result.source, expectedOutput, 'Invalid directive definition');
|
||||
});
|
||||
|
||||
it('should add an abstract directive if a ViewChild query is present', () => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Component, NgModule, ViewChild} from '@angular/core';
|
||||
export class BaseClass {
|
||||
@ViewChild('something') something: any;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
template: ''
|
||||
})
|
||||
export class MyComponent extends BaseClass {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [MyComponent]
|
||||
})
|
||||
export class MyModule {}
|
||||
`
|
||||
}
|
||||
};
|
||||
const expectedOutput = `
|
||||
const $e0_attrs$ = ["something"];
|
||||
// ...
|
||||
BaseClass.ɵdir = $r3$.ɵɵdefineDirective({
|
||||
type: BaseClass,
|
||||
viewQuery: function BaseClass_Query(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
$r3$.ɵɵviewQuery($e0_attrs$, true);
|
||||
}
|
||||
if (rf & 2) {
|
||||
var $tmp$;
|
||||
$r3$.ɵɵqueryRefresh($tmp$ = $r3$.ɵɵloadQuery()) && (ctx.something = $tmp$.first);
|
||||
}
|
||||
}
|
||||
});
|
||||
// ...
|
||||
`;
|
||||
const result = compile(files, angularFiles);
|
||||
expectEmit(result.source, expectedOutput, 'Invalid directive definition');
|
||||
});
|
||||
|
||||
it('should add an abstract directive if a ViewChildren query is present', () => {
|
||||
const files = {
|
||||
app: {
|
||||
...directive,
|
||||
'spec.ts': `
|
||||
import {Component, NgModule, ViewChildren} from '@angular/core';
|
||||
import {SomeDirective} from './some.directive';
|
||||
|
||||
export class BaseClass {
|
||||
@ViewChildren(SomeDirective) something: QueryList<SomeDirective>;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
template: ''
|
||||
})
|
||||
export class MyComponent extends BaseClass {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [MyComponent, SomeDirective]
|
||||
})
|
||||
export class MyModule {}
|
||||
`
|
||||
}
|
||||
};
|
||||
const expectedOutput = `
|
||||
// ...
|
||||
BaseClass.ɵdir = $r3$.ɵɵdefineDirective({
|
||||
type: BaseClass,
|
||||
viewQuery: function BaseClass_Query(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
$r3$.ɵɵviewQuery(SomeDirective, true);
|
||||
}
|
||||
if (rf & 2) {
|
||||
var $tmp$;
|
||||
$r3$.ɵɵqueryRefresh($tmp$ = $r3$.ɵɵloadQuery()) && (ctx.something = $tmp$);
|
||||
}
|
||||
}
|
||||
});
|
||||
// ...
|
||||
`;
|
||||
const result = compile(files, angularFiles);
|
||||
expectEmit(result.source, expectedOutput, 'Invalid directive definition');
|
||||
});
|
||||
|
||||
it('should add an abstract directive if a ContentChild query is present', () => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Component, NgModule, ContentChild} from '@angular/core';
|
||||
export class BaseClass {
|
||||
@ContentChild('something') something: any;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
template: ''
|
||||
})
|
||||
export class MyComponent extends BaseClass {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [MyComponent]
|
||||
})
|
||||
export class MyModule {}
|
||||
`
|
||||
}
|
||||
};
|
||||
const expectedOutput = `
|
||||
const $e0_attrs$ = ["something"];
|
||||
// ...
|
||||
BaseClass.ɵdir = $r3$.ɵɵdefineDirective({
|
||||
type: BaseClass,
|
||||
contentQueries: function BaseClass_ContentQueries(rf, ctx, dirIndex) {
|
||||
if (rf & 1) {
|
||||
$r3$.ɵɵcontentQuery(dirIndex, $e0_attrs$, true);
|
||||
}
|
||||
if (rf & 2) {
|
||||
var $tmp$;
|
||||
$r3$.ɵɵqueryRefresh($tmp$ = $r3$.ɵɵloadQuery()) && (ctx.something = $tmp$.first);
|
||||
}
|
||||
}
|
||||
});
|
||||
// ...
|
||||
`;
|
||||
const result = compile(files, angularFiles);
|
||||
expectEmit(result.source, expectedOutput, 'Invalid directive definition');
|
||||
});
|
||||
|
||||
it('should add an abstract directive if a ContentChildren query is present', () => {
|
||||
const files = {
|
||||
app: {
|
||||
...directive,
|
||||
'spec.ts': `
|
||||
import {Component, NgModule, ContentChildren} from '@angular/core';
|
||||
import {SomeDirective} from './some.directive';
|
||||
|
||||
export class BaseClass {
|
||||
@ContentChildren(SomeDirective) something: QueryList<SomeDirective>;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
template: ''
|
||||
})
|
||||
export class MyComponent extends BaseClass {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [MyComponent, SomeDirective]
|
||||
})
|
||||
export class MyModule {}
|
||||
`
|
||||
}
|
||||
};
|
||||
const expectedOutput = `
|
||||
// ...
|
||||
BaseClass.ɵdir = $r3$.ɵɵdefineDirective({
|
||||
type: BaseClass,
|
||||
contentQueries: function BaseClass_ContentQueries(rf, ctx, dirIndex) {
|
||||
if (rf & 1) {
|
||||
$r3$.ɵɵcontentQuery(dirIndex, SomeDirective, false);
|
||||
}
|
||||
if (rf & 2) {
|
||||
var $tmp$;
|
||||
$r3$.ɵɵqueryRefresh($tmp$ = $r3$.ɵɵloadQuery()) && (ctx.something = $tmp$);
|
||||
}
|
||||
}
|
||||
});
|
||||
// ...
|
||||
`;
|
||||
const result = compile(files, angularFiles);
|
||||
expectEmit(result.source, expectedOutput, 'Invalid directive definition');
|
||||
});
|
||||
|
||||
it('should add an abstract directive if a host binding is present', () => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Component, NgModule, HostBinding} from '@angular/core';
|
||||
export class BaseClass {
|
||||
@HostBinding('attr.tabindex')
|
||||
tabindex = -1;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
template: ''
|
||||
})
|
||||
export class MyComponent extends BaseClass {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [MyComponent]
|
||||
})
|
||||
export class MyModule {}
|
||||
`
|
||||
}
|
||||
};
|
||||
const expectedOutput = `
|
||||
// ...
|
||||
BaseClass.ɵdir = $r3$.ɵɵdefineDirective({
|
||||
type: BaseClass,
|
||||
hostVars: 1,
|
||||
hostBindings: function BaseClass_HostBindings(rf, ctx) {
|
||||
if (rf & 2) {
|
||||
$r3$.ɵɵattribute("tabindex", ctx.tabindex);
|
||||
}
|
||||
}
|
||||
});
|
||||
// ...
|
||||
`;
|
||||
const result = compile(files, angularFiles);
|
||||
expectEmit(result.source, expectedOutput, 'Invalid directive definition');
|
||||
});
|
||||
|
||||
it('should add an abstract directive if a host listener is present', () => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Component, NgModule, HostListener} from '@angular/core';
|
||||
export class BaseClass {
|
||||
@HostListener('mousedown', ['$event'])
|
||||
handleMousedown(event: any) {}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
template: ''
|
||||
})
|
||||
export class MyComponent extends BaseClass {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [MyComponent]
|
||||
})
|
||||
export class MyModule {}
|
||||
`
|
||||
}
|
||||
};
|
||||
const expectedOutput = `
|
||||
// ...
|
||||
BaseClass.ɵdir = $r3$.ɵɵdefineDirective({
|
||||
type: BaseClass,
|
||||
hostBindings: function BaseClass_HostBindings(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
$r3$.ɵɵlistener("mousedown", function BaseClass_mousedown_HostBindingHandler($event) {
|
||||
return ctx.handleMousedown($event);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
// ...
|
||||
`;
|
||||
const result = compile(files, angularFiles);
|
||||
expectEmit(result.source, expectedOutput, 'Invalid directive definition');
|
||||
});
|
||||
|
||||
it('should add an abstract directive when using any lifecycle hook', () => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Component, NgModule, Input} from '@angular/core';
|
||||
export class BaseClass {
|
||||
ngAfterContentChecked() {}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
template: \`<div>{{input1}} {{input2}}</div>\`
|
||||
})
|
||||
export class MyComponent extends BaseClass {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [MyComponent]
|
||||
})
|
||||
export class MyModule {}
|
||||
`
|
||||
}
|
||||
};
|
||||
const expectedOutput = `
|
||||
// ...
|
||||
BaseClass.ɵdir = $r3$.ɵɵdefineDirective({
|
||||
type: BaseClass
|
||||
});
|
||||
// ...
|
||||
`;
|
||||
const result = compile(files, angularFiles);
|
||||
expectEmit(result.source, expectedOutput, 'Invalid directive definition');
|
||||
});
|
||||
|
||||
|
||||
it('should add an abstract directive when using ngOnChanges', () => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Component, NgModule, Input} from '@angular/core';
|
||||
export class BaseClass {
|
||||
ngOnChanges() {}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
template: \`<div>{{input1}} {{input2}}</div>\`
|
||||
})
|
||||
export class MyComponent extends BaseClass {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [MyComponent]
|
||||
})
|
||||
export class MyModule {}
|
||||
`
|
||||
}
|
||||
};
|
||||
const expectedOutput = `
|
||||
// ...
|
||||
BaseClass.ɵdir = $r3$.ɵɵdefineDirective({
|
||||
type: BaseClass,
|
||||
features: [$r3$.ɵɵNgOnChangesFeature]
|
||||
});
|
||||
// ...
|
||||
`;
|
||||
const result = compile(files, angularFiles);
|
||||
expectEmit(result.source, expectedOutput, 'Invalid directive definition');
|
||||
});
|
||||
|
||||
it('should NOT add an abstract directive if @Component is present', () => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Component, NgModule, Output, EventEmitter} from '@angular/core';
|
||||
@Component({
|
||||
selector: 'whatever',
|
||||
template: '<button (click)="clicked()">Click {{input1}}</button>'
|
||||
})
|
||||
export class BaseClass {
|
||||
@Output()
|
||||
output1 = new EventEmitter<string>();
|
||||
|
||||
@Input()
|
||||
input1 = 'whatever';
|
||||
|
||||
clicked() {
|
||||
this.output1.emit('test');
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
template: \`<div>What is this developer doing?</div>\`
|
||||
})
|
||||
export class MyComponent extends BaseClass {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [MyComponent]
|
||||
})
|
||||
export class MyModule {}
|
||||
`
|
||||
}
|
||||
};
|
||||
const result = compile(files, angularFiles);
|
||||
expect(result.source).not.toContain('ɵdir');
|
||||
});
|
||||
|
||||
it('should NOT add an abstract directive if @Directive is present', () => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Component, Directive, NgModule, Output, EventEmitter} from '@angular/core';
|
||||
@Directive({
|
||||
selector: 'whatever',
|
||||
})
|
||||
export class BaseClass {
|
||||
@Output()
|
||||
output1 = new EventEmitter<string>();
|
||||
|
||||
@Input()
|
||||
input1 = 'whatever';
|
||||
|
||||
clicked() {
|
||||
this.output1.emit('test');
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
template: '<button (click)="clicked()">Click {{input1}}</button>'
|
||||
})
|
||||
export class MyComponent extends BaseClass {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [MyComponent]
|
||||
})
|
||||
export class MyModule {}
|
||||
`
|
||||
}
|
||||
};
|
||||
const result = compile(files, angularFiles);
|
||||
expect(result.source.match(/ɵdir/g)!.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -545,6 +545,7 @@ runInEachFileSystem(os => {
|
||||
Output as AngularOutput
|
||||
} from '@angular/core';
|
||||
|
||||
@AngularDirective()
|
||||
export class TestBase {
|
||||
@AngularInput() input: any;
|
||||
@AngularOutput() output: any;
|
||||
@ -884,10 +885,11 @@ runInEachFileSystem(os => {
|
||||
expect(jsContents).toContain('background-color: blue');
|
||||
});
|
||||
|
||||
it('should include generic type for undecorated class declarations', () => {
|
||||
it('should include generic type in directive definition', () => {
|
||||
env.write('test.ts', `
|
||||
import {Component, Input, NgModule} from '@angular/core';
|
||||
import {Directive, Input, NgModule} from '@angular/core';
|
||||
|
||||
@Directive()
|
||||
export class TestBase {
|
||||
@Input() input: any;
|
||||
}
|
||||
@ -905,6 +907,76 @@ runInEachFileSystem(os => {
|
||||
`static ɵdir: i0.ɵɵDirectiveDefWithMeta<TestBase, never, never, { "input": "input"; }, {}, never>;`);
|
||||
});
|
||||
|
||||
describe('undecorated classes using Angular features', () => {
|
||||
it('should error if @Input has been discovered',
|
||||
() => assertErrorUndecoratedClassWithField('Input'));
|
||||
it('should error if @Output has been discovered',
|
||||
() => assertErrorUndecoratedClassWithField('Output'));
|
||||
it('should error if @ViewChild has been discovered',
|
||||
() => assertErrorUndecoratedClassWithField('ViewChild'));
|
||||
it('should error if @ViewChildren has been discovered',
|
||||
() => assertErrorUndecoratedClassWithField('ViewChildren'));
|
||||
it('should error if @ContentChild has been discovered',
|
||||
() => assertErrorUndecoratedClassWithField('ContentChildren'));
|
||||
it('should error if @HostBinding has been discovered',
|
||||
() => assertErrorUndecoratedClassWithField('HostBinding'));
|
||||
it('should error if @HostListener has been discovered',
|
||||
() => assertErrorUndecoratedClassWithField('HostListener'));
|
||||
|
||||
it(`should error if ngOnChanges lifecycle hook has been discovered`,
|
||||
() => assertErrorUndecoratedClassWithLifecycleHook('ngOnChanges'));
|
||||
it(`should error if ngOnInit lifecycle hook has been discovered`,
|
||||
() => assertErrorUndecoratedClassWithLifecycleHook('ngOnInit'));
|
||||
it(`should error if ngOnDestroy lifecycle hook has been discovered`,
|
||||
() => assertErrorUndecoratedClassWithLifecycleHook('ngOnDestroy'));
|
||||
it(`should error if ngDoCheck lifecycle hook has been discovered`,
|
||||
() => assertErrorUndecoratedClassWithLifecycleHook('ngDoCheck'));
|
||||
it(`should error if ngAfterViewInit lifecycle hook has been discovered`,
|
||||
() => assertErrorUndecoratedClassWithLifecycleHook('ngAfterViewInit'));
|
||||
it(`should error if ngAfterViewChecked lifecycle hook has been discovered`,
|
||||
() => assertErrorUndecoratedClassWithLifecycleHook('ngAfterViewChecked'));
|
||||
it(`should error if ngAfterContentInit lifecycle hook has been discovered`,
|
||||
() => assertErrorUndecoratedClassWithLifecycleHook('ngAfterContentInit'));
|
||||
it(`should error if ngAfterContentChecked lifecycle hook has been discovered`,
|
||||
() => assertErrorUndecoratedClassWithLifecycleHook('ngAfterContentChecked'));
|
||||
|
||||
function assertErrorUndecoratedClassWithField(fieldDecoratorName: string) {
|
||||
env.write('test.ts', `
|
||||
import {Component, ${fieldDecoratorName}, NgModule} from '@angular/core';
|
||||
|
||||
export class SomeBaseClass {
|
||||
@${fieldDecoratorName}() someMember: any;
|
||||
}
|
||||
`);
|
||||
|
||||
const errors = env.driveDiagnostics();
|
||||
expect(errors.length).toBe(1);
|
||||
expect(trim(errors[0].messageText as string))
|
||||
.toContain(
|
||||
'Class is using Angular features but is not decorated. Please add an explicit ' +
|
||||
'Angular decorator.');
|
||||
}
|
||||
|
||||
function assertErrorUndecoratedClassWithLifecycleHook(lifecycleName: string) {
|
||||
env.write('test.ts', `
|
||||
import {Component, NgModule} from '@angular/core';
|
||||
|
||||
export class SomeBaseClass {
|
||||
${lifecycleName}() {
|
||||
// empty
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const errors = env.driveDiagnostics();
|
||||
expect(errors.length).toBe(1);
|
||||
expect(trim(errors[0].messageText as string))
|
||||
.toContain(
|
||||
'Class is using Angular features but is not decorated. Please add an explicit ' +
|
||||
'Angular decorator.');
|
||||
}
|
||||
});
|
||||
|
||||
it('should compile NgModules without errors', () => {
|
||||
env.write('test.ts', `
|
||||
import {Component, NgModule} from '@angular/core';
|
||||
|
Reference in New Issue
Block a user