From 95219cdcd98520aeebc7f413f294730052b042b8 Mon Sep 17 00:00:00 2001 From: Giorgio Aquino Date: Mon, 22 Jul 2024 10:05:07 +0200 Subject: [PATCH] fix(LocalGraphQLClient): handle middleware and responseReducer (#1206) --- examples/create-react-app/test/setup.js | 3 + jest.config.js | 3 +- packages/graphql-hooks/src/GraphQLClient.ts | 21 ++++-- .../graphql-hooks/src/LocalGraphQLClient.ts | 62 +++++++++------- .../unit/LocalGraphQLClient.test.tsx | 70 +++++++++++++++++-- 5 files changed, 125 insertions(+), 34 deletions(-) create mode 100644 examples/create-react-app/test/setup.js diff --git a/examples/create-react-app/test/setup.js b/examples/create-react-app/test/setup.js new file mode 100644 index 000000000..0f182ea9b --- /dev/null +++ b/examples/create-react-app/test/setup.js @@ -0,0 +1,3 @@ +if (typeof global.Response === 'undefined') { + global.Response = function () {} +} diff --git a/jest.config.js b/jest.config.js index 3616d05a7..5fd609c7b 100644 --- a/jest.config.js +++ b/jest.config.js @@ -45,7 +45,8 @@ const projects = [ '\\.[jt]sx?$': 'babel-jest' }, displayName: 'cra-example', - testEnvironment: 'jsdom' + testEnvironment: 'jsdom', + setupFiles: ['/examples/create-react-app/test/setup.js'] } } ] diff --git a/packages/graphql-hooks/src/GraphQLClient.ts b/packages/graphql-hooks/src/GraphQLClient.ts index 628b77b22..203fd3c70 100644 --- a/packages/graphql-hooks/src/GraphQLClient.ts +++ b/packages/graphql-hooks/src/GraphQLClient.ts @@ -348,11 +348,11 @@ class GraphQLClient { return response.json().then(({ errors, data }) => { return this.generateResult({ graphQLErrors: errors, - data: - // enrich data with responseReducer if defined - typeof options.responseReducer === 'function' - ? options.responseReducer(data, response) - : data, + data: applyResponseReducer( + options.responseReducer, + data, + response + ), headers: response.headers }) }) @@ -466,4 +466,15 @@ function isGraphQLWsClient(value: any): value is GraphQLWsClient { return typeof value.subscribe === 'function' } +export function applyResponseReducer( + responseReducer: RequestOptions['responseReducer'], + data, + response: Response +) { + // enrich data with responseReducer if defined + return typeof responseReducer === 'function' + ? responseReducer(data, response) + : data +} + export default GraphQLClient diff --git a/packages/graphql-hooks/src/LocalGraphQLClient.ts b/packages/graphql-hooks/src/LocalGraphQLClient.ts index 893989785..0b4596bb7 100644 --- a/packages/graphql-hooks/src/LocalGraphQLClient.ts +++ b/packages/graphql-hooks/src/LocalGraphQLClient.ts @@ -1,6 +1,12 @@ -import GraphQLClient from './GraphQLClient' +import GraphQLClient, { applyResponseReducer } from './GraphQLClient' import LocalGraphQLError from './LocalGraphQLError' -import { LocalClientOptions, LocalQueries, Result } from './types/common-types' +import { + LocalClientOptions, + LocalQueries, + Operation, + RequestOptions, + Result +} from './types/common-types' /** Local version of the GraphQLClient which only returns specified queries * Meant to be used as a way to easily mock and test queries during development. This client never contacts any actual server. @@ -27,7 +33,7 @@ class LocalGraphQLClient extends GraphQLClient { // Delay before sending responses in miliseconds for simulating latency requestDelayMs: number constructor(config: LocalClientOptions) { - super({ url: '', ...config }) + super({ url: 'http://localhost', ...config }) this.localQueries = config.localQueries this.requestDelayMs = config.requestDelayMs || 0 if (!this.localQueries) { @@ -41,29 +47,38 @@ class LocalGraphQLClient extends GraphQLClient { // Skips all config verification from the parent class because we're mocking the client } - request( - operation - ): Promise> { - if (!this.localQueries[operation.query]) { - throw new Error( - `LocalGraphQLClient: no query match for: ${operation.query}` - ) - } - return timeoutPromise(this.requestDelayMs) - .then(() => - Promise.resolve( - this.localQueries[operation.query]( - operation.variables, - operation.operationName - ) + requestViaHttp( + operation: Operation, + options: RequestOptions = {} + ): Promise> { + return timeoutPromise(this.requestDelayMs).then(() => { + if (!operation.query || !this.localQueries[operation.query]) { + throw new Error( + `LocalGraphQLClient: no query match for: ${operation.query}` ) + } + + const data = this.localQueries[operation.query]( + operation.variables, + operation.operationName ) + + return applyResponseReducer(options.responseReducer, data, new Response()) + }) + } + + request( + operation: Operation, + options?: RequestOptions + ): Promise> { + return super + .request(operation, options) .then(result => { if (result instanceof LocalGraphQLError) { return { error: result } } - const { data, errors } = collectErrorsFromObject(result) - if (errors.length > 0) { + const { data, errors } = collectErrors(result) + if (errors && errors.length > 0) { return { data, error: new LocalGraphQLError({ @@ -76,7 +91,6 @@ class LocalGraphQLClient extends GraphQLClient { }) } } - function timeoutPromise(delayInMs) { return new Promise(resolve => { setTimeout(resolve, delayInMs) @@ -95,7 +109,7 @@ function collectErrorsFromObject(objectIn: object): { const errors: Error[] = [] for (const [key, value] of Object.entries(objectIn)) { - const child = collectErrorsFromChild(value) + const child = collectErrors(value) data[key] = child.data if (child.errors != null) { errors.push(...child.errors) @@ -113,7 +127,7 @@ function collectErrorsFromArray(arrayIn: object[]): { const errors: Error[] = [] for (const [idx, entry] of arrayIn.entries()) { - const child = collectErrorsFromChild(entry) + const child = collectErrors(entry) data[idx] = child.data if (child.errors != null) { errors.push(...child.errors) @@ -123,7 +137,7 @@ function collectErrorsFromArray(arrayIn: object[]): { return { data, errors } } -function collectErrorsFromChild(entry: object) { +function collectErrors(entry: object) { if (entry instanceof Error) { return { data: null, errors: [entry] } } else if (Array.isArray(entry)) { diff --git a/packages/graphql-hooks/test-jsdom/unit/LocalGraphQLClient.test.tsx b/packages/graphql-hooks/test-jsdom/unit/LocalGraphQLClient.test.tsx index e1d28c3f3..f96099886 100644 --- a/packages/graphql-hooks/test-jsdom/unit/LocalGraphQLClient.test.tsx +++ b/packages/graphql-hooks/test-jsdom/unit/LocalGraphQLClient.test.tsx @@ -29,6 +29,10 @@ const QUERY_PARTIAL_ERROR_WITH_ARRAY = { query: 'PartialErrorQueryWithArray' } +const QUERY_ARRAY = { + query: 'ArrayQuery' +} + const HooksTestQuery = ` query { testQuery { @@ -54,15 +58,19 @@ const localQueries = { PartialErrorQuery: () => ({ property1: 'Hello World', property2: new Error('failed to resolve property 2'), - nested: {property3: new Error('failed to resolve nested property 3'), property4: 'Hello again'} + nested: { + property3: new Error('failed to resolve nested property 3'), + property4: 'Hello again' + } }), PartialErrorQueryWithArray: () => ({ property1: 'Hello World', arrayProperty: [ - {item: 'Hello item'}, + { item: 'Hello item' }, new Error('failed to resolve child of array') ] }), + ArrayQuery: () => [{ item: 'Hello item' }], [HooksTestQuery]: () => ({ testQuery: { value: 2 @@ -123,7 +131,9 @@ describe('LocalGraphQLClient', () => { expect(result.error.graphQLErrors).toEqual( expect.arrayContaining([ expect.objectContaining({ message: 'failed to resolve property 2' }), - expect.objectContaining({ message: 'failed to resolve nested property 3' }) + expect.objectContaining({ + message: 'failed to resolve nested property 3' + }) ]) ) }) @@ -138,10 +148,18 @@ describe('LocalGraphQLClient', () => { expect(result.error).toBeDefined() expect(result.error.graphQLErrors).toEqual( expect.arrayContaining([ - expect.objectContaining({ message: 'failed to resolve child of array' }), + expect.objectContaining({ + message: 'failed to resolve child of array' + }) ]) ) }) + it('should handle array result', async () => { + const result = await client.request(QUERY_ARRAY) + expect(result.data).toBeInstanceOf(Array) + expect(result.data).toHaveLength(1) + expect(result.data[0]).toHaveProperty('item', 'Hello item') + }) }) describe('integration with hooks', () => { let client, wrapper @@ -159,4 +177,48 @@ describe('LocalGraphQLClient', () => { expect(dataNode.textContent).toBe('2') }) }) + describe('middleware', () => { + let client: LocalGraphQLClient + const middlewareSpy = jest.fn() + const addResponseHookSpy = jest.fn() + + beforeEach(() => { + client = new LocalGraphQLClient({ + localQueries, + middleware: [ + ({ addResponseHook }, next) => { + addResponseHook(response => { + addResponseHookSpy() + return response + }) + middlewareSpy() + next() + } + ] + }) + }) + it('should run middleware', async () => { + const result = await client.request(QUERY_BASIC) + + expect(result.data.hello).toBe('Hello world') + expect(middlewareSpy).toHaveBeenCalledTimes(1) + expect(addResponseHookSpy).toHaveBeenCalledTimes(1) + }) + }) + describe('responseReducer option', () => { + let client: LocalGraphQLClient + + beforeEach(() => { + client = new LocalGraphQLClient({ + localQueries + }) + }) + it('should return responseReducer result', async () => { + const result = await client.request(QUERY_ARRAY, { + responseReducer: fetchedData => [...fetchedData, 'foo'] + }) + + expect(result.data).toStrictEqual([{ item: 'Hello item' }, 'foo']) + }) + }) })