
NgIf syntax has been extended to support else clause to display template when the condition is false. In addition the condition value can now be stored in local variable, for later reuse. This is especially useful when used with the `async` pipe. Example: ``` <div *ngIf="userObservable | async; else loading; let user"> Hello {{user.last}}, {{user.first}}! </div> <template #loading>Waiting...</template> ``` closes #13061 closes #13297
264 lines
12 KiB
TypeScript
264 lines
12 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 'reflect-metadata';
|
|
|
|
import * as ts from 'typescript';
|
|
|
|
import {LanguageServicePlugin} from '../src/ts_plugin';
|
|
|
|
import {toh} from './test_data';
|
|
import {MockTypescriptHost} from './test_utils';
|
|
|
|
describe('plugin', () => {
|
|
let documentRegistry = ts.createDocumentRegistry();
|
|
let mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts'], toh);
|
|
let service = ts.createLanguageService(mockHost, documentRegistry);
|
|
let program = service.getProgram();
|
|
|
|
it('should not report errors on tour of heroes', () => {
|
|
expectNoDiagnostics(service.getCompilerOptionsDiagnostics());
|
|
for (let source of program.getSourceFiles()) {
|
|
expectNoDiagnostics(service.getSyntacticDiagnostics(source.fileName));
|
|
expectNoDiagnostics(service.getSemanticDiagnostics(source.fileName));
|
|
}
|
|
});
|
|
|
|
let plugin = new LanguageServicePlugin({host: mockHost, service, registry: documentRegistry});
|
|
|
|
it('should not report template errors on tour of heroes', () => {
|
|
for (let source of program.getSourceFiles()) {
|
|
// Ignore all 'cases.ts' files as they intentionally contain errors.
|
|
if (!source.fileName.endsWith('cases.ts')) {
|
|
expectNoDiagnostics(plugin.getSemanticDiagnosticsFilter(source.fileName, []));
|
|
}
|
|
}
|
|
});
|
|
|
|
it('should be able to get entity completions',
|
|
() => { contains('app/app.component.ts', 'entity-amp', '&', '>', '<', 'ι'); });
|
|
|
|
it('should be able to return html elements', () => {
|
|
let htmlTags = ['h1', 'h2', 'div', 'span'];
|
|
let locations = ['empty', 'start-tag-h1', 'h1-content', 'start-tag', 'start-tag-after-h'];
|
|
for (let location of locations) {
|
|
contains('app/app.component.ts', location, ...htmlTags);
|
|
}
|
|
});
|
|
|
|
it('should be able to return element diretives',
|
|
() => { contains('app/app.component.ts', 'empty', 'my-app'); });
|
|
|
|
it('should be able to return h1 attributes',
|
|
() => { contains('app/app.component.ts', 'h1-after-space', 'id', 'dir', 'lang', 'onclick'); });
|
|
|
|
it('should be able to find common angular attributes', () => {
|
|
contains('app/app.component.ts', 'div-attributes', '(click)', '[ngClass]', '*ngIf', '*ngFor');
|
|
});
|
|
|
|
it('should be able to returned attribute names with an incompete attribute',
|
|
() => { contains('app/parsing-cases.ts', 'no-value-attribute', 'id', 'dir', 'lang'); });
|
|
|
|
it('should be able to return attributes of an incomplete element', () => {
|
|
contains('app/parsing-cases.ts', 'incomplete-open-lt', 'a');
|
|
contains('app/parsing-cases.ts', 'incomplete-open-a', 'a');
|
|
contains('app/parsing-cases.ts', 'incomplete-open-attr', 'id', 'dir', 'lang');
|
|
});
|
|
|
|
it('should be able to return completions with a missing closing tag',
|
|
() => { contains('app/parsing-cases.ts', 'missing-closing', 'h1', 'h2'); });
|
|
|
|
it('should be able to return common attributes of in an unknown tag',
|
|
() => { contains('app/parsing-cases.ts', 'unknown-element', 'id', 'dir', 'lang'); });
|
|
|
|
it('should be able to get the completions at the beginning of an interpolation',
|
|
() => { contains('app/app.component.ts', 'h2-hero', 'hero', 'title'); });
|
|
|
|
it('should not include private members of the of a class',
|
|
() => { contains('app/app.component.ts', 'h2-hero', '-internal'); });
|
|
|
|
it('should be able to get the completions at the end of an interpolation',
|
|
() => { contains('app/app.component.ts', 'sub-end', 'hero', 'title'); });
|
|
|
|
it('should be able to get the completions in a property read',
|
|
() => { contains('app/app.component.ts', 'h2-name', 'name', 'id'); });
|
|
|
|
it('should be able to get a list of pipe values', () => {
|
|
contains('app/parsing-cases.ts', 'before-pipe', 'lowercase', 'uppercase');
|
|
contains('app/parsing-cases.ts', 'in-pipe', 'lowercase', 'uppercase');
|
|
contains('app/parsing-cases.ts', 'after-pipe', 'lowercase', 'uppercase');
|
|
});
|
|
|
|
it('should be able get completions in an empty interpolation',
|
|
() => { contains('app/parsing-cases.ts', 'empty-interpolation', 'title', 'subTitle'); });
|
|
|
|
describe('with attributes', () => {
|
|
it('should be able to complete property value',
|
|
() => { contains('app/parsing-cases.ts', 'property-binding-model', 'test'); });
|
|
it('should be able to complete an event',
|
|
() => { contains('app/parsing-cases.ts', 'event-binding-model', 'modelChanged'); });
|
|
it('should be able to complete a two-way binding',
|
|
() => { contains('app/parsing-cases.ts', 'two-way-binding-model', 'test'); });
|
|
});
|
|
|
|
describe('with a *ngFor', () => {
|
|
it('should include a let for empty attribute',
|
|
() => { contains('app/parsing-cases.ts', 'for-empty', 'let'); });
|
|
it('should not suggest any entries if in the name part of a let',
|
|
() => { expectEmpty('app/parsing-cases.ts', 'for-let-empty'); });
|
|
it('should suggest NgForRow members for let initialization expression', () => {
|
|
contains(
|
|
'app/parsing-cases.ts', 'for-let-i-equal', 'index', 'count', 'first', 'last', 'even',
|
|
'odd');
|
|
});
|
|
it('should include a let', () => { contains('app/parsing-cases.ts', 'for-let', 'let'); });
|
|
it('should include an "of"', () => { contains('app/parsing-cases.ts', 'for-of', 'of'); });
|
|
it('should include field reference',
|
|
() => { contains('app/parsing-cases.ts', 'for-people', 'people'); });
|
|
it('should include person in the let scope',
|
|
() => { contains('app/parsing-cases.ts', 'for-interp-person', 'person'); });
|
|
// TODO: Enable when we can infer the element type of the ngFor
|
|
// it('should include determine person\'s type as Person', () => {
|
|
// contains('app/parsing-cases.ts', 'for-interp-name', 'name', 'age');
|
|
// contains('app/parsing-cases.ts', 'for-interp-age', 'name', 'age');
|
|
// });
|
|
});
|
|
|
|
describe('for pipes', () => {
|
|
it('should be able to resolve lowercase',
|
|
() => { contains('app/expression-cases.ts', 'string-pipe', 'substring'); });
|
|
});
|
|
|
|
describe('with references', () => {
|
|
it('should list references',
|
|
() => { contains('app/parsing-cases.ts', 'test-comp-content', 'test1', 'test2', 'div'); });
|
|
it('should reference the component',
|
|
() => { contains('app/parsing-cases.ts', 'test-comp-after-test', 'name'); });
|
|
// TODO: Enable when we have a flag that indicates the project targets the DOM
|
|
// it('should refernce the element if no component', () => {
|
|
// contains('app/parsing-cases.ts', 'test-comp-after-div', 'innerText');
|
|
// });
|
|
});
|
|
|
|
describe('for semantic errors', () => {
|
|
it('should report access to an unknown field', () => {
|
|
expectSemanticError(
|
|
'app/expression-cases.ts', 'foo',
|
|
'Identifier \'foo\' is not defined. The component declaration, template variable declarations, and element references do not contain such a member');
|
|
});
|
|
it('should report access to an unknown sub-field', () => {
|
|
expectSemanticError(
|
|
'app/expression-cases.ts', 'nam',
|
|
'Identifier \'nam\' is not defined. \'Person\' does not contain such a member');
|
|
});
|
|
it('should report access to a private member', () => {
|
|
expectSemanticError(
|
|
'app/expression-cases.ts', 'myField',
|
|
'Identifier \'myField\' refers to a private member of the component');
|
|
});
|
|
it('should report numeric operator erros',
|
|
() => { expectSemanticError('app/expression-cases.ts', 'mod', 'Expected a numeric type'); });
|
|
describe('in ngFor', () => {
|
|
function expectError(locationMarker: string, message: string) {
|
|
expectSemanticError('app/ng-for-cases.ts', locationMarker, message);
|
|
}
|
|
it('should report an unknown field', () => {
|
|
expectError(
|
|
'people_1',
|
|
'Identifier \'people_1\' is not defined. The component declaration, template variable declarations, and element references do not contain such a member');
|
|
});
|
|
it('should report an unknown context reference', () => {
|
|
expectError('even_1', 'The template context does not defined a member called \'even_1\'');
|
|
});
|
|
it('should report an unknown value in a key expression', () => {
|
|
expectError(
|
|
'trackBy_1',
|
|
'Identifier \'trackBy_1\' is not defined. The component declaration, template variable declarations, and element references do not contain such a member');
|
|
});
|
|
});
|
|
describe('in ngIf', () => {
|
|
function expectError(locationMarker: string, message: string) {
|
|
expectSemanticError('app/ng-if-cases.ts', locationMarker, message);
|
|
}
|
|
it('should report an implicit context reference', () => {
|
|
expectError('implicit', "The template context does not defined a member called 'unknown'");
|
|
});
|
|
});
|
|
});
|
|
|
|
function getMarkerLocation(fileName: string, locationMarker: string): number {
|
|
const location = mockHost.getMarkerLocations(fileName)[locationMarker];
|
|
if (location == null) {
|
|
throw new Error(`No marker ${locationMarker} found.`);
|
|
}
|
|
return location;
|
|
}
|
|
function contains(fileName: string, locationMarker: string, ...names: string[]) {
|
|
const location = getMarkerLocation(fileName, locationMarker);
|
|
expectEntries(locationMarker, plugin.getCompletionsAtPosition(fileName, location), ...names);
|
|
}
|
|
|
|
function expectEmpty(fileName: string, locationMarker: string) {
|
|
const location = getMarkerLocation(fileName, locationMarker);
|
|
expect(plugin.getCompletionsAtPosition(fileName, location).entries).toEqual([]);
|
|
}
|
|
|
|
function expectSemanticError(fileName: string, locationMarker: string, message: string) {
|
|
const start = getMarkerLocation(fileName, locationMarker);
|
|
const end = getMarkerLocation(fileName, locationMarker + '-end');
|
|
const errors = plugin.getSemanticDiagnosticsFilter(fileName, []);
|
|
for (const error of errors) {
|
|
if (error.messageText.toString().indexOf(message) >= 0) {
|
|
expect(error.start).toEqual(start);
|
|
expect(error.length).toEqual(end - start);
|
|
return;
|
|
}
|
|
}
|
|
throw new Error(
|
|
`Expected error messages to contain ${message}, in messages:\n ${errors.map(e => e.messageText.toString()).join(',\n ')}`);
|
|
}
|
|
});
|
|
|
|
|
|
function expectEntries(locationMarker: string, info: ts.CompletionInfo, ...names: string[]) {
|
|
let entries: {[name: string]: boolean} = {};
|
|
if (!info) {
|
|
throw new Error(
|
|
`Expected result from ${locationMarker} to include ${names.join(', ')} but no result provided`);
|
|
} else {
|
|
for (let entry of info.entries) {
|
|
entries[entry.name] = true;
|
|
}
|
|
let shouldContains = names.filter(name => !name.startsWith('-'));
|
|
let shouldNotContain = names.filter(name => name.startsWith('-'));
|
|
let missing = shouldContains.filter(name => !entries[name]);
|
|
let present = shouldNotContain.map(name => name.substr(1)).filter(name => entries[name]);
|
|
if (missing.length) {
|
|
throw new Error(
|
|
`Expected result from ${locationMarker} to include at least one of the following, ${missing.join(', ')}, in the list of entries ${info.entries.map(entry => entry.name).join(', ')}`);
|
|
}
|
|
if (present.length) {
|
|
throw new Error(
|
|
`Unexpected member${present.length > 1 ? 's': ''} included in result: ${present.join(', ')}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function expectNoDiagnostics(diagnostics: ts.Diagnostic[]) {
|
|
for (const diagnostic of diagnostics) {
|
|
let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
|
|
if (diagnostic.start) {
|
|
let {line, character} = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
|
|
console.error(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`);
|
|
} else {
|
|
console.error(`${message}`);
|
|
}
|
|
}
|
|
expect(diagnostics.length).toBe(0);
|
|
}
|