import { describe, it } from 'node:test';
import { expect } from 'chai';
import { expectEvents } from '../../__testUtils__/expectEvents.ts';
import { expectNoTracingActivity } from '../../__testUtils__/expectNoTracingActivity.ts';
import { expectPromise } from '../../__testUtils__/expectPromise.ts';
import { expectToThrow } from '../../__testUtils__/expectToThrow.ts';
import { getTracingChannel } from '../../__testUtils__/getTracingChannel.ts';
import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick.ts';
import type { OperationDefinitionNode } from '../../language/ast.ts';
import { OperationTypeNode } from '../../language/ast.ts';
import { parse } from '../../language/parser.ts';
import { GraphQLObjectType } from '../../type/definition.ts';
import { GraphQLString } from '../../type/scalars.ts';
import { GraphQLSchema } from '../../type/schema.ts';
import { buildSchema } from '../../utilities/buildASTSchema.ts';
import {
execute,
executeIgnoringIncremental,
executeSync,
} from '../execute.ts';
const schema = buildSchema(`
type Query {
sync: String
async: String
}
type Subscription {
tick: String
}
`);
const executeChannel = getTracingChannel('graphql:execute');
const executeRootSelectionSetChannel = getTracingChannel(
'graphql:execute:rootSelectionSet',
);
describe('execute diagnostics channel', () => {
it('emits start and end around a synchronous execute', async () => {
const document = parse('query Q($sync: String) { sync }');
const variableValues = { sync: 'ignored by the field' };
await expectEvents(
executeChannel,
() =>
execute({
schema,
document,
rootValue: { sync: () => 'hello' },
variableValues,
}),
(result) => [
{
channel: 'start',
context: {
schema,
document,
rawVariableValues: variableValues,
operationName: 'Q',
operationType: OperationTypeNode.QUERY,
},
},
{
channel: 'end',
context: {
schema,
document,
rawVariableValues: variableValues,
operationName: 'Q',
operationType: OperationTypeNode.QUERY,
result,
},
},
],
);
});
it('emits start, end, and async lifecycle when execute returns a promise', async () => {
const document = parse('query { async }');
await expectEvents(
executeChannel,
() =>
execute({
schema,
document,
rootValue: { async: () => Promise.resolve('hello-async') },
}),
(result) => [
{
channel: 'start',
context: {
schema,
document,
rawVariableValues: undefined,
operationName: undefined,
operationType: OperationTypeNode.QUERY,
},
},
{
channel: 'end',
context: {
schema,
document,
rawVariableValues: undefined,
operationName: undefined,
operationType: OperationTypeNode.QUERY,
},
},
{
channel: 'asyncStart',
context: {
schema,
document,
rawVariableValues: undefined,
operationName: undefined,
operationType: OperationTypeNode.QUERY,
result,
},
},
{
channel: 'asyncEnd',
context: {
schema,
document,
rawVariableValues: undefined,
operationName: undefined,
operationType: OperationTypeNode.QUERY,
result,
},
},
],
);
});
it('emits full async lifecycle with error when execute returns a rejected promise', async () => {
const asyncDeferSchema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
hero: {
type: new GraphQLObjectType({
name: 'Hero',
fields: {
id: { type: GraphQLString },
name: { type: GraphQLString },
},
}),
},
},
}),
});
const document = parse(`
query Deferred {
hero { name ... @defer { id } }
}
`);
await expectEvents(
executeChannel,
() =>
expectPromise(
execute({
schema: asyncDeferSchema,
document,
rootValue: {
hero: Promise.resolve({
id: '1',
name: async () => {
await resolveOnNextTick();
return 'slow';
},
}),
},
}),
).toReject(),
(error) => [
{
channel: 'start',
context: {
schema: asyncDeferSchema,
document,
rawVariableValues: undefined,
operationName: 'Deferred',
operationType: OperationTypeNode.QUERY,
},
},
{
channel: 'end',
context: {
schema: asyncDeferSchema,
document,
rawVariableValues: undefined,
operationName: 'Deferred',
operationType: OperationTypeNode.QUERY,
},
},
{
channel: 'error',
context: {
schema: asyncDeferSchema,
document,
rawVariableValues: undefined,
operationName: 'Deferred',
operationType: OperationTypeNode.QUERY,
error,
},
},
{
channel: 'asyncStart',
context: {
schema: asyncDeferSchema,
document,
rawVariableValues: undefined,
operationName: 'Deferred',
operationType: OperationTypeNode.QUERY,
error,
},
},
{
channel: 'asyncEnd',
context: {
schema: asyncDeferSchema,
document,
rawVariableValues: undefined,
operationName: 'Deferred',
operationType: OperationTypeNode.QUERY,
error,
},
},
],
);
});
it('emits once for executeSync via experimentalExecuteIncrementally', async () => {
const document = parse('{ sync }');
await expectEvents(
executeChannel,
() =>
executeSync({ schema, document, rootValue: { sync: () => 'hello' } }),
(result) => [
{
channel: 'start',
context: {
schema,
document,
rawVariableValues: undefined,
operationName: undefined,
operationType: OperationTypeNode.QUERY,
},
},
{
channel: 'end',
context: {
schema,
document,
rawVariableValues: undefined,
operationName: undefined,
operationType: OperationTypeNode.QUERY,
result,
},
},
],
);
});
it('emits start and end around executeIgnoringIncremental', async () => {
const document = parse('query Q { sync }');
await expectEvents(
executeChannel,
() =>
executeIgnoringIncremental({
schema,
document,
rootValue: { sync: () => 'hello' },
}),
(result) => [
{
channel: 'start',
context: {
schema,
document,
rawVariableValues: undefined,
operationName: 'Q',
operationType: OperationTypeNode.QUERY,
},
},
{
channel: 'end',
context: {
schema,
document,
rawVariableValues: undefined,
operationName: 'Q',
operationType: OperationTypeNode.QUERY,
result,
},
},
],
);
});
it('emits start, error, and end when execute throws synchronously', async () => {
const document = parse('{ sync }');
const invalidSchema = buildSchema(`
directive @defer on FIELD
type Query {
sync: String
}
`);
await expectEvents(
executeChannel,
() => expectToThrow(() => execute({ schema: invalidSchema, document })),
(error) => [
{
channel: 'start',
context: {
schema: invalidSchema,
document,
rawVariableValues: undefined,
operationName: undefined,
operationType: OperationTypeNode.QUERY,
},
},
{
channel: 'error',
context: {
schema: invalidSchema,
document,
rawVariableValues: undefined,
operationName: undefined,
operationType: OperationTypeNode.QUERY,
error,
},
},
{
channel: 'end',
context: {
schema: invalidSchema,
document,
rawVariableValues: undefined,
operationName: undefined,
operationType: OperationTypeNode.QUERY,
error,
},
},
],
);
});
it('does not call tracing methods when no subscribers are attached', async () => {
const document = parse('{ sync }');
const result = await expectNoTracingActivity(executeChannel, () =>
execute({
schema,
document,
rootValue: { sync: () => 'hello' },
}),
);
expect(result).to.deep.equal({ data: { sync: 'hello' } });
});
});
describe('execute root selection set diagnostics channel', () => {
it('emits start and end around a synchronous root selection set', async () => {
const document = parse('query Q($sync: String) { sync }');
const operation = document.definitions[0] as OperationDefinitionNode;
const variableValues = { sync: 'ignored by the field' };
await expectEvents(
executeRootSelectionSetChannel,
() =>
execute({
schema,
document,
rootValue: { sync: () => 'hello' },
variableValues,
}),
(result) => [
{
channel: 'start',
context: {
schema,
document,
operation,
rawVariableValues: variableValues,
operationName: 'Q',
operationType: OperationTypeNode.QUERY,
},
},
{
channel: 'end',
context: {
schema,
document,
operation,
rawVariableValues: variableValues,
operationName: 'Q',
operationType: OperationTypeNode.QUERY,
result,
},
},
],
);
});
it('emits the full async lifecycle when the root selection set returns a promise', async () => {
const document = parse('query { async }');
const operation = document.definitions[0] as OperationDefinitionNode;
await expectEvents(
executeRootSelectionSetChannel,
() =>
execute({
schema,
document,
rootValue: { async: () => Promise.resolve('hello-async') },
}),
(result) => [
{
channel: 'start',
context: {
schema,
document,
operation,
rawVariableValues: undefined,
operationName: undefined,
operationType: OperationTypeNode.QUERY,
},
},
{
channel: 'end',
context: {
schema,
document,
operation,
rawVariableValues: undefined,
operationName: undefined,
operationType: OperationTypeNode.QUERY,
},
},
{
channel: 'asyncStart',
context: {
schema,
document,
operation,
rawVariableValues: undefined,
operationName: undefined,
operationType: OperationTypeNode.QUERY,
result,
},
},
{
channel: 'asyncEnd',
context: {
schema,
document,
operation,
rawVariableValues: undefined,
operationName: undefined,
operationType: OperationTypeNode.QUERY,
result,
},
},
],
);
});
it('does not call tracing methods when no subscribers are attached', async () => {
const document = parse('{ sync }');
const result = await expectNoTracingActivity(
executeRootSelectionSetChannel,
() =>
execute({
schema,
document,
rootValue: { sync: () => 'hello' },
}),
);
expect(result).to.deep.equal({ data: { sync: 'hello' } });
});
});