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});