Skip to content

Commit 01548ff

Browse files
authored
chore(ls): add unit tests to the server (#20)
1 parent 78685d4 commit 01548ff

File tree

9 files changed

+257
-6
lines changed

9 files changed

+257
-6
lines changed

scripts/ci/test.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ set -ex -o pipefail
44

55
echo 'travis_fold:start:TEST'
66

7+
# Server unit tests
8+
(
9+
cd server
10+
npm run test
11+
)
12+
13+
# Server smoke test
714
tests/tests.sh
815

916
echo 'travis_fold:end:TEST'

server/.vscode/launch.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"outDir": "${workspaceRoot}/../client/server"
1111
},
1212
{
13-
"name": "Attach (cmd)",
13+
"name": "Attach (tests)",
1414
"type": "node",
1515
"request": "attach",
1616
"port": 5858,

server/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
"node": "*"
99
},
1010
"scripts": {
11-
"compile": "installServerIntoExtension ../client ./package.json ./tsconfig.json && tsc -p .",
12-
"watch": "installServerIntoExtension ../client ./package.json ./tsconfig.json && tsc --watch -p ."
11+
"compile": "installServerIntoExtension ../client ./package.json ./tsconfig-build.json && tsc -p tsconfig-build.json",
12+
"watch": "installServerIntoExtension ../client ./package.json ./tsconfig.json && tsc --watch -p tsconfig-build.json",
13+
"test": "tsc -p . && jasmine -- dist/**/*_spec.js"
1314
},
1415
"dependencies": {
1516
"@angular/language-service": "^2.3.1",
@@ -21,7 +22,9 @@
2122
"@angular/compiler": "^2.3.1",
2223
"@angular/compiler-cli": "^2.3.1",
2324
"@angular/core": "^2.3.1",
25+
"@types/jasmine": "^2.5.38",
2426
"@types/node": "^6.0.46",
27+
"jasmine": "^2.5.2",
2528
"rxjs": "5.0.0-rc.4",
2629
"zone.js": "^0.7.2"
2730
},

