2016-11-22 12:10:23 -05:00
|
|
|
/**
|
|
|
|
* @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 * as ts from 'typescript';
|
|
|
|
import {createLanguageService} from '../src/language_service';
|
2019-08-15 18:17:00 -04:00
|
|
|
import * as ng from '../src/types';
|
2016-11-22 12:10:23 -05:00
|
|
|
import {TypeScriptServiceHost} from '../src/typescript_host';
|
2019-08-15 18:17:00 -04:00
|
|
|
import {MockTypescriptHost} from './test_utils';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Note: If we want to test that a specific diagnostic message is emitted, then
|
2019-09-10 12:21:12 -04:00
|
|
|
* use the `mockHost.addCode()` helper method to add code to an existing file and check
|
2019-08-15 18:17:00 -04:00
|
|
|
* that the diagnostic messages contain the expected output.
|
|
|
|
*
|
|
|
|
* If the goal is to assert that there is no error in a specific file, then use
|
|
|
|
* `mockHost.override()` method to completely override an existing file, and
|
|
|
|
* make sure no diagnostics are produced. When doing so, be extra cautious
|
|
|
|
* about import statements and make sure to assert empty TS diagnostic messages
|
|
|
|
* as well.
|
|
|
|
*/
|
2016-11-22 12:10:23 -05:00
|
|
|
|
|
|
|
describe('diagnostics', () => {
|
2018-07-06 02:13:25 -04:00
|
|
|
let mockHost: MockTypescriptHost;
|
|
|
|
let ngHost: TypeScriptServiceHost;
|
2019-08-15 18:17:00 -04:00
|
|
|
let tsLS: ts.LanguageService;
|
|
|
|
let ngLS: ng.LanguageService;
|
2018-07-06 02:13:25 -04:00
|
|
|
|
|
|
|
beforeEach(() => {
|
2019-09-17 17:33:41 -04:00
|
|
|
mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts']);
|
2019-08-15 18:17:00 -04:00
|
|
|
tsLS = ts.createLanguageService(mockHost);
|
|
|
|
ngHost = new TypeScriptServiceHost(mockHost, tsLS);
|
|
|
|
ngLS = createLanguageService(ngHost);
|
2018-07-06 02:13:25 -04:00
|
|
|
});
|
2016-11-22 12:10:23 -05:00
|
|
|
|
2019-08-15 18:17:00 -04:00
|
|
|
it('should produce no diagnostics for test.ng', () => {
|
|
|
|
// there should not be any errors on existing external template
|
|
|
|
expect(ngLS.getDiagnostics('/app/test.ng')).toEqual([]);
|
|
|
|
});
|
2016-11-22 12:10:23 -05:00
|
|
|
|
2019-08-15 18:17:00 -04:00
|
|
|
it('should not return TS and NG errors for existing files', () => {
|
|
|
|
const files = [
|
|
|
|
'/app/app.component.ts',
|
|
|
|
'/app/main.ts',
|
|
|
|
];
|
|
|
|
for (const file of files) {
|
|
|
|
const syntaxDiags = tsLS.getSyntacticDiagnostics(file);
|
|
|
|
expect(syntaxDiags).toEqual([]);
|
|
|
|
const semanticDiags = tsLS.getSemanticDiagnostics(file);
|
|
|
|
expect(semanticDiags).toEqual([]);
|
|
|
|
const ngDiags = ngLS.getDiagnostics(file);
|
|
|
|
expect(ngDiags).toEqual([]);
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|
2019-08-15 18:17:00 -04:00
|
|
|
});
|
2016-11-22 12:10:23 -05:00
|
|
|
|
2019-08-15 18:17:00 -04:00
|
|
|
// #17611
|
|
|
|
it('should not report diagnostic on iteration of any', () => {
|
|
|
|
const fileName = '/app/test.ng';
|
|
|
|
mockHost.override(fileName, '<div *ngFor="let value of anyValue">{{value.someField}}</div>');
|
|
|
|
const diagnostics = ngLS.getDiagnostics(fileName);
|
|
|
|
expect(diagnostics).toEqual([]);
|
|
|
|
});
|
2016-11-22 12:10:23 -05:00
|
|
|
|
2019-08-15 18:17:00 -04:00
|
|
|
describe('with $event', () => {
|
|
|
|
it('should accept an event', () => {
|
|
|
|
const fileName = '/app/test.ng';
|
|
|
|
mockHost.override(fileName, '<div (click)="myClick($event)">Click me!</div>');
|
|
|
|
const diagnostics = ngLS.getDiagnostics(fileName);
|
|
|
|
expect(diagnostics).toEqual([]);
|
2017-03-28 12:37:24 -04:00
|
|
|
});
|
|
|
|
|
2019-08-15 18:17:00 -04:00
|
|
|
it('should reject it when not in an event binding', () => {
|
|
|
|
const fileName = '/app/test.ng';
|
|
|
|
const content = mockHost.override(fileName, '<div [tabIndex]="$event"></div>');
|
|
|
|
const diagnostics = ngLS.getDiagnostics(fileName) !;
|
|
|
|
expect(diagnostics.length).toBe(1);
|
|
|
|
const {messageText, start, length} = diagnostics[0];
|
|
|
|
expect(messageText)
|
|
|
|
.toBe(
|
2019-08-21 14:32:26 -04:00
|
|
|
`Identifier '$event' is not defined. The component declaration, template variable declarations, and element references do not contain such a member`);
|
2019-08-15 18:17:00 -04:00
|
|
|
const keyword = '$event';
|
|
|
|
expect(start).toBe(content.lastIndexOf(keyword));
|
|
|
|
expect(length).toBe(keyword.length);
|
2016-11-22 12:10:23 -05:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2019-08-15 18:17:00 -04:00
|
|
|
it('should not crash with a incomplete *ngFor', () => {
|
2019-09-10 12:21:12 -04:00
|
|
|
const fileName = mockHost.addCode(`
|
2019-08-15 18:17:00 -04:00
|
|
|
@Component({
|
|
|
|
template: '<div *ngFor></div> ~{after-div}'
|
|
|
|
})
|
|
|
|
export class MyComponent {}`);
|
|
|
|
expect(() => ngLS.getDiagnostics(fileName)).not.toThrow();
|
|
|
|
});
|
2016-11-22 12:10:23 -05:00
|
|
|
|
2019-08-15 18:17:00 -04:00
|
|
|
it('should report a component not in a module', () => {
|
2019-09-10 12:21:12 -04:00
|
|
|
const fileName = mockHost.addCode(`
|
2019-08-15 18:17:00 -04:00
|
|
|
@Component({
|
|
|
|
template: '<div></div>'
|
|
|
|
})
|
|
|
|
export class MyComponent {}`);
|
|
|
|
const diagnostics = ngLS.getDiagnostics(fileName) !;
|
|
|
|
expect(diagnostics.length).toBe(1);
|
|
|
|
const {messageText, start, length} = diagnostics[0];
|
|
|
|
expect(messageText)
|
|
|
|
.toBe(
|
2019-08-21 14:32:26 -04:00
|
|
|
`Component 'MyComponent' is not included in a module and will not be available inside a template. Consider adding it to a NgModule declaration.`);
|
2019-08-15 18:17:00 -04:00
|
|
|
const content = mockHost.getFileContent(fileName) !;
|
|
|
|
const keyword = '@Component';
|
|
|
|
expect(start).toBe(content.lastIndexOf(keyword) + 1); // exclude leading '@'
|
|
|
|
expect(length).toBe(keyword.length - 1); // exclude leading '@'
|
|
|
|
});
|
2016-11-22 12:10:23 -05:00
|
|
|
|
|
|
|
|
2019-08-15 18:17:00 -04:00
|
|
|
it(`should not report an error for a form's host directives`, () => {
|
|
|
|
const fileName = '/app/app.component.ts';
|
|
|
|
mockHost.override(fileName, `
|
|
|
|
import { Component } from '@angular/core';
|
2016-11-22 12:10:23 -05:00
|
|
|
|
2019-08-15 18:17:00 -04:00
|
|
|
@Component({
|
|
|
|
template: '<form></form>'})
|
|
|
|
export class AppComponent {}`);
|
|
|
|
const tsDiags = tsLS.getSemanticDiagnostics(fileName);
|
|
|
|
expect(tsDiags).toEqual([]);
|
|
|
|
const ngDiags = ngLS.getDiagnostics(fileName);
|
|
|
|
expect(ngDiags).toEqual([]);
|
|
|
|
});
|
2016-11-22 12:10:23 -05:00
|
|
|
|
2019-08-15 18:17:00 -04:00
|
|
|
it('should not throw getting diagnostics for an index expression', () => {
|
2019-09-10 12:21:12 -04:00
|
|
|
const fileName = mockHost.addCode(`
|
2019-08-15 18:17:00 -04:00
|
|
|
@Component({
|
|
|
|
template: '<a *ngIf="(auth.isAdmin | async) || (event.leads && event.leads[(auth.uid | async)])"></a>'
|
|
|
|
})
|
|
|
|
export class MyComponent {}`);
|
|
|
|
expect(() => ngLS.getDiagnostics(fileName)).not.toThrow();
|
|
|
|
});
|
2016-11-22 12:10:23 -05:00
|
|
|
|
2019-08-15 18:17:00 -04:00
|
|
|
it('should not throw using a directive with no value', () => {
|
2019-09-10 12:21:12 -04:00
|
|
|
const fileName = mockHost.addCode(`
|
2019-08-15 18:17:00 -04:00
|
|
|
@Component({
|
|
|
|
template: '<form><input [(ngModel)]="name" required /></form>'
|
|
|
|
})
|
|
|
|
export class MyComponent {
|
|
|
|
name = 'some name';
|
|
|
|
}`);
|
|
|
|
expect(() => ngLS.getDiagnostics(fileName)).not.toThrow();
|
|
|
|
});
|
2016-11-22 12:10:23 -05:00
|
|
|
|
2019-08-15 18:17:00 -04:00
|
|
|
it('should report an error for invalid metadata', () => {
|
|
|
|
const fileName = '/app/app.component.ts';
|
|
|
|
const content = mockHost.override(fileName, `
|
|
|
|
import { Component } from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
template: '<div></div>',
|
|
|
|
providers: [
|
|
|
|
{provide: 'foo', useFactory: () => 'foo' }
|
|
|
|
]
|
|
|
|
})
|
|
|
|
export class AppComponent {
|
|
|
|
name = 'some name';
|
|
|
|
}`);
|
|
|
|
const tsDiags = tsLS.getSemanticDiagnostics(fileName);
|
|
|
|
expect(tsDiags).toEqual([]);
|
|
|
|
const ngDiags = ngLS.getDiagnostics(fileName) !;
|
|
|
|
expect(ngDiags.length).toBe(1);
|
|
|
|
const {messageText, start, length} = ngDiags[0];
|
|
|
|
const keyword = `() => 'foo'`;
|
|
|
|
expect(start).toBe(content.lastIndexOf(keyword));
|
|
|
|
expect(length).toBe(keyword.length);
|
|
|
|
// messageText is a three-part chain
|
|
|
|
const firstPart = messageText as ts.DiagnosticMessageChain;
|
2019-08-21 14:32:26 -04:00
|
|
|
expect(firstPart.messageText).toBe(`Error during template compile of 'AppComponent'`);
|
2019-08-15 18:17:00 -04:00
|
|
|
const secondPart = firstPart.next !;
|
|
|
|
expect(secondPart.messageText).toBe('Function expressions are not supported in decorators');
|
|
|
|
const thirdPart = secondPart.next !;
|
|
|
|
expect(thirdPart.messageText)
|
|
|
|
.toBe('Consider changing the function expression into an exported function');
|
|
|
|
expect(thirdPart.next).toBeFalsy();
|
|
|
|
});
|
2016-12-06 12:56:30 -05:00
|
|
|
|
2019-08-15 18:17:00 -04:00
|
|
|
it('should not throw for an invalid class', () => {
|
2019-09-10 12:21:12 -04:00
|
|
|
const fileName = mockHost.addCode(`
|
2019-08-15 18:17:00 -04:00
|
|
|
@Component({
|
|
|
|
template: ''
|
|
|
|
}) class`);
|
|
|
|
expect(() => ngLS.getDiagnostics(fileName)).not.toThrow();
|
|
|
|
});
|
2018-12-14 16:40:01 -05:00
|
|
|
|
2019-08-15 18:17:00 -04:00
|
|
|
it('should not report an error for sub-types of string', () => {
|
|
|
|
const fileName = '/app/app.component.ts';
|
|
|
|
mockHost.override(fileName, `
|
|
|
|
import { Component } from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
template: \`<div *ngIf="something === 'foo'"></div>\`
|
|
|
|
})
|
|
|
|
export class AppComponent {
|
|
|
|
something: 'foo' | 'bar';
|
|
|
|
}`);
|
|
|
|
const tsDiags = tsLS.getSemanticDiagnostics(fileName);
|
|
|
|
expect(tsDiags).toEqual([]);
|
|
|
|
const ngDiags = ngLS.getDiagnostics(fileName);
|
|
|
|
expect(ngDiags).toEqual([]);
|
|
|
|
});
|
2016-12-13 14:20:45 -05:00
|
|
|
|
2019-08-15 18:17:00 -04:00
|
|
|
it('should not report an error for sub-types of number', () => {
|
|
|
|
const fileName = '/app/app.component.ts';
|
|
|
|
mockHost.override(fileName, `
|
|
|
|
import { Component } from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
template: '<div *ngIf="something === 123"></div>'
|
|
|
|
})
|
|
|
|
export class AppComponent {
|
|
|
|
something: 123 | 456;
|
|
|
|
}`);
|
|
|
|
const tsDiags = tsLS.getSemanticDiagnostics(fileName);
|
|
|
|
expect(tsDiags).toEqual([]);
|
|
|
|
const ngDiags = ngLS.getDiagnostics(fileName);
|
|
|
|
expect(ngDiags).toEqual([]);
|
|
|
|
});
|
2016-12-12 21:11:56 -05:00
|
|
|
|
2019-08-15 18:17:00 -04:00
|
|
|
it('should report a warning if an event results in a callable expression', () => {
|
|
|
|
const fileName = '/app/app.component.ts';
|
|
|
|
const content = mockHost.override(fileName, `
|
|
|
|
import { Component } from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
template: '<div (click)="onClick"></div>'
|
|
|
|
})
|
|
|
|
export class MyComponent {
|
|
|
|
onClick() { }
|
|
|
|
}`);
|
|
|
|
const diagnostics = ngLS.getDiagnostics(fileName) !;
|
|
|
|
const {messageText, start, length} = diagnostics[0];
|
|
|
|
expect(messageText).toBe('Unexpected callable expression. Expected a method call');
|
|
|
|
const keyword = `"onClick"`;
|
|
|
|
expect(start).toBe(content.lastIndexOf(keyword) + 1); // exclude leading quote
|
|
|
|
expect(length).toBe(keyword.length - 2); // exclude leading and trailing quotes
|
|
|
|
});
|
2016-12-12 18:59:12 -05:00
|
|
|
|
2019-08-15 18:17:00 -04:00
|
|
|
// #13412
|
|
|
|
it('should not report an error for using undefined', () => {
|
|
|
|
const fileName = '/app/app.component.ts';
|
|
|
|
mockHost.override(fileName, `
|
|
|
|
import { Component } from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
template: '<div *ngIf="something === undefined"></div>'
|
|
|
|
})
|
|
|
|
export class AppComponent {
|
|
|
|
something = 'foo';
|
|
|
|
}`);
|
|
|
|
const tsDiags = tsLS.getSemanticDiagnostics(fileName);
|
|
|
|
expect(tsDiags).toEqual([]);
|
|
|
|
const ngDiags = ngLS.getDiagnostics(fileName);
|
|
|
|
expect(ngDiags).toEqual([]);
|
|
|
|
});
|
2017-03-29 19:14:37 -04:00
|
|
|
|
2019-08-15 18:17:00 -04:00
|
|
|
// Issue #13326
|
|
|
|
it('should report a narrow span for invalid pipes', () => {
|
|
|
|
const fileName = '/app/app.component.ts';
|
|
|
|
const content = mockHost.override(fileName, `
|
|
|
|
import { Component } from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
template: '<p> Using an invalid pipe {{data | dat}} </p>'
|
|
|
|
})
|
|
|
|
export class AppComponent {
|
|
|
|
data = 'some data';
|
|
|
|
}`);
|
|
|
|
const tsDiags = tsLS.getSemanticDiagnostics(fileName);
|
|
|
|
expect(tsDiags).toEqual([]);
|
|
|
|
const ngDiags = ngLS.getDiagnostics(fileName);
|
|
|
|
expect(ngDiags.length).toBe(1);
|
|
|
|
const {messageText, start, length} = ngDiags[0];
|
|
|
|
expect(messageText).toBe(`The pipe 'dat' could not be found`);
|
|
|
|
const keyword = 'data | dat';
|
|
|
|
expect(start).toBe(content.lastIndexOf(keyword));
|
|
|
|
expect(length).toBe(keyword.length);
|
|
|
|
});
|
2017-11-22 15:09:49 -05:00
|
|
|
|
2019-08-15 18:17:00 -04:00
|
|
|
// Issue #19406
|
|
|
|
it('should allow empty template', () => {
|
|
|
|
const fileName = '/app/app.component.ts';
|
|
|
|
mockHost.override(fileName, `
|
|
|
|
import { Component } from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
template : '',
|
|
|
|
})
|
|
|
|
export class AppComponent {}`);
|
|
|
|
const tsDiags = tsLS.getSemanticDiagnostics(fileName);
|
|
|
|
expect(tsDiags).toEqual([]);
|
|
|
|
const ngDiags = ngLS.getDiagnostics(fileName);
|
|
|
|
expect(ngDiags).toEqual([]);
|
|
|
|
});
|
2017-04-04 11:30:38 -04:00
|
|
|
|
2019-08-15 18:17:00 -04:00
|
|
|
// Issue #15460
|
|
|
|
it('should be able to find members defined on an ancestor type', () => {
|
|
|
|
const fileName = '/app/app.component.ts';
|
|
|
|
mockHost.override(fileName, `
|
|
|
|
import { Component } from '@angular/core';
|
|
|
|
import { NgForm } from '@angular/forms';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'example-app',
|
|
|
|
template: \`
|
|
|
|
<form #f="ngForm" (ngSubmit)="onSubmit(f)" novalidate>
|
|
|
|
<input name="first" ngModel required #first="ngModel">
|
|
|
|
<input name="last" ngModel>
|
|
|
|
<button>Submit</button>
|
|
|
|
</form>
|
|
|
|
<p>First name value: {{ first.value }}</p>
|
|
|
|
<p>First name valid: {{ first.valid }}</p>
|
|
|
|
<p>Form value: {{ f.value | json }}</p>
|
|
|
|
<p>Form valid: {{ f.valid }}</p>
|
|
|
|
\`,
|
|
|
|
})
|
|
|
|
export class AppComponent {
|
|
|
|
onSubmit(form: NgForm) {}
|
|
|
|
}`);
|
|
|
|
const tsDiags = tsLS.getSemanticDiagnostics(fileName);
|
|
|
|
expect(tsDiags).toEqual([]);
|
|
|
|
const ngDiags = ngLS.getDiagnostics(fileName);
|
|
|
|
expect(ngDiags).toEqual([]);
|
|
|
|
});
|
2017-04-04 14:37:06 -04:00
|
|
|
|
2019-08-15 18:17:00 -04:00
|
|
|
it('should report an error for invalid providers', () => {
|
|
|
|
const fileName = '/app/app.component.ts';
|
|
|
|
const content = mockHost.override(fileName, `
|
|
|
|
import { Component } from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
template: '',
|
|
|
|
providers: [null]
|
|
|
|
})
|
|
|
|
export class AppComponent {}`);
|
|
|
|
const tsDiags = tsLS.getSemanticDiagnostics(fileName);
|
|
|
|
expect(tsDiags).toEqual([]);
|
|
|
|
const ngDiags = ngLS.getDiagnostics(fileName);
|
|
|
|
expect(ngDiags.length).toBe(1);
|
|
|
|
const {messageText, start, length} = ngDiags[0];
|
|
|
|
expect(messageText)
|
|
|
|
.toBe(
|
|
|
|
'Invalid providers for "AppComponent in /app/app.component.ts" - only instances of Provider and Type are allowed, got: [?null?]');
|
|
|
|
// TODO: Looks like this is the wrong span. Should point to 'null' instead.
|
|
|
|
const keyword = '@Component';
|
|
|
|
expect(start).toBe(content.lastIndexOf(keyword) + 1); // exclude leading '@'
|
|
|
|
expect(length).toBe(keyword.length - 1); // exclude leading '@
|
|
|
|
});
|
2016-12-12 19:42:20 -05:00
|
|
|
|
2019-08-15 18:17:00 -04:00
|
|
|
// Issue #15768
|
|
|
|
it('should be able to parse a template reference', () => {
|
|
|
|
const fileName = '/app/app.component.ts';
|
|
|
|
mockHost.override(fileName, `
|
|
|
|
import { Component } from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'my-component',
|
|
|
|
template: \`
|
|
|
|
<div *ngIf="comps | async; let comps; else loading">
|
|
|
|
</div>
|
|
|
|
<ng-template #loading>Loading comps...</ng-template>
|
|
|
|
\`
|
|
|
|
})
|
|
|
|
export class AppComponent {}`);
|
|
|
|
const tsDiags = tsLS.getSemanticDiagnostics(fileName);
|
|
|
|
expect(tsDiags).toEqual([]);
|
|
|
|
const ngDiags = ngLS.getDiagnostics(fileName);
|
|
|
|
expect(ngDiags).toEqual([]);
|
|
|
|
});
|
2017-04-04 13:31:01 -04:00
|
|
|
|
2019-08-15 18:17:00 -04:00
|
|
|
// Issue #15625
|
|
|
|
it('should not report errors for localization syntax', () => {
|
|
|
|
const fileName = '/app/app.component.ts';
|
|
|
|
mockHost.override(fileName, `
|
|
|
|
import { Component } from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'my-component',
|
|
|
|
template: \`
|
|
|
|
<div>
|
|
|
|
{fieldCount, plural, =0 {no fields} =1 {1 field} other {{{fieldCount}} fields}}
|
|
|
|
</div>
|
|
|
|
\`
|
|
|
|
})
|
|
|
|
export class AppComponent {
|
|
|
|
fieldCount: number;
|
|
|
|
}`);
|
|
|
|
const tsDiags = tsLS.getSemanticDiagnostics(fileName);
|
|
|
|
expect(tsDiags).toEqual([]);
|
|
|
|
const ngDiags = ngLS.getDiagnostics(fileName);
|
|
|
|
expect(ngDiags).toEqual([]);
|
|
|
|
});
|
2017-04-13 19:19:59 -04:00
|
|
|
|
2019-08-15 18:17:00 -04:00
|
|
|
// Issue #15885
|
|
|
|
it('should be able to remove null and undefined from a type', () => {
|
|
|
|
mockHost.overrideOptions(options => {
|
|
|
|
options.strictNullChecks = true;
|
|
|
|
return options;
|
2017-04-10 18:10:34 -04:00
|
|
|
});
|
2019-08-15 18:17:00 -04:00
|
|
|
const fileName = '/app/app.component.ts';
|
|
|
|
mockHost.override(fileName, `
|
|
|
|
import { Component } from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'my-component',
|
|
|
|
template: '{{test?.a}}',
|
|
|
|
})
|
|
|
|
export class AppComponent {
|
|
|
|
test: {a: number, b: number} | null = {
|
|
|
|
a: 1,
|
|
|
|
b: 2,
|
|
|
|
};
|
|
|
|
}`);
|
|
|
|
const tsDiags = tsLS.getSemanticDiagnostics(fileName);
|
|
|
|
expect(tsDiags).toEqual([]);
|
|
|
|
const ngDiags = ngLS.getDiagnostics(fileName);
|
|
|
|
expect(ngDiags).toEqual([]);
|
|
|
|
});
|
2017-04-10 18:10:34 -04:00
|
|
|
|
2019-08-15 18:17:00 -04:00
|
|
|
it('should be able to resolve modules using baseUrl', () => {
|
|
|
|
const fileName = '/app/app.component.ts';
|
|
|
|
mockHost.override(fileName, `
|
|
|
|
import { Component } from '@angular/core';
|
|
|
|
import { NgForm } from '@angular/forms';
|
|
|
|
import { Server } from 'app/server';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'example-app',
|
|
|
|
template: '...',
|
|
|
|
providers: [Server]
|
|
|
|
})
|
|
|
|
export class AppComponent {
|
|
|
|
onSubmit(form: NgForm) {}
|
|
|
|
}`);
|
|
|
|
mockHost.addScript('/other/files/app/server.ts', 'export class Server {}');
|
|
|
|
mockHost.overrideOptions(options => {
|
|
|
|
options.baseUrl = '/other/files';
|
|
|
|
return options;
|
2017-09-28 12:30:34 -04:00
|
|
|
});
|
2019-08-15 18:17:00 -04:00
|
|
|
const tsDiags = tsLS.getSemanticDiagnostics(fileName);
|
|
|
|
expect(tsDiags).toEqual([]);
|
|
|
|
const diagnostic = ngLS.getDiagnostics(fileName);
|
|
|
|
expect(diagnostic).toEqual([]);
|
|
|
|
});
|
2017-09-28 12:30:34 -04:00
|
|
|
|
2019-08-15 18:17:00 -04:00
|
|
|
it('should report errors for using the now removed OpaqueToken (deprecated)', () => {
|
|
|
|
const fileName = '/app/app.component.ts';
|
|
|
|
mockHost.override(fileName, `
|
|
|
|
import { Component, Inject, OpaqueToken } from '@angular/core';
|
|
|
|
import { NgForm } from '@angular/forms';
|
|
|
|
|
|
|
|
export const token = new OpaqueToken('some token');
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'example-app',
|
|
|
|
template: '...'
|
|
|
|
})
|
|
|
|
export class AppComponent {
|
|
|
|
constructor (@Inject(token) value: string) {}
|
|
|
|
onSubmit(form: NgForm) {}
|
|
|
|
}`);
|
|
|
|
const tsDiags = tsLS.getSemanticDiagnostics(fileName);
|
|
|
|
expect(tsDiags.length).toBe(1);
|
|
|
|
expect(tsDiags[0].messageText)
|
|
|
|
.toBe(
|
|
|
|
`Module '"../node_modules/@angular/core/core"' has no exported member 'OpaqueToken'.`);
|
2016-11-22 12:10:23 -05:00
|
|
|
});
|
2019-08-15 18:17:00 -04:00
|
|
|
|
2019-09-10 15:51:16 -04:00
|
|
|
describe('templates', () => {
|
2019-09-10 11:55:12 -04:00
|
|
|
it('should report errors for invalid templateUrls', () => {
|
|
|
|
const fileName = mockHost.addCode(`
|
|
|
|
@Component({
|
|
|
|
templateUrl: '«notAFile»',
|
|
|
|
})
|
|
|
|
export class MyComponent {}`);
|
|
|
|
|
|
|
|
const marker = mockHost.getReferenceMarkerFor(fileName, 'notAFile');
|
|
|
|
|
|
|
|
const diagnostics = ngLS.getDiagnostics(fileName) !;
|
|
|
|
const urlDiagnostic =
|
|
|
|
diagnostics.find(d => d.messageText === 'URL does not point to a valid file');
|
|
|
|
expect(urlDiagnostic).toBeDefined();
|
|
|
|
|
|
|
|
const {start, length} = urlDiagnostic !;
|
|
|
|
expect(start).toBe(marker.start);
|
|
|
|
expect(length).toBe(marker.length);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not report errors for valid templateUrls', () => {
|
|
|
|
const fileName = mockHost.addCode(`
|
|
|
|
@Component({
|
|
|
|
templateUrl: './test.ng',
|
|
|
|
})
|
|
|
|
export class MyComponent {}`);
|
|
|
|
|
|
|
|
const diagnostics = ngLS.getDiagnostics(fileName) !;
|
|
|
|
const urlDiagnostic =
|
|
|
|
diagnostics.find(d => d.messageText === 'URL does not point to a valid file');
|
|
|
|
expect(urlDiagnostic).toBeUndefined();
|
|
|
|
});
|
2019-09-13 14:58:01 -04:00
|
|
|
|
2019-09-10 15:51:16 -04:00
|
|
|
it('should report diagnostic for missing template or templateUrl', () => {
|
2019-09-18 17:11:21 -04:00
|
|
|
const fileName = '/app/app.component.ts';
|
|
|
|
const content = mockHost.override(fileName, `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
2019-09-10 15:51:16 -04:00
|
|
|
@Component({
|
|
|
|
selector: 'app-example',
|
|
|
|
})
|
2019-09-18 17:11:21 -04:00
|
|
|
export class AppComponent {}`);
|
2019-09-10 15:51:16 -04:00
|
|
|
const diags = ngLS.getDiagnostics(fileName);
|
2019-09-18 17:11:21 -04:00
|
|
|
expect(diags.length).toBe(1);
|
|
|
|
const {file, messageText, start, length} = diags[0];
|
|
|
|
expect(file !.fileName).toBe(fileName);
|
|
|
|
expect(messageText).toBe(`Component 'AppComponent' must have a template or templateUrl`);
|
|
|
|
expect(start).toBe(content.indexOf(`@Component`) + 1);
|
2019-09-10 15:51:16 -04:00
|
|
|
expect(length).toBe('Component'.length);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should report diagnostic for both template and templateUrl', () => {
|
2019-09-18 17:11:21 -04:00
|
|
|
const fileName = '/app/app.component.ts';
|
|
|
|
const content = mockHost.override(fileName, `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
2019-09-10 15:51:16 -04:00
|
|
|
@Component({
|
|
|
|
selector: 'app-example',
|
|
|
|
template: '<div></div>',
|
2019-09-18 17:11:21 -04:00
|
|
|
templateUrl: './test.ng',
|
2019-09-10 15:51:16 -04:00
|
|
|
})
|
2019-09-18 17:11:21 -04:00
|
|
|
export class AppComponent {}`);
|
2019-09-10 15:51:16 -04:00
|
|
|
const diags = ngLS.getDiagnostics(fileName);
|
2019-09-18 17:11:21 -04:00
|
|
|
expect(diags.length).toBe(1);
|
|
|
|
const {file, messageText, start, length} = diags[0];
|
|
|
|
expect(file !.fileName).toBe(fileName);
|
|
|
|
expect(messageText)
|
|
|
|
.toBe(`Component 'AppComponent' must not have both template and templateUrl`);
|
|
|
|
expect(start).toBe(content.indexOf(`@Component`) + 1);
|
2019-09-10 15:51:16 -04:00
|
|
|
expect(length).toBe('Component'.length);
|
|
|
|
});
|
|
|
|
|
2019-09-13 14:58:01 -04:00
|
|
|
it('should report errors for invalid styleUrls', () => {
|
|
|
|
const fileName = mockHost.addCode(`
|
|
|
|
@Component({
|
|
|
|
styleUrls: ['«notAFile»'],
|
|
|
|
})
|
|
|
|
export class MyComponent {}`);
|
|
|
|
|
|
|
|
const marker = mockHost.getReferenceMarkerFor(fileName, 'notAFile');
|
|
|
|
|
|
|
|
const diagnostics = ngLS.getDiagnostics(fileName) !;
|
|
|
|
const urlDiagnostic =
|
|
|
|
diagnostics.find(d => d.messageText === 'URL does not point to a valid file');
|
|
|
|
expect(urlDiagnostic).toBeDefined();
|
|
|
|
|
|
|
|
const {start, length} = urlDiagnostic !;
|
|
|
|
expect(start).toBe(marker.start);
|
|
|
|
expect(length).toBe(marker.length);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not report errors for valid styleUrls', () => {
|
|
|
|
const fileName = '/app/app.component.ts';
|
|
|
|
mockHost.override(fileName, `
|
|
|
|
@Component({
|
|
|
|
styleUrls: ['./test.css', './test.css'],
|
|
|
|
})
|
|
|
|
export class MyComponent {}`);
|
|
|
|
|
|
|
|
const diagnostics = ngLS.getDiagnostics(fileName) !;
|
|
|
|
expect(diagnostics.length).toBe(0);
|
|
|
|
});
|
2019-09-10 11:55:12 -04:00
|
|
|
});
|
|
|
|
|
2019-08-21 14:32:26 -04:00
|
|
|
// https://github.com/angular/vscode-ng-language-service/issues/235
|
|
|
|
// There is no easy fix for this issue currently due to the way template
|
|
|
|
// tokenization is done. In the example below, the whole string
|
|
|
|
// `\r\n{{line0}}\r\n{{line1}}\r\n{{line2}}` is tokenized as a whole, and then
|
|
|
|
// CR characters are stripped from it. Source span information is lost in the
|
|
|
|
// process. For more discussion, see the link above.
|
|
|
|
/*
|
|
|
|
it('should work correctly with CRLF endings', () => {
|
|
|
|
const fileName = '/app/test.ng';
|
|
|
|
const content = mockHost.override(fileName,
|
|
|
|
'\r\n<div>\r\n{{line0}}\r\n{{line1}}\r\n{{line2}}\r\n</div>');
|
|
|
|
const ngDiags = ngLS.getDiagnostics(fileName);
|
|
|
|
expect(ngDiags.length).toBe(3);
|
|
|
|
for (let i = 0; i < 3; ++i) {
|
|
|
|
const {messageText, start, length} = ngDiags[i];
|
|
|
|
expect(messageText)
|
|
|
|
.toBe(
|
|
|
|
`Identifier 'line${i}' is not defined. The component declaration, template variable
|
|
|
|
declarations, and element references do not contain such a member`);
|
|
|
|
// Assert that the span is actually highlight the bounded text. The span
|
|
|
|
// would be off if CRLF endings are not handled properly.
|
|
|
|
expect(content.substring(start !, start ! + length !)).toBe(`line${i}`);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
*/
|
|
|
|
|
2016-11-22 12:10:23 -05:00
|
|
|
});
|