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