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(__dirname, 'fixture-project-tada');
11describe('Fragment + operations', () => {
12 const outfileCombo = path.join(projectPath, 'simple.ts');
13 const outfileUnusedFragment = path.join(projectPath, 'unused-fragment.ts');
14 const outfileCombinations = path.join(projectPath, 'fragment.ts');
15
16 let server: TSServer;
17 beforeAll(async () => {
18 server = new TSServer(projectPath, { debugLog: false });
19
20 server.sendCommand('open', {
21 file: outfileCombo,
22 fileContent: '// empty',
23 scriptKindName: 'TS',
24 } satisfies ts.server.protocol.OpenRequestArgs);
25 server.sendCommand('open', {
26 file: outfileCombinations,
27 fileContent: '// empty',
28 scriptKindName: 'TS',
29 } satisfies ts.server.protocol.OpenRequestArgs);
30 server.sendCommand('open', {
31 file: outfileUnusedFragment,
32 fileContent: '// empty',
33 scriptKindName: 'TS',
34 } satisfies ts.server.protocol.OpenRequestArgs);
35
36 server.sendCommand('updateOpen', {
37 openFiles: [
38 {
39 file: outfileCombinations,
40 fileContent: fs.readFileSync(
41 path.join(projectPath, 'fixtures/fragment.ts'),
42 'utf-8'
43 ),
44 },
45 {
46 file: outfileCombo,
47 fileContent: fs.readFileSync(
48 path.join(projectPath, 'fixtures/simple.ts'),
49 'utf-8'
50 ),
51 },
52 {
53 file: outfileUnusedFragment,
54 fileContent: fs.readFileSync(
55 path.join(projectPath, 'fixtures/unused-fragment.ts'),
56 'utf-8'
57 ),
58 },
59 ],
60 } satisfies ts.server.protocol.UpdateOpenRequestArgs);
61
62 server.sendCommand('saveto', {
63 file: outfileCombo,
64 tmpfile: outfileCombo,
65 } satisfies ts.server.protocol.SavetoRequestArgs);
66 server.sendCommand('saveto', {
67 file: outfileCombinations,
68 tmpfile: outfileCombinations,
69 } satisfies ts.server.protocol.SavetoRequestArgs);
70 server.sendCommand('saveto', {
71 file: outfileUnusedFragment,
72 tmpfile: outfileUnusedFragment,
73 } satisfies ts.server.protocol.SavetoRequestArgs);
74 });
75
76 afterAll(() => {
77 try {
78 fs.unlinkSync(outfileUnusedFragment);
79 fs.unlinkSync(outfileCombinations);
80 fs.unlinkSync(outfileCombo);
81 } catch {}
82 });
83
84 it('gives semantic-diagnostics with preceding fragments', async () => {
85 await server.waitForResponse(
86 e => e.type === 'event' && e.event === 'semanticDiag'
87 );
88 const res = server.responses.filter(
89 resp =>
90 resp.type === 'event' &&
91 resp.event === 'semanticDiag' &&
92 resp.body?.file === outfileCombo
93 );
94 expect(res[0].body.diagnostics).toMatchInlineSnapshot(`
95 [
96 {
97 "category": "warning",
98 "code": 52004,
99 "end": {
100 "line": 12,
101 "offset": 1,
102 },
103 "start": {
104 "line": 11,
105 "offset": 7,
106 },
107 "text": "The field Pokemon.classification is deprecated. And this is the reason why",
108 },
109 ]
110 `);
111 }, 30000);
112
113 it('gives quick-info with preceding fragments', async () => {
114 server.send({
115 seq: 9,
116 type: 'request',
117 command: 'quickinfo',
118 arguments: {
119 file: outfileCombinations,
120 line: 7,
121 offset: 8,
122 },
123 });
124
125 await server.waitForResponse(
126 response =>
127 response.type === 'response' && response.command === 'quickinfo'
128 );
129
130 const res = server.responses
131 .reverse()
132 .find(resp => resp.type === 'response' && resp.command === 'quickinfo');
133
134 expect(res).toBeDefined();
135 expect(typeof res?.body).toEqual('object');
136 expect(res?.body.documentation).toEqual(`Pokemon.name: String!`);
137 }, 30000);
138
139 it('gives quick-info with documents', async () => {
140 server.send({
141 seq: 9,
142 type: 'request',
143 command: 'quickinfo',
144 arguments: {
145 file: outfileCombo,
146 line: 7,
147 offset: 10,
148 },
149 });
150
151 await server.waitForResponse(
152 response =>
153 response.type === 'response' && response.command === 'quickinfo'
154 );
155
156 const res = server.responses
157 .reverse()
158 .find(resp => resp.type === 'response' && resp.command === 'quickinfo');
159
160 expect(res).toBeDefined();
161 expect(typeof res?.body).toEqual('object');
162 expect(res?.body.documentation).toEqual(
163 `Query.pokemons: [Pokemon]
164
165List out all Pokémon, optionally in pages`
166 );
167 }, 30000);
168
169 it('gives suggestions with preceding fragments', async () => {
170 server.send({
171 seq: 10,
172 type: 'request',
173 command: 'completionInfo',
174 arguments: {
175 file: outfileCombinations,
176 line: 8,
177 offset: 5,
178 includeExternalModuleExports: true,
179 includeInsertTextCompletions: true,
180 triggerKind: 1,
181 },
182 });
183
184 await server.waitForResponse(
185 response =>
186 response.type === 'response' && response.command === 'completionInfo'
187 );
188
189 const res = server.responses
190 .reverse()
191 .find(
192 resp => resp.type === 'response' && resp.command === 'completionInfo'
193 );
194
195 expect(res).toBeDefined();
196 expect(typeof res?.body.entries).toEqual('object');
197 expect(res?.body.entries).toMatchInlineSnapshot(`
198 [
199 {
200 "kind": "var",
201 "kindModifiers": "declare",
202 "labelDetails": {
203 "detail": " AttacksConnection",
204 },
205 "name": "attacks",
206 "sortText": "0attacks",
207 },
208 {
209 "kind": "var",
210 "kindModifiers": "declare",
211 "labelDetails": {
212 "detail": " [EvolutionRequirement]",
213 },
214 "name": "evolutionRequirements",
215 "sortText": "2evolutionRequirements",
216 },
217 {
218 "kind": "var",
219 "kindModifiers": "declare",
220 "labelDetails": {
221 "detail": " [Pokemon]",
222 },
223 "name": "evolutions",
224 "sortText": "3evolutions",
225 },
226 {
227 "kind": "var",
228 "kindModifiers": "declare",
229 "labelDetails": {
230 "description": "Likelihood of an attempt to catch a Pokémon to fail.",
231 "detail": " Float",
232 },
233 "name": "fleeRate",
234 "sortText": "4fleeRate",
235 },
236 {
237 "kind": "var",
238 "kindModifiers": "declare",
239 "labelDetails": {
240 "detail": " PokemonDimension",
241 },
242 "name": "height",
243 "sortText": "5height",
244 },
245 {
246 "kind": "var",
247 "kindModifiers": "declare",
248 "labelDetails": {
249 "detail": " ID!",
250 },
251 "name": "id",
252 "sortText": "6id",
253 },
254 {
255 "kind": "var",
256 "kindModifiers": "declare",
257 "labelDetails": {
258 "description": "Maximum combat power a Pokémon may achieve at max level.",
259 "detail": " Int",
260 },
261 "name": "maxCP",
262 "sortText": "7maxCP",
263 },
264 {
265 "kind": "var",
266 "kindModifiers": "declare",
267 "labelDetails": {
268 "description": "Maximum health points a Pokémon may achieve at max level.",
269 "detail": " Int",
270 },
271 "name": "maxHP",
272 "sortText": "8maxHP",
273 },
274 {
275 "kind": "var",
276 "kindModifiers": "declare",
277 "labelDetails": {
278 "detail": " String!",
279 },
280 "name": "name",
281 "sortText": "9name",
282 },
283 {
284 "kind": "var",
285 "kindModifiers": "declare",
286 "labelDetails": {
287 "detail": " [PokemonType]",
288 },
289 "name": "resistant",
290 "sortText": "10resistant",
291 },
292 {
293 "kind": "var",
294 "kindModifiers": "declare",
295 "labelDetails": {
296 "detail": " [PokemonType]",
297 },
298 "name": "types",
299 "sortText": "11types",
300 },
301 {
302 "kind": "var",
303 "kindModifiers": "declare",
304 "labelDetails": {
305 "detail": " [PokemonType]",
306 },
307 "name": "weaknesses",
308 "sortText": "12weaknesses",
309 },
310 {
311 "kind": "var",
312 "kindModifiers": "declare",
313 "labelDetails": {
314 "detail": " PokemonDimension",
315 },
316 "name": "weight",
317 "sortText": "13weight",
318 },
319 {
320 "kind": "var",
321 "kindModifiers": "declare",
322 "labelDetails": {
323 "description": "The name of the current Object type at runtime.",
324 "detail": " String!",
325 },
326 "name": "__typename",
327 "sortText": "14__typename",
328 },
329 ]
330 `);
331 }, 30000);
332
333 it('gives semantic-diagnostics with unused fragments', async () => {
334 server.sendCommand('saveto', {
335 file: outfileUnusedFragment,
336 tmpfile: outfileUnusedFragment,
337 } satisfies ts.server.protocol.SavetoRequestArgs);
338
339 await server.waitForResponse(
340 e =>
341 e.type === 'event' &&
342 e.event === 'semanticDiag' &&
343 e.body?.file === outfileUnusedFragment
344 );
345
346 const res = server.responses.filter(
347 resp =>
348 resp.type === 'event' &&
349 resp.event === 'semanticDiag' &&
350 resp.body?.file === outfileUnusedFragment
351 );
352 expect(res[0].body.diagnostics).toMatchInlineSnapshot(`
353 [
354 {
355 "category": "warning",
356 "code": 52003,
357 "end": {
358 "line": 2,
359 "offset": 37,
360 },
361 "start": {
362 "line": 2,
363 "offset": 25,
364 },
365 "text": "Unused co-located fragment definition(s) \\"pokemonFields\\" in './fragment'",
366 },
367 ]
368 `);
369 }, 30000);
370
371 it('gives quick-info at start of word (#15)', async () => {
372 server.send({
373 seq: 11,
374 type: 'request',
375 command: 'quickinfo',
376 arguments: {
377 file: outfileCombinations,
378 line: 7,
379 offset: 5,
380 },
381 });
382
383 await server.waitForResponse(
384 response =>
385 response.type === 'response' && response.command === 'quickinfo'
386 );
387
388 const res = server.responses
389 .reverse()
390 .find(resp => resp.type === 'response' && resp.command === 'quickinfo');
391
392 expect(res).toBeDefined();
393 expect(typeof res?.body).toEqual('object');
394 expect(res?.body.documentation).toEqual(`Pokemon.name: String!`);
395 }, 30000);
396
397 it('gives suggestions with empty line (#190)', async () => {
398 server.send({
399 seq: 12,
400 type: 'request',
401 command: 'completionInfo',
402 arguments: {
403 file: outfileCombinations,
404 line: 19,
405 offset: 3,
406 includeExternalModuleExports: true,
407 includeInsertTextCompletions: true,
408 triggerKind: 1,
409 },
410 });
411
412 await server.waitForResponse(
413 response =>
414 response.type === 'response' && response.command === 'completionInfo'
415 );
416
417 const res = server.responses
418 .reverse()
419 .find(
420 resp => resp.type === 'response' && resp.command === 'completionInfo'
421 );
422
423 expect(res).toBeDefined();
424 expect(typeof res?.body.entries).toEqual('object');
425 expect(res?.body.entries).toMatchInlineSnapshot(`
426 [
427 {
428 "kind": "var",
429 "kindModifiers": "declare",
430 "labelDetails": {
431 "detail": " AttacksConnection",
432 },
433 "name": "attacks",
434 "sortText": "0attacks",
435 },
436 {
437 "kind": "var",
438 "kindModifiers": "declare",
439 "labelDetails": {
440 "detail": " [EvolutionRequirement]",
441 },
442 "name": "evolutionRequirements",
443 "sortText": "2evolutionRequirements",
444 },
445 {
446 "kind": "var",
447 "kindModifiers": "declare",
448 "labelDetails": {
449 "detail": " [Pokemon]",
450 },
451 "name": "evolutions",
452 "sortText": "3evolutions",
453 },
454 {
455 "kind": "var",
456 "kindModifiers": "declare",
457 "labelDetails": {
458 "description": "Likelihood of an attempt to catch a Pokémon to fail.",
459 "detail": " Float",
460 },
461 "name": "fleeRate",
462 "sortText": "4fleeRate",
463 },
464 {
465 "kind": "var",
466 "kindModifiers": "declare",
467 "labelDetails": {
468 "detail": " PokemonDimension",
469 },
470 "name": "height",
471 "sortText": "5height",
472 },
473 {
474 "kind": "var",
475 "kindModifiers": "declare",
476 "labelDetails": {
477 "detail": " ID!",
478 },
479 "name": "id",
480 "sortText": "6id",
481 },
482 {
483 "kind": "var",
484 "kindModifiers": "declare",
485 "labelDetails": {
486 "description": "Maximum combat power a Pokémon may achieve at max level.",
487 "detail": " Int",
488 },
489 "name": "maxCP",
490 "sortText": "7maxCP",
491 },
492 {
493 "kind": "var",
494 "kindModifiers": "declare",
495 "labelDetails": {
496 "description": "Maximum health points a Pokémon may achieve at max level.",
497 "detail": " Int",
498 },
499 "name": "maxHP",
500 "sortText": "8maxHP",
501 },
502 {
503 "kind": "var",
504 "kindModifiers": "declare",
505 "labelDetails": {
506 "detail": " String!",
507 },
508 "name": "name",
509 "sortText": "9name",
510 },
511 {
512 "kind": "var",
513 "kindModifiers": "declare",
514 "labelDetails": {
515 "detail": " [PokemonType]",
516 },
517 "name": "resistant",
518 "sortText": "10resistant",
519 },
520 {
521 "kind": "var",
522 "kindModifiers": "declare",
523 "labelDetails": {
524 "detail": " [PokemonType]",
525 },
526 "name": "types",
527 "sortText": "11types",
528 },
529 {
530 "kind": "var",
531 "kindModifiers": "declare",
532 "labelDetails": {
533 "detail": " [PokemonType]",
534 },
535 "name": "weaknesses",
536 "sortText": "12weaknesses",
537 },
538 {
539 "kind": "var",
540 "kindModifiers": "declare",
541 "labelDetails": {
542 "detail": " PokemonDimension",
543 },
544 "name": "weight",
545 "sortText": "13weight",
546 },
547 {
548 "kind": "var",
549 "kindModifiers": "declare",
550 "labelDetails": {
551 "description": "The name of the current Object type at runtime.",
552 "detail": " String!",
553 },
554 "name": "__typename",
555 "sortText": "14__typename",
556 },
557 ]
558 `);
559 }, 30000);
560});