Mirror: The spec-compliant minimum of client-side GraphQL.
1import { describe, it, expect } from 'vitest'; 2import { readFileSync } from 'fs'; 3 4import { parse as graphql_parse } from 'graphql'; 5import { parse, parseType, parseValue } from '../parser'; 6import { Kind } from '../kind'; 7 8describe('parse', () => { 9 it('parses the kitchen sink document like graphql.js does', () => { 10 const sink = readFileSync(__dirname + '/../../benchmark/kitchen_sink.graphql', { 11 encoding: 'utf8', 12 }); 13 const doc = parse(sink); 14 expect(doc).toMatchSnapshot(); 15 expect(doc).toEqual(graphql_parse(sink, { noLocation: true })); 16 }); 17 18 it('parses basic documents', () => { 19 expect(() => parse('{')).toThrow(); 20 expect(() => parse('{}x ')).toThrow(); 21 expect(() => parse('{ field }')).not.toThrow(); 22 expect(() => parse({ body: '{ field }' })).not.toThrow(); 23 }); 24 25 it('parses variable inline values', () => { 26 expect(() => { 27 return parse('{ field(complex: { a: { b: [ $var ] } }) }'); 28 }).not.toThrow(); 29 }); 30 31 it('parses constant default values', () => { 32 expect(() => { 33 return parse('query Foo($x: Complex = { a: { b: [ "test" ] } }) { field }'); 34 }).not.toThrow(); 35 expect(() => { 36 return parse('query Foo($x: Complex = { a: { b: [ $var ] } }) { field }'); 37 }).toThrow(); 38 }); 39 40 it('parses variable definition directives', () => { 41 expect(() => { 42 return parse('query Foo($x: Boolean = false @bar) { field }'); 43 }).not.toThrow(); 44 }); 45 46 it('does not accept fragments spread of "on"', () => { 47 expect(() => { 48 return parse('{ ...on }'); 49 }).toThrow(); 50 }); 51 52 it('parses multi-byte characters', () => { 53 // Note: \u0A0A could be naively interpreted as two line-feed chars. 54 const ast = parse(` 55 # This comment has a \u0A0A multi-byte character. 56 { field(arg: "Has a \u0A0A multi-byte character.") } 57 `); 58 59 expect(ast).toHaveProperty( 60 'definitions.0.selectionSet.selections.0.arguments.0.value.value', 61 'Has a \u0A0A multi-byte character.' 62 ); 63 }); 64 65 it('parses anonymous mutation operations', () => { 66 expect(() => { 67 return parse(` 68 mutation { 69 mutationField 70 } 71 `); 72 }).not.toThrow(); 73 }); 74 75 it('parses anonymous subscription operations', () => { 76 expect(() => { 77 return parse(` 78 subscription { 79 subscriptionField 80 } 81 `); 82 }).not.toThrow(); 83 }); 84 85 it('parses named mutation operations', () => { 86 expect(() => { 87 return parse(` 88 mutation Foo { 89 mutationField 90 } 91 `); 92 }).not.toThrow(); 93 }); 94 95 it('parses named subscription operations', () => { 96 expect(() => { 97 return parse(` 98 subscription Foo { 99 subscriptionField 100 } 101 `); 102 }).not.toThrow(); 103 }); 104 105 it('parses fragment definitions', () => { 106 expect(() => parse('fragment { test }')).toThrow(); 107 expect(() => parse('fragment name { test }')).toThrow(); 108 expect(() => parse('fragment name on name')).toThrow(); 109 expect(() => parse('fragment Name on Type { field }')).not.toThrow(); 110 }); 111 112 it('parses fields', () => { 113 expect(() => parse('{ field: }')).toThrow(); 114 expect(() => parse('{ alias: field() }')).toThrow(); 115 116 expect(parse('{ alias: field { child } }').definitions[0]).toHaveProperty( 117 'selectionSet.selections.0', 118 { 119 kind: Kind.FIELD, 120 directives: [], 121 arguments: [], 122 alias: { 123 kind: Kind.NAME, 124 value: 'alias', 125 }, 126 name: { 127 kind: Kind.NAME, 128 value: 'field', 129 }, 130 selectionSet: { 131 kind: Kind.SELECTION_SET, 132 selections: [ 133 { 134 kind: Kind.FIELD, 135 directives: [], 136 arguments: [], 137 name: { 138 kind: Kind.NAME, 139 value: 'child', 140 }, 141 }, 142 ], 143 }, 144 } 145 ); 146 }); 147 148 it('parses arguments', () => { 149 expect(() => parse('{ field() }')).toThrow(); 150 expect(() => parse('{ field(name) }')).toThrow(); 151 expect(() => parse('{ field(name:) }')).toThrow(); 152 expect(() => parse('{ field(name: null }')).toThrow(); 153 154 expect(parse('{ field(name: null) }').definitions[0]).toMatchObject({ 155 kind: Kind.OPERATION_DEFINITION, 156 selectionSet: { 157 kind: Kind.SELECTION_SET, 158 selections: [ 159 { 160 kind: Kind.FIELD, 161 name: { 162 kind: Kind.NAME, 163 value: 'field', 164 }, 165 arguments: [ 166 { 167 kind: Kind.ARGUMENT, 168 name: { 169 kind: Kind.NAME, 170 value: 'name', 171 }, 172 value: { 173 kind: Kind.NULL, 174 }, 175 }, 176 ], 177 }, 178 ], 179 }, 180 }); 181 }); 182 183 it('parses directives', () => { 184 expect(() => parse('{ field @ }')).toThrow(); 185 expect(() => parse('{ field @(test: null) }')).toThrow(); 186 187 expect(parse('{ field @test(name: null) }')).toHaveProperty( 188 'definitions.0.selectionSet.selections.0.directives.0', 189 { 190 kind: Kind.DIRECTIVE, 191 name: { 192 kind: Kind.NAME, 193 value: 'test', 194 }, 195 arguments: [ 196 { 197 kind: Kind.ARGUMENT, 198 name: { 199 kind: Kind.NAME, 200 value: 'name', 201 }, 202 value: { 203 kind: Kind.NULL, 204 }, 205 }, 206 ], 207 } 208 ); 209 }); 210 211 it('parses inline fragments', () => { 212 expect(() => parse('{ ... on Test }')).toThrow(); 213 expect(() => parse('{ ... {} }')).toThrow(); 214 expect(() => parse('{ ... }')).toThrow(); 215 216 expect(parse('{ ... on Test { field } }')).toHaveProperty( 217 'definitions.0.selectionSet.selections.0', 218 { 219 kind: Kind.INLINE_FRAGMENT, 220 directives: [], 221 typeCondition: { 222 kind: Kind.NAMED_TYPE, 223 name: { 224 kind: Kind.NAME, 225 value: 'Test', 226 }, 227 }, 228 selectionSet: expect.any(Object), 229 } 230 ); 231 232 expect(parse('{ ... { field } }')).toHaveProperty('definitions.0.selectionSet.selections.0', { 233 kind: Kind.INLINE_FRAGMENT, 234 directives: [], 235 typeCondition: undefined, 236 selectionSet: expect.any(Object), 237 }); 238 }); 239 240 it('parses variable definitions', () => { 241 expect(() => parse('query ( { test }')).toThrow(); 242 expect(() => parse('query ($var) { test }')).toThrow(); 243 expect(() => parse('query ($var:) { test }')).toThrow(); 244 expect(() => parse('query ($var: Int =) { test }')).toThrow(); 245 246 expect(parse('query ($var: Int = 1) { test }').definitions[0]).toMatchObject({ 247 kind: Kind.OPERATION_DEFINITION, 248 operation: 'query', 249 directives: [], 250 selectionSet: expect.any(Object), 251 variableDefinitions: [ 252 { 253 kind: Kind.VARIABLE_DEFINITION, 254 type: { 255 kind: Kind.NAMED_TYPE, 256 name: { 257 kind: Kind.NAME, 258 value: 'Int', 259 }, 260 }, 261 variable: { 262 kind: Kind.VARIABLE, 263 name: { 264 kind: Kind.NAME, 265 value: 'var', 266 }, 267 }, 268 defaultValue: { 269 kind: Kind.INT, 270 value: '1', 271 }, 272 }, 273 ], 274 }); 275 }); 276 277 it('creates ast', () => { 278 const result = parse(` 279 { 280 node(id: 4) { 281 id, 282 name 283 } 284 } 285 `); 286 287 expect(result).toMatchObject({ 288 kind: Kind.DOCUMENT, 289 definitions: [ 290 { 291 kind: Kind.OPERATION_DEFINITION, 292 operation: 'query', 293 name: undefined, 294 variableDefinitions: [], 295 directives: [], 296 selectionSet: { 297 kind: Kind.SELECTION_SET, 298 selections: [ 299 { 300 kind: Kind.FIELD, 301 alias: undefined, 302 name: { 303 kind: Kind.NAME, 304 value: 'node', 305 }, 306 arguments: [ 307 { 308 kind: Kind.ARGUMENT, 309 name: { 310 kind: Kind.NAME, 311 value: 'id', 312 }, 313 value: { 314 kind: Kind.INT, 315 value: '4', 316 }, 317 }, 318 ], 319 directives: [], 320 selectionSet: { 321 kind: Kind.SELECTION_SET, 322 selections: [ 323 { 324 kind: Kind.FIELD, 325 alias: undefined, 326 name: { 327 kind: Kind.NAME, 328 value: 'id', 329 }, 330 arguments: [], 331 directives: [], 332 selectionSet: undefined, 333 }, 334 { 335 kind: Kind.FIELD, 336 alias: undefined, 337 name: { 338 kind: Kind.NAME, 339 value: 'name', 340 }, 341 arguments: [], 342 directives: [], 343 selectionSet: undefined, 344 }, 345 ], 346 }, 347 }, 348 ], 349 }, 350 }, 351 ], 352 }); 353 }); 354 355 it('creates ast from nameless query without variables', () => { 356 const result = parse(` 357 query { 358 node { 359 id 360 } 361 } 362 `); 363 364 expect(result).toMatchObject({ 365 kind: Kind.DOCUMENT, 366 definitions: [ 367 { 368 kind: Kind.OPERATION_DEFINITION, 369 operation: 'query', 370 name: undefined, 371 variableDefinitions: [], 372 directives: [], 373 selectionSet: { 374 kind: Kind.SELECTION_SET, 375 selections: [ 376 { 377 kind: Kind.FIELD, 378 alias: undefined, 379 name: { 380 kind: Kind.NAME, 381 value: 'node', 382 }, 383 arguments: [], 384 directives: [], 385 selectionSet: { 386 kind: Kind.SELECTION_SET, 387 selections: [ 388 { 389 kind: Kind.FIELD, 390 alias: undefined, 391 name: { 392 kind: Kind.NAME, 393 value: 'id', 394 }, 395 arguments: [], 396 directives: [], 397 selectionSet: undefined, 398 }, 399 ], 400 }, 401 }, 402 ], 403 }, 404 }, 405 ], 406 }); 407 }); 408 409 it('allows parsing without source location information', () => { 410 const result = parse('{ id }', { noLocation: true }); 411 expect('loc' in result).toBe(false); 412 }); 413}); 414 415describe('parseValue', () => { 416 it('parses basic values', () => { 417 expect(() => parseValue('')).toThrow(); 418 expect(parseValue('null')).toEqual({ kind: Kind.NULL }); 419 expect(parseValue({ body: 'null' })).toEqual({ kind: Kind.NULL }); 420 }); 421 422 it('parses list values', () => { 423 const result = parseValue('[123 "abc"]'); 424 expect(result).toEqual({ 425 kind: Kind.LIST, 426 values: [ 427 { 428 kind: Kind.INT, 429 value: '123', 430 }, 431 { 432 kind: Kind.STRING, 433 value: 'abc', 434 block: false, 435 }, 436 ], 437 }); 438 }); 439 440 it('parses integers', () => { 441 expect(parseValue('12')).toEqual({ 442 kind: Kind.INT, 443 value: '12', 444 }); 445 446 expect(parseValue('-12')).toEqual({ 447 kind: Kind.INT, 448 value: '-12', 449 }); 450 }); 451 452 it('parses floats', () => { 453 expect(parseValue('12e2')).toEqual({ 454 kind: Kind.FLOAT, 455 value: '12e2', 456 }); 457 458 expect(parseValue('0.2E3')).toEqual({ 459 kind: Kind.FLOAT, 460 value: '0.2E3', 461 }); 462 463 expect(parseValue('-1.2e+3')).toEqual({ 464 kind: Kind.FLOAT, 465 value: '-1.2e+3', 466 }); 467 }); 468 469 it('parses strings', () => { 470 expect(parseValue('"test"')).toEqual({ 471 kind: Kind.STRING, 472 value: 'test', 473 block: false, 474 }); 475 476 expect(parseValue('"\\t\\t"')).toEqual({ 477 kind: Kind.STRING, 478 value: '\t\t', 479 block: false, 480 }); 481 }); 482 483 it('parses objects', () => { 484 expect(parseValue('{}')).toEqual({ 485 kind: Kind.OBJECT, 486 fields: [], 487 }); 488 489 expect(() => parseValue('{name}')).toThrow(); 490 expect(() => parseValue('{name:}')).toThrow(); 491 expect(() => parseValue('{name:null')).toThrow(); 492 493 expect(parseValue('{name:null}')).toEqual({ 494 kind: Kind.OBJECT, 495 fields: [ 496 { 497 kind: Kind.OBJECT_FIELD, 498 name: { 499 kind: Kind.NAME, 500 value: 'name', 501 }, 502 value: { 503 kind: Kind.NULL, 504 }, 505 }, 506 ], 507 }); 508 }); 509 510 it('parses lists', () => { 511 expect(parseValue('[]')).toEqual({ 512 kind: Kind.LIST, 513 values: [], 514 }); 515 516 expect(() => parseValue('[')).toThrow(); 517 expect(() => parseValue('[null')).toThrow(); 518 519 expect(parseValue('[null]')).toEqual({ 520 kind: Kind.LIST, 521 values: [ 522 { 523 kind: Kind.NULL, 524 }, 525 ], 526 }); 527 }); 528 529 it('parses block strings', () => { 530 expect(parseValue('["""long""" "short"]')).toEqual({ 531 kind: Kind.LIST, 532 values: [ 533 { 534 kind: Kind.STRING, 535 value: 'long', 536 block: true, 537 }, 538 { 539 kind: Kind.STRING, 540 value: 'short', 541 block: false, 542 }, 543 ], 544 }); 545 546 expect(parseValue('"""\n\n first\n second\n"""')).toEqual({ 547 kind: Kind.STRING, 548 value: 'first\nsecond', 549 block: true, 550 }); 551 }); 552 553 it('allows variables', () => { 554 const result = parseValue('{ field: $var }'); 555 expect(result).toEqual({ 556 kind: Kind.OBJECT, 557 fields: [ 558 { 559 kind: Kind.OBJECT_FIELD, 560 name: { 561 kind: Kind.NAME, 562 value: 'field', 563 }, 564 value: { 565 kind: Kind.VARIABLE, 566 name: { 567 kind: Kind.NAME, 568 value: 'var', 569 }, 570 }, 571 }, 572 ], 573 }); 574 }); 575 576 it('correct message for incomplete variable', () => { 577 expect(() => { 578 return parseValue('$'); 579 }).toThrow(); 580 }); 581 582 it('correct message for unexpected token', () => { 583 expect(() => { 584 return parseValue(':'); 585 }).toThrow(); 586 }); 587}); 588 589describe('parseType', () => { 590 it('parses basic types', () => { 591 expect(() => parseType('')).toThrow(); 592 expect(() => parseType('Type')).not.toThrow(); 593 expect(() => parseType({ body: 'Type' })).not.toThrow(); 594 }); 595 596 it('throws on invalid inputs', () => { 597 expect(() => parseType('!')).toThrow(); 598 expect(() => parseType('[String')).toThrow(); 599 expect(() => parseType('[String!')).toThrow(); 600 }); 601 602 it('parses well known types', () => { 603 const result = parseType('String'); 604 expect(result).toEqual({ 605 kind: Kind.NAMED_TYPE, 606 name: { 607 kind: Kind.NAME, 608 value: 'String', 609 }, 610 }); 611 }); 612 613 it('parses custom types', () => { 614 const result = parseType('MyType'); 615 expect(result).toEqual({ 616 kind: Kind.NAMED_TYPE, 617 name: { 618 kind: Kind.NAME, 619 value: 'MyType', 620 }, 621 }); 622 }); 623 624 it('parses list types', () => { 625 const result = parseType('[MyType]'); 626 expect(result).toEqual({ 627 kind: Kind.LIST_TYPE, 628 type: { 629 kind: Kind.NAMED_TYPE, 630 name: { 631 kind: Kind.NAME, 632 value: 'MyType', 633 }, 634 }, 635 }); 636 }); 637 638 it('parses non-null types', () => { 639 const result = parseType('MyType!'); 640 expect(result).toEqual({ 641 kind: Kind.NON_NULL_TYPE, 642 type: { 643 kind: Kind.NAMED_TYPE, 644 name: { 645 kind: Kind.NAME, 646 value: 'MyType', 647 }, 648 }, 649 }); 650 }); 651 652 it('parses nested types', () => { 653 const result = parseType('[MyType!]'); 654 expect(result).toEqual({ 655 kind: Kind.LIST_TYPE, 656 type: { 657 kind: Kind.NON_NULL_TYPE, 658 type: { 659 kind: Kind.NAMED_TYPE, 660 name: { 661 kind: Kind.NAME, 662 value: 'MyType', 663 }, 664 }, 665 }, 666 }); 667 }); 668});