diff --git a/package-lock.json b/package-lock.json index 63377abceb8..c438d3a2710 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1931,6 +1931,12 @@ "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==", "dev": true }, + "@types/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@types/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-ryxTolaNg1l809rknW9q9T7wG8QHcjtZX6syJx7kpOLY2qev75VzC9HMVimUxlA1YzjpGsDI29yLjHBotqhUhA==", + "dev": true + }, "@types/testing-library__dom": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/testing-library__dom/-/testing-library__dom-6.14.0.tgz", @@ -3771,6 +3777,11 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, + "get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==" + }, "get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -4250,6 +4261,11 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" + }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -4265,6 +4281,11 @@ "integrity": "sha1-DFLlS8yjkbssSUsh6GJtczbG45c=", "dev": true }, + "is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=" + }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", @@ -8869,6 +8890,16 @@ "safe-buffer": "~5.1.0" } }, + "stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "requires": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + } + }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", diff --git a/package.json b/package.json index 9686f75c70c..87459b27f37 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "hoist-non-react-statics": "^3.3.2", "optimism": "^0.16.0", "prop-types": "^15.7.2", + "stringify-object": "^3.3.0", "symbol-observable": "^4.0.0", "ts-invariant": "^0.7.0", "tslib": "^1.10.0", @@ -101,6 +102,7 @@ "@types/react": "17.0.3", "@types/react-dom": "17.0.2", "@types/recompose": "0.30.7", + "@types/stringify-object": "^3.3.0", "bundlesize": "0.18.1", "cross-fetch": "3.1.4", "crypto-hash": "1.3.0", diff --git a/src/utilities/testing/mocking/__tests__/MockedProvider.test.tsx b/src/utilities/testing/mocking/__tests__/MockedProvider.test.tsx index b8bd658edd1..2e5592f6c9b 100644 --- a/src/utilities/testing/mocking/__tests__/MockedProvider.test.tsx +++ b/src/utilities/testing/mocking/__tests__/MockedProvider.test.tsx @@ -1,6 +1,7 @@ -import React from 'react'; +import React, { FC } from 'react'; import { render, wait } from '@testing-library/react'; import gql from 'graphql-tag'; +import { act } from 'react-dom/test-utils'; import { itAsync } from '../../itAsync'; import { MockedProvider } from '../MockedProvider'; @@ -9,6 +10,7 @@ import { DocumentNode } from 'graphql'; import { useQuery } from '../../../../react/hooks/useQuery'; import { InMemoryCache } from '../../../../cache/inmemory/inMemoryCache'; import { ApolloLink } from '../../../../link/core'; +import { QueryResult } from '../../../../react/types/types'; const variables = { username: 'mock_username' @@ -567,3 +569,199 @@ describe('@client testing', () => { return wait().then(resolve, reject); }); }); + +describe('missing and undefined optional fields', () => { + describe('no mocks available', () => { + it("should print correct variables when no mocks available", async () => { + expect.assertions(3); + + const variablesWithUndefined = { + username: 'other_user', + bar: 123, + thisFieldIsPresentButUndefined: undefined, + }; + + let queryResult: QueryResult | undefined; + + const OptionalFieldsNoMocksAvailable: FC = () => { + queryResult = useQuery(query, { variables: variablesWithUndefined }); + return null; + }; + + const link = ApolloLink.from([errorLink, new MockLink([])]); + + await act(async () => { + render( + + + + ); + + await new Promise(resolve => setTimeout(resolve, 0)); + }); + + expect(queryResult?.data).toBeUndefined(); + expect(queryResult?.error?.message).toContain('No more mocked responses for the query'); + expect(queryResult?.error?.message).toContain('thisFieldIsPresentButUndefined'); + }); + }); + + describe('mocks available', () => { + const queryWithOptionalInputFields: DocumentNode = gql` + # type Input { + # required: String! + # optional: String + # } + query OptionalFields($input: Input) { + user(input: $input) { + id + } + } + `; + + const mockResult = { data: user }; + + type OptionalInputFieldVariables = { + input: { + required: string + optional?: string + } + }; + + const optionalUndefined: OptionalInputFieldVariables = { + input: { + required: "a", + optional: undefined, + } + }; + + const optionalMissing: OptionalInputFieldVariables = { + input: { + required: "a", + } + }; + + + const mocksOptional = (mockVariables: OptionalInputFieldVariables) => [ + { + request: { + query: queryWithOptionalInputFields, + variables: mockVariables, + }, + result: mockResult, + }, + ]; + + it("should load data when: undefined in input, undefined in mock", async () => { + expect.assertions(3); + + let queryResult: QueryResult | undefined; + + const Component: FC = () => { + queryResult = useQuery(queryWithOptionalInputFields, { + variables: optionalUndefined, + }); + return null; + }; + + await act(async () => { + render( + + + + ); + + await new Promise(resolve => setTimeout(resolve, 0)); + }); + + + expect(queryResult?.loading).toBe(false); + expect(queryResult?.error).toBeUndefined(); + expect(queryResult?.data).toEqual(mockResult); + }); + + it("should load data when: undefined in input, missing in mock", async () => { + expect.assertions(3); + + let queryResult: QueryResult | undefined; + + const Component: FC = () => { + queryResult = useQuery(queryWithOptionalInputFields, { + variables: optionalUndefined, + }); + return null; + }; + + await act(async () => { + render( + + + + ); + + await new Promise(resolve => setTimeout(resolve, 0)); + }); + + + expect(queryResult?.loading).toBe(false); + expect(queryResult?.error).toBeUndefined(); + expect(queryResult?.data).toEqual(mockResult); + }); + + it("should load data when: missing in input, undefined in mock", async () => { + expect.assertions(3); + + let queryResult: QueryResult | undefined; + + const Component: FC = () => { + queryResult = useQuery(queryWithOptionalInputFields, { + variables: optionalMissing, + }); + return null; + }; + + await act(async () => { + render( + + + + ); + + await new Promise(resolve => setTimeout(resolve, 0)); + }); + + + expect(queryResult?.loading).toBe(false); + expect(queryResult?.error).toBeUndefined(); + expect(queryResult?.data).toEqual(mockResult); + }); + + it("should load data when: missing in input, missing in mock", async () => { + expect.assertions(3); + + let queryResult: QueryResult | undefined; + + const Component: FC = () => { + queryResult = useQuery(queryWithOptionalInputFields, { + variables: optionalMissing, + }); + return null; + }; + + await act(async () => { + render( + + + + ); + + await new Promise(resolve => setTimeout(resolve, 0)); + }); + + + expect(queryResult?.loading).toBe(false); + expect(queryResult?.error).toBeUndefined(); + expect(queryResult?.data).toEqual(mockResult); + }); + }); +}); diff --git a/src/utilities/testing/mocking/__tests__/__snapshots__/MockedProvider.test.tsx.snap b/src/utilities/testing/mocking/__tests__/__snapshots__/MockedProvider.test.tsx.snap index 89d4ffd6cef..44e1c21fa7f 100644 --- a/src/utilities/testing/mocking/__tests__/__snapshots__/MockedProvider.test.tsx.snap +++ b/src/utilities/testing/mocking/__tests__/__snapshots__/MockedProvider.test.tsx.snap @@ -14,7 +14,9 @@ exports[`General use should error if the query in the mock and component do not __typename } } -, variables: {"username":"mock_username"}] +, variables: { + username: 'mock_username' +}] `; exports[`General use should error if the variables do not deep equal 1`] = ` @@ -24,7 +26,10 @@ exports[`General use should error if the variables do not deep equal 1`] = ` __typename } } -, variables: {"username":"some_user","age":42}] +, variables: { + username: 'some_user', + age: 42 +}] `; exports[`General use should error if the variables in the mock and component do not match 1`] = ` @@ -34,7 +39,9 @@ exports[`General use should error if the variables in the mock and component do __typename } } -, variables: {"username":"other_user"}] +, variables: { + username: 'other_user' +}] `; exports[`General use should mock the data 1`] = ` @@ -74,5 +81,7 @@ exports[`General use should support custom error handling using setOnError 1`] = __typename } } -, variables: {"username":"mock_username"}] +, variables: { + username: 'mock_username' +}] `; diff --git a/src/utilities/testing/mocking/mockLink.ts b/src/utilities/testing/mocking/mockLink.ts index a66e2a7f5a4..58e41f58d2f 100644 --- a/src/utilities/testing/mocking/mockLink.ts +++ b/src/utilities/testing/mocking/mockLink.ts @@ -1,6 +1,7 @@ import { print } from 'graphql'; import { equal } from '@wry/equality'; import { invariant } from 'ts-invariant'; +import stringifyObject from 'stringify-object' import { ApolloLink, @@ -91,7 +92,7 @@ export class MockLink extends ApolloLink { configError = new Error( `No more mocked responses for the query: ${print( operation.query - )}, variables: ${JSON.stringify(operation.variables)}` + )}, variables: ${stringifyObject(operation.variables)}` ); } else { this.mockedResponsesByKey[key].splice(responseIndex, 1);