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 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});