Skip to content

Commit

Permalink
MockLink: add query default variables if not specified in mock request
Browse files Browse the repository at this point in the history
resolves #8023
  • Loading branch information
phryneas committed Apr 24, 2024
1 parent 31c3df4 commit 8df6013
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/fluffy-badgers-rush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@apollo/client": patch
---

MockLink: add query default variables if not specified in mock request
84 changes: 81 additions & 3 deletions src/testing/core/mocking/__tests__/mockLink.ts
Original file line number Diff line number Diff line change
@@ -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 = () => {
Expand Down Expand Up @@ -72,16 +73,15 @@ 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
}
`;

it("should not require a result or error when delay equals Infinity", async () => {
using _fakeTimers = enableFakeTimers();

const mockLink = new MockLink([
{
request: {
Expand All @@ -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: {
Expand All @@ -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 } } });
}
});
});
9 changes: 9 additions & 0 deletions src/testing/core/mocking/mockLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
cloneDeep,
stringifyForDisplay,
print,
getOperationDefinition,
getDefaultValues,
} from "../../../utilities/index.js";

export type ResultFunction<T, V = Record<string, any>> = (variables: V) => T;
Expand Down Expand Up @@ -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,
Expand Down
26 changes: 26 additions & 0 deletions src/testing/internal/disposables/enableFakeTimers.ts
Original file line number Diff line number Diff line change
@@ -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();
}
});
}
1 change: 1 addition & 0 deletions src/testing/internal/disposables/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { disableActWarnings } from "./disableActWarnings.js";
export { spyOnConsole } from "./spyOnConsole.js";
export { withCleanup } from "./withCleanup.js";
export { enableFakeTimers } from "./enableFakeTimers.js";

0 comments on commit 8df6013

Please sign in to comment.