236 lines
10 KiB
TypeScript
236 lines
10 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 {create} from '../src/ts_plugin';
|
|
import {MockTypescriptHost} from './test_utils';
|
|
|
|
describe('plugin', () => {
|
|
const mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts']);
|
|
const service = ts.createLanguageService(mockHost);
|
|
const program = service.getProgram();
|
|
const plugin = createPlugin(service, mockHost);
|
|
|
|
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));
|
|
}
|
|
});
|
|
|
|
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.getSemanticDiagnostics(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 directives',
|
|
() => { 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 return 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 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 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',
|
|
() => { 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 to 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 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 reference the element if no component', () => {
|
|
// contains('/app/parsing-cases.ts', 'test-comp-after-div', 'innerText');
|
|
// });
|
|
});
|
|
|
|
describe('for semantic errors', () => {
|
|
describe(`with config 'angularOnly = true`, () => {
|
|
const ngLS = createPlugin(service, mockHost, {angularOnly: true});
|
|
it('should not report template errors on TOH', () => {
|
|
const sourceFiles = ngLS.getProgram() !.getSourceFiles();
|
|
expect(sourceFiles.length).toBeGreaterThan(0);
|
|
for (const {fileName} of sourceFiles) {
|
|
// Ignore all 'cases.ts' files as they intentionally contain errors.
|
|
if (!fileName.endsWith('cases.ts')) {
|
|
expectNoDiagnostics(ngLS.getSemanticDiagnostics(fileName));
|
|
}
|
|
}
|
|
});
|
|
|
|
it('should be able to get entity completions', () => {
|
|
const fileName = '/app/app.component.ts';
|
|
const marker = mockHost.getLocationMarkerFor(fileName, 'entity-amp');
|
|
const results = ngLS.getCompletionsAtPosition(fileName, marker.start, {} /* options */);
|
|
expect(results).toBeTruthy();
|
|
expectEntries('entity-amp', results !, ...['&', '>', '<', 'ι']);
|
|
});
|
|
|
|
it('should report template diagnostics', () => {
|
|
// TODO(kyliau): Rename these to end with '-error.ts'
|
|
const fileName = '/app/expression-cases.ts';
|
|
const diagnostics = ngLS.getSemanticDiagnostics(fileName);
|
|
expect(diagnostics.map(d => d.messageText)).toEqual([
|
|
`Identifier 'foo' is not defined. The component declaration, template variable declarations, and element references do not contain such a member`,
|
|
`Identifier 'nam' is not defined. 'Person' does not contain such a member`,
|
|
`Identifier 'myField' refers to a private member of the component`,
|
|
`Expected a numeric type`,
|
|
]);
|
|
});
|
|
});
|
|
});
|
|
|
|
function createPlugin(tsLS: ts.LanguageService, tsLSHost: ts.LanguageServiceHost, config = {}) {
|
|
const project = {projectService: {logger: {info() {}}}};
|
|
return create({
|
|
languageService: tsLS,
|
|
languageServiceHost: tsLSHost,
|
|
project: project as any,
|
|
serverHost: {} as any,
|
|
config: {...config},
|
|
});
|
|
}
|
|
|
|
function contains(fileName: string, locationMarker: string, ...names: string[]) {
|
|
const marker = mockHost.getLocationMarkerFor(fileName, locationMarker);
|
|
expectEntries(
|
|
locationMarker, plugin.getCompletionsAtPosition(fileName, marker.start, undefined) !,
|
|
...names);
|
|
}
|
|
});
|
|
|
|
|
|
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.file && 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);
|
|
}
|