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('{ alias: 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});