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
483 it('parses objects', () => {
484 expect(parseValue('{}')).toEqual({
485 kind: Kind.OBJECT,
486 fields: [],
487 });
488
489 expect(() => parseValue('{name}')).toThrow();
490 expect(() => parseValue('{name:}')).toThrow();
491 expect(() => parseValue('{name:null')).toThrow();
492
493 expect(parseValue('{name:null}')).toEqual({
494 kind: Kind.OBJECT,
495 fields: [
496 {
497 kind: Kind.OBJECT_FIELD,
498 name: {
499 kind: Kind.NAME,
500 value: 'name',
501 },
502 value: {
503 kind: Kind.NULL,
504 },
505 },
506 ],
507 });
508 });
509
510 it('parses lists', () => {
511 expect(parseValue('[]')).toEqual({
512 kind: Kind.LIST,
513 values: [],
514 });
515
516 expect(() => parseValue('[')).toThrow();
517 expect(() => parseValue('[null')).toThrow();
518
519 expect(parseValue('[null]')).toEqual({
520 kind: Kind.LIST,
521 values: [
522 {
523 kind: Kind.NULL,
524 },
525 ],
526 });
527 });
528
529 it('parses block strings', () => {
530 expect(parseValue('["""long""" "short"]')).toEqual({
531 kind: Kind.LIST,
532 values: [
533 {
534 kind: Kind.STRING,
535 value: 'long',
536 block: true,
537 },
538 {
539 kind: Kind.STRING,
540 value: 'short',
541 block: false,
542 },
543 ],
544 });
545
546 expect(parseValue('"""\n\n first\n second\n"""')).toEqual({
547 kind: Kind.STRING,
548 value: 'first\nsecond',
549 block: true,
550 });
551 });
552
553 it('allows variables', () => {
554 const result = parseValue('{ field: $var }');
555 expect(result).toEqual({
556 kind: Kind.OBJECT,
557 fields: [
558 {
559 kind: Kind.OBJECT_FIELD,
560 name: {
561 kind: Kind.NAME,
562 value: 'field',
563 },
564 value: {
565 kind: Kind.VARIABLE,
566 name: {
567 kind: Kind.NAME,
568 value: 'var',
569 },
570 },
571 },
572 ],
573 });
574 });
575
576 it('correct message for incomplete variable', () => {
577 expect(() => {
578 return parseValue('$');
579 }).toThrow();
580 });
581
582 it('correct message for unexpected token', () => {
583 expect(() => {
584 return parseValue(':');
585 }).toThrow();
586 });
587});
588
589describe('parseType', () => {
590 it('parses basic types', () => {
591 expect(() => parseType('')).toThrow();
592 expect(() => parseType('Type')).not.toThrow();
593 expect(() => parseType({ body: 'Type' })).not.toThrow();
594 });
595
596 it('throws on invalid inputs', () => {
597 expect(() => parseType('!')).toThrow();
598 expect(() => parseType('[String')).toThrow();
599 expect(() => parseType('[String!')).toThrow();
600 });
601
602 it('parses well known types', () => {
603 const result = parseType('String');
604 expect(result).toEqual({
605 kind: Kind.NAMED_TYPE,
606 name: {
607 kind: Kind.NAME,
608 value: 'String',
609 },
610 });
611 });
612
613 it('parses custom types', () => {
614 const result = parseType('MyType');
615 expect(result).toEqual({
616 kind: Kind.NAMED_TYPE,
617 name: {
618 kind: Kind.NAME,
619 value: 'MyType',
620 },
621 });
622 });
623
624 it('parses list types', () => {
625 const result = parseType('[MyType]');
626 expect(result).toEqual({
627 kind: Kind.LIST_TYPE,
628 type: {
629 kind: Kind.NAMED_TYPE,
630 name: {
631 kind: Kind.NAME,
632 value: 'MyType',
633 },
634 },
635 });
636 });
637
638 it('parses non-null types', () => {
639 const result = parseType('MyType!');
640 expect(result).toEqual({
641 kind: Kind.NON_NULL_TYPE,
642 type: {
643 kind: Kind.NAMED_TYPE,
644 name: {
645 kind: Kind.NAME,
646 value: 'MyType',
647 },
648 },
649 });
650 });
651
652 it('parses nested types', () => {
653 const result = parseType('[MyType!]');
654 expect(result).toEqual({
655 kind: Kind.LIST_TYPE,
656 type: {
657 kind: Kind.NON_NULL_TYPE,
658 type: {
659 kind: Kind.NAMED_TYPE,
660 name: {
661 kind: Kind.NAME,
662 value: 'MyType',
663 },
664 },
665 },
666 });
667 });
668});