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 4207a42503a..fe618ae4f48 100644 --- a/src/testing/core/mocking/__tests__/mockLink.ts +++ b/src/testing/core/mocking/__tests__/mockLink.ts @@ -1,7 +1,7 @@ import gql from "graphql-tag"; import { MockLink, MockedResponse } from "../mockLink"; import { execute } from "../../../../link/core/execute"; -import { ObservableStream } from "../../../internal"; +import { ObservableStream, enableFakeTimers } from "../../../internal"; describe("MockedResponse.newData", () => { const setup = () => { @@ -73,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 @@ -83,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: { @@ -104,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: { @@ -126,6 +127,81 @@ 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 + } + } + `; + 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 } } }); + } + }); }); test("removes @nonreactive directives from fields", async () => { diff --git a/src/testing/core/mocking/mockLink.ts b/src/testing/core/mocking/mockLink.ts index f38474e1c20..646567b2da9 100644 --- a/src/testing/core/mocking/mockLink.ts +++ b/src/testing/core/mocking/mockLink.ts @@ -16,6 +16,8 @@ import { cloneDeep, stringifyForDisplay, print, + getOperationDefinition, + getDefaultValues, removeDirectivesFromDocument, checkDocument, } from "../../../utilities/index.js"; @@ -233,17 +235,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); }; } 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";