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