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