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