From 59a6172c316ce4caad35b35284b9f0cd6b7bef31 Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Tue, 15 Nov 2016 17:43:21 -0800 Subject: [PATCH] Flow type tests & reorganize This cleans up the top level tests and flow-types them. It also moved one unrelated test into a more appropriate location. Inspired by #425 --- src/__tests__/starWarsData.js | 46 +- src/__tests__/starWarsIntrospection-test.js | 1 + src/__tests__/starWarsQuery-test.js | 527 +++++++++----------- src/__tests__/starWarsSchema.js | 22 +- src/__tests__/starWarsValidation-test.js | 1 + src/execution/__tests__/executor-test.js | 66 +++ 6 files changed, 364 insertions(+), 299 deletions(-) diff --git a/src/__tests__/starWarsData.js b/src/__tests__/starWarsData.js index 59f0857727..7b96e67ee6 100644 --- a/src/__tests__/starWarsData.js +++ b/src/__tests__/starWarsData.js @@ -1,3 +1,4 @@ +/* @flow */ /** * Copyright (c) 2015, Facebook, Inc. * All rights reserved. @@ -14,6 +15,7 @@ */ const luke = { + type: 'Human', id: '1000', name: 'Luke Skywalker', friends: [ '1002', '1003', '2000', '2001' ], @@ -22,6 +24,7 @@ const luke = { }; const vader = { + type: 'Human', id: '1001', name: 'Darth Vader', friends: [ '1004' ], @@ -30,6 +33,7 @@ const vader = { }; const han = { + type: 'Human', id: '1002', name: 'Han Solo', friends: [ '1000', '1003', '2001' ], @@ -37,6 +41,7 @@ const han = { }; const leia = { + type: 'Human', id: '1003', name: 'Leia Organa', friends: [ '1000', '1002', '2000', '2001' ], @@ -45,6 +50,7 @@ const leia = { }; const tarkin = { + type: 'Human', id: '1004', name: 'Wilhuff Tarkin', friends: [ '1001' ], @@ -60,6 +66,7 @@ const humanData = { }; const threepio = { + type: 'Droid', id: '2000', name: 'C-3PO', friends: [ '1000', '1002', '1003', '2001' ], @@ -68,6 +75,7 @@ const threepio = { }; const artoo = { + type: 'Droid', id: '2001', name: 'R2-D2', friends: [ '1000', '1002', '1003' ], @@ -80,6 +88,35 @@ const droidData = { '2001': artoo, }; +/** + * These are Flow types which correspond to the schema. + * They represent the shape of the data visited during field resolution. + */ +export type Character = { + id: string, + name: string, + friends: Array, + appearsIn: Array, +}; + +export type Human = { + type: 'Human', + id: string, + name: string, + friends: Array, + appearsIn: Array, + homePlanet: string, +}; + +export type Droid = { + type: 'Droid', + id: string, + name: string, + friends: Array, + appearsIn: Array, + primaryFunction: string +}; + /** * Helper function to get a character by ID. */ @@ -91,14 +128,15 @@ function getCharacter(id) { /** * Allows us to query for a character's friends. */ -export function getFriends(character) { +export function getFriends(character: Character): Array> { + // Notice that GraphQL accepts Arrays of Promises. return character.friends.map(id => getCharacter(id)); } /** * Allows us to fetch the undisputed hero of the Star Wars trilogy, R2-D2. */ -export function getHero(episode) { +export function getHero(episode: number): Character { if (episode === 5) { // Luke is the hero of Episode V. return luke; @@ -110,13 +148,13 @@ export function getHero(episode) { /** * Allows us to query for the human with the given id. */ -export function getHuman(id) { +export function getHuman(id: string): Human { return humanData[id]; } /** * Allows us to query for the droid with the given id. */ -export function getDroid(id) { +export function getDroid(id: string): Droid { return droidData[id]; } diff --git a/src/__tests__/starWarsIntrospection-test.js b/src/__tests__/starWarsIntrospection-test.js index e9faa7f95b..ef845190e1 100644 --- a/src/__tests__/starWarsIntrospection-test.js +++ b/src/__tests__/starWarsIntrospection-test.js @@ -1,3 +1,4 @@ +/* @flow */ /** * Copyright (c) 2015, Facebook, Inc. * All rights reserved. diff --git a/src/__tests__/starWarsQuery-test.js b/src/__tests__/starWarsQuery-test.js index ea3e51c798..91fd2204c4 100644 --- a/src/__tests__/starWarsQuery-test.js +++ b/src/__tests__/starWarsQuery-test.js @@ -1,3 +1,4 @@ +/* @flow */ /** * Copyright (c) 2015, Facebook, Inc. * All rights reserved. @@ -11,12 +12,6 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; import { StarWarsSchema } from './starWarsSchema.js'; import { graphql } from '../graphql'; -import { - GraphQLObjectType, - GraphQLNonNull, - GraphQLSchema, - GraphQLString, -} from '../type'; // 80+ char lines are useful in describe/it, so ignore in this file. /* eslint-disable max-len */ @@ -31,13 +26,14 @@ describe('Star Wars Query Tests', () => { } } `; - const expected = { - hero: { - name: 'R2-D2' - } - }; const result = await graphql(StarWarsSchema, query); - expect(result).to.deep.equal({ data: expected }); + expect(result).to.deep.equal({ + data: { + hero: { + name: 'R2-D2' + } + } + }); }); it('Allows us to query for the ID and friends of R2-D2', async () => { @@ -52,25 +48,26 @@ describe('Star Wars Query Tests', () => { } } `; - const expected = { - hero: { - id: '2001', - name: 'R2-D2', - friends: [ - { - name: 'Luke Skywalker', - }, - { - name: 'Han Solo', - }, - { - name: 'Leia Organa', - }, - ] - } - }; const result = await graphql(StarWarsSchema, query); - expect(result).to.deep.equal({ data: expected }); + expect(result).to.deep.equal({ + data: { + hero: { + id: '2001', + name: 'R2-D2', + friends: [ + { + name: 'Luke Skywalker', + }, + { + name: 'Han Solo', + }, + { + name: 'Leia Organa', + }, + ] + } + } + }); }); }); @@ -90,66 +87,67 @@ describe('Star Wars Query Tests', () => { } } `; - const expected = { - hero: { - name: 'R2-D2', - friends: [ - { - name: 'Luke Skywalker', - appearsIn: [ 'NEWHOPE', 'EMPIRE', 'JEDI' ], - friends: [ - { - name: 'Han Solo', - }, - { - name: 'Leia Organa', - }, - { - name: 'C-3PO', - }, - { - name: 'R2-D2', - }, - ] - }, - { - name: 'Han Solo', - appearsIn: [ 'NEWHOPE', 'EMPIRE', 'JEDI' ], - friends: [ - { - name: 'Luke Skywalker', - }, - { - name: 'Leia Organa', - }, - { - name: 'R2-D2', - }, - ] - }, - { - name: 'Leia Organa', - appearsIn: [ 'NEWHOPE', 'EMPIRE', 'JEDI' ], - friends: [ - { - name: 'Luke Skywalker', - }, - { - name: 'Han Solo', - }, - { - name: 'C-3PO', - }, - { - name: 'R2-D2', - }, - ] - }, - ] - } - }; const result = await graphql(StarWarsSchema, query); - expect(result).to.deep.equal({ data: expected }); + expect(result).to.deep.equal({ + data: { + hero: { + name: 'R2-D2', + friends: [ + { + name: 'Luke Skywalker', + appearsIn: [ 'NEWHOPE', 'EMPIRE', 'JEDI' ], + friends: [ + { + name: 'Han Solo', + }, + { + name: 'Leia Organa', + }, + { + name: 'C-3PO', + }, + { + name: 'R2-D2', + }, + ] + }, + { + name: 'Han Solo', + appearsIn: [ 'NEWHOPE', 'EMPIRE', 'JEDI' ], + friends: [ + { + name: 'Luke Skywalker', + }, + { + name: 'Leia Organa', + }, + { + name: 'R2-D2', + }, + ] + }, + { + name: 'Leia Organa', + appearsIn: [ 'NEWHOPE', 'EMPIRE', 'JEDI' ], + friends: [ + { + name: 'Luke Skywalker', + }, + { + name: 'Han Solo', + }, + { + name: 'C-3PO', + }, + { + name: 'R2-D2', + }, + ] + }, + ] + } + } + }); }); }); @@ -162,13 +160,14 @@ describe('Star Wars Query Tests', () => { } } `; - const expected = { - human: { - name: 'Luke Skywalker' - } - }; const result = await graphql(StarWarsSchema, query); - expect(result).to.deep.equal({ data: expected }); + expect(result).to.deep.equal({ + data: { + human: { + name: 'Luke Skywalker' + } + } + }); }); it('Allows us to create a generic query, then use it to fetch Luke Skywalker using his ID', async () => { @@ -179,16 +178,15 @@ describe('Star Wars Query Tests', () => { } } `; - const params = { - someId: '1000' - }; - const expected = { - human: { - name: 'Luke Skywalker' - } - }; + const params = { someId: '1000' }; const result = await graphql(StarWarsSchema, query, null, null, params); - expect(result).to.deep.equal({ data: expected }); + expect(result).to.deep.equal({ + data: { + human: { + name: 'Luke Skywalker' + } + } + }); }); it('Allows us to create a generic query, then use it to fetch Han Solo using his ID', async () => { @@ -199,16 +197,15 @@ describe('Star Wars Query Tests', () => { } } `; - const params = { - someId: '1002' - }; - const expected = { - human: { - name: 'Han Solo' - } - }; + const params = { someId: '1002' }; const result = await graphql(StarWarsSchema, query, null, null, params); - expect(result).to.deep.equal({ data: expected }); + expect(result).to.deep.equal({ + data: { + human: { + name: 'Han Solo' + } + } + }); }); it('Allows us to create a generic query, then pass an invalid ID to get null back', async () => { @@ -219,14 +216,13 @@ describe('Star Wars Query Tests', () => { } } `; - const params = { - id: 'not a valid id' - }; - const expected = { - human: null - }; + const params = { id: 'not a valid id' }; const result = await graphql(StarWarsSchema, query, null, null, params); - expect(result).to.deep.equal({ data: expected }); + expect(result).to.deep.equal({ + data: { + human: null + } + }); }); }); @@ -239,13 +235,14 @@ describe('Star Wars Query Tests', () => { } } `; - const expected = { - luke: { - name: 'Luke Skywalker' - }, - }; const result = await graphql(StarWarsSchema, query); - expect(result).to.deep.equal({ data: expected }); + expect(result).to.deep.equal({ + data: { + luke: { + name: 'Luke Skywalker' + } + } + }); }); it('Allows us to query for both Luke and Leia, using two root fields and an alias', async () => { @@ -259,16 +256,17 @@ describe('Star Wars Query Tests', () => { } } `; - const expected = { - luke: { - name: 'Luke Skywalker' - }, - leia: { - name: 'Leia Organa' - } - }; const result = await graphql(StarWarsSchema, query); - expect(result).to.deep.equal({ data: expected }); + expect(result).to.deep.equal({ + data: { + luke: { + name: 'Luke Skywalker' + }, + leia: { + name: 'Leia Organa' + } + } + }); }); }); @@ -286,18 +284,19 @@ describe('Star Wars Query Tests', () => { } } `; - const expected = { - luke: { - name: 'Luke Skywalker', - homePlanet: 'Tatooine' - }, - leia: { - name: 'Leia Organa', - homePlanet: 'Alderaan' - } - }; const result = await graphql(StarWarsSchema, query); - expect(result).to.deep.equal({ data: expected }); + expect(result).to.deep.equal({ + data: { + luke: { + name: 'Luke Skywalker', + homePlanet: 'Tatooine' + }, + leia: { + name: 'Leia Organa', + homePlanet: 'Alderaan' + } + } + }); }); it('Allows us to use a fragment to avoid duplicating content', async () => { @@ -316,18 +315,19 @@ describe('Star Wars Query Tests', () => { homePlanet } `; - const expected = { - luke: { - name: 'Luke Skywalker', - homePlanet: 'Tatooine' - }, - leia: { - name: 'Leia Organa', - homePlanet: 'Alderaan' - } - }; const result = await graphql(StarWarsSchema, query); - expect(result).to.deep.equal({ data: expected }); + expect(result).to.deep.equal({ + data: { + luke: { + name: 'Luke Skywalker', + homePlanet: 'Tatooine' + }, + leia: { + name: 'Leia Organa', + homePlanet: 'Alderaan' + } + } + }); }); }); @@ -341,14 +341,15 @@ describe('Star Wars Query Tests', () => { } } `; - const expected = { - hero: { - __typename: 'Droid', - name: 'R2-D2' - }, - }; const result = await graphql(StarWarsSchema, query); - expect(result).to.deep.equal({ data: expected }); + expect(result).to.deep.equal({ + data: { + hero: { + __typename: 'Droid', + name: 'R2-D2' + } + } + }); }); it('Allows us to verify that Luke is a human', async () => { @@ -360,14 +361,15 @@ describe('Star Wars Query Tests', () => { } } `; - const expected = { - hero: { - __typename: 'Human', - name: 'Luke Skywalker' - }, - }; const result = await graphql(StarWarsSchema, query); - expect(result).to.deep.equal({ data: expected }); + expect(result).to.deep.equal({ + data: { + hero: { + __typename: 'Human', + name: 'Luke Skywalker' + } + } + }); }); }); @@ -381,19 +383,22 @@ describe('Star Wars Query Tests', () => { } } `; - const expected = { - hero: { - name: 'R2-D2', - secretBackstory: null - } - }; - const expectedErrors = [ 'secretBackstory is secret.' ]; const result = await graphql(StarWarsSchema, query); - expect(result.data).to.deep.equal(expected); - expect(result.errors.map(e => e.message)).to.deep.equal(expectedErrors); - expect( - result.errors.map(e => e.path)).to.deep.equal( - [ [ 'hero', 'secretBackstory' ] ]); + expect(result).to.deep.equal({ + data: { + hero: { + name: 'R2-D2', + secretBackstory: null + } + }, + errors: [ + { + message: 'secretBackstory is secret.', + locations: [ { line: 5, column: 13 } ], + path: [ 'hero', 'secretBackstory' ] + } + ] + }); }); it('Correctly reports error on accessing secretBackstory in a list', async () => { @@ -408,41 +413,45 @@ describe('Star Wars Query Tests', () => { } } `; - const expected = { - hero: { - name: 'R2-D2', - friends: [ - { - name: 'Luke Skywalker', - secretBackstory: null, - }, - { - name: 'Han Solo', - secretBackstory: null, - }, - { - name: 'Leia Organa', - secretBackstory: null, - }, - ] - } - }; - const expectedErrors = [ - 'secretBackstory is secret.', - 'secretBackstory is secret.', - 'secretBackstory is secret.', - ]; const result = await graphql(StarWarsSchema, query); - expect(result.data).to.deep.equal(expected); - expect(result.errors.map(e => e.message)).to.deep.equal(expectedErrors); - expect( - result.errors.map(e => e.path) - ).to.deep.equal( - [ - [ 'hero', 'friends', 0, 'secretBackstory' ], - [ 'hero', 'friends', 1, 'secretBackstory' ], - [ 'hero', 'friends', 2, 'secretBackstory' ], - ]); + expect(result).to.deep.equal({ + data: { + hero: { + name: 'R2-D2', + friends: [ + { + name: 'Luke Skywalker', + secretBackstory: null, + }, + { + name: 'Han Solo', + secretBackstory: null, + }, + { + name: 'Leia Organa', + secretBackstory: null, + }, + ] + } + }, + errors: [ + { + message: 'secretBackstory is secret.', + locations: [ { line: 7, column: 15 } ], + path: [ 'hero', 'friends', 0, 'secretBackstory' ] + }, + { + message: 'secretBackstory is secret.', + locations: [ { line: 7, column: 15 } ], + path: [ 'hero', 'friends', 1, 'secretBackstory' ] + }, + { + message: 'secretBackstory is secret.', + locations: [ { line: 7, column: 15 } ], + path: [ 'hero', 'friends', 2, 'secretBackstory' ] + } + ] + }); }); it('Correctly reports error on accessing through an alias', async () => { @@ -454,78 +463,22 @@ describe('Star Wars Query Tests', () => { } } `; - const expected = { - mainHero: { - name: 'R2-D2', - story: null, - } - }; - const expectedErrors = [ - 'secretBackstory is secret.', - ]; const result = await graphql(StarWarsSchema, query); - expect(result.data).to.deep.equal(expected); - expect(result.errors.map(e => e.message)).to.deep.equal(expectedErrors); - expect( - result.errors.map(e => e.path) - ).to.deep.equal([ [ 'mainHero', 'story' ] ]); - }); - - it('Full response path is included when fields are non-nullable', async () => { - const A = new GraphQLObjectType({ - name: 'A', - fields: () => ({ - nullableA: { - type: A, - resolve: () => ({}), - }, - nonNullA: { - type: new GraphQLNonNull(A), - resolve: () => ({}), - }, - throws: { - type: new GraphQLNonNull(GraphQLString), - resolve: () => { throw new Error('Catch me if you can'); }, - }, - }), - }); - const queryType = new GraphQLObjectType({ - name: 'query', - fields: () => ({ - nullableA: { - type: A, - resolve: () => ({}) - } - }), - }); - const schema = new GraphQLSchema({ - query: queryType, - }); - - const query = ` - query { - nullableA { - nullableA { - nonNullA { - nonNullA { - throws - } - } - } + expect(result).to.deep.equal({ + data: { + mainHero: { + name: 'R2-D2', + story: null, } - } - `; - - const result = await graphql(schema, query); - const expected = { - nullableA: { - nullableA: null - } - }; - expect(result.data).to.deep.equal(expected); - expect( - result.errors.map(e => e.path)).to.deep.equal( - [ [ 'nullableA', 'nullableA', 'nonNullA', 'nonNullA', 'throws' ] ]); + }, + errors: [ + { + message: 'secretBackstory is secret.', + locations: [ { line: 5, column: 13 } ], + path: [ 'mainHero', 'story' ] + } + ] + }); }); }); }); diff --git a/src/__tests__/starWarsSchema.js b/src/__tests__/starWarsSchema.js index 9b6852971d..03ef3b0219 100644 --- a/src/__tests__/starWarsSchema.js +++ b/src/__tests__/starWarsSchema.js @@ -1,3 +1,4 @@ +/* @flow */ /** * Copyright (c) 2015, Facebook, Inc. * All rights reserved. @@ -131,8 +132,13 @@ const characterInterface = new GraphQLInterfaceType({ description: 'All secrets about their past.', }, }), - resolveType: character => { - return getHuman(character.id) ? humanType : droidType; + resolveType(character) { + if (character.type === 'Human') { + return humanType; + } + if (character.type === 'Droid') { + return droidType; + } } }); @@ -162,8 +168,8 @@ const humanType = new GraphQLObjectType({ }, friends: { type: new GraphQLList(characterInterface), - description: 'The friends of the human, or an empty list if they ' + - 'have none.', + description: + 'The friends of the human, or an empty list if they have none.', resolve: human => getFriends(human), }, appearsIn: { @@ -177,7 +183,7 @@ const humanType = new GraphQLObjectType({ secretBackstory: { type: GraphQLString, description: 'Where are they from and how they came to be who they are.', - resolve: () => { + resolve() { throw new Error('secretBackstory is secret.'); }, }, @@ -212,8 +218,8 @@ const droidType = new GraphQLObjectType({ }, friends: { type: new GraphQLList(characterInterface), - description: 'The friends of the droid, or an empty list if they ' + - 'have none.', + description: + 'The friends of the droid, or an empty list if they have none.', resolve: droid => getFriends(droid), }, appearsIn: { @@ -223,7 +229,7 @@ const droidType = new GraphQLObjectType({ secretBackstory: { type: GraphQLString, description: 'Construction date and the name of the designer.', - resolve: () => { + resolve() { throw new Error('secretBackstory is secret.'); }, }, diff --git a/src/__tests__/starWarsValidation-test.js b/src/__tests__/starWarsValidation-test.js index 45f3eddb98..cb326e467f 100644 --- a/src/__tests__/starWarsValidation-test.js +++ b/src/__tests__/starWarsValidation-test.js @@ -1,3 +1,4 @@ +/* @flow */ /** * Copyright (c) 2015, Facebook, Inc. * All rights reserved. diff --git a/src/execution/__tests__/executor-test.js b/src/execution/__tests__/executor-test.js index 28acdb3ed3..be788e5a30 100644 --- a/src/execution/__tests__/executor-test.js +++ b/src/execution/__tests__/executor-test.js @@ -19,6 +19,7 @@ import { GraphQLBoolean, GraphQLInt, GraphQLString, + GraphQLNonNull, } from '../../type'; describe('Execute: Handles basic execution tasks', () => { @@ -438,6 +439,71 @@ describe('Execute: Handles basic execution tasks', () => { ]); }); + + it('Full response path is included for non-nullable fields', async () => { + const A = new GraphQLObjectType({ + name: 'A', + fields: () => ({ + nullableA: { + type: A, + resolve: () => ({}), + }, + nonNullA: { + type: new GraphQLNonNull(A), + resolve: () => ({}), + }, + throws: { + type: new GraphQLNonNull(GraphQLString), + resolve: () => { + throw new Error('Catch me if you can'); + }, + }, + }), + }); + const queryType = new GraphQLObjectType({ + name: 'query', + fields: () => ({ + nullableA: { + type: A, + resolve: () => ({}) + } + }), + }); + const schema = new GraphQLSchema({ + query: queryType, + }); + + const query = ` + query { + nullableA { + aliasedA: nullableA { + nonNullA { + anotherA: nonNullA { + throws + } + } + } + } + } + `; + + const result = await execute(schema, parse(query)); + expect(result).to.deep.equal({ + data: { + nullableA: { + aliasedA: null + } + }, + errors: [ + { + message: 'Catch me if you can', + locations: [ { line: 7, column: 17 } ], + path: [ 'nullableA', 'aliasedA', 'nonNullA', 'anotherA', 'throws' ] + } + ] + }); + }); + it('uses the inline operation if no operation name is provided', async () => { const doc = '{ a }'; const data = { a: 'b' };