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