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