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