diff --git a/packages/language-service/test/BUILD.bazel b/packages/language-service/test/BUILD.bazel index 4529a90fa3..0312c107d2 100644 --- a/packages/language-service/test/BUILD.bazel +++ b/packages/language-service/test/BUILD.bazel @@ -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", diff --git a/packages/language-service/test/definitions_spec.ts b/packages/language-service/test/definitions_spec.ts index 286b8cf587..f9b9a00ee1 100644 --- a/packages/language-service/test/definitions_spec.ts +++ b/packages/language-service/test/definitions_spec.ts @@ -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', () => { diff --git a/packages/language-service/test/project/app/app.component.ts b/packages/language-service/test/project/app/app.component.ts new file mode 100644 index 0000000000..36a97d5055 --- /dev/null +++ b/packages/language-service/test/project/app/app.component.ts @@ -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}}} + + ~{after-h1}

{{~{h2-hero}hero.~{h2-name}name}} details!

+
{{~{label-hero}hero.~{label-id}id}}
+
+ +
+ &~{entity-amp}amp; + ` +}) +export class AppComponent { + title = 'Tour of Heroes'; + hero: Hero = {id: 1, name: 'Windstorm'}; + private internal: string; +} diff --git a/packages/language-service/test/project/app/expression-cases.ts b/packages/language-service/test/project/app/expression-cases.ts new file mode 100644 index 0000000000..7dd2cacf36 --- /dev/null +++ b/packages/language-service/test/project/app/expression-cases.ts @@ -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; +} diff --git a/packages/language-service/test/project/app/main.ts b/packages/language-service/test/project/app/main.ts new file mode 100644 index 0000000000..1564789f7f --- /dev/null +++ b/packages/language-service/test/project/app/main.ts @@ -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); diff --git a/packages/language-service/test/project/app/ng-for-cases.ts b/packages/language-service/test/project/app/ng-for-cases.ts new file mode 100644 index 0000000000..1515c2ab2f --- /dev/null +++ b/packages/language-service/test/project/app/ng-for-cases.ts @@ -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: ` +
+ {{person.name}} +
`, +}) +export class UnknownPeople { +} + +@Component({ + template: ` +
+ {{person.name}} +
`, +}) +export class UnknownEven { + people: Person[]; +} + +@Component({ + template: ` +
+ {{person.name}} +
`, +}) +export class UnknownTrackBy { + people: Person[]; +} diff --git a/packages/language-service/test/project/app/ng-if-cases.ts b/packages/language-service/test/project/app/ng-if-cases.ts new file mode 100644 index 0000000000..9d556235dc --- /dev/null +++ b/packages/language-service/test/project/app/ng-if-cases.ts @@ -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: ` +
+ Showing now! +
`, +}) +export class ShowIf { + show = false; +} diff --git a/packages/language-service/test/project/app/parsing-cases.ts b/packages/language-service/test/project/app/parsing-cases.ts new file mode 100644 index 0000000000..3fcf215fcd --- /dev/null +++ b/packages/language-service/test/project/app/parsing-cases.ts @@ -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: ` +

+ Some <~{incomplete-open-lt}a~{incomplete-open-a} ~{incomplete-open-attr} text +

