From 017bf0b794d469573ee0dd5e6ca5838f50139d38 Mon Sep 17 00:00:00 2001 From: Keen Yee Liau Date: Fri, 19 Apr 2019 08:53:19 -0700 Subject: [PATCH] test(language-service): Add tests for quickinfo and definition (#29990) `quickinfo` is used for hover tooltip. `definition` is used for "Go to definition". PR Close #29990 --- .../goldens/definition.json | 20 ++++++ .../goldens/quickinfo.json | 22 ++++++ .../language_service_plugin/matcher.ts | 12 ++-- .../language_service_plugin/package.json | 2 +- integration/language_service_plugin/test.ts | 42 +++++++++++- .../language_service_plugin/tsclient.ts | 67 ++++++++++--------- .../language_service_plugin/tsconfig.json | 2 +- 7 files changed, 127 insertions(+), 40 deletions(-) create mode 100644 integration/language_service_plugin/goldens/definition.json create mode 100644 integration/language_service_plugin/goldens/quickinfo.json diff --git a/integration/language_service_plugin/goldens/definition.json b/integration/language_service_plugin/goldens/definition.json new file mode 100644 index 0000000000..8d7bf0cc93 --- /dev/null +++ b/integration/language_service_plugin/goldens/definition.json @@ -0,0 +1,20 @@ +{ + "seq": 0, + "type": "response", + "command": "definition", + "request_seq": 2, + "success": true, + "body": [ + { + "file": "${PWD}/project/app/app.component.ts", + "start": { + "line": 7, + "offset": 30 + }, + "end": { + "line": 7, + "offset": 47 + } + } + ] +} diff --git a/integration/language_service_plugin/goldens/quickinfo.json b/integration/language_service_plugin/goldens/quickinfo.json new file mode 100644 index 0000000000..66a9771dcf --- /dev/null +++ b/integration/language_service_plugin/goldens/quickinfo.json @@ -0,0 +1,22 @@ +{ + "seq": 0, + "type": "response", + "command": "quickinfo", + "request_seq": 2, + "success": true, + "body": { + "kind": "angular", + "kindModifiers": "what does this do?", + "start": { + "line": 5, + "offset": 26 + }, + "end": { + "line": 5, + "offset": 30 + }, + "displayString": "property name of AppComponent", + "documentation": "", + "tags": [] + } +} \ No newline at end of file diff --git a/integration/language_service_plugin/matcher.ts b/integration/language_service_plugin/matcher.ts index b46edbf3bc..fc4b4b3180 100644 --- a/integration/language_service_plugin/matcher.ts +++ b/integration/language_service_plugin/matcher.ts @@ -1,4 +1,4 @@ -import { writeFileSync } from 'fs'; +import { writeFileSync, readFileSync } from 'fs'; const goldens: string[] = process.argv.slice(2); @@ -6,16 +6,18 @@ export const goldenMatcher: jasmine.CustomMatcherFactories = { toMatchGolden(util: jasmine.MatchersUtil): jasmine.CustomMatcher { return { compare(actual: {command: string}, golden: string): jasmine.CustomMatcherResult { - const expected = require(`./goldens/${golden}`); - const pass = util.equals(actual, expected); - if (!pass && goldens.indexOf(golden) >= 0) { + if (goldens.includes(golden)) { console.error(`Writing golden file ${golden}`); writeFileSync(`./goldens/${golden}`, JSON.stringify(actual, null, 2)); return { pass : true }; } + const content = readFileSync(`./goldens/${golden}`, 'utf-8'); + const expected = JSON.parse(content.replace("${PWD}", process.env.PWD!)); + const pass = util.equals(actual, expected); return { pass, - message: `Expected response for '${actual.command}' to match golden file ${golden}.\n` + + message: `Expected ${JSON.stringify(actual, null, 2)} to match golden ` + + `${JSON.stringify(expected, null, 2)}.\n` + `To generate new golden file, run "yarn golden ${golden}".`, }; } diff --git a/integration/language_service_plugin/package.json b/integration/language_service_plugin/package.json index c5036b18a1..3fe1900776 100644 --- a/integration/language_service_plugin/package.json +++ b/integration/language_service_plugin/package.json @@ -13,7 +13,7 @@ "scripts": { "build": "tsc -p tsconfig.json", "cleanup": "rm -rf ti-*.log tsserver.log", - "golden": "node generate.js", + "golden": "yarn build && node generate.js", "test": "yarn cleanup && yarn build && jasmine test.js" } } diff --git a/integration/language_service_plugin/test.ts b/integration/language_service_plugin/test.ts index 0147ed1ebb..0875a7f1d3 100644 --- a/integration/language_service_plugin/test.ts +++ b/integration/language_service_plugin/test.ts @@ -50,7 +50,6 @@ describe('Angular Language Service', () => { // https://github.com/Microsoft/TypeScript/blob/master/lib/protocol.d.ts#L1055 client.sendRequest('open', { file: `${PWD}/project/app/app.module.ts`, - fileContent: "" }); // Server does not send response to geterr request // https://github.com/Microsoft/TypeScript/blob/master/lib/protocol.d.ts#L1770 @@ -77,7 +76,6 @@ describe('Angular Language Service', () => { client.sendRequest('open', { file: `${PWD}/project/app/app.component.ts`, - fileContent: "import { Component } from '@angular/core';\n\n@Component({\n selector: 'my-app',\n template: `

Hello {{name}}

`,\n})\nexport class AppComponent { name = 'Angular'; }\n" }); client.sendRequest('geterr', { @@ -101,4 +99,44 @@ describe('Angular Language Service', () => { }); expect(response).toMatchGolden('completionInfo.json'); }); + + it('should perform quickinfo', async () => { + client.sendRequest('open', { + file: `${PWD}/project/app/app.component.ts`, + }); + + const resp1 = await client.sendRequest('reload', { + file: `${PWD}/project/app/app.component.ts`, + tmpFile: `${PWD}/project/app/app.component.ts`, + }) as any; + expect(resp1.command).toBe('reload'); + expect(resp1.success).toBe(true); + + const resp2 = await client.sendRequest('quickinfo', { + file: `${PWD}/project/app/app.component.ts`, + line: 5, + offset: 28, + }); + expect(resp2).toMatchGolden('quickinfo.json'); + }); + + it('should perform definition', async () => { + client.sendRequest('open', { + file: `${PWD}/project/app/app.component.ts`, + }); + + const resp1 = await client.sendRequest('reload', { + file: `${PWD}/project/app/app.component.ts`, + tmpFile: `${PWD}/project/app/app.component.ts`, + }) as any; + expect(resp1.command).toBe('reload'); + expect(resp1.success).toBe(true); + + const resp2 = await client.sendRequest('definition', { + file: `${PWD}/project/app/app.component.ts`, + line: 5, + offset: 28, + }); + expect(resp2).toMatchGolden('definition.json'); + }); }); diff --git a/integration/language_service_plugin/tsclient.ts b/integration/language_service_plugin/tsclient.ts index 05dd3c0e0d..b35356a824 100644 --- a/integration/language_service_plugin/tsclient.ts +++ b/integration/language_service_plugin/tsclient.ts @@ -15,51 +15,56 @@ export class Client { listen() { this.server.stdout.on('data', (data: Buffer) => { this.data = this.data ? Buffer.concat([this.data, data]) : data; + // tsserver could batch multiple responses together so we have to go + // through the entire buffer to keep looking for messages. const CONTENT_LENGTH = 'Content-Length: ' - const index = this.data.indexOf(CONTENT_LENGTH); - if (index < 0) { - return; - } - let start = index + CONTENT_LENGTH.length; - let end = this.data.indexOf('\r\n', start); - if (end < start) { - return; - } - const contentLengthStr = this.data.slice(start, end).toString(); - const contentLength = Number(contentLengthStr); - if (isNaN(contentLength) || contentLength < 0) { - return; - } - start = end + 4; - end = start + contentLength; - if (end > this.data.length) { - return; - } - const content = this.data.slice(start, end).toString(); - this.data = this.data.slice(end); - try { - const payload = JSON.parse(content); - if (payload.type === "event") { + do { + const index = this.data.indexOf(CONTENT_LENGTH); + if (index < 0) { return; } - this.responseEmitter.emit('response', payload); - } - catch (error) { - this.responseEmitter.emit('error', error); - } + let start = index + CONTENT_LENGTH.length; + let end = this.data.indexOf('\r\n', start); + if (end < start) { + return; + } + const contentLengthStr = this.data.slice(start, end).toString(); + const contentLength = Number(contentLengthStr); + if (isNaN(contentLength) || contentLength < 0) { + return; + } + start = end + 4; + end = start + contentLength; + if (end > this.data.length) { + return; + } + const content = this.data.slice(start, end).toString(); + this.data = this.data.slice(end); + try { + const payload = JSON.parse(content); + if (payload.type === "response") { + const seq = `${payload.request_seq}`; + this.responseEmitter.emit(seq, payload); + } + } + catch (error) { + this.responseEmitter.emit('error', error); + } + } while (this.data.length > 0) }); } async send(type: string, command: string, params: {}) { + const seq = this.id++; const request = { - seq: this.id++, + seq, type, command, arguments: params }; this.server.stdin.write(JSON.stringify(request) + '\r\n'); return new Promise((resolve, reject) => { - this.responseEmitter.once('response', resolve); + this.responseEmitter.once(`${seq}`, resolve); this.responseEmitter.once('error', reject); }); } diff --git a/integration/language_service_plugin/tsconfig.json b/integration/language_service_plugin/tsconfig.json index 9730ac2fe3..bb0a08cec4 100644 --- a/integration/language_service_plugin/tsconfig.json +++ b/integration/language_service_plugin/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { /* Basic Options */ - "target": "es2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ + "target": "es2016", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ // "lib": [], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */