/// import { graphql, IntrospectionQuery, GraphQLError } from "graphql"; import { buildClientSchema, printSchema } from "graphql"; import { makeExecutableSchema, addMockFunctionsToSchema } from "graphql-tools"; export interface MockGraphQLOptions extends SetOperationsOpts { schema?: string | string[] | IntrospectionQuery; mocks?: CypressMockBaseTypes; } export interface SetOperationsOpts { name?: string; endpoint?: string; /* Operations object. Make sure that mocks must not be wrapped with `data` property */ operations?: Partial; /* Delay for stubbed responses (in ms) */ delay?: number; } export interface GQLRequestPayload { operationName: Extract; query: string; variables: any; } let commonMocks: any = null; let commonOptions: any = null; declare global { interface CypressMockBaseTypes {} interface CypressMockOperationTypes extends Record {} namespace Cypress { interface Chainable { mockGraphql(options?: MockGraphQLOptions): Cypress.Chainable; mockGraphqlOps(options?: SetOperationsOpts): Cypress.Chainable; } } } const wait = (timeout: number) => (response?: T) => new Promise(resolve => setTimeout(() => resolve(response), timeout)); /** * Add .baseGraphqlMocks() to the Cypress chain. Used when we want to set the * mocks as common between multiple commands. */ export const setBaseGraphqlMocks = (mocks: CypressMockBaseTypes) => { if (!commonMocks) { commonMocks = mocks; } else { throw new Error( `setBaseGraphqlMocks may only be called once, already called.` ); } return mocks; }; /** * Add .baseGraphqlMocks() to the Cypress chain. Used when we want to set the * mocks as common between multiple commands. */ export const setBaseOperationOptions = (options: CypressMockBaseTypes) => { if (!commonOptions) { commonOptions = options; } else { throw new Error( `setBaseOperationOptions may only be called once, already called.` ); } return options; }; /** * Adds a .mockGraphql() and .mockGraphqlOps() methods to the cypress chain. * * The .mockGraphql should be called in the cypress "before" or "beforeEach" block * config to setup the server. * * By default, it will use the /graphql endpoint, but this can be changed * depending on the server implementation * * It takes an "operations" object, representing the named operations * of the GraphQL server. This is combined with the "mocks" option, * to modify the output behavior per test. * * The .mockGraphqlOps() allows you to configure the mock responses at a * more granular level * * For example, if we has a query called "UserQuery" and wanted to * explicitly force a state where a viewer is null (logged out), it would * look something like: * * .mockGraphqlOps({ * operations: { * UserQuery: { * viewer: null * } * } * }) */ Cypress.Commands.add("mockGraphql", (options?: MockGraphQLOptions) => { const mergedOptions = { ...(commonOptions || {}), ...(options || {}) }; const { endpoint = "/graphql", delay = 0, operations = {}, mocks = {}, schema = undefined } = mergedOptions; if (!schema) { throw new Error( "Schema must be provided to the mockGraphql or setBaseOperationOptions" ); } const executableSchema = makeExecutableSchema({ typeDefs: schemaAsSDL(schema) }); addMockFunctionsToSchema({ schema: executableSchema, mocks: { ...commonMocks, ...mocks } }); let currentDelay = delay; let currentOps = operations; cy.on("window:before:load", win => { const originalFetch = win.fetch; function fetch(input: RequestInfo, init?: RequestInit) { if (typeof input !== "string") { throw new Error( "Currently only support fetch(url, options), saw fetch(Request)" ); } if (input.indexOf(endpoint) !== -1 && init && init.method === "POST") { const payload: GQLRequestPayload = JSON.parse(init.body as string); const { operationName, query, variables } = payload; const rootValue = getRootValue(currentOps, operationName, variables); if ( // Additional checks here because of transpilation. // We will loose instanceof if we are not using specific babel plugin, or using pure TS to compile front-end rootValue instanceof GraphQLError || rootValue.constructor === GraphQLError || rootValue.constructor.name === "GraphQLError" ) { return Promise.resolve() .then(wait(currentDelay)) .then( () => new Response( JSON.stringify({ data: {}, errors: [rootValue] }) ) ); } return graphql({ schema, source: query, variableValues: variables, operationName, rootValue }) .then(wait(currentDelay)) .then((data: any) => new Response(JSON.stringify(data))); } return originalFetch(input, init); } cy.stub(win, "fetch", fetch).as("fetchStub"); }); // cy.wrap({ setOperations: (options: SetOperationsOpts) => { currentDelay = options.delay || 0; currentOps = { ...currentOps, ...options.operations }; } }).as(getAlias(mergedOptions)); }); Cypress.Commands.add("mockGraphqlOps", (options: SetOperationsOpts) => { cy.get(`@${getAlias(options)}`).invoke("setOperations" as any, options); }); const getAlias = ({ name, endpoint }: { name?: string; endpoint?: string }) => { if (name || endpoint) { return `mockGraphqlOps:${name || endpoint}`; } return "mockGraphqlOps"; }; // Takes the schema either as the full .graphql file (string) or // the introspection object. function schemaAsSDL(schema: string | string[] | IntrospectionQuery) { if (typeof schema === "string" || Array.isArray(schema)) { return schema; } return printSchema(buildClientSchema(schema)); } function getRootValue( operations: Partial, operationName: Extract, variables: any ) { if (!operationName || !operations[operationName]) { return {}; } const op = operations[operationName]; if (typeof op === "function") { try { return op(variables); } catch (e) { return e; // properly handle dynamic throw new GraphQLError("message") } } return op; }