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