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('print', () => {
9 it('prints 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('parse provides errors', () => {
19 expect(() => parse('{')).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('creates ast', () => {
103 const result = parse(`
104 {
105 node(id: 4) {
106 id,
107 name
108 }
109 }
110 `);
111
112 expect(result).toMatchObject({
113 kind: Kind.DOCUMENT,
114 definitions: [
115 {
116 kind: Kind.OPERATION_DEFINITION,
117 operation: 'query',
118 name: undefined,
119 variableDefinitions: [],
120 directives: [],
121 selectionSet: {
122 kind: Kind.SELECTION_SET,
123 selections: [
124 {
125 kind: Kind.FIELD,
126 alias: undefined,
127 name: {
128 kind: Kind.NAME,
129 value: 'node',
130 },
131 arguments: [
132 {
133 kind: Kind.ARGUMENT,
134 name: {
135 kind: Kind.NAME,
136 value: 'id',
137 },
138 value: {
139 kind: Kind.INT,
140 value: '4',
141 },
142 },
143 ],
144 directives: [],
145 selectionSet: {
146 kind: Kind.SELECTION_SET,
147 selections: [
148 {
149 kind: Kind.FIELD,
150 alias: undefined,
151 name: {
152 kind: Kind.NAME,
153 value: 'id',
154 },
155 arguments: [],
156 directives: [],
157 selectionSet: undefined,
158 },
159 {
160 kind: Kind.FIELD,
161 alias: undefined,
162 name: {
163 kind: Kind.NAME,
164 value: 'name',
165 },
166 arguments: [],
167 directives: [],
168 selectionSet: undefined,
169 },
170 ],
171 },
172 },
173 ],
174 },
175 },
176 ],
177 });
178 });
179
180 it('creates ast from nameless query without variables', () => {
181 const result = parse(`
182 query {
183 node {
184 id
185 }
186 }
187 `);
188
189 expect(result).toMatchObject({
190 kind: Kind.DOCUMENT,
191 definitions: [
192 {
193 kind: Kind.OPERATION_DEFINITION,
194 operation: 'query',
195 name: undefined,
196 variableDefinitions: [],
197 directives: [],
198 selectionSet: {
199 kind: Kind.SELECTION_SET,
200 selections: [
201 {
202 kind: Kind.FIELD,
203 alias: undefined,
204 name: {
205 kind: Kind.NAME,
206 value: 'node',
207 },
208 arguments: [],
209 directives: [],
210 selectionSet: {
211 kind: Kind.SELECTION_SET,
212 selections: [
213 {
214 kind: Kind.FIELD,
215 alias: undefined,
216 name: {
217 kind: Kind.NAME,
218 value: 'id',
219 },
220 arguments: [],
221 directives: [],
222 selectionSet: undefined,
223 },
224 ],
225 },
226 },
227 ],
228 },
229 },
230 ],
231 });
232 });
233
234 it('allows parsing without source location information', () => {
235 const result = parse('{ id }', { noLocation: true });
236 expect('loc' in result).toBe(false);
237 });
238
239 describe('parseValue', () => {
240 it('parses null value', () => {
241 const result = parseValue('null');
242 expect(result).toEqual({ kind: Kind.NULL });
243 });
244
245 it('parses list values', () => {
246 const result = parseValue('[123 "abc"]');
247 expect(result).toEqual({
248 kind: Kind.LIST,
249 values: [
250 {
251 kind: Kind.INT,
252 value: '123',
253 },
254 {
255 kind: Kind.STRING,
256 value: 'abc',
257 block: false,
258 },
259 ],
260 });
261 });
262
263 it('parses block strings', () => {
264 const result = parseValue('["""long""" "short"]');
265 expect(result).toEqual({
266 kind: Kind.LIST,
267 values: [
268 {
269 kind: Kind.STRING,
270 value: 'long',
271 block: true,
272 },
273 {
274 kind: Kind.STRING,
275 value: 'short',
276 block: false,
277 },
278 ],
279 });
280 });
281
282 it('allows variables', () => {
283 const result = parseValue('{ field: $var }');
284 expect(result).toEqual({
285 kind: Kind.OBJECT,
286 fields: [
287 {
288 kind: Kind.OBJECT_FIELD,
289 name: {
290 kind: Kind.NAME,
291 value: 'field',
292 },
293 value: {
294 kind: Kind.VARIABLE,
295 name: {
296 kind: Kind.NAME,
297 value: 'var',
298 },
299 },
300 },
301 ],
302 });
303 });
304
305 it('correct message for incomplete variable', () => {
306 expect(() => {
307 return parseValue('$');
308 }).toThrow();
309 });
310
311 it('correct message for unexpected token', () => {
312 expect(() => {
313 return parseValue(':');
314 }).toThrow();
315 });
316 });
317
318 describe('parseType', () => {
319 it('parses well known types', () => {
320 const result = parseType('String');
321 expect(result).toEqual({
322 kind: Kind.NAMED_TYPE,
323 name: {
324 kind: Kind.NAME,
325 value: 'String',
326 },
327 });
328 });
329
330 it('parses custom types', () => {
331 const result = parseType('MyType');
332 expect(result).toEqual({
333 kind: Kind.NAMED_TYPE,
334 name: {
335 kind: Kind.NAME,
336 value: 'MyType',
337 },
338 });
339 });
340
341 it('parses list types', () => {
342 const result = parseType('[MyType]');
343 expect(result).toEqual({
344 kind: Kind.LIST_TYPE,
345 type: {
346 kind: Kind.NAMED_TYPE,
347 name: {
348 kind: Kind.NAME,
349 value: 'MyType',
350 },
351 },
352 });
353 });
354
355 it('parses non-null types', () => {
356 const result = parseType('MyType!');
357 expect(result).toEqual({
358 kind: Kind.NON_NULL_TYPE,
359 type: {
360 kind: Kind.NAMED_TYPE,
361 name: {
362 kind: Kind.NAME,
363 value: 'MyType',
364 },
365 },
366 });
367 });
368
369 it('parses nested types', () => {
370 const result = parseType('[MyType!]');
371 expect(result).toEqual({
372 kind: Kind.LIST_TYPE,
373 type: {
374 kind: Kind.NON_NULL_TYPE,
375 type: {
376 kind: Kind.NAMED_TYPE,
377 name: {
378 kind: Kind.NAME,
379 value: 'MyType',
380 },
381 },
382 },
383 });
384 });
385 });
386});