Mirror: The spec-compliant minimum of client-side GraphQL.
1import { describe, it, expect } from 'vitest';
2import { readFileSync } from 'fs';
3
4import { parse as graphql_parse } from 'graphql';
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 sink = readFileSync(__dirname + '/../../benchmark/kitchen_sink.graphql', {
11 encoding: 'utf8',
12 });
13 const doc = parse(sink);
14 expect(doc).toMatchSnapshot();
15 expect(doc).toEqual(graphql_parse(sink, { noLocation: true }));
16 });
17
18 it('parses basic documents', () => {
19 expect(() => parse('{')).toThrow();
20 expect(() => parse('{}x ')).toThrow();
21 expect(() => parse('{ field }')).not.toThrow();
22 expect(() => parse({ body: '{ field }' })).not.toThrow();
23 });
24
25 it('parses variable inline values', () => {
26 expect(() => {
27 return parse('{ field(complex: { a: { b: [ $var ] } }) }');
28 }).not.toThrow();
29 });
30
31 it('parses constant default values', () => {
32 expect(() => {
33 return parse('query Foo($x: Complex = { a: { b: [ "test" ] } }) { field }');
34 }).not.toThrow();
35 expect(() => {
36 return parse('query Foo($x: Complex = { a: { b: [ $var ] } }) { field }');
37 }).toThrow();
38 });
39
40 it('parses variable definition directives', () => {
41 expect(() => {
42 return parse('query Foo($x: Boolean = false @bar) { field }');
43 }).not.toThrow();
44 });
45
46 it('does not accept fragments spread of "on"', () => {
47 expect(() => {
48 return parse('{ ...on }');
49 }).toThrow();
50 });
51
52 it('parses multi-byte characters', () => {
53 // Note: \u0A0A could be naively interpreted as two line-feed chars.
54 const ast = parse(`
55 # This comment has a \u0A0A multi-byte character.
56 { field(arg: "Has a \u0A0A multi-byte character.") }
57 `);
58
59 expect(ast).toHaveProperty(
60 'definitions.0.selectionSet.selections.0.arguments.0.value.value',
61 'Has a \u0A0A multi-byte character.'
62 );
63 });
64
65 it('parses anonymous mutation operations', () => {
66 expect(() => {
67 return parse(`
68 mutation {
69 mutationField
70 }
71 `);
72 }).not.toThrow();
73 });
74
75 it('parses anonymous subscription operations', () => {
76 expect(() => {
77 return parse(`
78 subscription {
79 subscriptionField
80 }
81 `);
82 }).not.toThrow();
83 });
84
85 it('parses named mutation operations', () => {
86 expect(() => {
87 return parse(`
88 mutation Foo {
89 mutationField
90 }
91 `);
92 }).not.toThrow();
93 });
94
95 it('parses named subscription operations', () => {
96 expect(() => {
97 return parse(`
98 subscription Foo {
99 subscriptionField
100 }
101 `);
102 }).not.toThrow();
103 });
104
105 it('parses fragment definitions', () => {
106 expect(() => parse('fragment { test }')).toThrow();
107 expect(() => parse('fragment name { test }')).toThrow();
108 expect(() => parse('fragment name on name')).toThrow();
109 expect(() => parse('fragment Name on Type { field }')).not.toThrow();
110 });
111
112 it('parses fields', () => {
113 expect(() => parse('{ field: }')).toThrow();
114 expect(() => parse('{ alias: field() }')).toThrow();
115
116 expect(parse('{ alias: field { child } }').definitions[0]).toHaveProperty(
117 'selectionSet.selections.0',
118 {
119 kind: Kind.FIELD,
120 directives: [],
121 arguments: [],
122 alias: {
123 kind: Kind.NAME,
124 value: 'alias',
125 },
126 name: {
127 kind: Kind.NAME,
128 value: 'field',
129 },
130 selectionSet: {
131 kind: Kind.SELECTION_SET,
132 selections: [
133 {
134 kind: Kind.FIELD,
135 directives: [],
136 arguments: [],
137 name: {
138 kind: Kind.NAME,
139 value: 'child',
140 },
141 },
142 ],
143 },
144 }
145 );
146 });
147
148 it('parses arguments', () => {
149 expect(() => parse('{ field() }')).toThrow();
150 expect(() => parse('{ field(name) }')).toThrow();
151 expect(() => parse('{ field(name:) }')).toThrow();
152 expect(() => parse('{ field(name: null }')).toThrow();
153
154 expect(parse('{ field(name: null) }').definitions[0]).toMatchObject({
155 kind: Kind.OPERATION_DEFINITION,
156 selectionSet: {
157 kind: Kind.SELECTION_SET,
158 selections: [
159 {
160 kind: Kind.FIELD,
161 name: {
162 kind: Kind.NAME,
163 value: 'field',
164 },
165 arguments: [
166 {
167 kind: Kind.ARGUMENT,
168 name: {
169 kind: Kind.NAME,
170 value: 'name',
171 },
172 value: {
173 kind: Kind.NULL,
174 },
175 },
176 ],
177 },
178 ],
179 },
180 });
181 });
182
183 it('parses directives', () => {
184 expect(() => parse('{ field @ }')).toThrow();
185 expect(() => parse('{ field @(test: null) }')).toThrow();
186
187 expect(parse('{ field @test(name: null) }')).toHaveProperty(
188 'definitions.0.selectionSet.selections.0.directives.0',
189 {
190 kind: Kind.DIRECTIVE,
191 name: {
192 kind: Kind.NAME,
193 value: 'test',
194 },
195 arguments: [
196 {
197 kind: Kind.ARGUMENT,
198 name: {
199 kind: Kind.NAME,
200 value: 'name',
201 },
202 value: {
203 kind: Kind.NULL,
204 },
205 },
206 ],
207 }
208 );
209 });
210
211 it('parses inline fragments', () => {
212 expect(() => parse('{ ... on Test }')).toThrow();
213 expect(() => parse('{ ... {} }')).toThrow();
214 expect(() => parse('{ ... }')).toThrow();
215
216 expect(parse('{ ... on Test { field } }')).toHaveProperty(
217 'definitions.0.selectionSet.selections.0',
218 {
219 kind: Kind.INLINE_FRAGMENT,
220 directives: [],
221 typeCondition: {
222 kind: Kind.NAMED_TYPE,
223 name: {
224 kind: Kind.NAME,
225 value: 'Test',
226 },
227 },
228 selectionSet: expect.any(Object),
229 }
230 );
231
232 expect(parse('{ ... { field } }')).toHaveProperty('definitions.0.selectionSet.selections.0', {
233 kind: Kind.INLINE_FRAGMENT,
234 directives: [],
235 typeCondition: undefined,
236 selectionSet: expect.any(Object),
237 });
238 });
239
240 it('parses variable definitions', () => {
241 expect(() => parse('query ( { test }')).toThrow();
242 expect(() => parse('query ($var) { test }')).toThrow();
243 expect(() => parse('query ($var:) { test }')).toThrow();
244 expect(() => parse('query ($var: Int =) { test }')).toThrow();
245
246 expect(parse('query ($var: Int = 1) { test }').definitions[0]).toMatchObject({
247 kind: Kind.OPERATION_DEFINITION,
248 operation: 'query',
249 directives: [],
250 selectionSet: expect.any(Object),
251 variableDefinitions: [
252 {
253 kind: Kind.VARIABLE_DEFINITION,
254 type: {
255 kind: Kind.NAMED_TYPE,
256 name: {
257 kind: Kind.NAME,
258 value: 'Int',
259 },
260 },
261 variable: {
262 kind: Kind.VARIABLE,
263 name: {
264 kind: Kind.NAME,
265 value: 'var',
266 },
267 },
268 defaultValue: {
269 kind: Kind.INT,
270 value: '1',
271 },
272 },
273 ],
274 });
275 });
276
277 it('creates ast', () => {
278 const result = parse(`
279 {
280 node(id: 4) {
281 id,
282 name
283 }
284 }
285 `);
286
287 expect(result).toMatchObject({
288 kind: Kind.DOCUMENT,
289 definitions: [
290 {
291 kind: Kind.OPERATION_DEFINITION,
292 operation: 'query',
293 name: undefined,
294 variableDefinitions: [],
295 directives: [],
296 selectionSet: {
297 kind: Kind.SELECTION_SET,
298 selections: [
299 {
300 kind: Kind.FIELD,
301 alias: undefined,
302 name: {
303 kind: Kind.NAME,
304 value: 'node',
305 },
306 arguments: [
307 {
308 kind: Kind.ARGUMENT,
309 name: {
310 kind: Kind.NAME,
311 value: 'id',
312 },
313 value: {
314 kind: Kind.INT,
315 value: '4',
316 },
317 },
318 ],
319 directives: [],
320 selectionSet: {
321 kind: Kind.SELECTION_SET,
322 selections: [
323 {
324 kind: Kind.FIELD,
325 alias: undefined,
326 name: {
327 kind: Kind.NAME,
328 value: 'id',
329 },
330 arguments: [],
331 directives: [],
332 selectionSet: undefined,
333 },
334 {
335 kind: Kind.FIELD,
336 alias: undefined,
337 name: {
338 kind: Kind.NAME,
339 value: 'name',
340 },
341 arguments: [],
342 directives: [],
343 selectionSet: undefined,
344 },
345 ],
346 },
347 },
348 ],
349 },
350 },
351 ],
352 });
353 });
354
355 it('creates ast from nameless query without variables', () => {
356 const result = parse(`
357 query {
358 node {
359 id
360 }
361 }
362 `);
363
364 expect(result).toMatchObject({
365 kind: Kind.DOCUMENT,
366 definitions: [
367 {
368 kind: Kind.OPERATION_DEFINITION,
369 operation: 'query',
370 name: undefined,
371 variableDefinitions: [],
372 directives: [],
373 selectionSet: {
374 kind: Kind.SELECTION_SET,
375 selections: [
376 {
377 kind: Kind.FIELD,
378 alias: undefined,
379 name: {
380 kind: Kind.NAME,
381 value: 'node',
382 },
383 arguments: [],
384 directives: [],
385 selectionSet: {
386 kind: Kind.SELECTION_SET,
387 selections: [
388 {
389 kind: Kind.FIELD,
390 alias: undefined,
391 name: {
392 kind: Kind.NAME,
393 value: 'id',
394 },
395 arguments: [],
396 directives: [],
397 selectionSet: undefined,
398 },
399 ],
400 },
401 },
402 ],
403 },
404 },
405 ],
406 });
407 });
408
409 it('allows parsing without source location information', () => {
410 const result = parse('{ id }', { noLocation: true });
411 expect('loc' in result).toBe(false);
412 });
413});
414
415describe('parseValue', () => {
416 it('parses basic values', () => {
417 expect(() => parseValue('')).toThrow();
418 expect(parseValue('null')).toEqual({ kind: Kind.NULL });
419 expect(parseValue({ body: 'null' })).toEqual({ kind: Kind.NULL });
420 });
421
422 it('parses list values', () => {
423 const result = parseValue('[123 "abc"]');
424 expect(result).toEqual({
425 kind: Kind.LIST,
426 values: [
427 {
428 kind: Kind.INT,
429 value: '123',
430 },
431 {
432 kind: Kind.STRING,
433 value: 'abc',
434 block: false,
435 },
436 ],
437 });
438 });
439
440 it('parses integers', () => {
441 expect(parseValue('12')).toEqual({
442 kind: Kind.INT,
443 value: '12',
444 });
445
446 expect(parseValue('-12')).toEqual({
447 kind: Kind.INT,
448 value: '-12',
449 });
450 });
451
452 it('parses floats', () => {
453 expect(parseValue('12e2')).toEqual({
454 kind: Kind.FLOAT,
455 value: '12e2',
456 });
457
458 expect(parseValue('0.2E3')).toEqual({
459 kind: Kind.FLOAT,
460 value: '0.2E3',
461 });
462
463 expect(parseValue('-1.2e+3')).toEqual({
464 kind: Kind.FLOAT,
465 value: '-1.2e+3',
466 });
467 });
468
469 it('parses strings', () => {
470 expect(parseValue('"test"')).toEqual({
471 kind: Kind.STRING,
472 value: 'test',
473 block: false,
474 });
475
476 expect(parseValue('"\\t\\t"')).toEqual({
477 kind: Kind.STRING,
478 value: '\t\t',
479 block: false,
480 });
481
482 expect(parseValue('" \\" "')).toEqual({
483 kind: Kind.STRING,
484 value: ' " ',
485 block: false,
486 });
487
488 expect(parseValue('"x" "x"')).toEqual({
489 kind: Kind.STRING,
490 value: 'x',
491 block: false,
492 });
493 });
494
495 it('parses objects', () => {
496 expect(parseValue('{}')).toEqual({
497 kind: Kind.OBJECT,
498 fields: [],
499 });
500
501 expect(() => parseValue('{name}')).toThrow();
502 expect(() => parseValue('{name:}')).toThrow();
503 expect(() => parseValue('{name:null')).toThrow();
504
505 expect(parseValue('{name:null}')).toEqual({
506 kind: Kind.OBJECT,
507 fields: [
508 {
509 kind: Kind.OBJECT_FIELD,
510 name: {
511 kind: Kind.NAME,
512 value: 'name',
513 },
514 value: {
515 kind: Kind.NULL,
516 },
517 },
518 ],
519 });
520 });
521
522 it('parses lists', () => {
523 expect(parseValue('[]')).toEqual({
524 kind: Kind.LIST,
525 values: [],
526 });
527
528 expect(() => parseValue('[')).toThrow();
529 expect(() => parseValue('[null')).toThrow();
530
531 expect(parseValue('[null]')).toEqual({
532 kind: Kind.LIST,
533 values: [
534 {
535 kind: Kind.NULL,
536 },
537 ],
538 });
539 });
540
541 it('parses block strings', () => {
542 expect(parseValue('["""long""" "short"]')).toEqual({
543 kind: Kind.LIST,
544 values: [
545 {
546 kind: Kind.STRING,
547 value: 'long',
548 block: true,
549 },
550 {
551 kind: Kind.STRING,
552 value: 'short',
553 block: false,
554 },
555 ],
556 });
557
558 expect(parseValue('"""\n\n first\n second\n"""')).toEqual({
559 kind: Kind.STRING,
560 value: 'first\nsecond',
561 block: true,
562 });
563
564 expect(parseValue('""" \\""" """')).toEqual({
565 kind: Kind.STRING,
566 value: ' """ ',
567 block: true,
568 });
569
570 expect(parseValue('"""x""" """x"""')).toEqual({
571 kind: Kind.STRING,
572 value: 'x',
573 block: true,
574 });
575 });
576
577 it('allows variables', () => {
578 const result = parseValue('{ field: $var }');
579 expect(result).toEqual({
580 kind: Kind.OBJECT,
581 fields: [
582 {
583 kind: Kind.OBJECT_FIELD,
584 name: {
585 kind: Kind.NAME,
586 value: 'field',
587 },
588 value: {
589 kind: Kind.VARIABLE,
590 name: {
591 kind: Kind.NAME,
592 value: 'var',
593 },
594 },
595 },
596 ],
597 });
598 });
599
600 it('correct message for incomplete variable', () => {
601 expect(() => {
602 return parseValue('$');
603 }).toThrow();
604 });
605
606 it('correct message for unexpected token', () => {
607 expect(() => {
608 return parseValue(':');
609 }).toThrow();
610 });
611});
612
613describe('parseType', () => {
614 it('parses basic types', () => {
615 expect(() => parseType('')).toThrow();
616 expect(() => parseType('Type')).not.toThrow();
617 expect(() => parseType({ body: 'Type' })).not.toThrow();
618 });
619
620 it('throws on invalid inputs', () => {
621 expect(() => parseType('!')).toThrow();
622 expect(() => parseType('[String')).toThrow();
623 expect(() => parseType('[String!')).toThrow();
624 });
625
626 it('parses well known types', () => {
627 const result = parseType('String');
628 expect(result).toEqual({
629 kind: Kind.NAMED_TYPE,
630 name: {
631 kind: Kind.NAME,
632 value: 'String',
633 },
634 });
635 });
636
637 it('parses custom types', () => {
638 const result = parseType('MyType');
639 expect(result).toEqual({
640 kind: Kind.NAMED_TYPE,
641 name: {
642 kind: Kind.NAME,
643 value: 'MyType',
644 },
645 });
646 });
647
648 it('parses list types', () => {
649 const result = parseType('[MyType]');
650 expect(result).toEqual({
651 kind: Kind.LIST_TYPE,
652 type: {
653 kind: Kind.NAMED_TYPE,
654 name: {
655 kind: Kind.NAME,
656 value: 'MyType',
657 },
658 },
659 });
660 });
661
662 it('parses non-null types', () => {
663 const result = parseType('MyType!');
664 expect(result).toEqual({
665 kind: Kind.NON_NULL_TYPE,
666 type: {
667 kind: Kind.NAMED_TYPE,
668 name: {
669 kind: Kind.NAME,
670 value: 'MyType',
671 },
672 },
673 });
674 });
675
676 it('parses nested types', () => {
677 const result = parseType('[MyType!]');
678 expect(result).toEqual({
679 kind: Kind.LIST_TYPE,
680 type: {
681 kind: Kind.NON_NULL_TYPE,
682 type: {
683 kind: Kind.NAMED_TYPE,
684 name: {
685 kind: Kind.NAME,
686 value: 'MyType',
687 },
688 },
689 },
690 });
691 });
692});