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