Mirror: TypeScript LSP plugin that finds GraphQL documents in your code and provides diagnostics, auto-complete and hover-information.
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});