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