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`;