Mirror: The small sibling of the graphql package, slimmed down for client-side libraries.
1// See: https://github.com/graphql/graphql-js/blob/976d64b/src/language/__tests__/parser-test.ts
2// Note: Tests regarding reserved keywords have been removed.
3
4import { describe, it, expect } from 'vitest';
5import { Kind } from 'graphql';
6import { parse, parseValue, parseType } from '../parser';
7
8describe('Parser', () => {
9 it('parse provides errors', () => {
10 expect(() => parse('{')).toThrow();
11 });
12
13 it('parses variable inline values', () => {
14 expect(() => {
15 return parse('{ field(complex: { a: { b: [ $var ] } }) }');
16 }).not.toThrow();
17 });
18
19 it('parses constant default values', () => {
20 expect(() => {
21 return parse('query Foo($x: Complex = { a: { b: [ $var ] } }) { field }');
22 }).not.toThrow();
23 });
24
25 it('parses variable definition directives', () => {
26 expect(() => {
27 return parse('query Foo($x: Boolean = false @bar) { field }');
28 }).not.toThrow();
29 });
30
31 it('does not accept fragments spread of "on"', () => {
32 expect(() => {
33 return parse('{ ...on }');
34 }).toThrow();
35 });
36
37 it('parses multi-byte characters', () => {
38 // Note: \u0A0A could be naively interpreted as two line-feed chars.
39 const ast = parse(`
40 # This comment has a \u0A0A multi-byte character.
41 { field(arg: "Has a \u0A0A multi-byte character.") }
42 `);
43
44 expect(ast).toHaveProperty(
45 'definitions.0.selectionSet.selections.0.arguments.0.value.value',
46 'Has a \u0A0A multi-byte character.'
47 );
48 });
49
50 it('parses kitchen sink', () => {
51 let query;
52 expect(() => {
53 return (query = parse(kitchenSinkQuery));
54 }).not.toThrow();
55
56 expect(query.definitions.length).toBe(6);
57 });
58
59 it('parses anonymous mutation operations', () => {
60 expect(() => {
61 return parse(`
62 mutation {
63 mutationField
64 }
65 `);
66 }).not.toThrow();
67 });
68
69 it('parses anonymous subscription operations', () => {
70 expect(() => {
71 return parse(`
72 subscription {
73 subscriptionField
74 }
75 `);
76 }).not.toThrow();
77 });
78
79 it('parses named mutation operations', () => {
80 expect(() => {
81 return parse(`
82 mutation Foo {
83 mutationField
84 }
85 `);
86 }).not.toThrow();
87 });
88
89 it('parses named subscription operations', () => {
90 expect(() => {
91 return parse(`
92 subscription Foo {
93 subscriptionField
94 }
95 `);
96 }).not.toThrow();
97 });
98
99 it('creates ast', () => {
100 const result = parse(`
101 {
102 node(id: 4) {
103 id,
104 name
105 }
106 }
107 `);
108
109 expect(result).toMatchObject({
110 kind: Kind.DOCUMENT,
111 definitions: [
112 {
113 kind: Kind.OPERATION_DEFINITION,
114 operation: 'query',
115 name: undefined,
116 variableDefinitions: [],
117 directives: [],
118 selectionSet: {
119 kind: Kind.SELECTION_SET,
120 selections: [
121 {
122 kind: Kind.FIELD,
123 alias: undefined,
124 name: {
125 kind: Kind.NAME,
126 value: 'node',
127 },
128 arguments: [
129 {
130 kind: Kind.ARGUMENT,
131 name: {
132 kind: Kind.NAME,
133 value: 'id',
134 },
135 value: {
136 kind: Kind.INT,
137 value: '4',
138 },
139 },
140 ],
141 directives: [],
142 selectionSet: {
143 kind: Kind.SELECTION_SET,
144 selections: [
145 {
146 kind: Kind.FIELD,
147 alias: undefined,
148 name: {
149 kind: Kind.NAME,
150 value: 'id',
151 },
152 arguments: [],
153 directives: [],
154 selectionSet: undefined,
155 },
156 {
157 kind: Kind.FIELD,
158 alias: undefined,
159 name: {
160 kind: Kind.NAME,
161 value: 'name',
162 },
163 arguments: [],
164 directives: [],
165 selectionSet: undefined,
166 },
167 ],
168 },
169 },
170 ],
171 },
172 },
173 ],
174 });
175 });
176
177 it('creates ast from nameless query without variables', () => {
178 const result = parse(`
179 query {
180 node {
181 id
182 }
183 }
184 `);
185
186 expect(result).toMatchObject({
187 kind: Kind.DOCUMENT,
188 definitions: [
189 {
190 kind: Kind.OPERATION_DEFINITION,
191 operation: 'query',
192 name: undefined,
193 variableDefinitions: [],
194 directives: [],
195 selectionSet: {
196 kind: Kind.SELECTION_SET,
197 selections: [
198 {
199 kind: Kind.FIELD,
200 alias: undefined,
201 name: {
202 kind: Kind.NAME,
203 value: 'node',
204 },
205 arguments: [],
206 directives: [],
207 selectionSet: {
208 kind: Kind.SELECTION_SET,
209 selections: [
210 {
211 kind: Kind.FIELD,
212 alias: undefined,
213 name: {
214 kind: Kind.NAME,
215 value: 'id',
216 },
217 arguments: [],
218 directives: [],
219 selectionSet: undefined,
220 },
221 ],
222 },
223 },
224 ],
225 },
226 },
227 ],
228 });
229 });
230
231 it('allows parsing without source location information', () => {
232 const result = parse('{ id }', { noLocation: true });
233 expect('loc' in result).toBe(false);
234 });
235
236 describe('parseValue', () => {
237 it('parses null value', () => {
238 const result = parseValue('null');
239 expect(result).toEqual({ kind: Kind.NULL });
240 });
241
242 it('parses list values', () => {
243 const result = parseValue('[123 "abc"]');
244 expect(result).toEqual({
245 kind: Kind.LIST,
246 values: [
247 {
248 kind: Kind.INT,
249 value: '123',
250 },
251 {
252 kind: Kind.STRING,
253 value: 'abc',
254 },
255 ],
256 });
257 });
258
259 it('parses block strings', () => {
260 const result = parseValue('["""long""" "short"]');
261 expect(result).toEqual({
262 kind: Kind.LIST,
263 values: [
264 {
265 kind: Kind.STRING,
266 value: 'long',
267 },
268 {
269 kind: Kind.STRING,
270 value: 'short',
271 },
272 ],
273 });
274 });
275
276 it('allows variables', () => {
277 const result = parseValue('{ field: $var }');
278 expect(result).toEqual({
279 kind: Kind.OBJECT,
280 fields: [
281 {
282 kind: Kind.OBJECT_FIELD,
283 name: {
284 kind: Kind.NAME,
285 value: 'field',
286 },
287 value: {
288 kind: Kind.VARIABLE,
289 name: {
290 kind: Kind.NAME,
291 value: 'var',
292 },
293 },
294 },
295 ],
296 });
297 });
298
299 it('correct message for incomplete variable', () => {
300 expect(() => {
301 return parseValue('$');
302 }).toThrow();
303 });
304
305 it('correct message for unexpected token', () => {
306 expect(() => {
307 return parseValue(':');
308 }).toThrow();
309 });
310 });
311
312 describe('parseType', () => {
313 it('parses well known types', () => {
314 const result = parseType('String');
315 expect(result).toEqual({
316 kind: Kind.NAMED_TYPE,
317 name: {
318 kind: Kind.NAME,
319 value: 'String',
320 },
321 });
322 });
323
324 it('parses custom types', () => {
325 const result = parseType('MyType');
326 expect(result).toEqual({
327 kind: Kind.NAMED_TYPE,
328 name: {
329 kind: Kind.NAME,
330 value: 'MyType',
331 },
332 });
333 });
334
335 it('parses list types', () => {
336 const result = parseType('[MyType]');
337 expect(result).toEqual({
338 kind: Kind.LIST_TYPE,
339 type: {
340 kind: Kind.NAMED_TYPE,
341 name: {
342 kind: Kind.NAME,
343 value: 'MyType',
344 },
345 },
346 });
347 });
348
349 it('parses non-null types', () => {
350 const result = parseType('MyType!');
351 expect(result).toEqual({
352 kind: Kind.NON_NULL_TYPE,
353 type: {
354 kind: Kind.NAMED_TYPE,
355 name: {
356 kind: Kind.NAME,
357 value: 'MyType',
358 },
359 },
360 });
361 });
362
363 it('parses nested types', () => {
364 const result = parseType('[MyType!]');
365 expect(result).toEqual({
366 kind: Kind.LIST_TYPE,
367 type: {
368 kind: Kind.NON_NULL_TYPE,
369 type: {
370 kind: Kind.NAMED_TYPE,
371 name: {
372 kind: Kind.NAME,
373 value: 'MyType',
374 },
375 },
376 },
377 });
378 });
379 });
380});
381
382const kitchenSinkQuery = String.raw`
383query queryName($foo: ComplexType, $site: Site = MOBILE) @onQuery {
384 whoever123is: node(id: [123, 456]) {
385 id
386 ... on User @onInlineFragment {
387 field2 {
388 id
389 alias: field1(first: 10, after: $foo) @include(if: $foo) {
390 id
391 ...frag @onFragmentSpread
392 }
393 }
394 }
395 ... @skip(unless: $foo) {
396 id
397 }
398 ... {
399 id
400 }
401 }
402}
403mutation likeStory @onMutation {
404 like(story: 123) @onField {
405 story {
406 id @onField
407 }
408 }
409}
410subscription StoryLikeSubscription(
411 $input: StoryLikeSubscribeInput @onVariableDefinition
412)
413 @onSubscription {
414 storyLikeSubscribe(input: $input) {
415 story {
416 likers {
417 count
418 }
419 likeSentence {
420 text
421 }
422 }
423 }
424}
425fragment frag on Friend @onFragmentDefinition {
426 foo(
427 size: $size
428 bar: $b
429 obj: { key: "value" }
430 )
431}
432{
433 unnamed(truthy: true, falsy: false, nullish: null)
434 query
435}
436query {
437 __typename
438}
439`;