server/src/editorServices.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ export interface ProjectServiceHost {
3333
resolvePath(path: string): string;
3434
fileExists(path: string): boolean;
3535
getDirectories(path: string): string[];
36-
watchDirectory?(path: string, callback: ts.DirectoryWatcherCallback, recursive?: boolean): ts.FileWatcher;
37-
watchFile?(path: string, callback: ts.FileWatcherCallback): ts.FileWatcher;
36+
watchDirectory(path: string, callback: ts.DirectoryWatcherCallback, recursive?: boolean): ts.FileWatcher;
37+
watchFile(path: string, callback: ts.FileWatcherCallback): ts.FileWatcher;
3838
readDirectory(path: string, extensions?: string[], exclude?: string[], include?: string[]): string[];
3939

4040
useCaseSensitiveFileNames: boolean;

server/tests/editorServices_spec.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/// <reference path="../node_modules/@types/jasmine/index.d.ts" />
2+
3+
import * as ts from 'typescript';
4+
import {LineIndexSnapshot, Logger, ProjectService, ProjectServiceHost, ScriptInfo} from '../src/editorServices';
5+
6+
import * as u from './test_utils';
7+
import {QUICKSTART} from './test_data';
8+
9+
describe('editor services', () => {
10+
let projectService: ProjectService;
11+
beforeAll(() => {
12+
projectService = new ProjectService(new u.MockProjectServiceHost(QUICKSTART), new u.MockLogger());
13+
});
14+
15+
it('should be able to get the information for a file', () => {
16+
projectService.openClientFile('/app/app.component.ts');
17+
const info = projectService.getScriptInfo('/app/app.component.ts');
18+
expect(info).not.toBeUndefined();
19+
});
20+
21+
describe('file', () => {
22+
function tests(fileName: string, content: string) {
23+
let info: ScriptInfo;
24+
let snap: LineIndexSnapshot;
25+
let len: number;
26+
27+
beforeEach(() => {
28+
projectService.openClientFile(fileName, content);
29+
info = projectService.getScriptInfo(fileName);
30+
snap = info.snap();
31+
len = snap.getLength();
32+
})
33+
34+
it('should be able to get pieces of a file', () => {
35+
const firstText = snap.getText(0, len);
36+
for (let i = 0; i < len; i++) {
37+
expect(snap.getText(i, i + 1)).toEqual(firstText[i]);
38+
}
39+
});
40+
41+
it('should be able to modify the file and get the expected content', () => {
42+
const offsetOfComponent = content.indexOf('@Component');
43+
projectService.clientFileChanges(fileName, [{start: offsetOfComponent, end: offsetOfComponent, insertText: ' '}]);
44+
const text = info.getText();
45+
expect(text).toEqual(content.replace('@Component', ' @Component'));
46+
});
47+
48+
describe('and line starts', () => {
49+
let lineStarts: number[];
50+
51+
beforeAll(() => {
52+
lineStarts = getLineStarts(content);
53+
});
54+
55+
it('should be able to get the expected line columns', () => {
56+
for (let i = 0; i < len; i++) {
57+
const expected = lineColOf(i, lineStarts);
58+
const result = projectService.positionsToLineOffsets(fileName, [i]);
59+
expect(result).toEqual([expected]);
60+
}
61+
});
62+
63+
it('should be able to turn line columns in offsets', () => {
64+
for (let i = 0; i < len; i++) {
65+
const expected = lineColOf(i, lineStarts);
66+
const result = projectService.lineOffsetsToPositions(fileName, [expected]);
67+
expect(result).toEqual([i]);
68+
}
69+
});
70+
});
71+
}
72+
73+
const fileName = '/app/app.component.ts';
74+
describe('with LF', () => {
75+
tests(fileName, u.read(fileName, QUICKSTART));
76+
});
77+
describe('with LF/CR', () => {
78+
tests(fileName, u.read(fileName, QUICKSTART).replace(/\n/g, '\r\n'));
79+
});
80+
});
81+
});
82+
83+
function getLineStarts(content: string): number[] {
84+
const result = [0];
85+
const len = content.length;
86+
for (let i = 0; i < len; i++) {
87+
if (content[i] == `\n`) result.push(i + 1);
88+
}
89+
result.push(len);
90+
return result;
91+
}
92+
93+
function lineColOf(position: number, lineStarts: number[]) {
94+
for (let line = 0; line < lineStarts.length; line++) {
95+
if (lineStarts[line + 1] > position) {
96+
return {line: line + 1, col: position - lineStarts[line] + 1};
97+
}
98+
}
99+
}

server/tests/test_data.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {MockData} from './test_utils';
2+
3+
export const QUICKSTART: MockData = {
4+
'tsconfig.json': `
5+
{
6+
"compilerOptions": {
7+
"target": "es5",
8+
"module": "commonjs",
9+
"moduleResolution": "node",
10+
"sourceMap": true,
11+
"emitDecoratorMetadata": true,
12+
"experimentalDecorators": true,
13+
"lib": [ "es2015", "dom" ],
14+
"noImplicitAny": true,
15+
"suppressImplicitAnyIndexErrors": true
16+
}
17+
}
18+
`,
19+
'app': {
20+
21+
'app.component.ts': `
22+
import { Component } from '@angular/core';
23+
24+
@Component({
25+
selector: 'my-app',
26+
template: \`<h1>Hello {{name}}</h1> ><div *ngIf="name !== undefined"></div>\`,
27+
})
28+
export class AppComponent { name = 'Angular'; }`,
29+
30+
31+
'app.module.ts': `
32+
import { NgModule } from '@angular/core';
33+
import { BrowserModule } from '@angular/platform-browser';
34+
35+
import { AppComponent } from './app.component';
36+
37+
@NgModule({
38+
imports: [ BrowserModule ],
39+
declarations: [ AppComponent ],
40+
bootstrap: [ AppComponent ]
41+
})
42+
export class AppModule { } `,
43+
44+
45+
'main.ts': `
46+
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
47+
48+
import { AppModule } from './app.module';
49+
50+
platformBrowserDynamic().bootstrapModule(AppModule);`,
51+
}
52+
};

server/tests/test_utils.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import * as ts from 'typescript';
2+
import {Logger, ProjectServiceHost} from '../src/editorServices';
3+
4+
export type MockData = string | MockDirectory;
5+
6+
export type MockDirectory = {
7+
[name: string]: MockData | undefined;
8+
}
9+
10+
export function find(fileName: string, data: MockData): MockData|undefined {
11+
const names = fileName.split('/');
12+
if (names.length && !names[0].length) names.shift();
13+
let current = data;
14+
for (let name of names) {
15+
if (typeof current === 'string') {
16+
return undefined;
17+
} else {
18+
current = (current as MockDirectory)[name];
19+
}
20+
if (!current) return undefined;
21+
}
22+
return current;
23+
}
24+
25+
export function read(fileName: string, data: MockData): string|undefined {
26+
const result = find(fileName, data);
27+
if (typeof result === 'string') {
28+
return result;
29+
}
30+
return undefined;
31+
}
32+
33+
export function fileExists(fileName: string, data: MockData): boolean {
34+
let result = find(fileName, data);
35+
return result && typeof result == 'string';
36+
}
37+
38+
export function directoryExists(dirname: string, data: MockData): boolean {
39+
let result = find(dirname, data);
40+
return result && typeof result !== 'string';
41+
}
42+
43+
export function getDirectories(path: string, data: MockData): string[] {
44+
let result = find(path, data);
45+
return result && typeof result !== 'string' && Object.keys(result);
46+
}
47+
48+
export class MockProjectServiceHost implements ProjectServiceHost {
49+
constructor(private data: MockData) {}
50+
getCurrentDirectory(): string { return "/"; }
51+
readFile(path: string, encoding?: string): string { return read(path, this.data); }
52+
directoryExists(path: string): boolean { return directoryExists(path, this.data); }
53+
getExecutingFilePath(): string { return "/"; }
54+
resolvePath(path: string): string { return path; }
55+
fileExists(path: string): boolean { return fileExists(path, this.data); }
56+
getDirectories(path: string): string[] { return }
57+
watchDirectory(path: string, callback: ts.DirectoryWatcherCallback, recursive?: boolean): ts.FileWatcher { return new MockFileWatcher(); }
58+
watchFile(path: string, callback: ts.FileWatcherCallback): ts.FileWatcher { return new MockFileWatcher(); }
59+
readDirectory(path: string, extensions?: string[], exclude?: string[], include?: string[]): string[] { return []; }
60+
useCaseSensitiveFileNames: false;
61+
setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): any { return undefined; }
62+
clearTimeout(timeoutId: any): void { }
63+
}
64+
65+
export class MockLogger implements Logger {
66+
close(): void {}
67+
isVerbose(): boolean { return false; }
68+
info(s: string): void { }
69+
startGroup(): void {}
70+
endGroup(): void {}
71+
msg(s: string, type?: string): void {}
72+
}
73+
74+
export class MockFileWatcher {
75+
close() {}
76+
}

server/tsconfig-build.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES5",
4+
"module": "commonjs",
5+
"moduleResolution": "node",
6+
"sourceMap": true,
7+
"outDir": "../client/server",
8+
"lib": ["es5", "es2015.core", "es2015.promise", "es2015.collection", "es2015.iterable", "dom"],
9+
"types": []
10+
},
11+
"exclude": [
12+
"node_modules", "tests"
13+
]
14+
}

server/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"module": "commonjs",
55
"moduleResolution": "node",
66
"sourceMap": true,
7-
"outDir": "../client/server",
7+
"outDir": "dist",
88
"lib": ["es5", "es2015.core", "es2015.promise", "es2015.collection", "es2015.iterable", "dom"],
99
"types": []
1010
},

0 commit comments

Comments
 (0)