1import { expect, afterAll, beforeAll, it, describe } from 'vitest';
2import { TSServer } from './server';
3import path from 'node:path';
4import fs from 'node:fs';
5import url from 'node:url';
6import ts from 'typescript/lib/tsserverlibrary';
7
8const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
9
10const projectPath = path.resolve(
11 __dirname,
12 'fixture-project-tada-multi-schema'
13);
14describe('Multiple schemas', () => {
15 const outfilePokemonTest = path.join(projectPath, 'simple-pokemon.ts');
16 const outfileTodoTest = path.join(projectPath, 'simple-todo.ts');
17 const outfileStarImport = path.join(projectPath, 'star-import.ts');
18
19 let server: TSServer;
20 beforeAll(async () => {
21 server = new TSServer(projectPath, { debugLog: false });
22
23 server.sendCommand('open', {
24 file: outfilePokemonTest,
25 fileContent: '// empty',
26 scriptKindName: 'TS',
27 } satisfies ts.server.protocol.OpenRequestArgs);
28 server.sendCommand('open', {
29 file: outfileTodoTest,
30 fileContent: '// empty',
31 scriptKindName: 'TS',
32 } satisfies ts.server.protocol.OpenRequestArgs);
33 server.sendCommand('open', {
34 file: outfileStarImport,
35 fileContent: '// empty',
36 scriptKindName: 'TS',
37 } satisfies ts.server.protocol.OpenRequestArgs);
38
39 server.sendCommand('updateOpen', {
40 openFiles: [
41 {
42 file: outfilePokemonTest,
43 fileContent: fs.readFileSync(
44 path.join(projectPath, 'fixtures/simple-pokemon.ts'),
45 'utf-8'
46 ),
47 },
48 {
49 file: outfileTodoTest,
50 fileContent: fs.readFileSync(
51 path.join(projectPath, 'fixtures/simple-todo.ts'),
52 'utf-8'
53 ),
54 },
55 {
56 file: outfileStarImport,
57 fileContent: fs.readFileSync(
58 path.join(projectPath, 'fixtures/star-import.ts'),
59 'utf-8'
60 ),
61 },
62 ],
63 } satisfies ts.server.protocol.UpdateOpenRequestArgs);
64
65 server.sendCommand('saveto', {
66 file: outfilePokemonTest,
67 tmpfile: outfilePokemonTest,
68 } satisfies ts.server.protocol.SavetoRequestArgs);
69 server.sendCommand('saveto', {
70 file: outfileTodoTest,
71 tmpfile: outfileTodoTest,
72 } satisfies ts.server.protocol.SavetoRequestArgs);
73 server.sendCommand('saveto', {
74 file: outfileStarImport,
75 tmpfile: outfileStarImport,
76 } satisfies ts.server.protocol.SavetoRequestArgs);
77
78 // Give TS some time to figure this out...
79 await new Promise(resolve => setTimeout(resolve, 1000));
80 });
81
82 afterAll(() => {
83 try {
84 fs.unlinkSync(outfilePokemonTest);
85 fs.unlinkSync(outfileTodoTest);
86 fs.unlinkSync(outfileStarImport);
87 } catch {}
88 });
89
90 it('gives diagnostics about unused fields', async () => {
91 await server.waitForResponse(
92 e => e.type === 'event' && e.event === 'semanticDiag'
93 );
94 const res = server.responses.filter(
95 resp =>
96 resp.type === 'event' &&
97 resp.event === 'semanticDiag' &&
98 resp.body?.file === outfilePokemonTest
99 );
100
101 expect(res).toBeDefined();
102 expect(res).toHaveLength(1);
103 expect(res[0].body.diagnostics).toHaveLength(1);
104 expect(res[0].body.diagnostics[0]).toMatchInlineSnapshot(`
105 {
106 "category": "warning",
107 "code": 52004,
108 "end": {
109 "line": 12,
110 "offset": 1,
111 },
112 "start": {
113 "line": 11,
114 "offset": 7,
115 },
116 "text": "The field Pokemon.classification is deprecated. And this is the reason why",
117 }
118 `);
119 }, 30000);
120
121 it('gives quick-info for the pokemon document', async () => {
122 server.send({
123 seq: 9,
124 type: 'request',
125 command: 'quickinfo',
126 arguments: {
127 file: outfilePokemonTest,
128 line: 8,
129 offset: 8,
130 },
131 });
132
133 await server.waitForResponse(
134 response =>
135 response.type === 'response' && response.command === 'quickinfo'
136 );
137
138 const res = server.responses
139 .reverse()
140 .find(resp => resp.type === 'response' && resp.command === 'quickinfo');
141
142 expect(res).toBeDefined();
143 expect(typeof res?.body).toEqual('object');
144 expect(res?.body.documentation).toEqual(`Pokemon.name: String!`);
145 }, 30000);
146
147 it('gives quick-info for the pokemon document namespace', async () => {
148 server.send({
149 seq: 20,
150 type: 'request',
151 command: 'quickinfo',
152 arguments: {
153 file: outfileStarImport,
154 line: 8,
155 offset: 9,
156 },
157 });
158
159 await server.waitForResponse(
160 response =>
161 response.type === 'response' && response.command === 'quickinfo'
162 );
163
164 const res = server.responses
165 .reverse()
166 .find(resp => resp.type === 'response' && resp.command === 'quickinfo');
167
168 expect(res).toBeDefined();
169 expect(typeof res?.body).toEqual('object');
170 expect(res?.body.documentation).toEqual(`Pokemon.name: String!`);
171 }, 30000);
172
173 it('gives quick-info for the todo document', async () => {
174 server.send({
175 seq: 10,
176 type: 'request',
177 command: 'quickinfo',
178 arguments: {
179 file: outfileTodoTest,
180 line: 7,
181 offset: 8,
182 },
183 });
184
185 await server.waitForResponse(
186 response =>
187 response.type === 'response' && response.command === 'quickinfo'
188 );
189
190 const res = server.responses
191 .reverse()
192 .find(resp => resp.type === 'response' && resp.command === 'quickinfo');
193
194 expect(res).toBeDefined();
195 expect(typeof res?.body).toEqual('object');
196 expect(res?.body.documentation).toEqual(`Todo.id: ID!`);
197 }, 30000);
198
199 it('gives completion-info for the pokemon document', async () => {
200 server.send({
201 seq: 11,
202 type: 'request',
203 command: 'completionInfo',
204 arguments: {
205 file: outfilePokemonTest,
206 line: 9,
207 offset: 7,
208 includeExternalModuleExports: true,
209 includeInsertTextCompletions: true,
210 triggerKind: 1,
211 },
212 });
213
214 await server.waitForResponse(
215 response =>
216 response.type === 'response' && response.command === 'completionInfo'
217 );
218
219 const res = server.responses
220 .reverse()
221 .find(
222 resp => resp.type === 'response' && resp.command === 'completionInfo'
223 );
224
225 expect(res).toBeDefined();
226 expect(res).toMatchInlineSnapshot(`
227 {
228 "body": {
229 "entries": [
230 {
231 "kind": "var",
232 "kindModifiers": "declare",
233 "labelDetails": {
234 "detail": " AttacksConnection",
235 },
236 "name": "attacks",
237 "sortText": "0attacks",
238 },
239 {
240 "kind": "var",
241 "kindModifiers": "declare",
242 "labelDetails": {
243 "detail": " [EvolutionRequirement]",
244 },
245 "name": "evolutionRequirements",
246 "sortText": "2evolutionRequirements",
247 },
248 {
249 "kind": "var",
250 "kindModifiers": "declare",
251 "labelDetails": {
252 "detail": " [Pokemon]",
253 },
254 "name": "evolutions",
255 "sortText": "3evolutions",
256 },
257 {
258 "kind": "var",
259 "kindModifiers": "declare",
260 "labelDetails": {
261 "detail": " PokemonDimension",
262 },
263 "name": "height",
264 "sortText": "5height",
265 },
266 {
267 "kind": "var",
268 "kindModifiers": "declare",
269 "labelDetails": {
270 "description": "Maximum combat power a Pokémon may achieve at max level.",
271 "detail": " Int",
272 },
273 "name": "maxCP",
274 "sortText": "7maxCP",
275 },
276 {
277 "kind": "var",
278 "kindModifiers": "declare",
279 "labelDetails": {
280 "description": "Maximum health points a Pokémon may achieve at max level.",
281 "detail": " Int",
282 },
283 "name": "maxHP",
284 "sortText": "8maxHP",
285 },
286 {
287 "kind": "var",
288 "kindModifiers": "declare",
289 "labelDetails": {
290 "detail": " [PokemonType]",
291 },
292 "name": "resistant",
293 "sortText": "10resistant",
294 },
295 {
296 "kind": "var",
297 "kindModifiers": "declare",
298 "labelDetails": {
299 "detail": " [PokemonType]",
300 },
301 "name": "types",
302 "sortText": "11types",
303 },
304 {
305 "kind": "var",
306 "kindModifiers": "declare",
307 "labelDetails": {
308 "detail": " [PokemonType]",
309 },
310 "name": "weaknesses",
311 "sortText": "12weaknesses",
312 },
313 {
314 "kind": "var",
315 "kindModifiers": "declare",
316 "labelDetails": {
317 "detail": " PokemonDimension",
318 },
319 "name": "weight",
320 "sortText": "13weight",
321 },
322 ],
323 "isGlobalCompletion": false,
324 "isMemberCompletion": false,
325 "isNewIdentifierLocation": false,
326 },
327 "command": "completionInfo",
328 "request_seq": 11,
329 "seq": 0,
330 "success": true,
331 "type": "response",
332 }
333 `);
334 }, 30000);
335
336 it('gives completion-info for the todo document', async () => {
337 server.send({
338 seq: 11,
339 type: 'request',
340 command: 'completionInfo',
341 arguments: {
342 file: outfileTodoTest,
343 line: 8,
344 offset: 7,
345 includeExternalModuleExports: true,
346 includeInsertTextCompletions: true,
347 triggerKind: 1,
348 },
349 });
350
351 await server.waitForResponse(
352 response =>
353 response.type === 'response' && response.command === 'completionInfo'
354 );
355
356 const res = server.responses
357 .reverse()
358 .find(
359 resp => resp.type === 'response' && resp.command === 'completionInfo'
360 );
361
362 expect(res).toBeDefined();
363 expect(res).toMatchInlineSnapshot(`
364 {
365 "body": {
366 "entries": [
367 {
368 "kind": "var",
369 "kindModifiers": "declare",
370 "labelDetails": {
371 "detail": " String!",
372 },
373 "name": "text",
374 "sortText": "1text",
375 },
376 {
377 "kind": "var",
378 "kindModifiers": "declare",
379 "labelDetails": {
380 "detail": " Boolean!",
381 },
382 "name": "completed",
383 "sortText": "2completed",
384 },
385 {
386 "kind": "var",
387 "kindModifiers": "declare",
388 "labelDetails": {
389 "description": "The name of the current Object type at runtime.",
390 "detail": " String!",
391 },
392 "name": "__typename",
393 "sortText": "3__typename",
394 },
395 ],
396 "isGlobalCompletion": false,
397 "isMemberCompletion": false,
398 "isNewIdentifierLocation": false,
399 },
400 "command": "completionInfo",
401 "request_seq": 11,
402 "seq": 0,
403 "success": true,
404 "type": "response",
405 }
406 `);
407 }, 30000);
408});