`, +}) +export class CaseIncompleteOpen { +} + +@Component({ + template: '

Some ~{missing-closing} text

', +}) +export class CaseMissingClosing { +} + +@Component({ + template: '

Some text

', +}) +export class CaseUnknown { +} + +@Component({ + template: '

{{data | ~{before-pipe}lowe~{in-pipe}rcase~{after-pipe} }}', +}) +export class Pipes { + data = 'Some string'; +} + +@Component({ + template: '

', +}) +export class NoValueAttribute { +} + + +@Component({ + template: '

', +}) +export class AttributeBinding { + test: string; +} + +@Component({ + template: '

', +}) +export class PropertyBinding { + test: string; +} + +@Component({ + template: '

', +}) +export class EventBinding { + test: string; + + modelChanged() {} +} + +@Component({ + template: '

', +}) +export class TwoWayBinding { + test: string; +} + +@Directive({ + selector: '[string-model]', +}) +export class StringModel { + @Input() model: string; + @Output() modelChanged: EventEmitter; +} + +@Directive({ + selector: '[number-model]', +}) +export class NumberModel { + @Input('inputAlias') model: number; + @Output('outputAlias') modelChanged: EventEmitter; +} + +interface Person { + name: string; + age: number; +} + +@Component({ + template: '
', +}) +export class ForOfEmpty { +} + +@Component({ + template: '
', +}) +export class ForOfLetEmpty { +} + +@Component({ + template: '
', +}) +export class ForLetIEqual { +} + +@Component({ + template: ` +
+ Name: {{~{for-interp-person}person.~{for-interp-name}name}} + Age: {{person.~{for-interp-age}age}} +
`, +}) +export class ForUsingComponent { + people: Person[]; +} + +@Component({ + template: ` +
+ + {{~{test-comp-content}}} + {{test1.~{test-comp-after-test}name}} + {{div.~{test-comp-after-div}.innerText}} + +
+ `, +}) +export class References { +} + +/*BeginTestComponent*/ @Component({ + selector: 'test-comp', + template: '
Testing: {{name}}
', +}) +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'; +} diff --git a/packages/language-service/test/project/app/test.css b/packages/language-service/test/project/app/test.css new file mode 100644 index 0000000000..6af55237a9 --- /dev/null +++ b/packages/language-service/test/project/app/test.css @@ -0,0 +1,3 @@ +body, html { + width: 100%; +} diff --git a/packages/language-service/test/project/app/test.ng b/packages/language-service/test/project/app/test.ng new file mode 100644 index 0000000000..aec70cabc9 --- /dev/null +++ b/packages/language-service/test/project/app/test.ng @@ -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}}} + +~{after-h1}

{{~{h2-hero}hero.~{h2-name}name}} details!

+
{{~{label-hero}hero.~{label-id}id}}
+
+ +
+&~{entity-amp}amp; diff --git a/packages/language-service/test/project/foo.ts b/packages/language-service/test/project/foo.ts new file mode 100644 index 0000000000..35fcc18480 --- /dev/null +++ b/packages/language-service/test/project/foo.ts @@ -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'; diff --git a/packages/language-service/test/test_utils.ts b/packages/language-service/test/test_utils.ts index 53508169a2..5542f3642d 100644 --- a/packages/language-service/test/test_utils.ts +++ b/packages/language-service/test/test_utils.ts @@ -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(); +const missingCache = new Set([ + '/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(); const reportedMissing = new Set(); @@ -39,7 +50,7 @@ const reportedMissing = new Set(); 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 { + const {TEST_SRCDIR} = process.env; + const root = + path.join(TEST_SRCDIR !, 'angular', 'packages', 'language-service', 'test', 'project'); + const dirs = [root]; + const files = new Map(); + 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(); - private overrides = new Map(); + private angularPath?: string; + private readonly nodeModulesPath: string; + private readonly scriptVersion = new Map(); + private readonly overrides = new Map(); private projectVersion = 0; private options: ts.CompilerOptions; - private overrideDirectory = new Set(); - private existsCache = new Map(); - private fileCache = new Map(); + private readonly overrideDirectory = new Set(); + private readonly existsCache = new Map(); + private readonly fileCache = new Map(); 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(iterator: IterableIterator) { - 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 = (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 { diff --git a/packages/language-service/test/ts_plugin_spec.ts b/packages/language-service/test/ts_plugin_spec.ts index 86c4fd5213..c3c8b7f803 100644 --- a/packages/language-service/test/ts_plugin_spec.ts +++ b/packages/language-service/test/ts_plugin_spec.ts @@ -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`, diff --git a/packages/tsconfig.json b/packages/tsconfig.json index 25db54d1ea..67d052db68 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -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.