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});