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