import 'source-map-support/register'; import { assert } from 'chai'; import { DocumentNode, FragmentDefinitionNode } from 'graphql'; import gql from './index'; const loader = require('../loader'); describe('gql', () => { it('parses queries', () => { assert.equal(gql`{ testQuery }`.kind, 'Document'); }); it('parses queries when called as a function', () => { assert.equal(gql('{ testQuery }').kind, 'Document'); }); it('parses queries with weird substitutions', () => { const obj = Object.create(null); assert.equal(gql`{ field(input: "${obj.missing}") }`.kind, 'Document'); assert.equal(gql`{ field(input: "${null}") }`.kind, 'Document'); assert.equal(gql`{ field(input: "${0}") }`.kind, 'Document'); }); it('allows interpolation of documents generated by the webpack loader', () => { const sameFragment = "fragment SomeFragmentName on SomeType { someField }"; const jsSource = loader.call( { cacheable() {} }, sameFragment, ); const module = { exports: Object.create(null) }; Function("module", jsSource)(module); const document = gql`query { ...SomeFragmentName } ${module.exports}`; assert.equal(document.kind, 'Document'); assert.equal(document.definitions.length, 2); assert.equal(document.definitions[0].kind, 'OperationDefinition'); assert.equal(document.definitions[1].kind, 'FragmentDefinition'); }); it('parses queries through webpack loader', () => { const jsSource = loader.call({ cacheable() {} }, '{ testQuery }'); const module = { exports: Object.create(null) }; Function("module", jsSource)(module); assert.equal(module.exports.kind, 'Document'); }); it('parses single query through webpack loader', () => { const jsSource = loader.call({ cacheable() {} }, ` query Q1 { testQuery } `); const module = { exports: Object.create(null) }; Function("module", jsSource)(module); assert.equal(module.exports.kind, 'Document'); assert.exists(module.exports.Q1); assert.equal(module.exports.Q1.kind, 'Document'); assert.equal(module.exports.Q1.definitions.length, 1); }); it('parses single query and exports as default', () => { const jsSource = loader.call({ cacheable() {} }, ` query Q1 { testQuery } `); const module = { exports: Object.create(null) }; Function("module", jsSource)(module); assert.deepEqual(module.exports.definitions, module.exports.Q1.definitions); }); it('parses multiple queries through webpack loader', () => { const jsSource = loader.call({ cacheable() {} }, ` query Q1 { testQuery } query Q2 { testQuery2 } `); const module = { exports: Object.create(null) }; Function("module", jsSource)(module); assert.exists(module.exports.Q1); assert.exists(module.exports.Q2); assert.equal(module.exports.Q1.kind, 'Document'); assert.equal(module.exports.Q2.kind, 'Document'); assert.equal(module.exports.Q1.definitions.length, 1); assert.equal(module.exports.Q2.definitions.length, 1); }); it('parses fragments with variable definitions', () => { gql.enableExperimentalFragmentVariables(); const parsed: any = gql`fragment A ($arg: String!) on Type { testQuery }`; assert.equal(parsed.kind, 'Document'); assert.exists(parsed.definitions[0].variableDefinitions); gql.disableExperimentalFragmentVariables() }); // see https://github.com/apollographql/graphql-tag/issues/168 it('does not nest queries needlessly in named exports', () => { const jsSource = loader.call({ cacheable() {} }, ` query Q1 { testQuery } query Q2 { testQuery2 } query Q3 { test Query3 } `); const module = { exports: Object.create(null) }; Function("module", jsSource)(module); assert.notExists(module.exports.Q2.Q1); assert.notExists(module.exports.Q3.Q1); assert.notExists(module.exports.Q3.Q2); }); it('tracks fragment dependencies from multiple queries through webpack loader', () => { const jsSource = loader.call({ cacheable() {} }, ` fragment F1 on F { testQuery } fragment F2 on F { testQuery2 } fragment F3 on F { testQuery3 } query Q1 { ...F1 } query Q2 { ...F2 } query Q3 { ...F1 ...F2 } `); const module = { exports: Object.create(null) }; Function("module", jsSource)(module); assert.exists(module.exports.Q1); assert.exists(module.exports.Q2); assert.exists(module.exports.Q3); const Q1 = module.exports.Q1.definitions; const Q2 = module.exports.Q2.definitions; const Q3 = module.exports.Q3.definitions; assert.equal(Q1.length, 2); assert.equal(Q1[0].name.value, 'Q1'); assert.equal(Q1[1].name.value, 'F1'); assert.equal(Q2.length, 2); assert.equal(Q2[0].name.value, 'Q2'); assert.equal(Q2[1].name.value, 'F2'); assert.equal(Q3.length, 3); assert.equal(Q3[0].name.value, 'Q3'); assert.equal(Q3[1].name.value, 'F1'); assert.equal(Q3[2].name.value, 'F2'); const F1 = module.exports.F1.definitions; const F2 = module.exports.F2.definitions; const F3 = module.exports.F3.definitions; assert.equal(F1.length, 1); assert.equal(F1[0].name.value, 'F1'); assert.equal(F2.length, 1); assert.equal(F2[0].name.value, 'F2'); assert.equal(F3.length, 1); assert.equal(F3[0].name.value, 'F3'); }); it('tracks fragment dependencies across nested fragments', () => { const jsSource = loader.call({ cacheable() {} }, ` fragment F11 on F { testQuery } fragment F22 on F { ...F11 testQuery2 } fragment F33 on F { ...F22 testQuery3 } query Q1 { ...F33 } query Q2 { id } `); const module = { exports: Object.create(null) }; Function("module", jsSource)(module); assert.exists(module.exports.Q1); assert.exists(module.exports.Q2); const Q1 = module.exports.Q1.definitions; const Q2 = module.exports.Q2.definitions; assert.equal(Q1.length, 4); assert.equal(Q1[0].name.value, 'Q1'); assert.equal(Q1[1].name.value, 'F33'); assert.equal(Q1[2].name.value, 'F22'); assert.equal(Q1[3].name.value, 'F11'); assert.equal(Q2.length, 1); const F11 = module.exports.F11.definitions; const F22 = module.exports.F22.definitions; const F33 = module.exports.F33.definitions; assert.equal(F11.length, 1); assert.equal(F11[0].name.value, 'F11'); assert.equal(F22.length, 2); assert.equal(F22[0].name.value, 'F22'); assert.equal(F22[1].name.value, 'F11'); assert.equal(F33.length, 3); assert.equal(F33[0].name.value, 'F33'); assert.equal(F33[1].name.value, 'F22'); assert.equal(F33[2].name.value, 'F11'); }); it('correctly imports other files through the webpack loader', () => { const query = `#import "./fragment_definition.graphql" query { author { ...authorDetails } }`; const jsSource = loader.call({ cacheable() {} }, query); const module = { exports: Object.create(null) }; const require = (path: string) => { assert.equal(path, './fragment_definition.graphql'); return gql` fragment authorDetails on Author { firstName lastName }`; }; Function("module,require", jsSource)(module, require); assert.equal(module.exports.kind, 'Document'); const definitions = module.exports.definitions; assert.equal(definitions.length, 2); assert.equal(definitions[0].kind, 'OperationDefinition'); assert.equal(definitions[1].kind, 'FragmentDefinition'); }); it('tracks fragment dependencies across fragments loaded via the webpack loader', () => { const query = `#import "./fragment_definition.graphql" fragment F111 on F { ...F222 } query Q1 { ...F111 } query Q2 { a } `; const jsSource = loader.call({ cacheable() {} }, query); const module = { exports: Object.create(null) }; const require = (path: string) => { assert.equal(path, './fragment_definition.graphql'); return gql` fragment F222 on F { f1 f2 }`; }; Function("module,require", jsSource)(module, require); assert.exists(module.exports.Q1); assert.exists(module.exports.Q2); const Q1 = module.exports.Q1.definitions; const Q2 = module.exports.Q2.definitions; assert.equal(Q1.length, 3); assert.equal(Q1[0].name.value, 'Q1'); assert.equal(Q1[1].name.value, 'F111'); assert.equal(Q1[2].name.value, 'F222'); assert.equal(Q2.length, 1); }); it('does not complain when presented with normal comments', (done) => { assert.doesNotThrow(() => { const query = `#normal comment query { author { ...authorDetails } }`; const jsSource = loader.call({ cacheable() {} }, query); const module = { exports: Object.create(null) }; Function("module", jsSource)(module); assert.equal(module.exports.kind, 'Document'); done(); }); }); it('returns the same object for the same query', () => { assert.isTrue(gql`{ sameQuery }` === gql`{ sameQuery }`); }); it('returns the same object for the same query, even with whitespace differences', () => { assert.isTrue(gql`{ sameQuery }` === gql` { sameQuery, }`); }); const fragmentAst = gql` fragment UserFragment on User { firstName lastName } `; it('returns the same object for the same fragment', () => { assert.isTrue(gql`fragment same on Same { sameQuery }` === gql`fragment same on Same { sameQuery }`); }); it('returns the same object for the same document with substitution', () => { // We know that calling `gql` on a fragment string will always return // the same document, so we can reuse `fragmentAst` assert.isTrue(gql`{ ...UserFragment } ${fragmentAst}` === gql`{ ...UserFragment } ${fragmentAst}`); }); it('can reference a fragment that references as fragment', () => { const secondFragmentAst = gql` fragment SecondUserFragment on User { ...UserFragment } ${fragmentAst} `; const ast = gql` { user(id: 5) { ...SecondUserFragment } } ${secondFragmentAst} `; assert.deepEqual(ast, gql` { user(id: 5) { ...SecondUserFragment } } fragment SecondUserFragment on User { ...UserFragment } fragment UserFragment on User { firstName lastName } `); }); describe('fragment warnings', () => { let warnings = []; const oldConsoleWarn = console.warn; beforeEach(() => { gql.resetCaches(); warnings = []; console.warn = (w: string) => warnings.push(w); }); afterEach(() => { console.warn = oldConsoleWarn; }); it('warns if you use the same fragment name for different fragments', () => { const frag1 = gql`fragment TestSame on Bar { fieldOne }`; const frag2 = gql`fragment TestSame on Bar { fieldTwo }`; assert.isFalse(frag1 === frag2); assert.equal(warnings.length, 1); }); it('does not warn if you use the same fragment name for the same fragment', () => { const frag1 = gql`fragment TestDifferent on Bar { fieldOne }`; const frag2 = gql`fragment TestDifferent on Bar { fieldOne }`; assert.isTrue(frag1 === frag2); assert.equal(warnings.length, 0); }); it('does not warn if you use the same embedded fragment in two different queries', () => { const frag1 = gql`fragment TestEmbedded on Bar { field }`; const query1 = gql`{ bar { fieldOne ...TestEmbedded } } ${frag1}`; const query2 = gql`{ bar { fieldTwo ...TestEmbedded } } ${frag1}`; assert.isFalse(query1 === query2); assert.equal(warnings.length, 0); }); it('does not warn if you use the same fragment name for embedded and non-embedded fragments', () => { const frag1 = gql`fragment TestEmbeddedTwo on Bar { field }`; gql`{ bar { ...TestEmbedded } } ${frag1}`; gql`{ bar { ...TestEmbedded } } fragment TestEmbeddedTwo on Bar { field }`; assert.equal(warnings.length, 0); }); }); describe('unique fragments', () => { beforeEach(() => { gql.resetCaches(); }); it('strips duplicate fragments from the document', () => { const frag1 = gql`fragment TestDuplicate on Bar { field }`; const query1 = gql`{ bar { fieldOne ...TestDuplicate } } ${frag1} ${frag1}`; const query2 = gql`{ bar { fieldOne ...TestDuplicate } } ${frag1}`; assert.equal(query1.definitions.length, 2); assert.equal(query1.definitions[1].kind, 'FragmentDefinition'); // We don't test strict equality between the two queries because the source.body parsed from the // document is not the same, but the set of definitions should be. assert.deepEqual(query1.definitions, query2.definitions); }); it('ignores duplicate fragments from second-level imports when using the webpack loader', () => { // take a require function and a query string, use the webpack loader to process it const load = ( require: (path: string) => DocumentNode | null, query: string, ): DocumentNode | null => { const jsSource = loader.call({ cacheable() {} }, query); const module = { exports: Object.create(null) }; Function("require,module", jsSource)(require, module); return module.exports; } const test_require = (path: string) => { switch (path) { case './friends.graphql': return load(test_require, [ '#import "./person.graphql"', 'fragment friends on Hero { friends { ...person } }', ].join('\n')); case './enemies.graphql': return load(test_require, [ '#import "./person.graphql"', 'fragment enemies on Hero { enemies { ...person } }', ].join('\n')); case './person.graphql': return load(test_require, 'fragment person on Person { name }\n'); default: return null; }; }; const result = load(test_require, [ '#import "./friends.graphql"', '#import "./enemies.graphql"', 'query { hero { ...friends ...enemies } }', ].join('\n'))!; assert.equal(result.kind, 'Document'); assert.equal(result.definitions.length, 4, 'after deduplication, only 4 fragments should remain'); assert.equal(result.definitions[0].kind, 'OperationDefinition'); // the rest of the definitions should be fragments and contain one of // each: "friends", "enemies", "person". Order does not matter const fragments = result.definitions.slice(1) as FragmentDefinitionNode[]; assert(fragments.every(fragment => fragment.kind === 'FragmentDefinition')) assert(fragments.some(fragment => fragment.name.value === 'friends')) assert(fragments.some(fragment => fragment.name.value === 'enemies')) assert(fragments.some(fragment => fragment.name.value === 'person')) }); }); // How to make this work? // it.only('can reference a fragment passed as a document via shorthand', () => { // const ast = gql` // { // user(id: 5) { // ...${userFragmentDocument} // } // } // `; // // assert.deepEqual(ast, gql` // { // user(id: 5) { // ...UserFragment // } // } // fragment UserFragment on User { // firstName // lastName // } // `); // }); });