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