graphql-language-service-server
Version:
Server process backing the GraphQL Language Service
275 lines (247 loc) • 7.92 kB
text/typescript
/**
* Copyright (c) 2021 GraphQL Contributors
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { join } from 'node:path';
import { GraphQLConfig } from 'graphql-config';
import { GraphQLLanguageService } from '../GraphQLLanguageService';
import { SymbolKind } from 'vscode-languageserver-protocol';
import { Position } from 'graphql-language-service';
import { NoopLogger } from '../Logger';
import { GraphQLEnumType } from 'graphql';
const MOCK_CONFIG = {
filepath: join(__dirname, '.graphqlrc.yml'),
config: {
schema: './__schema__/StarWarsSchema.graphql',
documents: ['./queries/**', '**/*.graphql'],
},
};
describe('GraphQLLanguageService', () => {
const mockCache = {
async getSchema() {
const config = this.getGraphQLConfig();
return config.getDefault().getSchema();
},
getGraphQLConfig() {
return new GraphQLConfig(MOCK_CONFIG, []);
},
getProjectForFile(uri: string) {
return this.getGraphQLConfig().getProjectForFile(uri);
},
getFragmentDefinitions() {
const definitions = new Map();
definitions.set('TestFragment', {
filePath: 'fake file path',
content: 'fake file content',
definition: {
kind: 'FragmentDefinition',
name: {
value: 'TestFragment',
},
loc: {
start: 293,
end: 335,
},
},
});
return definitions;
},
// setting the defs here in duplicate as with object types below
// leads to duplicates, perhaps related to a bug, or perhaps just a test bug?
getFragmentDependenciesForAST() {
return [];
},
getObjectTypeDefinitions() {
const definitions = new Map();
definitions.set('Episode', {
filePath: 'fake file path',
content: 'fake file content',
definition: {
name: {
value: 'Episode',
},
loc: {
start: 293,
end: 335,
},
},
});
definitions.set('Human', {
filePath: 'fake file path',
content: 'fake file content',
definition: {
name: {
value: 'Human',
},
fields: [
{
name: { value: 'name' },
loc: {
start: 293,
end: 335,
},
arguments: [
{
name: { value: 'arg' },
loc: {
start: 293,
end: 335,
},
type: GraphQLEnumType,
},
],
},
],
loc: {
start: 293,
end: 335,
},
},
});
return definitions;
},
getObjectTypeDependenciesForAST() {
return [
{
filePath: 'fake file path',
content: 'fake file content',
definition: {
name: {
value: 'Episode',
},
loc: {
start: 293,
end: 335,
},
},
},
{
filePath: 'fake file path',
content: 'fake file content',
definition: {
name: {
value: 'Human',
},
loc: {
start: 293,
end: 335,
},
},
},
];
},
};
let languageService: GraphQLLanguageService;
beforeEach(() => {
languageService = new GraphQLLanguageService(
mockCache as any,
new NoopLogger(),
);
});
it('runs diagnostic service as expected', async () => {
const diagnostics = await languageService.getDiagnostics(
'invalidKeyword',
'./queries/testQuery.graphql',
);
expect(diagnostics.length).toEqual(1);
const diagnostic = diagnostics[0];
expect(diagnostic.message).toEqual(
'Syntax Error: Unexpected Name "invalidKeyword".',
);
});
it('avoids reporting validation errors when not enough characters are present', async () => {
const diagnostics = await languageService.getDiagnostics(
' \n \n \n\n',
'./queries/testQuery.graphql',
);
expect(diagnostics.length).toEqual(0);
});
it('still reports errors on empty anonymous op', async () => {
const diagnostics = await languageService.getDiagnostics(
' \n {\n \n}\n\n',
'./queries/testQuery.graphql',
);
expect(diagnostics.length).toEqual(1);
expect(diagnostics[0].message).toEqual(
'Syntax Error: Expected Name, found "}".',
);
});
it('runs definition service as expected', async () => {
const definitionQueryResult = await languageService.getDefinition(
'type Query { hero(episode: Episode): Character }',
{ line: 0, character: 28 } as Position,
'./queries/definitionQuery.graphql',
);
expect(definitionQueryResult?.definitions.length).toEqual(1);
});
it('runs definition service on fragment definition', async () => {
const definitionQueryResult = await languageService.getDefinition(
'fragment TestFragment on Human { name }',
{ line: 0, character: 14 } as Position,
'./queries/definitionQuery.graphql',
);
expect(definitionQueryResult?.definitions.length).toEqual(1);
});
it('runs definition service on fragment spread', async () => {
const definitionQueryResult = await languageService.getDefinition(
'fragment TestFragment on Human { name }\nquery { ...TestFragment }',
{ line: 1, character: 14 } as Position,
'./queries/definitionQuery.graphql',
);
expect(definitionQueryResult?.definitions.length).toEqual(1);
});
it('runs definition service on field as expected', async () => {
const definitionQueryResult = await languageService.getDefinition(
'query XXX { human { name } }',
{ line: 0, character: 21 } as Position,
'./queries/definitionQuery.graphql',
);
expect(definitionQueryResult?.definitions.length).toEqual(1);
});
it('can find a definition for a union', async () => {
const query =
'union X = A | B\ntype A { x: String }\ntype B { x: String }\ntype Query { a: X }';
const definitionQueryResult = await languageService.getDefinition(
query,
{ line: 3, character: 16 } as Position,
'./queries/definitionQuery.graphql',
);
expect(definitionQueryResult?.definitions.length).toEqual(1);
});
it('runs hover service as expected', async () => {
const hoverInformation = await languageService.getHoverInformation(
'type Query { hero(episode: String): String }',
{ line: 0, character: 28 } as Position,
'./queries/definitionQuery.graphql',
);
expect(hoverInformation).toEqual(
'String\n\nThe `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.',
);
});
it('runs document symbol requests as expected', async () => {
const validQuery = `
query OperationExample {
item(episode: EMPIRE){
...testFragment
}
}
`;
const result = await languageService.getDocumentSymbols(
validQuery,
'file://file.graphql',
);
expect(result).not.toBeUndefined();
expect(result.length).toEqual(3);
// expect(result[0].name).toEqual('item');
expect(result[1].name).toEqual('item');
expect(result[1].kind).toEqual(SymbolKind.Field);
expect(result[1].location.range.start.line).toEqual(2);
expect(result[1].location.range.start.character).toEqual(4);
expect(result[1].location.range.end.line).toEqual(4);
expect(result[1].location.range.end.character).toEqual(5);
});
});