test(language-service): Create proper test project (#32653)
Language service uses a canonical "Tour of Heroes" project to test various features, but the files are all contained in test_data.ts which is hard to read and often contains errors that are difficult to catch without proper IDE syntax highlighting. The directory structure is also not clear from first glance. This PR refactors the test project into standalone files in the proper format. Next up: [ ] Update the interface of MockTypeScript to only accept scriptNames. [ ] Remove test_data.ts PR Close #32653
This commit is contained in:
parent
2846505dbd
commit
9d8dc793da
|
@ -3,7 +3,11 @@ load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")
|
|||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob(["**/*.ts"]),
|
||||
srcs = glob(
|
||||
include = ["**/*.ts"],
|
||||
exclude = ["project/**/*"],
|
||||
),
|
||||
data = glob(["project/**/*"]),
|
||||
deps = [
|
||||
"//packages:types",
|
||||
"//packages/compiler",
|
||||
|
|
|
@ -156,7 +156,14 @@ describe('definitions', () => {
|
|||
expect(def.fileName).toBe(refFileName);
|
||||
expect(def.name).toBe('TestComponent');
|
||||
expect(def.kind).toBe('component');
|
||||
expect(def.textSpan).toEqual(mockHost.getLocationMarkerFor(refFileName, 'test-comp'));
|
||||
const content = mockHost.getFileContent(refFileName) !;
|
||||
const begin = '/*BeginTestComponent*/ ';
|
||||
const start = content.indexOf(begin) + begin.length;
|
||||
const end = content.indexOf(' /*EndTestComponent*/');
|
||||
expect(def.textSpan).toEqual({
|
||||
start,
|
||||
length: end - start,
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to find an event provider', () => {
|
||||
|
@ -186,7 +193,12 @@ describe('definitions', () => {
|
|||
expect(def.fileName).toBe(refFileName);
|
||||
expect(def.name).toBe('testEvent');
|
||||
expect(def.kind).toBe('event');
|
||||
expect(def.textSpan).toEqual(mockHost.getDefinitionMarkerFor(refFileName, 'test'));
|
||||
const content = mockHost.getFileContent(refFileName) !;
|
||||
const ref = `@Output('test') testEvent = new EventEmitter();`;
|
||||
expect(def.textSpan).toEqual({
|
||||
start: content.indexOf(ref),
|
||||
length: ref.length,
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to find an input provider', () => {
|
||||
|
@ -219,7 +231,12 @@ describe('definitions', () => {
|
|||
expect(def.fileName).toBe(refFileName);
|
||||
expect(def.name).toBe('name');
|
||||
expect(def.kind).toBe('property');
|
||||
expect(def.textSpan).toEqual(mockHost.getDefinitionMarkerFor(refFileName, 'tcName'));
|
||||
const content = mockHost.getFileContent(refFileName) !;
|
||||
const ref = `@Input('tcName') name = 'test';`;
|
||||
expect(def.textSpan).toEqual({
|
||||
start: content.indexOf(ref),
|
||||
length: ref.length,
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to find a pipe', () => {
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* @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 {Component} from '@angular/core';
|
||||
|
||||
export class Hero {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-app',
|
||||
template: `~{empty}
|
||||
<~{start-tag}h~{start-tag-after-h}1~{start-tag-h1} ~{h1-after-space}>
|
||||
~{h1-content} {{~{sub-start}title~{sub-end}}}
|
||||
</h1>
|
||||
~{after-h1}<h2>{{~{h2-hero}hero.~{h2-name}name}} details!</h2>
|
||||
<div><label>id: </label>{{~{label-hero}hero.~{label-id}id}}</div>
|
||||
<div ~{div-attributes}>
|
||||
<label>name: </label>
|
||||
</div>
|
||||
&~{entity-amp}amp;
|
||||
`
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'Tour of Heroes';
|
||||
hero: Hero = {id: 1, name: 'Windstorm'};
|
||||
private internal: string;
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* @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 {Component} from '@angular/core';
|
||||
|
||||
export interface Person {
|
||||
name: string;
|
||||
age: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '{{~{foo}foo~{foo-end}}}',
|
||||
})
|
||||
export class WrongFieldReference {
|
||||
bar = 'bar';
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '{{~{nam}person.nam~{nam-end}}}',
|
||||
})
|
||||
export class WrongSubFieldReference {
|
||||
person: Person = {name: 'Bob', age: 23};
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '{{~{myField}myField~{myField-end}}}',
|
||||
})
|
||||
export class PrivateReference {
|
||||
private myField = 'My Field';
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '{{~{mod}"a" ~{mod-end}% 2}}',
|
||||
})
|
||||
export class ExpectNumericType {
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '{{ (name | lowercase).~{string-pipe}substring }}',
|
||||
})
|
||||
export class LowercasePipe {
|
||||
name: string;
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* @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 {CommonModule} from '@angular/common';
|
||||
import {NgModule} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
|
||||
import {AppComponent} from './app.component';
|
||||
import {ExpectNumericType, LowercasePipe, PrivateReference, WrongFieldReference, WrongSubFieldReference} from './expression-cases';
|
||||
import {UnknownEven, UnknownPeople, UnknownTrackBy} from './ng-for-cases';
|
||||
import {ShowIf} from './ng-if-cases';
|
||||
import {AttributeBinding, CaseIncompleteOpen, CaseMissingClosing, CaseUnknown, EmptyInterpolation, EventBinding, ForLetIEqual, ForOfEmpty, ForOfLetEmpty, ForUsingComponent, NoValueAttribute, NumberModel, Pipes, PropertyBinding, References, StringModel, TemplateReference, TestComponent, TwoWayBinding} from './parsing-cases';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, FormsModule],
|
||||
declarations: [
|
||||
AppComponent,
|
||||
CaseIncompleteOpen,
|
||||
CaseMissingClosing,
|
||||
CaseUnknown,
|
||||
Pipes,
|
||||
TemplateReference,
|
||||
NoValueAttribute,
|
||||
AttributeBinding,
|
||||
StringModel,
|
||||
NumberModel,
|
||||
PropertyBinding,
|
||||
EventBinding,
|
||||
TwoWayBinding,
|
||||
EmptyInterpolation,
|
||||
ForOfEmpty,
|
||||
ForOfLetEmpty,
|
||||
ForLetIEqual,
|
||||
ForUsingComponent,
|
||||
References,
|
||||
TestComponent,
|
||||
WrongFieldReference,
|
||||
WrongSubFieldReference,
|
||||
PrivateReference,
|
||||
ExpectNumericType,
|
||||
UnknownPeople,
|
||||
UnknownEven,
|
||||
UnknownTrackBy,
|
||||
ShowIf,
|
||||
LowercasePipe,
|
||||
]
|
||||
})
|
||||
export class AppModule {
|
||||
}
|
||||
|
||||
declare function bootstrap(v: any): void;
|
||||
|
||||
bootstrap(AppComponent);
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* @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 {Component} from '@angular/core';
|
||||
|
||||
export interface Person {
|
||||
name: string;
|
||||
age: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div *ngFor="let person of ~{people_1}people_1~{people_1-end}">
|
||||
<span>{{person.name}}</span>
|
||||
</div>`,
|
||||
})
|
||||
export class UnknownPeople {
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div ~{even_1}*ngFor="let person of people; let e = even_1"~{even_1-end}>
|
||||
<span>{{person.name}}</span>
|
||||
</div>`,
|
||||
})
|
||||
export class UnknownEven {
|
||||
people: Person[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div *ngFor="let person of people; trackBy ~{trackBy_1}trackBy_1~{trackBy_1-end}">
|
||||
<span>{{person.name}}</span>
|
||||
</div>`,
|
||||
})
|
||||
export class UnknownTrackBy {
|
||||
people: Person[];
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* @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 {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div ~{implicit}*ngIf="show; let l=unknown"~{implicit-end}>
|
||||
Showing now!
|
||||
</div>`,
|
||||
})
|
||||
export class ShowIf {
|
||||
show = false;
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
/**
|
||||
* @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 {Component, Directive, EventEmitter, Input, Output} from '@angular/core';
|
||||
|
||||
import {Hero} from './app.component';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<h1>
|
||||
Some <~{incomplete-open-lt}a~{incomplete-open-a} ~{incomplete-open-attr} text
|
||||
</h1>`,
|
||||
})
|
||||
export class CaseIncompleteOpen {
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '<h1>Some <a> ~{missing-closing} text</h1>',
|
||||
})
|
||||
export class CaseMissingClosing {
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '<h1>Some <unknown ~{unknown-element}> text</h1>',
|
||||
})
|
||||
export class CaseUnknown {
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '<h1>{{data | ~{before-pipe}lowe~{in-pipe}rcase~{after-pipe} }}',
|
||||
})
|
||||
export class Pipes {
|
||||
data = 'Some string';
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '<h1 h~{no-value-attribute}></h1>',
|
||||
})
|
||||
export class NoValueAttribute {
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
template: '<h1 model="~{attribute-binding-model}test"></h1>',
|
||||
})
|
||||
export class AttributeBinding {
|
||||
test: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '<h1 [model]="~{property-binding-model}test"></h1>',
|
||||
})
|
||||
export class PropertyBinding {
|
||||
test: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '<h1 (model)="~{event-binding-model}modelChanged()"></h1>',
|
||||
})
|
||||
export class EventBinding {
|
||||
test: string;
|
||||
|
||||
modelChanged() {}
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '<h1 [(model)]="~{two-way-binding-model}test"></h1>',
|
||||
})
|
||||
export class TwoWayBinding {
|
||||
test: string;
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: '[string-model]',
|
||||
})
|
||||
export class StringModel {
|
||||
@Input() model: string;
|
||||
@Output() modelChanged: EventEmitter<string>;
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: '[number-model]',
|
||||
})
|
||||
export class NumberModel {
|
||||
@Input('inputAlias') model: number;
|
||||
@Output('outputAlias') modelChanged: EventEmitter<number>;
|
||||
}
|
||||
|
||||
interface Person {
|
||||
name: string;
|
||||
age: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '<div *ngFor="~{for-empty}"></div>',
|
||||
})
|
||||
export class ForOfEmpty {
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '<div *ngFor="let ~{for-let-empty}"></div>',
|
||||
})
|
||||
export class ForOfLetEmpty {
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '<div *ngFor="let i = ~{for-let-i-equal}"></div>',
|
||||
})
|
||||
export class ForLetIEqual {
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div *ngFor="~{for-let}let ~{for-person}person ~{for-of}of ~{for-people}people">
|
||||
<span>Name: {{~{for-interp-person}person.~{for-interp-name}name}}</span>
|
||||
<span>Age: {{person.~{for-interp-age}age}}</span>
|
||||
</div>`,
|
||||
})
|
||||
export class ForUsingComponent {
|
||||
people: Person[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div #div>
|
||||
<test-comp #test1>
|
||||
{{~{test-comp-content}}}
|
||||
{{test1.~{test-comp-after-test}name}}
|
||||
{{div.~{test-comp-after-div}.innerText}}
|
||||
</test-comp>
|
||||
</div>
|
||||
<test-comp #test2></test-comp>`,
|
||||
})
|
||||
export class References {
|
||||
}
|
||||
|
||||
/*BeginTestComponent*/ @Component({
|
||||
selector: 'test-comp',
|
||||
template: '<div>Testing: {{name}}</div>',
|
||||
})
|
||||
export class TestComponent {
|
||||
@Input('tcName') name = 'test';
|
||||
@Output('test') testEvent = new EventEmitter();
|
||||
} /*EndTestComponent*/
|
||||
|
||||
@Component({
|
||||
templateUrl: 'test.ng',
|
||||
})
|
||||
export class TemplateReference {
|
||||
title = 'Some title';
|
||||
hero: Hero = {id: 1, name: 'Windstorm'};
|
||||
anyValue: any;
|
||||
myClick(event: any) {}
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '{{~{empty-interpolation}}}',
|
||||
})
|
||||
export class EmptyInterpolation {
|
||||
title = 'Some title';
|
||||
subTitle = 'Some sub title';
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
body, html {
|
||||
width: 100%;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
~{empty}
|
||||
<~{start-tag}h~{start-tag-after-h}1~{start-tag-h1} ~{h1-after-space}>
|
||||
~{h1-content} {{~{sub-start}title~{sub-end}}}
|
||||
</h1>
|
||||
~{after-h1}<h2>{{~{h2-hero}hero.~{h2-name}name}} details!</h2>
|
||||
<div><label>id: </label>{{~{label-hero}hero.~{label-id}id}}</div>
|
||||
<div ~{div-attributes}>
|
||||
<label>name: </label>
|
||||
</div>
|
||||
&~{entity-amp}amp;
|
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
export * from './app/app.component';
|
|
@ -27,9 +27,20 @@ const tsxfile = /\.tsx$/;
|
|||
|
||||
/* The missing cache does two things. First it improves performance of the
|
||||
tests as it reduces the number of OS calls made during testing. Also it
|
||||
improves debugging experience as fewer exceptions are raised allow you
|
||||
improves debugging experience as fewer exceptions are raised to allow you
|
||||
to use stopping on all exceptions. */
|
||||
const missingCache = new Map<string, boolean>();
|
||||
const missingCache = new Set<string>([
|
||||
'/node_modules/@angular/core.d.ts',
|
||||
'/node_modules/@angular/animations.d.ts',
|
||||
'/node_modules/@angular/platform-browser/animations.d.ts',
|
||||
'/node_modules/@angular/common.d.ts',
|
||||
'/node_modules/@angular/forms.d.ts',
|
||||
'/node_modules/@angular/core/src/di/provider.metadata.json',
|
||||
'/node_modules/@angular/core/src/change_detection/pipe_transform.metadata.json',
|
||||
'/node_modules/@angular/core/src/reflection/types.metadata.json',
|
||||
'/node_modules/@angular/core/src/reflection/platform_reflection_capabilities.metadata.json',
|
||||
'/node_modules/@angular/forms/src/directives/form_interface.metadata.json',
|
||||
]);
|
||||
const cacheUsed = new Set<string>();
|
||||
const reportedMissing = new Set<string>();
|
||||
|
||||
|
@ -39,7 +50,7 @@ const reportedMissing = new Set<string>();
|
|||
export function validateCache(): {exists: string[], unused: string[], reported: string[]} {
|
||||
const exists: string[] = [];
|
||||
const unused: string[] = [];
|
||||
for (const fileName of iterableToArray(missingCache.keys())) {
|
||||
for (const fileName of missingCache) {
|
||||
if (fs.existsSync(fileName)) {
|
||||
exists.push(fileName);
|
||||
}
|
||||
|
@ -47,37 +58,72 @@ export function validateCache(): {exists: string[], unused: string[], reported:
|
|||
unused.push(fileName);
|
||||
}
|
||||
}
|
||||
return {exists, unused, reported: iterableToArray(reportedMissing.keys())};
|
||||
return {
|
||||
exists,
|
||||
unused,
|
||||
reported: Array.from(reportedMissing),
|
||||
};
|
||||
}
|
||||
|
||||
missingCache.set('/node_modules/@angular/core.d.ts', true);
|
||||
missingCache.set('/node_modules/@angular/animations.d.ts', true);
|
||||
missingCache.set('/node_modules/@angular/platform-browser/animations.d.ts', true);
|
||||
missingCache.set('/node_modules/@angular/common.d.ts', true);
|
||||
missingCache.set('/node_modules/@angular/forms.d.ts', true);
|
||||
missingCache.set('/node_modules/@angular/core/src/di/provider.metadata.json', true);
|
||||
missingCache.set(
|
||||
'/node_modules/@angular/core/src/change_detection/pipe_transform.metadata.json', true);
|
||||
missingCache.set('/node_modules/@angular/core/src/reflection/types.metadata.json', true);
|
||||
missingCache.set(
|
||||
'/node_modules/@angular/core/src/reflection/platform_reflection_capabilities.metadata.json',
|
||||
true);
|
||||
missingCache.set('/node_modules/@angular/forms/src/directives/form_interface.metadata.json', true);
|
||||
function isFile(path: string) {
|
||||
return fs.statSync(path).isFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a Map with key = directory / file path, value = file content.
|
||||
* [
|
||||
* /app => [[directory]]
|
||||
* /app/main.ts => ...
|
||||
* /app/app.component.ts => ...
|
||||
* /app/expression-cases.ts => ...
|
||||
* /app/ng-for-cases.ts => ...
|
||||
* /app/ng-if-cases.ts => ...
|
||||
* /app/parsing-cases.ts => ...
|
||||
* /app/test.css => ...
|
||||
* /app/test.ng => ...
|
||||
* ]
|
||||
*/
|
||||
function loadTourOfHeroes(): ReadonlyMap<string, string> {
|
||||
const {TEST_SRCDIR} = process.env;
|
||||
const root =
|
||||
path.join(TEST_SRCDIR !, 'angular', 'packages', 'language-service', 'test', 'project');
|
||||
const dirs = [root];
|
||||
const files = new Map<string, string>();
|
||||
while (dirs.length) {
|
||||
const dirPath = dirs.pop() !;
|
||||
for (const filePath of fs.readdirSync(dirPath)) {
|
||||
const absPath = path.join(dirPath, filePath);
|
||||
if (isFile(absPath)) {
|
||||
const key = path.join('/', path.relative(root, absPath));
|
||||
const value = fs.readFileSync(absPath, 'utf8');
|
||||
files.set(key, value);
|
||||
} else {
|
||||
const key = path.join('/', filePath);
|
||||
files.set(key, '[[directory]]');
|
||||
dirs.push(absPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
const TOH = loadTourOfHeroes();
|
||||
|
||||
export class MockTypescriptHost implements ts.LanguageServiceHost {
|
||||
private angularPath: string|undefined;
|
||||
private nodeModulesPath: string;
|
||||
private scriptVersion = new Map<string, number>();
|
||||
private overrides = new Map<string, string>();
|
||||
private angularPath?: string;
|
||||
private readonly nodeModulesPath: string;
|
||||
private readonly scriptVersion = new Map<string, number>();
|
||||
private readonly overrides = new Map<string, string>();
|
||||
private projectVersion = 0;
|
||||
private options: ts.CompilerOptions;
|
||||
private overrideDirectory = new Set<string>();
|
||||
private existsCache = new Map<string, boolean>();
|
||||
private fileCache = new Map<string, string|undefined>();
|
||||
private readonly overrideDirectory = new Set<string>();
|
||||
private readonly existsCache = new Map<string, boolean>();
|
||||
private readonly fileCache = new Map<string, string|undefined>();
|
||||
|
||||
constructor(
|
||||
private scriptNames: string[], private data: MockData,
|
||||
private node_modules: string = 'node_modules', private myPath: typeof path = path) {
|
||||
private readonly scriptNames: string[], _: MockData,
|
||||
private readonly node_modules: string = 'node_modules',
|
||||
private readonly myPath: typeof path = path) {
|
||||
const support = setup();
|
||||
this.nodeModulesPath = path.posix.join(support.basePath, 'node_modules');
|
||||
this.angularPath = path.posix.join(this.nodeModulesPath, '@angular');
|
||||
|
@ -143,14 +189,14 @@ export class MockTypescriptHost implements ts.LanguageServiceHost {
|
|||
|
||||
directoryExists(directoryName: string): boolean {
|
||||
if (this.overrideDirectory.has(directoryName)) return true;
|
||||
let effectiveName = this.getEffectiveName(directoryName);
|
||||
const effectiveName = this.getEffectiveName(directoryName);
|
||||
if (effectiveName === directoryName) {
|
||||
return directoryExists(directoryName, this.data);
|
||||
} else if (effectiveName == '/' + this.node_modules) {
|
||||
return true;
|
||||
} else {
|
||||
return this.pathExists(effectiveName);
|
||||
return TOH.has(directoryName);
|
||||
}
|
||||
if (effectiveName === '/' + this.node_modules) {
|
||||
return true;
|
||||
}
|
||||
return this.pathExists(effectiveName);
|
||||
}
|
||||
|
||||
fileExists(fileName: string): boolean { return this.getRawFileContent(fileName) != null; }
|
||||
|
@ -184,29 +230,28 @@ export class MockTypescriptHost implements ts.LanguageServiceHost {
|
|||
if (/^lib.*\.d\.ts$/.test(basename)) {
|
||||
let libPath = ts.getDefaultLibFilePath(this.getCompilationSettings());
|
||||
return fs.readFileSync(this.myPath.join(path.dirname(libPath), basename), 'utf8');
|
||||
} else {
|
||||
if (missingCache.has(fileName)) {
|
||||
cacheUsed.add(fileName);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
if (missingCache.has(fileName)) {
|
||||
cacheUsed.add(fileName);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const effectiveName = this.getEffectiveName(fileName);
|
||||
if (effectiveName === fileName) {
|
||||
return open(fileName, this.data);
|
||||
} else if (
|
||||
!fileName.match(angularts) && !fileName.match(rxjsts) && !fileName.match(rxjsmetadata) &&
|
||||
!fileName.match(tsxfile)) {
|
||||
if (this.fileCache.has(effectiveName)) {
|
||||
return this.fileCache.get(effectiveName);
|
||||
} else if (this.pathExists(effectiveName)) {
|
||||
const content = fs.readFileSync(effectiveName, 'utf8');
|
||||
this.fileCache.set(effectiveName, content);
|
||||
return content;
|
||||
} else {
|
||||
missingCache.set(fileName, true);
|
||||
reportedMissing.add(fileName);
|
||||
cacheUsed.add(fileName);
|
||||
}
|
||||
const effectiveName = this.getEffectiveName(fileName);
|
||||
if (effectiveName === fileName) {
|
||||
return TOH.get(fileName);
|
||||
}
|
||||
if (!fileName.match(angularts) && !fileName.match(rxjsts) && !fileName.match(rxjsmetadata) &&
|
||||
!fileName.match(tsxfile)) {
|
||||
if (this.fileCache.has(effectiveName)) {
|
||||
return this.fileCache.get(effectiveName);
|
||||
} else if (this.pathExists(effectiveName)) {
|
||||
const content = fs.readFileSync(effectiveName, 'utf8');
|
||||
this.fileCache.set(effectiveName, content);
|
||||
return content;
|
||||
} else {
|
||||
missingCache.add(fileName);
|
||||
reportedMissing.add(fileName);
|
||||
cacheUsed.add(fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -313,43 +358,6 @@ export class MockTypescriptHost implements ts.LanguageServiceHost {
|
|||
}
|
||||
}
|
||||
|
||||
function iterableToArray<T>(iterator: IterableIterator<T>) {
|
||||
const result: T[] = [];
|
||||
while (true) {
|
||||
const next = iterator.next();
|
||||
if (next.done) break;
|
||||
result.push(next.value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function find(fileName: string, data: MockData): MockData|undefined {
|
||||
let names = fileName.split('/');
|
||||
if (names.length && !names[0].length) names.shift();
|
||||
let current = data;
|
||||
for (let name of names) {
|
||||
if (typeof current === 'string')
|
||||
return undefined;
|
||||
else
|
||||
current = (<MockDirectory>current)[name] !;
|
||||
if (!current) return undefined;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function open(fileName: string, data: MockData): string|undefined {
|
||||
let result = find(fileName, data);
|
||||
if (typeof result === 'string') {
|
||||
return result;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function directoryExists(dirname: string, data: MockData): boolean {
|
||||
let result = find(dirname, data);
|
||||
return !!result && typeof result !== 'string';
|
||||
}
|
||||
|
||||
const locationMarker = /\~\{(\w+(-\w+)*)\}/g;
|
||||
|
||||
function removeLocationMarkers(value: string): string {
|
||||
|
|
|
@ -36,129 +36,131 @@ describe('plugin', () => {
|
|||
});
|
||||
|
||||
it('should be able to get entity completions',
|
||||
() => { contains('app/app.component.ts', 'entity-amp', '&', '>', '<', 'ι'); });
|
||||
() => { 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);
|
||||
contains('/app/app.component.ts', location, ...htmlTags);
|
||||
}
|
||||
});
|
||||
|
||||
it('should be able to return element directives',
|
||||
() => { contains('app/app.component.ts', 'empty', 'my-app'); });
|
||||
() => { 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 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');
|
||||
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'); });
|
||||
() => { 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');
|
||||
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'); });
|
||||
() => { 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'); });
|
||||
() => { 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'); });
|
||||
() => { 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'); });
|
||||
() => { 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'); });
|
||||
() => { 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'); });
|
||||
() => { 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');
|
||||
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'); });
|
||||
() => { 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'); });
|
||||
() => { 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'); });
|
||||
() => { 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'); });
|
||||
() => { 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'); });
|
||||
() => { 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',
|
||||
'/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 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'); });
|
||||
() => { 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'); });
|
||||
() => { 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');
|
||||
// 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'); });
|
||||
() => { 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'); });
|
||||
() => { 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'); });
|
||||
() => { 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');
|
||||
// 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',
|
||||
'/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',
|
||||
'/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',
|
||||
'/app/expression-cases.ts', 'myField',
|
||||
'Identifier \'myField\' refers to a private member of the component');
|
||||
});
|
||||
it('should report numeric operator errors',
|
||||
() => { expectSemanticError('app/expression-cases.ts', 'mod', 'Expected a numeric type'); });
|
||||
it('should report numeric operator errors', () => {
|
||||
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);
|
||||
expectSemanticError('/app/ng-for-cases.ts', locationMarker, message);
|
||||
}
|
||||
it('should report an unknown field', () => {
|
||||
expectError(
|
||||
|
@ -176,7 +178,7 @@ describe('plugin', () => {
|
|||
});
|
||||
describe('in ngIf', () => {
|
||||
function expectError(locationMarker: string, message: string) {
|
||||
expectSemanticError('app/ng-if-cases.ts', locationMarker, message);
|
||||
expectSemanticError('/app/ng-if-cases.ts', locationMarker, message);
|
||||
}
|
||||
it('should report an implicit context reference', () => {
|
||||
expectError('implicit', 'The template context does not define a member called \'unknown\'');
|
||||
|
@ -197,7 +199,7 @@ describe('plugin', () => {
|
|||
});
|
||||
|
||||
it('should be able to get entity completions', () => {
|
||||
const fileName = 'app/app.component.ts';
|
||||
const fileName = '/app/app.component.ts';
|
||||
const marker = 'entity-amp';
|
||||
const position = getMarkerLocation(fileName, marker);
|
||||
const results = ngLS.getCompletionsAtPosition(fileName, position, {} /* options */);
|
||||
|
@ -207,7 +209,7 @@ describe('plugin', () => {
|
|||
|
||||
it('should report template diagnostics', () => {
|
||||
// TODO(kyliau): Rename these to end with '-error.ts'
|
||||
const fileName = 'app/expression-cases.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`,
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
// Http doesn't need to built since it is no longer maintained and
|
||||
// will be removed eventually. See: FW-1392.
|
||||
"http/**",
|
||||
"language-service/test/project",
|
||||
"platform-server/integrationtest",
|
||||
// The webworker packages have deprecated and are not made compatible with the
|
||||
// strict flag. Until these packages are removed, we exclude them here.
|
||||
|
|
Loading…
Reference in New Issue