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: [ "test" ] } }) { 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 block: false,
254 value: 'abc',
255 },
256 ],
257 });
258 });
259
260 it('parses block strings', () => {
261 const result = parseValue('["""long""" "short"]');
262 expect(result).toEqual({
263 kind: Kind.LIST,
264 values: [
265 {
266 block: true,
267 kind: Kind.STRING,
268 value: 'long',
269 },
270 {
271 kind: Kind.STRING,
272 block: false,
273 value: 'short',
274 },
275 ],
276 });
277 });
278
279 it('allows variables', () => {
280 const result = parseValue('{ field: $var }');
281 expect(result).toEqual({
282 kind: Kind.OBJECT,
283 fields: [
284 {
285 kind: Kind.OBJECT_FIELD,
286 name: {
287 kind: Kind.NAME,
288 value: 'field',
289 },
290 value: {
291 kind: Kind.VARIABLE,
292 name: {
293 kind: Kind.NAME,
294 value: 'var',
295 },
296 },
297 },
298 ],
299 });
300 });
301
302 it('correct message for incomplete variable', () => {
303 expect(() => {
304 return parseValue('$');
305 }).toThrow();
306 });
307
308 it('correct message for unexpected token', () => {
309 expect(() => {
310 return parseValue(':');
311 }).toThrow();
312 });
313 });
314
315 describe('parseType', () => {
316 it('parses well known types', () => {
317 const result = parseType('String');
318 expect(result).toEqual({
319 kind: Kind.NAMED_TYPE,
320 name: {
321 kind: Kind.NAME,
322 value: 'String',
323 },
324 });
325 });
326
327 it('parses custom types', () => {
328 const result = parseType('MyType');
329 expect(result).toEqual({
330 kind: Kind.NAMED_TYPE,
331 name: {
332 kind: Kind.NAME,
333 value: 'MyType',
334 },
335 });
336 });
337
338 it('parses list types', () => {
339 const result = parseType('[MyType]');
340 expect(result).toEqual({
341 kind: Kind.LIST_TYPE,
342 type: {
343 kind: Kind.NAMED_TYPE,
344 name: {
345 kind: Kind.NAME,
346 value: 'MyType',
347 },
348 },
349 });
350 });
351
352 it('parses non-null types', () => {
353 const result = parseType('MyType!');
354 expect(result).toEqual({
355 kind: Kind.NON_NULL_TYPE,
356 type: {
357 kind: Kind.NAMED_TYPE,
358 name: {
359 kind: Kind.NAME,
360 value: 'MyType',
361 },
362 },
363 });
364 });
365
366 it('parses nested types', () => {
367 const result = parseType('[MyType!]');
368 expect(result).toEqual({
369 kind: Kind.LIST_TYPE,
370 type: {
371 kind: Kind.NON_NULL_TYPE,
372 type: {
373 kind: Kind.NAMED_TYPE,
374 name: {
375 kind: Kind.NAME,
376 value: 'MyType',
377 },
378 },
379 },
380 });
381 });
382 });
383});
384
385const kitchenSinkQuery = String.raw`
386query queryName($foo: ComplexType, $site: Site = MOBILE) @onQuery {
387 whoever123is: node(id: [123, 456]) {
388 id
389 ... on User @onInlineFragment {
390 field2 {
391 id
392 alias: field1(first: 10, after: $foo) @include(if: $foo) {
393 id
394 ...frag @onFragmentSpread
395 }
396 }
397 }
398 ... @skip(unless: $foo) {
399 id
400 }
401 ... {
402 id
403 }
404 }
405}
406mutation likeStory @onMutation {
407 like(story: 123) @onField {
408 story {
409 id @onField
410 }
411 }
412}
413subscription StoryLikeSubscription(
414 $input: StoryLikeSubscribeInput @onVariableDefinition
415)
416 @onSubscription {
417 storyLikeSubscribe(input: $input) {
418 story {
419 likers {
420 count
421 }
422 likeSentence {
423 text
424 }
425 }
426 }
427}
428fragment frag on Friend @onFragmentDefinition {
429 foo(
430 size: $size
431 bar: $b
432 obj: { key: "value" }
433 )
434}
435{
436 unnamed(truthy: true, falsy: false, nullish: null)
437 query
438}
439query {
440 __typename
441}
442`;