From 8df6013b6b45452ec058fab3e068b5b6d6c493f7 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 24 Apr 2024 12:38:00 +0200 Subject: [PATCH 1/3] MockLink: add query default variables if not specified in mock request resolves #8023 --- .changeset/fluffy-badgers-rush.md | 5 ++ .../core/mocking/__tests__/mockLink.ts | 84 ++++++++++++++++++- src/testing/core/mocking/mockLink.ts | 9 ++ .../internal/disposables/enableFakeTimers.ts | 26 ++++++ src/testing/internal/disposables/index.ts | 1 + 5 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 .changeset/fluffy-badgers-rush.md create mode 100644 src/testing/internal/disposables/enableFakeTimers.ts diff --git a/.changeset/fluffy-badgers-rush.md b/.changeset/fluffy-badgers-rush.md new file mode 100644 index 00000000000..62f55a2b0f0 --- /dev/null +++ b/.changeset/fluffy-badgers-rush.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +MockLink: add query default variables if not specified in mock request diff --git a/src/testing/core/mocking/__tests__/mockLink.ts b/src/testing/core/mocking/__tests__/mockLink.ts index dc68b654505..119c9b1ba45 100644 --- a/src/testing/core/mocking/__tests__/mockLink.ts +++ b/src/testing/core/mocking/__tests__/mockLink.ts @@ -1,6 +1,7 @@ import gql from "graphql-tag"; import { MockLink, MockedResponse } from "../mockLink"; import { execute } from "../../../../link/core/execute"; +import { ObservableStream, enableFakeTimers } from "../../../internal"; describe("MockedResponse.newData", () => { const setup = () => { @@ -72,9 +73,6 @@ We've chosen this value as the MAXIMUM_DELAY since values that don't fit into a const MAXIMUM_DELAY = 0x7f_ff_ff_ff; describe("mockLink", () => { - beforeAll(() => jest.useFakeTimers()); - afterAll(() => jest.useRealTimers()); - const query = gql` query A { a @@ -82,6 +80,8 @@ describe("mockLink", () => { `; it("should not require a result or error when delay equals Infinity", async () => { + using _fakeTimers = enableFakeTimers(); + const mockLink = new MockLink([ { request: { @@ -103,6 +103,8 @@ describe("mockLink", () => { }); it("should require result or error when delay is just large", (done) => { + using _fakeTimers = enableFakeTimers(); + const mockLink = new MockLink([ { request: { @@ -125,4 +127,80 @@ describe("mockLink", () => { jest.advanceTimersByTime(MAXIMUM_DELAY); }); + + it("should fill in default variables if they are missing in mocked requests", async () => { + const query = gql` + query GetTodo($done: Boolean = true, $user: String!) { + todo(user: $user, done: $done) { + id + title + } + } + `; + const mocks = [ + { + // default should get filled in here + request: { query, variables: { user: "Tim" } }, + result: { + data: { todo: { id: 1 } }, + }, + }, + { + // we provide our own `done`, so it should not get filled in + request: { query, variables: { user: "Tim", done: false } }, + result: { + data: { todo: { id: 2 } }, + }, + }, + { + // one more that has a different user variable and should never match + request: { query, variables: { user: "Tom" } }, + result: { + data: { todo: { id: 2 } }, + }, + }, + ]; + + // Apollo Client will always fill in default values for missing variables + // in the operation before calling the Link, so we have to do the same here + // when we call `execute` + const defaults = { done: true }; + const link = new MockLink(mocks, false, { showWarnings: false }); + { + // Non-optional variable is missing, should not match. + const stream = new ObservableStream( + execute(link, { query, variables: { ...defaults } }) + ); + await stream.takeError(); + } + { + // Execute called incorrectly without a default variable filled in. + // This will never happen in Apollo Client since AC always fills these + // before calling `execute`, so it's okay if it results in a "no match" + // scenario here. + const stream = new ObservableStream( + execute(link, { query, variables: { user: "Tim" } }) + ); + await stream.takeError(); + } + { + // Expect default value to be filled in the mock request. + const stream = new ObservableStream( + execute(link, { query, variables: { ...defaults, user: "Tim" } }) + ); + const result = await stream.takeNext(); + expect(result).toEqual({ data: { todo: { id: 1 } } }); + } + { + // Test that defaults don't overwrite explicitly different values in a mock request. + const stream = new ObservableStream( + execute(link, { + query, + variables: { ...defaults, user: "Tim", done: false }, + }) + ); + const result = await stream.takeNext(); + expect(result).toEqual({ data: { todo: { id: 2 } } }); + } + }); }); diff --git a/src/testing/core/mocking/mockLink.ts b/src/testing/core/mocking/mockLink.ts index 46b43cca6ad..1258dae115d 100644 --- a/src/testing/core/mocking/mockLink.ts +++ b/src/testing/core/mocking/mockLink.ts @@ -17,6 +17,8 @@ import { cloneDeep, stringifyForDisplay, print, + getOperationDefinition, + getDefaultValues, } from "../../../utilities/index.js"; export type ResultFunction> = (variables: V) => T; @@ -212,6 +214,13 @@ ${unmatchedVars.map((d) => ` ${stringifyForDisplay(d)}`).join("\n")} newMockedResponse.request.query = query; } + newMockedResponse.request.variables = { + ...getDefaultValues( + getOperationDefinition(newMockedResponse.request.query) + ), + ...newMockedResponse.request.variables, + }; + mockedResponse.maxUsageCount = mockedResponse.maxUsageCount ?? 1; invariant( mockedResponse.maxUsageCount > 0, diff --git a/src/testing/internal/disposables/enableFakeTimers.ts b/src/testing/internal/disposables/enableFakeTimers.ts new file mode 100644 index 00000000000..6be85d24730 --- /dev/null +++ b/src/testing/internal/disposables/enableFakeTimers.ts @@ -0,0 +1,26 @@ +import { withCleanup } from "./withCleanup.js"; + +declare global { + interface DateConstructor { + /* Jest uses @sinonjs/fake-timers, that add this flag */ + isFake: boolean; + } +} + +export function enableFakeTimers( + config?: FakeTimersConfig | LegacyFakeTimersConfig +) { + if (global.Date.isFake === true) { + // Nothing to do here, fake timers have already been set up. + // That also means we don't want to clean that up later. + return withCleanup({}, () => {}); + } + + jest.useFakeTimers(config); + return withCleanup({}, () => { + if (global.Date.isFake === true) { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); + } + }); +} diff --git a/src/testing/internal/disposables/index.ts b/src/testing/internal/disposables/index.ts index 9895d129589..9d61c88fd90 100644 --- a/src/testing/internal/disposables/index.ts +++ b/src/testing/internal/disposables/index.ts @@ -1,3 +1,4 @@ export { disableActWarnings } from "./disableActWarnings.js"; export { spyOnConsole } from "./spyOnConsole.js"; export { withCleanup } from "./withCleanup.js"; +export { enableFakeTimers } from "./enableFakeTimers.js"; From bcd51e071b55c790116d7183022e83e6ed778737 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 24 Apr 2024 13:08:40 +0200 Subject: [PATCH 2/3] move logic --- src/testing/core/mocking/mockLink.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/testing/core/mocking/mockLink.ts b/src/testing/core/mocking/mockLink.ts index 1258dae115d..46e6dbd760a 100644 --- a/src/testing/core/mocking/mockLink.ts +++ b/src/testing/core/mocking/mockLink.ts @@ -214,13 +214,6 @@ ${unmatchedVars.map((d) => ` ${stringifyForDisplay(d)}`).join("\n")} newMockedResponse.request.query = query; } - newMockedResponse.request.variables = { - ...getDefaultValues( - getOperationDefinition(newMockedResponse.request.query) - ), - ...newMockedResponse.request.variables, - }; - mockedResponse.maxUsageCount = mockedResponse.maxUsageCount ?? 1; invariant( mockedResponse.maxUsageCount > 0, @@ -233,17 +226,21 @@ ${unmatchedVars.map((d) => ` ${stringifyForDisplay(d)}`).join("\n")} } private normalizeVariableMatching(mockedResponse: MockedResponse) { - const variables = mockedResponse.request.variables; - if (mockedResponse.variableMatcher && variables) { + const request = mockedResponse.request; + if (mockedResponse.variableMatcher && request.variables) { throw new Error( "Mocked response should contain either variableMatcher or request.variables" ); } if (!mockedResponse.variableMatcher) { + request.variables = { + ...getDefaultValues(getOperationDefinition(request.query)), + ...request.variables, + }; mockedResponse.variableMatcher = (vars) => { const requestVariables = vars || {}; - const mockedResponseVariables = variables || {}; + const mockedResponseVariables = request.variables || {}; return equal(requestVariables, mockedResponseVariables); }; } From 7249ff6d9feffca15a36bbe3331ce98146a49747 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Thu, 2 May 2024 11:37:42 +0200 Subject: [PATCH 3/3] Update src/testing/core/mocking/__tests__/mockLink.ts Co-authored-by: Jerel Miller --- src/testing/core/mocking/__tests__/mockLink.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/testing/core/mocking/__tests__/mockLink.ts b/src/testing/core/mocking/__tests__/mockLink.ts index 119c9b1ba45..6c2d8e1d65a 100644 --- a/src/testing/core/mocking/__tests__/mockLink.ts +++ b/src/testing/core/mocking/__tests__/mockLink.ts @@ -133,7 +133,6 @@ describe("mockLink", () => { query GetTodo($done: Boolean = true, $user: String!) { todo(user: $user, done: $done) { id - title } } `;