diff --git a/.gitignore b/.gitignore index 6acc570..ef08393 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules dist -.DS_Store \ No newline at end of file +.idea +.DS_Store diff --git a/README.md b/README.md index e2c7a79..ef532c0 100644 --- a/README.md +++ b/README.md @@ -276,6 +276,21 @@ const cache = new InMemoryCache({ const mockClient = createMockClient({ cache }); ``` +Additionally, you can specify a `missingHandlerPolicy` to define the behavior of the mock client when a request handler for a particular operation is not found. + +The `missingHandlerPolicy` accepts one of three string values: +- `'throw-error'`: The client throws an error when it encounters a missing handler. +- `'warn-and-return-error'`: The client logs a warning message in the console and returns an error. +- `'return-error'`: The client returns an error without any warning message. + +Here's an example of how you can set the `missingHandlerPolicy`: + +```typescript +const mockClient = createMockClient({ missingHandlerPolicy: 'warn-and-return-error' }); +``` + +In this example, if a request handler for a given operation is not found, the client will log a warning message to the console and then return an error. + Note: it is not possible to specify the `link` to use as this is how `mock-apollo-client` injects its behaviour. ### Fragments diff --git a/src/mockClient.ts b/src/mockClient.ts index 92bda8d..a97c8e9 100644 --- a/src/mockClient.ts +++ b/src/mockClient.ts @@ -1,6 +1,6 @@ import { ApolloClientOptions, ApolloClient, DocumentNode } from '@apollo/client/core'; import { InMemoryCache as Cache, NormalizedCacheObject } from '@apollo/client/cache'; -import { MockLink } from './mockLink'; +import { MissingHandlerPolicy, MockLink } from './mockLink'; import { IMockSubscription } from './mockSubscription'; export type RequestHandler = @@ -15,7 +15,11 @@ export type RequestHandlerResponse = export type MockApolloClient = ApolloClient & { setRequestHandler: (query: DocumentNode, handler: RequestHandler) => void }; -export type MockApolloClientOptions = Partial, 'link'>> | undefined; +interface CustomOptions { + missingHandlerPolicy?: MissingHandlerPolicy; +} + +export type MockApolloClientOptions = Partial, 'link'>> & CustomOptions | undefined; export const createMockClient = (options?: MockApolloClientOptions): MockApolloClient => { if ((options as any)?.link) { diff --git a/src/mockLink.test.ts b/src/mockLink.test.ts index e5776a7..acfb7a2 100644 --- a/src/mockLink.test.ts +++ b/src/mockLink.test.ts @@ -10,6 +10,14 @@ describe('class MockLink', () => { const queryOne = gql`query One {one}`; const queryTwo = gql`query Two {two}`; + const queryOneOperation = { query: queryOne, variables: { a: 'one' } } as Partial as Operation; + + const createMockObserver = (): jest.Mocked> => ({ + next: jest.fn(), + error: jest.fn(), + complete: jest.fn(), + }); + beforeEach(() => { jest.spyOn(console, 'warn') .mockReset(); @@ -81,19 +89,6 @@ describe('class MockLink', () => { }); describe('method request', () => { - const queryOneOperation = { query: queryOne, variables: { a: 'one' } } as Partial as Operation; - - const createMockObserver = (): jest.Mocked> => ({ - next: jest.fn(), - error: jest.fn(), - complete: jest.fn(), - }); - - it('throws when a handler is not defined for the query', () => { - expect(() => mockLink.request(queryOneOperation)) - .toThrowError(`Request handler not defined for query: ${print(queryOne)}`) - }); - it('correctly executes the handler when the handler is defined as a promise and it and successfully resolves', async () => { const handler = jest.fn().mockResolvedValue({ data: 'Query one result' }); mockLink.setRequestHandler(queryOne, handler); @@ -242,4 +237,46 @@ describe('class MockLink', () => { expect(observer.complete).toBeCalledTimes(1); }); }); + + describe('constructor option "missingHandlerPolicy"', () => { + it('when "throw-error" throws when a handler is not defined for the query', () => { + mockLink = new MockLink({missingHandlerPolicy: 'throw-error'}) + + expect(() => mockLink.request(queryOneOperation)) + .toThrowError(`Request handler not defined for query: ${print(queryOne)}`) + }); + + it('when "warn-and-return-error" logs a warning when a handler is not defined for the query', async () => { + mockLink = new MockLink({missingHandlerPolicy: 'warn-and-return-error'}) + + const observable = mockLink.request(queryOneOperation); + const observer = createMockObserver(); + + observable.subscribe(observer); + + await new Promise(r => setTimeout(r, 0)); + + expect(observer.next).not.toBeCalled(); + expect(observer.error).toBeCalled(); + expect(observer.complete).not.toBeCalled(); + expect(console.warn).toBeCalledTimes(1); + expect(console.warn).toBeCalledWith(`Request handler not defined for query: ${print(queryOne)}`); + }); + + it('when "return-error" returns an error when a handler is not defined for the query', async () => { + mockLink = new MockLink({missingHandlerPolicy: 'return-error'}) + + const observable = mockLink.request(queryOneOperation); + const observer = createMockObserver(); + + observable.subscribe(observer); + + await new Promise(r => setTimeout(r, 0)); + + expect(observer.next).not.toBeCalled(); + expect(observer.error).toBeCalledTimes(1); + expect(observer.error).toBeCalledWith(new Error(`Request handler not defined for query: ${print(queryOne)}`)); + expect(observer.complete).not.toBeCalled(); + }); + }) }); diff --git a/src/mockLink.ts b/src/mockLink.ts index 2cf06b3..388e720 100644 --- a/src/mockLink.ts +++ b/src/mockLink.ts @@ -4,7 +4,22 @@ import { RequestHandler, RequestHandlerResponse } from './mockClient'; import { removeClientSetsFromDocument, removeConnectionDirectiveFromDocument } from '@apollo/client/utilities'; import { IMockSubscription, MockSubscription } from './mockSubscription'; +export type MissingHandlerPolicy = 'throw-error' | 'warn-and-return-error' | 'return-error'; + +interface MockLinkOptions { + missingHandlerPolicy?: MissingHandlerPolicy; +} + +const DEFAULT_MISSING_HANDLER_POLICY: MissingHandlerPolicy = 'throw-error'; + export class MockLink extends ApolloLink { + constructor(options?: MockLinkOptions) { + super(); + + this.missingHandlerPolicy = options?.missingHandlerPolicy || DEFAULT_MISSING_HANDLER_POLICY; + } + + private readonly missingHandlerPolicy: MissingHandlerPolicy; private requestHandlers: Record = {}; setRequestHandler(requestQuery: DocumentNode, handler: RequestHandler): void { @@ -29,11 +44,19 @@ export class MockLink extends ApolloLink { const handler = this.requestHandlers[key]; - if (!handler) { - throw new Error(`Request handler not defined for query: ${print(operation.query)}`); + if (!handler && this.missingHandlerPolicy === 'throw-error') { + throw new Error(getNotDefinedHandlerMessage(operation)); } return new Observable(observer => { + if (!handler) { + if (this.missingHandlerPolicy === 'warn-and-return-error') { + console.warn(getNotDefinedHandlerMessage(operation)); + } + throw new Error(getNotDefinedHandlerMessage(operation)); + } + + let result: | Promise> | IMockSubscription @@ -101,3 +124,7 @@ const isPromise = (maybePromise: any): maybePromise is Promise => const isSubscription = (maybeSubscription: any): maybeSubscription is MockSubscription => maybeSubscription && maybeSubscription instanceof MockSubscription; + +const getNotDefinedHandlerMessage = (operation: Operation) => { + return `Request handler not defined for query: ${print(operation.query)}` +}