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
This commit is contained in:
Keen Yee Liau 2019-04-19 08:53:19 -07:00 committed by Ben Lesh
parent f348deae92
commit 017bf0b794
7 changed files with 127 additions and 40 deletions

View File

@ -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
}
}
]
}

View File

@ -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": []
}
}

View File

@ -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}".`,
};
}

View File

@ -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"
}
}

View File

@ -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: `<h1>Hello {{name}}</h1>`,\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');
});
});

View File

@ -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);
});
}

View File

@ -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. */