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